图片和纹理

本篇文章介绍图片和纹理的相关内容,分为四个部分。

第一部分介绍如果读取并显示图片,并介绍如何使用 alpha 分量混合图片。

第二部分介绍 UV 坐标,讲解如何将图片纹理映射到我们绘制的三角形上。

第三部分介绍双线性插值,使用它可以更加平滑的映射纹理。

第四部分介绍纹理的寻址模式,讲解两种寻址模式——重复和镜像。

1. 图片显示和颜色混合

这节实验没有“数学”上的概念,都是“工程”上的实现。

因为涉及到颜色混合,需要使用到 alpha 分量。而之前项目里都是使用的 RGB 相关结构来使用的,所以为了方便,首先将工程颜色相关的地方,全部改写成了 RGBA。仅为简单的替换,对应提交 ab4b643

如代码清单 1.1 所示,我们新增加了一个 TImage 类,内部使用 stb 库读取图片。需要注意,如之前所讲,当前 windows 平台底层使用的像素格式是 BGRA,而读取的是 RGBA,所以我们这边交换一下 B 和 R 分量。

代码清单 1.1 使用 stb 读取图片
  1. TImage::TImage(const char* filePath)
  2. {
  3.     m_data = stbi_load(filePath, &m_width, &m_height, &m_channels, 4);
  4.     assert(m_data);
  5.  
  6.     unsigned char tmp;
  7.     for (int i = 0; i < m_width * m_height * 4; i += 4)
  8.     {
  9.         tmp = m_data[i];
  10.         m_data[i] = m_data[i + 2];
  11.         m_data[i + 2] = tmp;
  12.     }
  13. }

实现了图片读取后,我们再实现一个图片绘制接口,用于验证效果。如代码清单 1.2 所示,很简单,对图片的像素依次调用 SetPixel,设置像素值即可。

代码清单 1.2 图片绘制
  1. void TRasterizer::DrawImage(const TImage& image, int startX, int startY)
  2. {
  3.     int x, y;
  4.     BGRA* data = (BGRA*)image.GetData();
  5.  
  6.     for (int h = 0; h < image.GetHeight(); h++)
  7.     {
  8.         y = startY + h;
  9.         for (int w = 0; w < image.GetWidth(); w++)
  10.         {
  11.             x = startX + w;
  12.             SetPixel(x, y, data + h * image.GetWidth() + w);
  13.         }
  14.     }
  15. }

实现好图片绘制之后,我们就可以开始实现颜色混合了。基于 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,所以需要转化一下。

代码清单 1.3 颜色混合
  1. void TRasterizer::BlendPixel(int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
  2. {
  3.     BGRA* dstPixel = reinterpret_cast<BGRA*>(&m_pBits[y * m_width + x]);
  4.  
  5.     float srcAlpha = a / 255.0f;
  6.     float dstAlpha = 1.0f - srcAlpha;
  7.  
  8.     dstPixel->b = (b * srcAlpha + dstPixel->b * dstAlpha);
  9.     dstPixel->g = (g * srcAlpha + dstPixel->g * dstAlpha);
  10.     dstPixel->r = (r * srcAlpha + dstPixel->r * dstAlpha);
  11. }

我们可以定义变量指示是否开启颜色混合,并定义相关设置接口,当前代码中定义了 SetBlend 接口。因为逻辑简单,就不贴出代码赘述了。

我们将混合的操作放在 SetPixel 处。如代码清单 1.4 所示,如果开启了颜色混合,则调用 BlendPixel 函数;否则,就使用原始颜色。

代码清单 1.4 使用颜色混合
  1. void TRasterizer::SetPixel(int x, int y, BGRA* color)
  2. {
  3.     if (x < 0 || x >= m_width || y < 0 || y >= m_height)
  4.         return;
  5.  
  6.     if (m_state.IsBlendEnabled())
  7.         BlendPixel(x, y, color->r, color->g, color->b, color->a);
  8.     else
  9.         m_pBits[y * m_width + x] = *reinterpret_cast<uint32_t*>(color);
  10. }

最后,我们写一个测试用例。如代码清单 1.5 所示,我们加载一张完全不透明的 jpeg 图片,以及一张背景透明的 png 图片。然后依次在不开启混合和开启混合的情况下进行图片绘制。

代码清单 1.5 测试用例
  1. #include "TImageDisplayTask.h"
  2.  
  3. TImageDisplayTask::TImageDisplayTask()
  4.     : m_img("image/dog.jpg"),
  5.       m_image_png("image/ivysaur.png")
  6. {
  7. }
  8.  
  9. void TImageDisplayTask::Render(TRasterizer& rz)
  10. {
  11.     rz.SetBlend(false);
  12.     rz.DrawImage(m_img, 0, 0);
  13.     rz.DrawImage(m_image_png, 0, m_img.GetHeight());
  14.     rz.DrawImage(m_img, 0, m_img.GetHeight() + m_image_png.GetWidth());
  15.     rz.DrawImage(m_image_png, 0, m_img.GetHeight() + m_image_png.GetWidth());
  16.  
  17.     rz.SetBlend(true);
  18.     rz.DrawImage(m_img, m_img.GetWidth(), 0);
  19.     rz.DrawImage(m_image_png, m_image_png.GetWidth(), m_img.GetHeight());
  20.     rz.DrawImage(m_img, m_img.GetWidth(), m_img.GetHeight() + m_image_png.GetWidth());
  21.     rz.DrawImage(m_image_png, m_img.GetWidth(), m_img.GetHeight() + m_image_png.GetWidth());
  22. }

运行结果如图 1 所示,可以看到,第一行不透明的小狗是否开启混合,效果都没有影响。第二行和第三行可以看到,背景透明的妙蛙草,会混合(这边是直接显示)下面一层的颜色。

此节完整代码在 tag/img_blend

图1 运行结果

可以在 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.1 纹理版本的 DrawTriangle
  1. void TRasterizer::DrawTriangle(const tmath::Point2i& p1, const tmath::Point2i& p2, const tmath::Point2i& p3,
  2.     const tmath::UV2f& uv1, const tmath::UV2f& uv2, const tmath::UV2f& uv3)
  3. {
  4.     int minX = std::min(p1.x(), std::min(p2.x(), p3.x()));
  5.     int maxX = std::max(p1.x(), std::max(p2.x(), p3.x()));
  6.     int minY = std::min(p1.y(), std::min(p2.y(), p3.y()));
  7.     int maxY = std::max(p1.y(), std::max(p2.y(), p3.y()));
  8.  
  9.     tmath::Vec2i p, pp1, pp2, pp3;
  10.     int c1, c2, c3;
  11.     tmath::UV2f uv;
  12.     float alpha, beta, gamma;
  13.     float area = (float)std::abs(tmath::cross(p2 - p1, p3 - p1));
  14.  
  15.     for (int i = minX; i <= maxX; i++)
  16.     {
  17.         p.x() = i;
  18.         for (int j = minY; j <= maxY; j++)
  19.         {
  20.             p.y() = j;
  21.  
  22.             pp1.x() = p1.x() - p.x(); pp1.y() = p1.y() - p.y(); // pp1 = p1 - p;
  23.             pp2.x() = p2.x() - p.x(); pp2.y() = p2.y() - p.y(); // pp2 = p2 - p;
  24.             pp3.x() = p3.x() - p.x(); pp3.y() = p3.y() - p.y(); // pp3 = p3 - p;
  25.  
  26.             c1 = tmath::cross(pp1, pp2);
  27.             c2 = tmath::cross(pp2, pp3);
  28.             c3 = tmath::cross(pp3, pp1);
  29.  
  30.             if ((c1 >= 0 && c2 >= 0 && c3 >= 0) ||
  31.                 (c1 <= 0 && c2 <= 0 && c3 <= 0))
  32.             {
  33.                 alpha = std::abs(c2) / area;
  34.                 beta = std::abs(c3) / area;
  35.                 gamma = std::abs(c1) / area;
  36.  
  37.                 uv.u() = uv1.u() * alpha + uv2.u() * beta + uv3.u() * gamma;
  38.                 uv.v() = uv1.v() * alpha + uv2.v() * beta + uv3.v() * gamma;
  39.  
  40.                 SetPixel(i, j, SampleTexture(uv));
  41.             }
  42.         }
  43.     }
  44. }

如代码清单 2.2 所示,采样实现非常容易理解。之前已经说过 UV 坐标对应宽高上的百分比位置,以此,我们就可以得到对应位置的像素值。

代码清单 2.2 采样
  1. BGRA* TRasterizer::SampleTexture(const tmath::UV2f& uv)
  2. {
  3.     const TImage* m_texture = m_state.GetTexture();
  4.  
  5.     int w = m_texture->GetWidth();
  6.     int h = m_texture->GetHeight();
  7.  
  8.     int x = static_cast<int>(uv.u() * w) % (w - 1);
  9.     int y = static_cast<int>(uv.v() * h) % (h - 1);
  10.     BGRA* data = (BGRA*)m_texture->GetData();
  11.     return (data + y * w + x);
  12. }

最后我们实现测试用例。如代码清单 2.3 所示,我们用两个三角形组成一个长方形图片,并设置好对应的 UV 坐标。

代码清单 2.3 测试用例
  1. #include "TTextureUVRenderTask.h"
  2.  
  3. TTextureUVRenderTask::TTextureUVRenderTask(TBasicWindow& win)
  4.     : m_texture("image/dog.jpg")
  5. {
  6.     int maxWidth = win.GetWindowWidth();
  7.     int maxHeight = win.GetWindowHeight();
  8.  
  9.     int textureWidth = m_texture.GetWidth();
  10.     int textureHeight = m_texture.GetHeight();
  11.  
  12.     float scale = std::min(
  13.         maxWidth / (float)textureWidth,
  14.         maxHeight / (float)textureHeight);
  15.     int width = (int)(textureWidth * scale);
  16.     int height = (int)(textureHeight * scale);
  17.  
  18.     p1 = tmath::Point2i(0, 0);
  19.     p2 = tmath::Point2i(width, 0);
  20.     p3 = tmath::Point2i(0, height);
  21.     p4 = tmath::Point2i(width, height);
  22.  
  23.     uv1 = tmath::UV2f(0.0f, 0.0f);
  24.     uv2 = tmath::UV2f(1.0f, 0.0f);
  25.     uv3 = tmath::UV2f(0.0f, 1.0f);
  26.     uv4 = tmath::UV2f(1.0f, 1.0f);
  27. }
  28.  
  29. void TTextureUVRenderTask::Render(TRasterizer& rz)
  30. {
  31.     rz.SetTexture(&m_texture);
  32.  
  33.     rz.DrawTriangle(p1, p2, p3, uv1, uv2, uv3);
  34.     rz.DrawTriangle(p2, p3, p4, uv2, uv3, uv4);
  35. }

运行结果如图 2 所示,尺寸较小的小狗图片被正确的拉伸绘制。

此节完整代码在 tag/uv_mapping

图2 运行结果

3. 双线性插值

上一节中的采样方式其实是最近邻插值,从图 2 中可以看到,小狗的毛发比较像素化,不太平滑。这一节介绍双线性插值,相比最近邻插值,它可以产生更平滑的图像。

双线性插值会考虑目标点周围的四个像素点,并基于它们的距离进行加权平均,来计算目标点的像素值。

具体方式如图 3 所示,中间的灰色点是目标点,四个边角的点就是目标点周围的四个像素点。双线性的意思是,我们做两次线性插值:第一次在横向做一次插值,根据左上和右上求出一个插值点、根据左下和右下求出一个插值点,即图上橘色的点;第二次在纵向上做插值,我们基于第一次橘色的点,计算插值。

图3 双线性插值

代码实现如代码清单 3.1 所示,写的容易理解,就是上述的原理介绍:第一次插值,计算得到 interpTop 和 interpBottom;第二次插值,得到最终像素值。我们这边的复用了之前的 Lerp 函数,注意比例作用的对象。我们只需关注离这个颜色越近,越像这个颜色即可。

代码清单 3.1 双线性插值实现
  1. TRGBA TRasterizer::SampleTextureBilinear(const tmath::UV2f& uv)
  2. {
  3.     const TImage* texture = m_state.GetTexture();
  4.  
  5.     int w = texture->GetWidth();
  6.     int h = texture->GetHeight();
  7.  
  8.     float fx = uv.u() * (w - 1);
  9.     float fy = uv.v() * (h - 1);
  10.  
  11.     int left = (int)floorf(fx) % (w - 1);
  12.     int right = (int)ceilf(fx) % (w - 1);
  13.     int top = (int)floorf(fy) % (h - 1);
  14.     int bottom = (int)ceilf(fy) % (h - 1);
  15.  
  16.     float lerpX = fx - left;
  17.     float lerpY = fy - top;
  18.  
  19.     BGRA* data = (BGRA*)texture->GetData();
  20.     TRGBA topLeft(&data[top * w + left]);
  21.     TRGBA topRight(&data[top * w + right]);
  22.     TRGBA bottomLeft(&data[bottom * w + left]);
  23.     TRGBA bottomRight(&data[bottom * w + right]);
  24.  
  25.     TRGBA interpTop = topLeft.Lerp(topRight, lerpX);
  26.     TRGBA interpBottom = bottomLeft.Lerp(bottomRight, lerpY);
  27.  
  28.     return interpTop.Lerp(interpBottom, lerpY);
  29. }

如代码清单 3.2 所示,我们在 DrawTriangle 中进行采样方式选择,如果是双线性插值,则调用 SampleTextureBilinear;如果是最邻近插值,则调用 SampleTextureNearest。

代码清单 3.2 采样选择
  1. void TRasterizer::DrawTriangle(const tmath::Point2i& p1, const tmath::Point2i& p2, const tmath::Point2i& p3,
  2.     const tmath::UV2f& uv1, const tmath::UV2f& uv2, const tmath::UV2f& uv3)
  3. {
  4.     int minX = std::min(p1.x(), std::min(p2.x(), p3.x()));
  5.     int maxX = std::max(p1.x(), std::max(p2.x(), p3.x()));
  6.     int minY = std::min(p1.y(), std::min(p2.y(), p3.y()));
  7.     int maxY = std::max(p1.y(), std::max(p2.y(), p3.y()));
  8.  
  9.     tmath::Vec2i p, pp1, pp2, pp3;
  10.     int c1, c2, c3;
  11.     tmath::UV2f uv;
  12.     float alpha, beta, gamma;
  13.     float area = (float)std::abs(tmath::cross(p2 - p1, p3 - p1));
  14.  
  15.     for (int i = minX; i <= maxX; i++)
  16.     {
  17.         p.x() = i;
  18.         for (int j = minY; j <= maxY; j++)
  19.         {
  20.             p.y() = j;
  21.  
  22.             pp1.x() = p1.x() - p.x(); pp1.y() = p1.y() - p.y(); // pp1 = p1 - p;
  23.             pp2.x() = p2.x() - p.x(); pp2.y() = p2.y() - p.y(); // pp2 = p2 - p;
  24.             pp3.x() = p3.x() - p.x(); pp3.y() = p3.y() - p.y(); // pp3 = p3 - p;
  25.  
  26.             c1 = tmath::cross(pp1, pp2);
  27.             c2 = tmath::cross(pp2, pp3);
  28.             c3 = tmath::cross(pp3, pp1);
  29.  
  30.             if ((c1 >= 0 && c2 >= 0 && c3 >= 0) ||
  31.                 (c1 <= 0 && c2 <= 0 && c3 <= 0))
  32.             {
  33.                 alpha = std::abs(c2) / area;
  34.                 beta = std::abs(c3) / area;
  35.                 gamma = std::abs(c1) / area;
  36.  
  37.                 uv.u() = uv1.u() * alpha + uv2.u() * beta + uv3.u() * gamma;
  38.                 uv.v() = uv1.v() * alpha + uv2.v() * beta + uv3.v() * gamma;
  39.  
  40.                 switch (m_state.GetSampleMode())
  41.                 {
  42.                 case TSampleMode::Bilinear:
  43.                     SetPixel(i, j, SampleTextureBilinear(uv));
  44.                     break;
  45.  
  46.                 case TSampleMode::Nearest:
  47.                 default:
  48.                     SetPixel(i, j, SampleTextureNearest(uv));
  49.                     break;
  50.                 }
  51.             }
  52.         }
  53.     }
  54. }

最后我们实现测试用例。和上一节实现一样,如代码清单 3.3 所示,不同的是,我们使用 SetSampleMode 设置采样模式为双线性插值。

代码清单 3.3 测试用例
  1. void TBilinearUVRenderTask::Render(TRasterizer& rz)
  2. {
  3.     rz.SetTexture(&m_texture);
  4.     rz.SetSampleMode(TSampleMode::Bilinear);
  5.  
  6.     rz.DrawTriangle(p1, p2, p3, uv1, uv2, uv3);
  7.     rz.DrawTriangle(p2, p3, p4, uv2, uv3, uv4);
  8. }

运行结果如图 4 所示,可以看到小狗脸部的毛发柔顺平滑了许多。

此节完整代码在 tag/bilinear_texture

图4 运行结果

4. 纹理寻址模式

纹理寻址模式是图形渲染中使用的一种技术,它决定了当纹理坐标超出0到1的范围时,如何对纹理进行采样。

我们之前的实现,都是默认重复的寻址模式。如图 5 所示,当纹理坐标超出 0 到 1 的范围时,纹理会在水平和垂直方向上重复。

图5 重复模式

这边我们再实现一种镜像模式。如图 6 所示,在这种模式下,纹理也会重复,但每次重复时会在水平或垂直方向上镜像翻转。

图6 镜像模式

为了之前的处理能复用,我们对传入的 UV 坐标进行统一处理。我们直接看到代码清单 4.1,来讲解如何处理重复和镜像两种模式。

重复模式很简单,就是“取模”的思路。我们先使用 fmodf 取除以 1.0 的余数。因为余数可能是负数,我们加上 1.0,再取余。

镜像模式稍微复杂一点,但是在了解了处理手段后还是很好理解的:重复模式的最小范围在 [0,1],镜像模式的最小范围在 [0,2]。镜像的意思是,到“对称轴” 1.0 距离相同的点,内容是一致的。

所以镜像模式一开始的处理逻辑和重复模式一致,镜像模式把范围取模限定在 [0,2] 范围内。接着计算到“对称轴” 1.0 的距离,最后根据距离,规范到 [0,1] 的范围。

代码清单 4.1 UV 坐标处理
  1. tmath::UV2f TRasterizer::AdjustUV(const tmath::UV2f& uv)
  2. {
  3.     switch (m_state.GetWrapMode())
  4.     {
  5.     case TWrapMode::Mirror:
  6.         return {
  7.             1.0f - fabs(fmodf(fmodf(uv.u(), 2.0f) + 2.0f, 2.0f) - 1.0f),
  8.             1.0f - fabs(fmodf(fmodf(uv.v(), 2.0f) + 2.0f, 2.0f) - 1.0f)
  9.         };
  10.         break;
  11.     case TWrapMode::Repeat:
  12.     default:
  13.         return {
  14.             fmodf(fmodf(uv.u(), 1.0f) + 1.0f, 1.0f),
  15.             fmodf(fmodf(uv.v(), 1.0f) + 1.0f, 1.0f)
  16.         };
  17.         break;
  18.     }
  19. }

寻址模式的处理,我们放在采样函数里。如代码清单 4.2 所示,我们在采样时调用 AdjustUV 函数,根据不同寻址模式,获取新的 UV 坐标。后续的处理逻辑和之前一致,只不过使用的是新的、调整过后的 UV 坐标。

代码清单 4.2 处理寻址模式
  1. BGRA* TRasterizer::SampleTextureNearest(const tmath::UV2f& uv)
  2. {
  3.     tmath::UV2f adjustedUV = AdjustUV(uv);
  4.     const TImage* texture = m_state.GetTexture();
  5.  
  6.     int w = texture->GetWidth();
  7.     int h = texture->GetHeight();
  8.  
  9.     int x = static_cast<int>(adjustedUV.u() * (w - 1));
  10.     int y = static_cast<int>(adjustedUV.v() * (h - 1));
  11.     BGRA* data = (BGRA*)texture->GetData();
  12.     return (data + y * w + x);
  13. }
  14.  
  15. TRGBA TRasterizer::SampleTextureBilinear(const tmath::UV2f& uv)
  16. {
  17.     tmath::UV2f adjustedUV = AdjustUV(uv);
  18.     const TImage* texture = m_state.GetTexture();
  19.  
  20.     int w = texture->GetWidth();
  21.     int h = texture->GetHeight();
  22.  
  23.     float fx = adjustedUV.u() * (w - 1);
  24.     float fy = adjustedUV.v() * (h - 1);
  25.  
  26.     int left = (int)floorf(fx);
  27.     int right = (int)ceilf(fx);
  28.     int top = (int)floorf(fy);
  29.     int bottom = (int)ceilf(fy);
  30.  
  31.     float lerpX = fx - left;
  32.     float lerpY = fy - top;
  33.  
  34.     BGRA* data = (BGRA*)texture->GetData();
  35.     TRGBA topLeft(&data[top * w + left]);
  36.     TRGBA topRight(&data[top * w + right]);
  37.     TRGBA bottomLeft(&data[bottom * w + left]);
  38.     TRGBA bottomRight(&data[bottom * w + right]);
  39.  
  40.     TRGBA interpTop = topLeft.Lerp(topRight, lerpX);
  41.     TRGBA interpBottom = bottomLeft.Lerp(bottomRight, lerpY);
  42.  
  43.     return interpTop.Lerp(interpBottom, lerpY);
  44. }

最后我们实现测试用例,以图 5 和图 6 的样式作为参照进行检查。所以如代码清单 4.3 所示,我们也画三行三列,UV 坐标的范围覆盖到 [0, 3]。

代码清单 4.3 测试用例
  1. #include "TWrapModeRenderTask.h"
  2.  
  3. TWrapModeRenderTask::TWrapModeRenderTask(TWrapMode mode)
  4.     : m_texture("image/ivysaur.png"),
  5.       m_wrapMode(mode)
  6. {
  7. }
  8.  
  9. void TWrapModeRenderTask::Render(TRasterizer& rz)
  10. {
  11.     int width = m_texture.GetWidth();
  12.     int height = m_texture.GetHeight();
  13.  
  14.     rz.SetTexture(&m_texture);
  15.     rz.SetWrapMode(m_wrapMode);
  16.  
  17.     for (int row = 0; row < 3; row++)
  18.     {
  19.         tmath::Point2i p1 = tmath::Point2i(0, height * row);
  20.         tmath::Point2i p2 = tmath::Point2i(width, height * row);
  21.         tmath::Point2i p3 = tmath::Point2i(0, height * (row + 1));
  22.         tmath::Point2i p4 = tmath::Point2i(width, height * (row + 1));
  23.  
  24.         tmath::UV2f uv1 = tmath::UV2f(0.0f, static_cast<float>(row));
  25.         tmath::UV2f uv2 = tmath::UV2f(1.0f, static_cast<float>(row));
  26.         tmath::UV2f uv3 = tmath::UV2f(0.0f, static_cast<float>(row + 1));
  27.         tmath::UV2f uv4 = tmath::UV2f(1.0f, static_cast<float>(row + 1));
  28.  
  29.         for (int i = 0; i < 3; i++)
  30.         {
  31.             rz.DrawTriangle(p1, p2, p3, uv1, uv2, uv3);
  32.             rz.DrawTriangle(p2, p3, p4, uv2, uv3, uv4);
  33.  
  34.             p1.x() += width;
  35.             p2.x() += width;
  36.             p3.x() += width;
  37.             p4.x() += width;
  38.  
  39.             uv1.u() += 1.0f;
  40.             uv2.u() += 1.0f;
  41.             uv3.u() += 1.0f;
  42.             uv4.u() += 1.0f;
  43.         }
  44.     }
  45. }

图 7 和 图 8 是运行结果。图 7 是重复模式,结果显然是正确的。图 8 是镜像模式,结合图 6 进行检查,同样也绘制正确。

此节完整代码在 tag/wrap_mode

图7 重复模式运行结果
图8 镜像模式运行结果