闲聊js6: 创建一个演示用的渲染库4(渲染表面,像素格式,光栅化,位块传输,图形与图像)

本篇的目的是要了解:

  • canvas 像素格式

  • canvas渲染表面以及内存大小的计算

  • 光栅化

  • 位块传输

  • 图形和图像的区别

上一篇我们了解到: 当css sizeelem size 不一致的时候,会导致渲染后的效果发生形变。

css size 决定了显示区域的大小,但是实际渲染区域却是由elem size确定的!!!

计算机在渲染图形图像时,总是以像素(px)这个物理单位进行定位置,定尺寸,定效果的。

而elem size 只能以px为单位设置元素大小,因为canvas需要使用这个尺寸分配渲染表面的像素集合内存,所有的canvas后续绘图操作都是绘制到这个渲染表面上的。

所以上一篇中的文字绘制实际上是绘制到了渲染表面上去了,而不是绘制到css size规定的显示区中!

一些基础概念:

什么是像素格式(pixel format)?

像素格式就是像素色彩每个分量大小排列方式。这种格式以每个像素所使用的总位数以及用于存储像素色彩的红、绿、蓝和 alpha 分量的位数指定。这个是百度解释,太学术化了。简单说(按最常规的情况进行解释),就是:

  • 分量(component)包括: R(red)、G(green)、B(blue)、A(alpha)

  • 一个像素(pixel)可以由3个(RGB)或4个(RGBA)分量组成(一般情况下如此,但还有很多特殊像素格式并不是由3/4个分量组成)

  • 分量大小是指每个分量的bit(不是bytes)数是多少,例如我们一般都是使用每个分量为8 bit的颜色表示法(canvas2d中就是如此),R8G8B8A8, 1 pixle = 4 compent = 4 componet * 8 bits = 32 bits = 32 bits / 8 bits(1 bytes = 8 bits) = 4 bytes。也就是说R8G8B8A8(canvas2d像素格式)的话,一个像素占用4个字节的内存

  • 实际上很多3D api,例如dx/gl还有更多像素格式:R16B16G16A16 /R32G32B32A32...,每种api都支持遍历拥有的像素格式方法,可以列出一长串

  • 分量的排列方式: 不同的渲染API可能使用不同的分量排列方式,例如:canvas RGBA方式排序各个分量,而GL/DirectX可以使用ARGB,BGRA,RBGA....等排序方式

总结一下像素格式:

 像素是由哪些分量组成
 每个分量多少个bit数量(不是byte)
 分量是如何排列的

什么是渲染表面?

渲染表面就是以像素为单位表示的内存块(若分配在显存,则是显存块)。

简而言之,渲染表面(render surface)就是内存图像,所有的绘制操作最后都以像素色彩方式填充到这个内存图像中

具有两个必须的特性:

  • size(width/height)
  • 像素格式(RGB,RGBA,R8G8B8A8,R32G32B32A32,....,......,n多种)

有了上面两个特性,就能确定内存块的大小了,计算公式很简单:
RGBA RenderSurface memory bytes = width * height * 4bytes

所以elem size 以及canvas2d raga像素格式确定了canvas2d渲染表面的内存大小

有了渲染表面,就可以直接操作像素进行颜色获取或设置。

什么是光栅化:

光栅化就是把顶点数据转换为像素片元的过程。像素片元中的每一个元素对应于渲染表面中的一个像素。

简而言之,光栅化就是将图形【graphics】变为图像【image】的过程。

还是有点学术化,解释如下:

  • 顶点数据(既矢量数据【vector】,或图形【graphics】),因为矢量或图形是以顶点【vertex】方式进行定义的。例如一条线段由起点/终点来定义,而三角形由三个点来确定。

  • 一条线两个点,然后渲染表面需要使用像素表示,所以必须将两个点变成一系列像素,这就是光栅化【rasterization】(点线面体各种光栅化算法)。

  • 上面解释使用像素片元(fragment)而不是像素,是因为像素仅仅包含三个信息:分量的大小(8/16/32等),分量的数量(3/4),分量的排列方式【就是像素格式】,但是片元包含更多信息,例如3D api在光栅话的过程中片元还带有深度信息,纹理坐标

位块传输(bit-block transfer,bitblt for short):

当我们将canvas2d中的rendersurface 绘制到显示区表面时,发生了位块传输操作

位块传输需要两个(源【source】/目标【destination】)内存表面(或位图,图像等等,反正就是具有大小和像素格式的内存块)。

能将源表面的全部或一部分区域像素色彩数据传输到目标表面的全部或一部分区域。

并非是简单的数据拷贝(copy),因为bitblt不止是原图拷贝,有可能是进行源和目标表面之间像素的与运算、或运算,异或运算等等

如果源表面和目标表面的size不一致的情况下,将源矩形中的位图拷贝到目标矩形中,可以扩展或压缩该位图使其与目标矩形尺寸吻合。这个过程中会使用各种过滤插值算法让颜色尽量平和过度。但是会导致图像发生形变,这也是为什么上一篇中发生了形变!

bitblit会尽可能使用硬件加速,如果实在不行,它们会使用具有很好优化手段的软件传输方法(例如有硬件加速,则使用硬件加速,没有硬件加速,但支持simd(单指令多数据流),则使用simd算法,最后不行,就直接内存操作,反正bitblit是极其核心的一个功能,每个操作系统或硬件供应商都有关键优化代码对其支持)

图形处理和图像处理的区别:

图形学: 输入的是模型顶点数据(矢量表示),输出是图像(像素)

图像学: 输入是像素,输出还是像素,对像素进行处理,例如ps中的各种滤镜,现在很流行的图像识别等等

两者完美的结合: AR技术(Augmented Reality) = 图像识别处理+图形渲染效果

聊了这么多,我们总结一下canvas中的绘图的流程:

  • elem size 以及rgba像素格式决定了canvas渲染表面分配的内存大小

  • 所有canvas2d 的绘图操作(文字/各种形体都是矢量),经过光栅化后,将顶点表示的数据生成了像素表示,最终填充到了渲染表面中

  • canvas2d中的drawImage函数并不进行光栅化,而是进行位块传输(bitblt),根据源表面和目标表面尺寸的不同,自动进行stretch blt操作

  • css 拥有一个虚拟坐标系统,可以适应各种位置和尺寸单位(px,cm,rem...),是上层表示,但是最后必须要转换成px表示,所以render surface最后需要进行位块传输,将结果像素传送到css size定义的显示表面上去

所以,对于<canvas>/<image>等具有elem size以及显示效果的元素,如果不想发生明显的变形,就需要掌握两个关键诀窍:

  1. 尽量规定elem size,不要使用css size。即使使用css size,尽量和elem size 一致

  2. 如果使用css size,为了不变形,请尽量和elem size 的纵横比(height/width)一致,这时候render surface stretch blt to destination surface的时候,会进行等比扩展或缩放,至少比例是正确的,不会明显变形(iphone的纵横比基本只有两个:1.5/1.78,相对android各式各样的纵横比,好多了,不过目前来说,主流的android机的纵横比基本都是1.78)

推荐阅读更多精彩内容