本篇文章介绍图片和纹理的相关内容,分为四个部分。
第一部分介绍如果读取并显示图片,并介绍如何使用 alpha 分量混合图片。
第二部分介绍 UV 坐标,讲解如何将图片纹理映射到我们绘制的三角形上。
第三部分介绍双线性插值,使用它可以更加平滑的映射纹理。
第四部分介绍纹理的寻址模式,讲解两种寻址模式——重复和镜像。
1. 图片显示和颜色混合
这节实验没有“数学”上的概念,都是“工程”上的实现。
因为涉及到颜色混合,需要使用到 alpha 分量。而之前项目里都是使用的 RGB 相关结构来使用的,所以为了方便,首先将工程颜色相关的地方,全部改写成了 RGBA。仅为简单的替换,对应提交 ab4b643。
如代码清单 1.1 所示,我们新增加了一个 TImage 类,内部使用 stb 库读取图片。需要注意,如之前所讲,当前 windows 平台底层使用的像素格式是 BGRA,而读取的是 RGBA,所以我们这边交换一下 B 和 R 分量。
实现了图片读取后,我们再实现一个图片绘制接口,用于验证效果。如代码清单 1.2 所示,很简单,对图片的像素依次调用 SetPixel,设置像素值即可。
实现好图片绘制之后,我们就可以开始实现颜色混合了。基于 alpha 混合的原理也不复杂。我们定义透明度的范围为 0 到 1,0 表示完全透明,1 表示完全不透明。
我们定义即将绘制的前景颜色为 C_front,透明度为 A_front;背景颜色为 C_back,透明度为 A_back。那么最终混合的颜色 C 为
C = A_front * C_front + (1 - A_front) * C_back
对应的实现如代码清单 1.3 所示,需要注意,存储的 alpha 分量范围是 0 到 255,所以需要转化一下。
我们可以定义变量指示是否开启颜色混合,并定义相关设置接口,当前代码中定义了 SetBlend 接口。因为逻辑简单,就不贴出代码赘述了。
我们将混合的操作放在 SetPixel 处。如代码清单 1.4 所示,如果开启了颜色混合,则调用 BlendPixel 函数;否则,就使用原始颜色。
最后,我们写一个测试用例。如代码清单 1.5 所示,我们加载一张完全不透明的 jpeg 图片,以及一张背景透明的 png 图片。然后依次在不开启混合和开启混合的情况下进行图片绘制。
运行结果如图 1 所示,可以看到,第一行不透明的小狗是否开启混合,效果都没有影响。第二行和第三行可以看到,背景透明的妙蛙草,会混合(这边是直接显示)下面一层的颜色。
此节完整代码在 tag/img_blend。
可以在 VS 中设置,编译时将资源文件复制到 exe 所在目录:
1. 右击项目 - 属性页 - 生成事件
2. 命令行里输入
xcopy /Y /D "$(ProjectDir)image\*" "$(OutDir)image\"
2. UV 坐标
UV 坐标用于在图形表面上映射纹理。U 和 V 分别代表纹理图像的水平和垂直坐标轴。
但是绘制的图形可能和纹理图像大小不一致,即可能被“拉伸”或“压缩”,为了处理这个问题,我们需要应用纹理插值算法。比较神奇的是,这个插值算法,我们在上节文章中已经了解过,就是——三角形重心插值算法。说它神奇,是因为我现在还不清楚它是怎么能“迁移和套用”到纹理映射上的,之前了解的是应用于颜色渐变效果。
在三角形重心插值算法中,我们已经求得加权的三个系数 α、β 和 γ。可以根据这个系数求得插值的 UV 坐标:
UV = α * UV_1 + β * UV_2 + γ * UV_3
我们定义 UV 坐标的范围是 0 至 1,因为这样得到的 UV 坐标就是纹理图像宽高上的百分比位置。在我们当前的例子中,UV 坐标原点在图片左上角,并且向右向下增长。这样设置仅仅是为了方便,因为我们当前的 GDI 存储方式以及图片像素存储方式,均是如此。
我现在用特例来“说服”自己:如果只考虑两个加权参数,那么点位于三角形边上。无论“拉伸”还是“压缩”,点在边上的百分比位置是一样,比如都在边的正中间。
留作问题。
知道了插值公式后,我们开始实现代码。如代码清单 2.1 所示,我们为纹理绘制单独实现一个 DrawTriangle。它和之前的 DrawTriangle 几乎是一样,不同的内容在 33 至 40 行:我们求得 α、β 和 γ,计算得出三角形各点映射的 UV 坐标;根据 UV 坐标采样纹理图片上对应位置的像素值。
如代码清单 2.2 所示,采样实现非常容易理解。之前已经说过 UV 坐标对应宽高上的百分比位置,以此,我们就可以得到对应位置的像素值。
最后我们实现测试用例。如代码清单 2.3 所示,我们用两个三角形组成一个长方形图片,并设置好对应的 UV 坐标。
运行结果如图 2 所示,尺寸较小的小狗图片被正确的拉伸绘制。
此节完整代码在 tag/uv_mapping。
3. 双线性插值
上一节中的采样方式其实是最近邻插值,从图 2 中可以看到,小狗的毛发比较像素化,不太平滑。这一节介绍双线性插值,相比最近邻插值,它可以产生更平滑的图像。
双线性插值会考虑目标点周围的四个像素点,并基于它们的距离进行加权平均,来计算目标点的像素值。
具体方式如图 3 所示,中间的灰色点是目标点,四个边角的点就是目标点周围的四个像素点。双线性的意思是,我们做两次线性插值:第一次在横向做一次插值,根据左上和右上求出一个插值点、根据左下和右下求出一个插值点,即图上橘色的点;第二次在纵向上做插值,我们基于第一次橘色的点,计算插值。
代码实现如代码清单 3.1 所示,写的容易理解,就是上述的原理介绍:第一次插值,计算得到 interpTop 和 interpBottom;第二次插值,得到最终像素值。我们这边的复用了之前的 Lerp 函数,注意比例作用的对象。我们只需关注离这个颜色越近,越像这个颜色即可。
如代码清单 3.2 所示,我们在 DrawTriangle 中进行采样方式选择,如果是双线性插值,则调用 SampleTextureBilinear;如果是最邻近插值,则调用 SampleTextureNearest。
最后我们实现测试用例。和上一节实现一样,如代码清单 3.3 所示,不同的是,我们使用 SetSampleMode 设置采样模式为双线性插值。
运行结果如图 4 所示,可以看到小狗脸部的毛发柔顺平滑了许多。
此节完整代码在 tag/bilinear_texture。
4. 纹理寻址模式
纹理寻址模式是图形渲染中使用的一种技术,它决定了当纹理坐标超出0到1的范围时,如何对纹理进行采样。
我们之前的实现,都是默认重复的寻址模式。如图 5 所示,当纹理坐标超出 0 到 1 的范围时,纹理会在水平和垂直方向上重复。
这边我们再实现一种镜像模式。如图 6 所示,在这种模式下,纹理也会重复,但每次重复时会在水平或垂直方向上镜像翻转。
为了之前的处理能复用,我们对传入的 UV 坐标进行统一处理。我们直接看到代码清单 4.1,来讲解如何处理重复和镜像两种模式。
重复模式很简单,就是“取模”的思路。我们先使用 fmodf 取除以 1.0 的余数。因为余数可能是负数,我们加上 1.0,再取余。
镜像模式稍微复杂一点,但是在了解了处理手段后还是很好理解的:重复模式的最小范围在 [0,1],镜像模式的最小范围在 [0,2]。镜像的意思是,到“对称轴” 1.0 距离相同的点,内容是一致的。
所以镜像模式一开始的处理逻辑和重复模式一致,镜像模式把范围取模限定在 [0,2] 范围内。接着计算到“对称轴” 1.0 的距离,最后根据距离,规范到 [0,1] 的范围。
寻址模式的处理,我们放在采样函数里。如代码清单 4.2 所示,我们在采样时调用 AdjustUV 函数,根据不同寻址模式,获取新的 UV 坐标。后续的处理逻辑和之前一致,只不过使用的是新的、调整过后的 UV 坐标。
最后我们实现测试用例,以图 5 和图 6 的样式作为参照进行检查。所以如代码清单 4.3 所示,我们也画三行三列,UV 坐标的范围覆盖到 [0, 3]。
图 7 和 图 8 是运行结果。图 7 是重复模式,结果显然是正确的。图 8 是镜像模式,结合图 6 进行检查,同样也绘制正确。
此节完整代码在 tag/wrap_mode。