《Fundamentals of Computer Graphics》第十一章 纹理映射
<h1 id="开篇">开篇</h1><p> 当尝试再现现实世界的外观时,你可能会意识到几乎任何表面都有特征。比如树木上的纹路、皮肤上的皱纹、衣物上的编制结构、油画上刷子的痕迹,甚至是光滑的塑料上都有一些凹凸,还有那些光滑的金属也会展现一些机械加工的痕迹。<br>
在计算机图形学中,我们称这些为“空间变化的表面属性”,即不会显著地改变表面的<strong>形状</strong>并在表面上随位置变化的属性。为了实现这些东西,非常多的建模和渲染系统提供了叫做<strong>纹理映射</strong>(<strong>Texture Mapping</strong>)的方法,即使用一张叫<strong>纹理贴图</strong>(<strong>Texture Map</strong>)、<strong>纹理图像</strong>(<strong>Texture Image</strong>)、<strong>纹理</strong>(<strong>Texture</strong>)的图像来存储表面上的细节,接着用数学方法把图像“映射”到表面上。<br>
一旦映射纹理的机制建立,就没有太多明显的方法能被使用来超过引入表面细节的基本目的。纹理可以被用来创造阴影和反射,它还能提供光照,甚至是用来定义表面形状。在复杂的交互式程序中,纹理会被用来存储与图片无关的各种各样的数据。<br>
这章将介绍使用纹理表示表面的细节,还有利用纹理创造阴影和反射。虽然基础的想法很简单,但是在实践中的一些问题会让纹理的使用变复杂。首先,纹理很容易被扭曲,因此设计一个把纹理映射到表面的函数有挑战性。另外,纹理映射是个重采样过程,就像缩放图像那样,通过前一个章节,我们已经知道了重采样很容易引入走样伪影。纹理映射和动画的一起使用已经能产生明显的走样,因此纹理映射系统的大部分复杂性都是那些用来抵抗这些伪影的<strong>抗走样</strong>(<strong>Antialiasing</strong>)措施创造的。</p>
<h1 id="查找纹理的值looking-up-texture-values">查找纹理的值(Looking Up Texture Values)</h1>
<p> 让我们先考虑纹理映射的简单应用,假如要渲染木地板,我们想让地板的漫反射由图像控制。不管是用光线追踪还是光栅化,最终都要知道着色点的颜色是什么。为了知道这个颜色,着色器会进行一次<strong>纹理查找</strong>(<strong>Texture Lookup</strong>),它会找到在纹理坐标系中对应着着色点的位置,接着读取图像在那个位置的颜色,即进行<strong>纹理采样</strong>(<strong>Texture Sample</strong>)。采样出的颜色接着就被用于着色,对于每个能看到地板的像素来说,纹理查找会发生在不同的位置,这样不同颜色的图案就会在着色后的图像中出现。下面给出伪代码</p>
<pre><code class="language-cpp">Color texture_lookup(Texture t, float u, float v)
{
int i = round(u * t.width() - 0.5)
int j = round(v * t.height() -0.5)
return t.get_pixel(i,j)
}
Color shade_surface_point(Surface s, Point p, Texture t)
{
Vector normal = s.get_normal(p)
(u,v) = s.get_texcoord(p)
Color diffuse_color = texture_lookup(u,v)
//使用漫反射颜色和表面法线计算颜色
//返回着色结果
}
</code></pre>
<p> 在上面的代码中,着色器会询问表面应该查找纹理的哪个位置,每个要使用纹理进行着色的表面都要能提供查找位置。这就是纹理映射的入手点,对于每个像素来说,我们需要一个能被轻易计算的把表面映射到纹理上的函数,这就是<strong>纹理坐标函数</strong>(<strong>Texture Coordinate Function</strong>)。下方是一张示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250824191635152-813095420.png"></p>
<p>上图展示的就是使用纹理坐标函数<span class="math inline">\(\phi\)</span>把球面上的坐标<span class="math inline">\(S\)</span>映射到了纹理上的坐标<span class="math inline">\(T\)</span></p>
<p></p><div class="math display">\[\begin{align*}
\phi &: S \rightarrow T \\
&:(x,y,z) \mapsto (u,v)
\end{align*}
\]</div><p></p><p>纹理坐标<span class="math inline">\(T\)</span>的集合通常被称作“纹理空间”,它通常只是包含着图像的矩形,而矩形的大小一般都是单位正方形<span class="math inline">\((u,v) \in ^2\)</span>。纹理映射在很多方面和第八章的观察投影很像,我们称观察投影为<span class="math inline">\(\pi\)</span>,它会把表面上的点映射到图像上的点,这两者都是三维到二维的映射,而且两者都是被用于渲染的。一个是为了知道从图像的哪个位置获得值,另一个是为了知道该在图像的哪个位置放置着色结果。但是也有区别,<span class="math inline">\(\pi\)</span>几乎总是透视或正交投影,然而<span class="math inline">\(\phi\)</span>有很多种形式。对于一张图像来说只有一种投影方式,但是每个物体可能有完全不相关的纹理坐标函数。<br>
对于木地板的情况,如果地板的每个位置都有常量<span class="math inline">\(z\)</span>坐标,且地板与<span class="math inline">\(x\)</span>和<span class="math inline">\(y\)</span>轴对齐,那么我们就能使用如下的映射</p>
<p></p><div class="math display">\[u=ax; \quad v=by,
\]</div><p></p><p>选择合适的缩放因子,我们就能把<span class="math inline">\((x,y,z)_\mathrm{floor}\)</span>映射到<span class="math inline">\((u,v)\)</span>,接着再取纹理在<span class="math inline">\((u,v)\)</span>坐标附*的纹理像素值或<strong>纹素</strong>(<strong>Texel</strong>),这样我们就能像下图那样渲染出图像。<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250824200319521-1375422012.png"></p>
<p> 不过像这样映射坐标会带来很大的限制,如果我们把地板旋转个角度,或是换个地方把木纹理映射到椅子上呢?因此我们需要更好的方法来为表面上的点计算纹理坐标。<br>
当以接**视的视角把一张高对比纹理渲染到低分辨率图像时,这种简单的纹理映射方法会发生另一种问题。下方是一个示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250824201703876-979242821.png"></p>
<p>由上图可见,在远处有显眼的走样伪影,和没有使用合适的滤波器重采样图像时出现的问题相似。尽管这是一种极端情况,但是在动画中这些图案会四处移动,甚至当它们很小的时候也很让人分心。<br>
现在我们知道了基础的纹理映射会遇到的两个主要问题:</p>
<ul>
<li>
<p>定义纹理坐标函数</p>
</li>
<li>
<p>在不引入太多走样的情况下查找纹理的值</p>
</li>
</ul>
<p>这两个要关心的,是所有的纹理映射的应用的基础,会在后面的部分讨论。一旦理解了它们和一些解决它们的方法,那么就会理解纹理映射。剩下的只是如何为不同用途应用基本的纹理机制,当然了这个也会在后面进行讨论。</p>
<h1 id="纹理坐标函数texture-coordinate-functions">纹理坐标函数(Texture Coordinate Functions)</h1>
<p> 为了让纹理映射有好的结果,把纹理坐标函数<span class="math inline">\(\phi\)</span>设计好是关键。可以把它想象成,如何变形一个*的矩形图像,把它贴到想绘制的三维表面上。或者反过来,可以想象温柔地让表面变*整,最后让表面无褶皱、撕裂、折叠并*躺在图像上。有些时候做到这些很简单,比如有可能三维表面已经是*整的矩形了。在别的情况下则会非常棘手,三维形状可能非常复杂,比如角色身体的表面。<br>
定义纹理坐标函数的问题对于计算机图形学来说不是特别新鲜,制图师在设计覆盖地球表面一大片区域的地图时也遇到了相同的问题,把弯曲的球面映射到*整的地图上会不可避免地导致区域、角度、距离的扭曲,这会让地图有误导性。数世纪一来有很多地图映射方法被提出,用来最小化各种各样的覆盖一大片区域的扭曲,这也是纹理映射要面对的问题。<br>
在一些应用中,有明确的原因使用特定的映射。但是在大多数情况下,设计纹理坐标映射是一项微妙的任务,因为要*衡相冲突的需求,这也是有经验的建模师的努力花费的地方。<br>
你可以以任意方式定义<span class="math inline">\(\phi\)</span>,但是有些相互冲突的目标要考虑</p>
<ul>
<li>
<p><strong>双射性</strong>:在大多数情况下,<span class="math inline">\(\phi\)</span>应该是双射的,在表面上的每个点要被映射到纹理空间的不同位置。在某些情况下你可能想让纹理在表面上重复,表面上的一些点会被映射到相同的纹理空间中的位置,这种故意引入多对一的映射是合理的,但是这种情况不能意外地发生。</p>
</li>
<li>
<p><strong>尺寸扭曲</strong>:纹理在表面上的尺度应该保持不变,也就是说那些在表面上距离相同的点被映射到纹理空间中应该隔着相同的一段距离。这就要求<span class="math inline">\(\phi\)</span>的导数的大小不应该变化太大。</p>
</li>
<li>
<p><strong>形状扭曲</strong>:纹理应该不能被很大地扭曲,在表面上画的圆被映射到纹理空间后,看起来应该还得像个圆,而不是那种被极度压扁或拉长的形状。这就要求<span class="math inline">\(\phi\)</span>的导数在不同的方向上的值应该都差不多。</p>
</li>
<li>
<p><strong>连续性</strong>:应该不能有太多的缝隙,在表面上相邻的点被映射到纹理空间应该也是相邻的。也就是说<span class="math inline">\(\phi\)</span>应该尽可能地连续,不过在大多数情况下有一些不连续是无法避免的,我们应该把不连续的地方放在不显眼的位置。</p>
</li>
</ul>
<p> 使用参数方程定义的表面,可以通过对定义表面的函数求反来得到纹理坐标函数,然后使用表面的两个参数作为纹理坐标。取决于表面,这些纹理坐标可能有或没有期望的属性,不过它们确实提供了一种映射。<br>
对于那些隐式定义的表面,或是那些通过一个三角形网格定义的表面,我们需要其它的方法来定义纹理坐标,而不是依赖现成的参数化方法。一般来说,定义纹理坐标的两个方法都是从几何上计算它们,从表面点的空间坐标得到,或是对于网格表面来说,通过顶点存储的纹理坐标插值得到它们。接下来让我们一个一个看。</p>
<h2 id="从几何确定坐标geometrically-determined-coordinates">从几何确定坐标(Geometrically Determined Coordinates)</h2>
<p> 从几何确定纹理坐标是被用于那些简单的形状或是特殊情况下的,作为一个快速的方案或是设计手工修改后的纹理坐标映射的起点。接下来将使用下方的测试图像展示不同的纹理坐标函数,通过图中数字我们可以知道*似的<span class="math inline">\((u,v)\)</span>坐标,此外通过黑线网格我们还能知道映射是如何扭曲测试图像的。<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250825225123035-265723223.png"></p>
<h3 id="*面投影planar-projection">*面投影(Planar Projection)</h3>
<p> 也许最简单的三维到二维映射是*行投影,和之前章节提到的正交视图所用到的一样,我们之前为它开发的机制能被直接重新利用来定义纹理坐标,通过简单地与矩阵相乘接着抛弃掉变换后的<span class="math inline">\(z\)</span>分量,我们就能得到纹理坐标</p>
<p></p><div class="math display">\[\phi(x,y,z) = (u,v)
\]</div><p></p><p></p><div class="math display">\[\begin{bmatrix} u \\ v \\ * \\ 1 \end{bmatrix} = M_t \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix}
\]</div><p></p><p>上方的公式中<span class="math inline">\(M_t\)</span>表示的是一次仿射变换。<br>
对于那些几乎*整的表面来说,这个方法很有用,而且通过*均法线可以找到一个很好的投影方向。对于任意封闭的形状,*面投影就不会有单射性,正面和背面的点会被映射到纹理空间中相同的位置,详见下图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250825234146480-237375322.png"></p>
<p>把矩阵替换成透视投影矩阵,我们能得到投影的纹理坐标</p>
<p></p><div class="math display">\[\phi(x,y,z) = (\tilde{u}/w,\tilde{v}/w)
\]</div><p></p><p></p><div class="math display">\[\begin{bmatrix} \tilde{u} \\ \tilde{v} \\ * \\ w \end{bmatrix} = P_t \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix}
\]</div><p></p><p>投影的纹理坐标对于后面要讨论的阴影映射的技术来说非常重要。</p>
<h3 id="球面坐标spherical-coordinate">球面坐标(Spherical Coordinate)</h3>
<p> 对于球面来说,经纬度参数化是被广泛使用的。它在两极附*有很多扭曲,这会导致一些困难,不过它确实覆盖了整个球面,而且只在一条经度线上不连续。那些可以被大致认为是球面的表面可以使用一个把表面上的点投影到球面上的点的纹理坐标函数。首先连接球心与表面上的点得到一条线段,接着求线段与球面的交点,交点的球面坐标就是纹理坐标。因此有</p>
<p></p><div class="math display">\[\phi(x,y,z) = ([\pi + \mathrm{atan2}{(y,z)}]/2\pi,[\pi-\mathrm{acos}(z/||x||)]/\pi)
\]</div><p></p><p> 如果能从球心看到整个表面,那么除两极外的球面坐标的映射都有双射性,它会继承相同的在两极附*的扭曲。下方是一张示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250826005526211-77221643.png"></p>
<h3 id="柱面坐标cylindrical-coordinate">柱面坐标(Cylindrical Coordinate)</h3>
<p> 对于那些更像更像圆柱的物体,从一条轴向外投影到圆柱面上可能更有用。下面分别给出公式和一张示例图</p>
<p></p><div class="math display">\[\phi(x,y,z) = (\frac{1}{2\pi}[\pi+\mathrm{atan2}(y,x)]/2\pi,\frac{1}{2})
\]</div><p></p><p><img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250826010435214-1649867907.png"></p>
<h3 id="立方体贴图cubemaps">立方体贴图(Cubemaps)</h3>
<p> 使用球面坐标来参数化球面或是那些像球的表面会导致两极附*的高度扭曲,这通常会导致明显的伪影,揭示出纹理在两个特殊的点出了问题。有一个受欢迎的替代方案以更多不连续的代价换来了更多的均匀性,它的想法是投影到立方体表面上而不是球面,为立方体的六个面使用六张分开的纹理。六个方形纹理的集合被称作<strong>立方体贴图</strong>(<strong>Cubemap</strong>)。在立方体的边缘上会有不连续,但是能保持形状和面积的低扭曲。<br>
计算立方体贴图的纹理坐标比球面坐标要快速,因为投影到*面只需要一次除法,例如一个点投影到<span class="math inline">\(+z\)</span>面只需要</p>
<p></p><div class="math display">\[(x,y,z) \mapsto (\frac{x}{z},\frac{y}{z})
\]</div><p></p><p>一个令人困惑的地方是<span class="math inline">\(u\)</span>和<span class="math inline">\(v\)</span>的方向是如何在六个面上定义的。其实使用任意约定都可以,不过采用的约定会影响纹理的内容,因此标准化很重要。因为那些使用立方体贴图的纹理通常用于从内部往外看,通常的约定是从立方体内部观察时,<span class="math inline">\(u\)</span>轴相对于<span class="math inline">\(v\)</span>轴是顺时针方向。OpenGL使用的约定如下,下标表示的是映射到哪一个面。</p>
<p></p><div class="math display">\[\begin{align*}
\phi_{-x}(x,y,z)&=\frac{1}{2}, \\
\phi_{+x}(x,y,z)&=\frac{1}{2}, \\
\phi_{-y}(x,y,z)&=\frac{1}{2}, \\
\phi_{+y}(x,y,z)&=\frac{1}{2}, \\
\phi_{-z}(x,y,z)&=\frac{1}{2}, \\
\phi_{+z}(x,y,z)&=\frac{1}{2},
\end{align*}
\]</div><p></p><p> 一个使用立方体贴图的纹理有六个正方形片段,如下图所示<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250826022722408-1303542680.png"></p>
<p>这些片段通常都是被打包在一个图像中存储的,就好像立方体被展开了一样。</p>
<h2 id="插值的纹理坐标interpolated-texture-coordinate">插值的纹理坐标(Interpolated Texture Coordinate)</h2>
<p> 为了更加精细地控制三角形网格表面的纹理坐标函数,可以显式地在每个顶点上存储纹理坐标,然后在三角形上使用重心插值。让我们来看个只有一个三角形的例子<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250826025150835-270712563.png"></p>
<p>上图展示了使用插值后的纹理坐标采样图像,从而把纹理映射到三角形上。下图展示了一种常用的方法,利用顶点的纹理坐标在纹理空间中绘制三角形,可视化整个网格的纹理坐标。<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250826042929992-763898557.png"></p>
<p>使用这种可视化方法可以知道纹理的什么部分被哪个三角形使用,而且它也是很便捷的工具用来评估纹理坐标,此外还能用来调试各种各样的纹理映射代码。<br>
由顶点的纹理坐标定义的纹理坐标的质量取决于顶点被给予的坐标,也就是网格是如何在纹理空间中布局的。不管给予了什么坐标,只要在网格上的三角形共享顶点,那么纹理坐标的映射总是连续的。但是之前描述的其它的期望的属性就不一定会有了。单射性意味着三角形不会在纹理空间中重叠,如果发生了重叠那么纹理中的一些点会出现在表面上的多个位置。<br>
当三角形在纹理空间中的面积与其在三维空间中的面积成比时,尺寸扭曲会很低。比如,角色的脸部通常会使用连续的纹理坐标函数映射,结果通常导致鼻子在纹理空间中被挤进了一个相对小的区域,就如下图所示<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250826203605184-774952234.png"></p>
<p>尽管在鼻子上的三角形比在脸颊上的小,但是它的尺寸比例在纹理空间上更极端。这会导致纹理在鼻子上被放大,因为在纹理空间上小的区域覆盖了表面上的一大块区域。相似的,对比额头和太阳穴上的三角形,这两个地方的三角形在三维中有相似的面积,但是在太阳穴附*的三角形在纹理空间中更大,导致纹理要被缩小。<br>
相似的,当三角形在纹理空间中的形状和在三维空间中的形状相似时,形状扭曲很低。刚才提供的脸部的例子就有相对低的形状扭曲,但是在下面的例子中,球面的两极附*有很大的形状扭曲。<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250826205736776-1404124497.png"></p>
<h2 id="*铺环绕模式和纹理变换tiling-wrapping-modes-and-texture-transformations">*铺,环绕模式,和纹理变换(Tiling, Wrapping Modes, and Texture Transformations)</h2>
<p> 允许纹理坐标超出纹理图像的边界通常是有用的,有时纹理坐标计算的舍入误差会导致顶点正好略微在纹理边界外,纹理映射机制应该也要处理这种情况。<br>
如果纹理只被用来覆盖表面的一部分,但是纹理坐标已经被设置为要映射整个表面到单位正方形,一个选项是准备一张绝大部分都是空白只在一片小的区域有内容的纹理图像。但是这样的图像需要非常高的分辨率才能让相关的区域有足够的细节。另一个替代方法是缩放所有的纹理坐标,来覆盖一大片区域。<br>
像上述这种情况,单位正方形区域外的纹理查找应该返回一个常量背景颜色。一个方法是设置一个背景颜色,当纹理查找在单位正方形外时就返回这个背景颜色。另一个方法是延伸边界,当超出边界时返回在边缘上离采样点最*的点的颜色,这可以通过把<span class="math inline">\(uv\)</span>坐标<strong>钳制</strong>(<strong>Clamping</strong>)到图像中第一个像素到最后一个像素的范围内做到。<br>
有时我们也想重复图案,就像棋盘那样。如果图案在矩形网格上重复,创建一张有重复图案的图像是很浪费的,我们其实可以使用环绕索引来解决来解决这一问题。打个比方来说,当超出右边界时,我们将查找点环绕至左边界。使用整数求余操作可以很简单地解决这个问题,下面分别提供实现环绕方式和钳制方式的伪代码.</p>
<pre><code class="language-cpp">Color texture_lookup_wrap(Texture t, float u, float v)
{
int i = round(u * t.width() - 0.5)
int j = round(v * t.height() - 0.5)
return t.get_pixel(i % t.width(), j % t.height())
}
Color texture_lookup_clamp(Texture t, float u, float v)
{
int i = round(u * t.width() - 0.5)
int j = round(v * t.height() - 0.5)
return t.get_pixel(max(0, min(i, t.width()-1)), max(0,min(j, t.height()-1)))
}
</code></pre>
<p>处理越界查找的方法一般都是通过从一个列表中挑选<strong>环绕模式</strong>(<strong>Wrapping Mode</strong>)做到的,通常有*铺、钳制和两者的组合或变种可选。使用环绕模式,我们能把纹理当作一个能为无限的二维*面上的任意一点返回一个颜色的函数。当我们使用一张图像来定义纹理时,这些模式就描述了有限的图像数据是如何被用来定义这个函数的。在后面的部分我们会看到过程式纹理可以自然地在一个无限的*面上延伸,因为它们不被有限的图像数据所限制。<br>
当调整纹理的尺度和位置时,可以通过对纹理坐标施加矩阵变换,从而避免修改生成纹理坐标的函数或存储在网格上的顶点的纹理坐标值</p>
<p></p><div class="math display">\[\phi(\mathbf{x}) = \mathbf{M}_T \phi_\mathrm{model}(\mathbf{x})
\]</div><p></p><p><span class="math inline">\(\phi_\mathrm{model}\)</span>是模型提供的纹理坐标函数,<span class="math inline">\(\mathbf{M}_T\)</span>是一个3x3的矩阵,它会对二维纹理坐标进行仿射变换或投影变换。这样的一个变换,虽然有时被限制到了缩放或*移,但是被大多数使用纹理映射的渲染器支持。</p>
<h2 id="连续与缝隙continuity-and-seams">连续与缝隙(Continuity and Seams)</h2>
<p> 尽管低扭曲和连续对于纹理坐标函数是非常好的属性,但是不连续有时是无法避免的。对于任何封闭的三维表面,拓扑的基本结果是没有连续的双射函数能把整个表面映射到纹理图像上。因此在某些地方必须做出让步,通过在表面上纹理坐标突然改变的地方引入缝隙,我们能让其它地方有低扭曲。之前讨论的一些从几何确定的映射已经包含了缝隙,在球面和柱面坐标中,缝隙在通过<span class="math inline">\(\mathrm{atan2}\)</span>计算的角度从<span class="math inline">\(\pi\)</span>环绕到<span class="math inline">\(-\pi\)</span>的地方,而在立方体贴图中,缝隙在立方体的边缘,当映射在六个方形纹理中进行切换的时候就会有。<br>
当使用插值后的纹理坐标时,缝隙需要特殊考虑,因为它们不会自然地发生。正如之前提到的那样,在有着共享顶点的网格上,纹理坐标会自动地连续。但是当三角形跨过一条缝隙时,就会引发问题,就比如下图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250827040559087-1436440318.png"></p>
<p>这里稍微观察下就会发现问题,在上图用红线标出的这种三角形中,它们的三个顶点虽然在三维空间中的位置相隔较*,但是由于使用的是共享顶点,导致它们中的某几个顶点存储的纹理坐标相隔非常远,这就造成了横跨整个纹理的采样!为了修正这一错误,我们可以让顶点在缝隙处重复,但是赋予不同的纹理坐标,如下图所示<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250827044609442-459416231.png"></p>
<p>对于上图红三角形来说,我们给它分配两个新的复制后的顶点,但是把复制后的顶点的纹理坐标放到纹理空间左侧,这样就能得到正确的渲染结果。</p>
<h2 id="在渲染系统中的纹理坐标texture-coordinates-in-rendering-systems">在渲染系统中的纹理坐标(Texture coordinates in rendering systems)</h2>
<p> 纹理被用于各种各样的渲染系统中,尽管基本原理是一样的,但是对于光线追踪和光栅化系统的细节是不同的。纹理坐标是被渲染的模型的一部分,场景的描述需要包含足够的信息来定义它们。在大多数情况下,这意味着让纹理坐标作为顶点的属性。如果渲染系统直接支持几何图元而不是网格,这些图元通常有预定义的纹理坐标,而且可能会为每个图元类型选择一个映射方案。<br>
在光线追踪渲染器中,支持与光线相交的任何类型的表面必须不止能给出交点的位置和法线,还要能给出交点的纹理坐标。像其它的关于交点的信息一样,纹理坐标必须被存储在一次命中记录(<code>HitRecord</code>)中。在通常的由三角形网格表示的几何体的情况下,光线与三角形交汇的代码将会使用重心坐标插值顶点存储的纹理坐标,对于其它类型的几何体,交汇代码必须直接计算纹理坐标。<br>
在基于光栅化的系统中,三角形通常是唯一被支持的几何体,因此所有表面都得被转化成这一形式存在。一般情况下纹理坐标会与模型一起被读取,或者对于那些使用代码生成的三角形网格来说,当网格被创建时纹理坐标也会在同时一时间被计算和存储。另外,纹理坐标也可以通过其它的顶点数据计算,比如一开始提到的那个木地板的情况,此外纹理坐标也可以在顶点着色器计算,被传递给光栅器,这样每个片段能有合适的纹理坐标。</p>
<h1 id="抗走样的纹理查找antialiasing-texture-lookups">抗走样的纹理查找(Antialiasing Texture Lookups)</h1>
<p> 纹理映射的第二个基本的问题是抗走样。渲染一张使用纹理映射的图像是一个采样过程,即把纹理映射到表面,接着把表面投影到图像上,产生图像*面上的二维函数,最后以像素为单位进行采样。就如第十章看到的那样,当图像包含许多细节或锐利的边缘时,使用点采样会产生走样伪影。<br>
和第九章讲过的直线或三角形的抗走样光栅化,还有第十章讲过的降采样图像一样,解决方法是让每个像素不是点采样,而是图像中部分区域内的*均。使用同样的超采样方法,只要有足够的样本,在不改变纹理映射机制的情况下就能获得非常好的结果。像素区域内的许多样本对应着查找纹理贴图的不同位置,使用不同位置的纹理查找结果进行着色计算,接着再求着色结果的*均值就能精确地接*图像上像素范围内的*均颜色。然而细节丰富的纹理通常需要非常多的样本来获得好的结果,而且要花费很多时间。因此<strong>有效率地</strong>计算区域内的*均值,是纹理抗走样的第一个关键问题。<br>
纹理图像通常是用光栅图像定义的,因此还有个重建的问题要考虑。和升采样图像一样,解决方法是使用重建滤波器在纹素间进行插值。接下来让我们逐个讨论这些问题。</p>
<h2 id="像素的覆盖范围the-footprint-of-a-pixel">像素的覆盖范围(The Footprint of a Pixel)</h2>
<p> 对于其他类型的抗走样来说,让抗走样纹理更困难的原因是被渲染的图像和纹理的关系会持续变化。每个像素值应该是图像中像素范围内的*均颜色,最常见的情况是像素看着单独一个表面,这就变成了求表面上一定区域内的*均。如果表面的颜色来自一个纹理,那么又变成了求纹理中对应区域内的*均,这个对应区域被称为像素在<strong>纹理空间的覆盖范围</strong>(<strong>Texture Space Footprint</strong>),下图展示了图像空间中一些不同位置的正方形区域映射到纹理空间中的大小和形状是怎样的<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250827233227768-1582480200.png"></p>
<p> 回顾使用纹理进行渲染涉及到的三个空间,投影<span class="math inline">\(\pi\)</span>映射三维点到图像,纹理坐标函数<span class="math inline">\(\phi\)</span>映射三维点到纹理空间。为了让这些和像素覆盖范围配合起来,我们需要理解这两种映射的组合,首先使用<span class="math inline">\(\pi^{-1}\)</span>从图像到表面,接着使用<span class="math inline">\(\phi\)</span>从表面到纹理空间,因此这个组合<span class="math inline">\(\psi = \phi \circ \pi^{-1}\)</span>就决定了像素的覆盖范围。<br>
纹理抗走样的核心问题是在纹理上的像素覆盖范围内求*均值,要精确地做到会变得很复杂。对于那些在远处并且有着复杂表面形状的物体来说,像素在纹理空间的覆盖范围会很大而且形状也会很复杂,甚至会有一些不连续的区域。但是在典型情况下,一个像素会对应着表面的一部分光滑区域,被映射到纹理空间中单独的一个区域。<br>
因为<span class="math inline">\(\psi\)</span>包含从图像到表面的映射和从表面到纹理的映射,覆盖范围的大小和形状取决于如何观察和纹理坐标函数。当一个表面接*相机时,像素的覆盖范围会变小,反之则会变大。当以*视的视角观察表面时,像素的覆盖范围会变得细长。甚至是固定视角观察时,纹理坐标函数也会导致覆盖范围变化。<br>
为了找到一个有效率的计算抗走样查找的算法,必须使用相当多的*似。当一个函数足够*滑的时候,线性*似通常是有用的。在纹理抗走样的情况下,这意味着从图像空间到纹理空间的<span class="math inline">\(\psi\)</span>映射是从二维到二维的线性映射,用公式表达为</p>
<p></p><div class="math display">\[\psi(\mathbf{x}) = \psi(\mathbf{x}_0) + \mathbf{J}(\mathbf{x}-\mathbf{x}_0)
\]</div><p></p><p>公式中的2x2矩阵<span class="math inline">\(\mathbf{J}\)</span>是<span class="math inline">\(\psi\)</span>导数的*似,它有四个元素,如果我们令图像空间的位置为<span class="math inline">\(\mathbf{x}=(x,y)\)</span>,它在纹理空间中的位置为<span class="math inline">\(\mathbf{u}=(u,v)\)</span>那么有</p>
<p></p><div class="math display">\[\mathbf{J} = \begin{bmatrix} \frac{du}{dx} & \frac{du}{dy} \\ \frac{dv}{dx} & \frac{dv}{dy} \end{bmatrix}
\]</div><p></p><p> 一个几何层面的理解是图像中的一个中心在<span class="math inline">\(\mathbf{x}\)</span>的单位大小的方形像素区域,会被*似映射到纹理空间中的一个中心在<span class="math inline">\(\psi(\mathbf{x})\)</span>处的*行四边形,而且它的边会与向量<span class="math inline">\(\mathbf{u}_x=(du/dx,dv/dx)\)</span>和向量<span class="math inline">\(\mathbf{u}_y=(du/dy,dv/dy)\)</span>对齐。下方是一张形象的示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250828023225332-584970481.png"></p>
<p> 导数矩阵<span class="math inline">\(\mathbf{J}\)</span>非常有用,因为它告诉了图像上纹理空间的覆盖范围的变化情况。数值更大的导数意味着在纹理空间中的覆盖范围更大,导数向量<span class="math inline">\(\mathbf{u}_x\)</span>和<span class="math inline">\(\mathbf{u}_y\)</span>的关系告诉了我们形状是怎样的,当它们变得倾斜或是长度区别很大时,覆盖范围就会变得细长。<br>
通过上面的*似,我们现在可以认为一次在图像空间中特定位置的滤波的纹理采样结果,应该是纹理贴图上的*行四边形形状的覆盖范围内的*均值。这个覆盖范围应该由纹理坐标在那个点的导数决定,虽然我们在这里已经做了从图像到纹理的映射是*滑的假设,但是其实已经足够精确,能让渲染出的图像有非常好的质量。然而求*行四边形区域内的*均的开销已经是非常昂贵的了,因此又需要各种*似。不同的实现纹理抗走样的方法在速度和质量上做了权衡。接下来的部分将讨论这些东西。</p>
<h2 id="重建reconstruction">重建(Reconstruction)</h2>
<p> 当覆盖范围比纹素还小时,这意味着我们在放大图像。这个情况就好像升采样一张图像一样,最主要的考虑就是在纹素之间进行插值来产生一张光滑的图像。和升采样一样,这个光滑过程是由重建滤波器定义的。<br>
和图像重采样的考虑基本上差不多,但是有一个重要的区别,因为采样会在随机的任意位置进行,我们不能使用应用于可分离滤波器的优化方法,而这意味着大的高质量重建滤波器的开销将会变得非常昂贵。正因为这样,用于纹理的最高质量的滤波器通常是双线性插值。下方为伪代码</p>
<pre><code class="language-cpp">Color tex_sample_bilinear(Texture t, float u, float v)
{
u_p = u * t.width - 0.5
v_p = v * t.height - 0.5
iu0 = floor(u_p); iu1 = iu0 + 1
iv0 = floor(v_p); iv1 = iv0 + 1
a_u = (iu1 - u_p); b_u = 1 - a_u
a_v = (iv1 - v_p); b_v = 1 - a_v
return a_u * a_v * t + a_u * b_v * t +
b_u * a_v * t + b_u * b_v * t
}
</code></pre>
<p> 在许多系统中,这个操作是一个很重要的性能瓶颈,因为它涉及四次纹素值获取,而且采样点的分布也是不规则的。但是由于相*的图像点通常也会被映射到相*的纹理坐标,这意味着可能读取相同的纹素。因为这个原因,很多高性能系统都有特殊的硬件致力于纹理采样,用于处理插值还有管理最*使用的纹理数据的缓存,来最小化从内存存储纹理数据的地方进行缓慢读取的次数。</p>
<h2 id="mipmapping">Mipmapping</h2>
<p> 很好地进行插值只满足了纹理在放大情况下的应用,当像素覆盖了很多纹素时,好的抗走样就需要计算许多纹素的*均来*滑信号,从而让信号能被安全地采样。<br>
一个非常精确的方法是计算纹理上覆盖范围内的*均值,但是当覆盖范围过大时开销会非常昂贵。因此有个更好的方法,我们可以预计算并且存储纹理在不同地方一定范围内的*均值。<br>
实现这个想法的一个非常受欢迎的版本被称为“MIP mapping”或只是mipmapping。一个mipmap是一序列纹理,这些纹理表示的都是相同的图像,只不过分辨率会越来越低。原始的完整分辨率纹理图像叫<strong>基层</strong>(<strong>Base Level</strong>)或者0层,1层是通过降采样0层到一个每个维度只有0层<span class="math inline">\(\frac{1}{2}\)</span>的纹理做到的。这个过程可以一直继续,就像下图这样<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250828032037044-1674929655.png"></p>
<p>通常mipmap层级也可以有非常多,比如从1024x1024的纹理图像开始,我们最多可以有11个层级,从1024x1024到512x512一直到最后一层就只有一个纹素。这种使用越来越低的采样率表示相同图像的结构被称为<strong>图像金字塔</strong>(<strong>Image Pyramid</strong>),基于从大到小堆叠图像的视觉隐喻。</p>
<h2 id="使用mipmaps的基本纹理滤波basic-texture-filtering-with-mipmaps">使用Mipmaps的基本纹理滤波(Basic Texture Filtering with Mipmaps)</h2>
<p> 使用mipmap或者图像金字塔,纹理滤波可以被更加有效率地完成,而不是单独地读取许多纹素。当我们需要大范围内的*均值时,我们可以直接读取mimap中更高层的值。当然了像素的覆盖范围的形状可能与正方形相差很大,如果不做处理可以预见的是会产生一些伪影。让我们先不管当像素的覆盖范围的形状比较细长的时候的处理,假设覆盖范围是个方形且在完整尺寸的纹理中测量的宽度为<span class="math inline">\(D\)</span>,设要采样的层级为<span class="math inline">\(k\)</span>,我们可以得到</p>
<p></p><div class="math display">\[2^k \approx D
\]</div><p></p><p>因此我们让<span class="math inline">\(k = \mathrm{log}_2D\)</span>,不过这会给我们一个非整数<span class="math inline">\(k\)</span>值,两个可能的解决方案是采样最*的一层或是在这两层之间进行线性插值。在给出代码时,我们得想想当覆盖范围不是正方形的时候,我们得决定如何选择宽度<span class="math inline">\(D\)</span>,一个最简单的方法是选择最长的边</p>
<p></p><div class="math display">\[D = \mathrm{max}\{||\mathbf{u}_x,||\mathbf{u}_y||\}
\]</div><p></p><pre><code class="language-cpp">Color mipmap_sample_trilinear(Texture mip[], float u, float v, matrix J)
{
D = max_column_norm(J)
k = log2(D)
k0 = floor(k); k1 = k0 + 1
a = k1 - k; b = 1 - a
c0 = tex_sample_bilinear(mip, u, v)
c1 = tex_sample_bilinear(mip, u, v)
return a * c0 + b * c1
}
</code></pre>
<p>基础的mipmapping在移除走样上做得很好,但是无法解决细长的或<strong>各向异性</strong>(<strong>Anisotropic</strong>)的像素覆盖范围,比如当表面以*视视角观察时表现不好。当观察者站在地板上*视观察时,那些在地板上离观察者很远的点,会有非常各向异性的覆盖范围,如果仍然选择在更高层级进行采样,最终的图像则会在水*方向上显得模糊。</p>
<h2 id="各向异性的滤波anisotropic-filtering">各向异性的滤波(Anisotropic Filtering)</h2>
<p> 一个mipmap可以被多次查找来接*细长的覆盖范围,有个想法是基于<strong>最短轴</strong>来挑选mipmap层级,然后在长轴方向上进行多次查找求均值。下方是一张示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250828042406718-1664882351.png"></p>
<h1 id="纹理映射的应用applications-of-texture-mapping">纹理映射的应用(Applications of Texture Mapping)</h1>
<p> 纹理映射的机制有许多的应用,在这个部分我们来看看一些重要的纹理映射技术。</p>
<h2 id="控制着色参数controlling-shading-parameters">控制着色参数(Controlling Shading Parameters)</h2>
<p> 纹理映射最基础的应用就是用来引入用于着色计算的漫反射颜色,当然了不只是漫反射颜色,镜面反射率或镜面粗糙度也可以被存储在纹理中,只要有插值后的纹理坐标,使用它采样着色计算所需要的纹理,我们就能让物体的表面有各种各样的细节。</p>
<h2 id="法线贴图和凹凸贴图normal-maps-and-bump-maps">法线贴图和凹凸贴图(Normal Maps and Bump Maps)</h2>
<p> 另一个对于着色重要的是表面法线,使用叫<strong>法线映射</strong>(<strong>Normal Mapping</strong>)的技术,我们能从法线贴图读取值来调整顶点存储的表面法线。背后的原理其实很简单,除了表面法线normal外我们还需要切线tangent和副切线bitangent,要求是这三个向量可以构成一个单位正交基,令这三个向量分别为<span class="math inline">\(\mathbf{N}\)</span>、<span class="math inline">\(\mathbf{T}\)</span>、<span class="math inline">\(\mathbf{B}\)</span>。假设从法线贴图中读取到的值为<span class="math inline">\(\mathbf{C}\)</span>,我们首先得把<span class="math inline">\(\mathbf{C}\)</span>转化为这个值表示的法线<span class="math inline">\(\mathbf{N}_\mathrm{Tex}\)</span></p>
<p></p><div class="math display">\[\mathbf{N}_\mathrm{Tex} = \frac{2\mathbf{C} - \begin{bmatrix} 1 & 1 & 1 \end{bmatrix}^\mathrm{T}}{||2\mathbf{C} - \begin{bmatrix} 1 & 1 & 1 \end{bmatrix}^\mathrm{T}||}
\]</div><p></p><p>计算出法线贴图实际存储的法线后,接下来我们可以用下方的公式来调整顶点存储的法线</p>
<p></p><div class="math display">\[\mathbf{N}^\prime = \begin{bmatrix} | & | & | \\ \mathbf{T} & \mathbf{B} & \mathbf{N} \\ | & | & | \end{bmatrix} \mathbf{N}_\mathrm{Tex}
\]</div><p></p><p>严格地来说是把法线贴图存储的切线空间内的法线变换到了世界空间中,换个方式可以理解为新求得的法线<span class="math inline">\(\mathbf{N}^\prime\)</span>是<span class="math inline">\(\mathbf{T}\)</span>、<span class="math inline">\(\mathbf{B}\)</span>、<span class="math inline">\(\mathbf{N}\)</span>这三个向量使用<span class="math inline">\(\mathbf{N}_\mathrm{Tex}\)</span>的分量作为权重,加权后的总和。<br>
有时你可能会想法线贴图是哪来的?通常它们是从一个更精细的模型计算得来的,或者有时直接从真实的表面测量。当然了它们也可以作为建模过程的一部分进行编写,在这种情况下,有个很好想法是使用<strong>凹凸贴图</strong>(<strong>Bump Map</strong>)来间接声明法线,这个贴图实际上是个高度场,通过在上面求偏导数我们就能获得法线。</p>
<h2 id="位移贴图displacement-maps">位移贴图(Displacement Maps)</h2>
<p> 法线贴图的一个问题是不会真正改变表面,只是着色后给你带来了这种感觉而已。纹理不止能用来着色,它们也能被用来改变几何体。就比如位移贴图,如同它的名字一样,我们可以通过这个贴图存储的值来改变表面。最常用的方法是先把光滑表面细分到更小的三角形,然后从位移贴图中读取值来移动小三角形的顶点。</p>
<h2 id="阴影贴图shadow-maps">阴影贴图(Shadow Maps)</h2>
<p> 阴影是重要的视觉信息来告诉我们场景中物体之间的关系,在光线追踪的图像中很容易实现,然而在光栅化渲染中却有点难,正因为使用的是基于物体顺序的渲染。阴影贴图就是被用来解决这个问题的,背后的原理很简单。首先以光源的视角观察整个场景,把所有物体变换到光源的视图空间中,接着记录各个方向上最短的距离,并把结果保存到一张叫阴影贴图的纹理上。当渲染物体的时候,就把待渲染的表面变换到阴影贴图的纹理空间中,比较纹理中相应位置存储的距离<span class="math inline">\(d_\mathrm{map}\)</span>和表面与光源的实际距离<span class="math inline">\(d\)</span>。下方是一张形象的示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250828213148379-968342072.png"></p>
<p>当距离相同时,着色点就会被点亮。如果<span class="math inline">\(d>d_\mathrm{map}\)</span>就意味着在这个方向上有物体距离光源更*,因此着色点被遮蔽。这里要注意“当距离相同时”是个非常理想的情况,在实践中往往有精度问题要注意,有时候<span class="math inline">\(d\)</span>会比<span class="math inline">\(d_\mathrm{map}\)</span>大一点点或小一点点。因此需要使用一个小的容差<span class="math inline">\(\epsilon\)</span>来进行更宽松的判断(<span class="math inline">\(d-d_\mathrm{map}<\epsilon\)</span>),容差<span class="math inline">\(\epsilon\)</span>通常被称为<strong>阴影偏移</strong>(<strong>Shadow Bias</strong>)。<br>
采样阴影贴图时,在深度值之间进行插值是不合理的。虽然在光滑的区域会有更精确的深度,但是在深度值突然改变的地方会有大问题。因此阴影贴图的采样通常使用点采样,为了减少走样可以采样周围的深度进行多次判断,然后让<span class="math inline">\(判断为真的次数/总判断次数\)</span>作为着色点的未遮蔽度参与着色计算,这个方法通常被称为<strong>百分比更*过滤</strong>(<strong>Percentage Closer Filtering</strong>)。</p>
<h2 id="环境贴图environment-maps">环境贴图(Environment Maps)</h2>
<p> 环境贴图可以用来存储那些远方的光,由于光源的距离非常远,我们可以直接认为着色只与方向有关,因此我们还需要把一个把三维方向映射到纹理空间中的二维坐标的函数,有了映射坐标的函数我们就能利用环境贴图来增加一些效果,就比如镜面反射</p>
<pre><code class="language-cpp">shade_fragment(view_dir, normal)
{
out_color = diffuse_shading(k_d, normal)
out_color += specular_shading(K_s, view_dir, normal)
u, v = spheremap_coords(reflect(view_dir, normal))
out_color += k_m * texture_lookup(environment_map, u, v)
}
</code></pre>
<p>这个技术通常被称为<strong>反射映射</strong>(<strong>Reflection Mapping</strong>)。除此之外环境贴图还能被用来进行更高级的光照计算,比如累计不同方向上的光对表面着色的影响,也就是<strong>环境光照</strong>(<strong>Environment Lighting</strong>)。<br>
环境贴图能以多种方式被存储,在这些方式中,立方体贴图是个非常好的选择,它的效率非常高并且在各种交互式应用程序中都有被用到。下方是一张相关的示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250828225923067-1396017427.png"></p>
<h1 id="过程式三维纹理procedural-3d-textures">过程式三维纹理(Procedural 3D Textures)</h1>
<p> 在前面的章节,我们让<span class="math inline">\(c_r\)</span>作为物体上某点的漫反射率。对于非纯色的物体,其实我们还可以把它看成函数<span class="math inline">\(c_r(\mathbf{p})\)</span>,这个函数必须为包含<span class="math inline">\(\mathbf{p}\)</span>的物体返回反射率。对于那些使用纹理的物体,<span class="math inline">\(c_r(\mathbf{p})\)</span>应该随着<span class="math inline">\(\mathbf{p}\)</span>在表面移动而变化。<br>
另一个定义纹理映射函数的方法是创建一个三维纹理,为三维空间中的任意一点提供RGB颜色。使用三维纹理会让映射函数的定义变得简单,因为表面正好在三维空间中,而且从三维到纹理空间的映射也没有扭曲。不过缺点也很明显,三维光栅图像会消耗大量的内存。因此,三维纹理坐标通常在<strong>过程式纹理</strong>(<strong>Procedural Texture</strong>)中被使用,在这种纹理中纹理值都是用数学过程计算出来的,而不是从纹理图像中读取。接下来让我们看看那些用来定义过程式纹理的基础工具,这些也能被用来定义二维过程式纹理。</p>
<h2 id="三维条纹纹理3d-stripe-textures">三维条纹纹理(3D Stripe Textures)</h2>
<p> 有非常多的方法来制造条纹纹理,假设我们用两个颜色<span class="math inline">\(c_0\)</span>和<span class="math inline">\(c_1\)</span>来制造条纹,我们需要一些振荡函数来在这两个颜色之间切换,其中非常简单的一个就是正弦波<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829010226962-1629193697.png"></p>
<p>我们可以用宽度<span class="math inline">\(w\)</span>控制条纹<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829010626994-528443988.png"></p>
<p>如果我们想在条纹的颜色之间进行插值,可以使用参数<span class="math inline">\(t\)</span>进行控制<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829011036114-1187410956.png"></p>
<p>下方为这三种示例代码的示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829011129353-1844223796.png"></p>
<h2 id="三维噪声solid-noise">三维噪声(Solid Noise)</h2>
<p> 尽管像条纹这样的规则纹理很有用,但是我们也希望能创造“斑驳”纹理,就像我们在鸟蛋上看到的那样。这通常是使用一种“三维噪声”做到的,它通常被称作<strong>柏林噪声</strong>(<strong>Perlin Noise</strong>)。<br>
通过为每个点使用随机数来获得噪声是不合适的,因为这会像TV电视上的“白噪声”那样,我们想让它更*滑的同时不失去随机的特性。一个可能的方法是模糊白噪声,但是没人这么做。另一个可能性是创建一个大的晶格,并为每个晶格点分配一个随机数,插值这些随机数就能得到在晶格节点之间的值。这实际上就是个有着随机数的三维纹理。不过这个技术会让晶格过于显眼。柏林使用了不同的技巧来改善这种基础的晶格技术,来让晶格不是那么显眼。结果看起来有点复杂,不过本质上就是从线性插值改变了三样东西。第一个改变就是使用<strong>厄米插值</strong>(<strong>Hermite Interpolation</strong>)来避免马赫带。第二个改变是使用随机向量而不是随机值,使用点乘就能得到一个随机数,通过把局部极大值和极小值从网格顶点移开,这样就能使背后的网格结构不那么显眼。第三个改变是使用一维数组和哈希运算来创建随机向量的一个虚拟三维数组,这样能降低运算时的内存使用。下面提供他的基础方法</p>
<p></p><div class="math display">\[n(x,y,z) = \sum_{i=\lfloor x \rfloor}^{\lfloor x \rfloor + 1} \sum_{j=\lfloor y \rfloor}^{\lfloor y \rfloor + 1} \sum_{k=\lfloor z \rfloor}^{\lfloor z \rfloor + 1} \Omega_{ijk}(x-i,y-j,z-k)
\]</div><p></p><p><span class="math inline">\((x,y,z)\)</span>为<span class="math inline">\(\mathbf{x}\)</span>的笛卡尔坐标,而且</p>
<p></p><div class="math display">\[\Omega_{ijk}(u,v,w)=\omega(u)\omega(v)\omega(w)(\Gamma_{ijk} \cdot (u,v,w))
\]</div><p></p><p><span class="math inline">\(\omega(t)\)</span>为三次权重函数</p>
<p></p><div class="math display">\[\omega(t) = \begin{cases} 2|t|^3-3|t|^2+1,|t|<1 \\ 0,其它情况 \end{cases}
\]</div><p></p><p>拼图的最后一片是<span class="math inline">\(\Gamma_{ijk}\)</span>,它是在晶格点<span class="math inline">\((x,y,z)=(i,j,k)\)</span>的随机单位向量,因为它和任何可能的<span class="math inline">\(ijk\)</span>有关,我们可以使用一个随机表</p>
<p></p><div class="math display">\[\Gamma_{ijk} = \mathbf{G}(\phi(i+\phi(j+\phi(k))))
\]</div><p></p><p>其中<span class="math inline">\(\mathbf{G}\)</span>是一个预计算的有<span class="math inline">\(n\)</span>个随机单位向量的数组,而<span class="math inline">\(\phi(i) = P\)</span>,<span class="math inline">\(P\)</span>是一个长度为<span class="math inline">\(n\)</span>的数组包含<span class="math inline">\(0\)</span>到<span class="math inline">\(n-1\)</span>的整数的排列。在实践中,柏林认为<span class="math inline">\(n=256\)</span>的表现很好。为了选择一个随机单位向量<span class="math inline">\((v_x,v_y,v_z)\)</span>,我们可以让</p>
<p></p><div class="math display">\[\begin{align*}
v_x &= 2\xi-1,\\
v_y &= 2\xi^\prime-1,\\
v_z &= 2\xi^{\prime\prime}-1
\end{align*}
\]</div><p></p><p>其中<span class="math inline">\(\xi\)</span>、<span class="math inline">\(\xi^\prime\)</span>、<span class="math inline">\(\xi^{\prime\prime}\)</span>为归一化随机数(<span class="math inline">\([0,1)\)</span>),如果<span class="math inline">\((v_x^2+v_y^2+v_z^2)<1\)</span>,就让这个向量变成一个单位向量,否则一直随机直到小于<span class="math inline">\(1\)</span>为止。<br>
因为三维噪声可正可负,在被转化为颜色前一定要进行变换。下图是噪声在10x10的正方形上的绝对值的示例图,与之一起的还有拉伸后的版本,这些噪声版本是通过缩放输入噪声函数的点做到的。<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829031805023-197449767.png"></p>
<p>上图中的黑色曲线会出现在原始的噪声函数从正值改变到负值的地方。此外更大的缩放比例可以增加对比度,下方为一张示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829033144788-952211827.png"></p>
<p>上图使用了0.5(noise+1),下图使用了0.8(noise+1)</p>
<h2 id="湍流turbulence">湍流(Turbulence)</h2>
<p> 许多自然的纹理在相同的纹理中包含各种各样的特征尺寸,Perlin使用了一个伪分形“湍流”函数</p>
<p></p><div class="math display">\[n_t(\mathbf{x})=\sum_i \frac{|n(2^i\mathbf{x})|}{2^i}
\]</div><p></p><p>这实际上就是累加很多缩放后的噪声函数,下方是一张关于它的示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829032828010-335256828.png"></p>
<p> 湍流可以被用来扰动条纹函数,下面给出伪代码<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829032919687-1446674776.png"></p>
<p>各种各样的<span class="math inline">\(k_1\)</span>和<span class="math inline">\(k_2\)</span>值都能被使用来产生不同的效果,下方是一张示例图<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202508/2774734-20250829033021653-1201209091.png"></p>
</div>
<div id="MySignature" role="contentinfo">
<p>本文来自博客园,作者:TiredInkRaven,转载请注明原文链接:https://www.cnblogs.com/TiredInkRaven/p/19054982</p><br><br>
来源:https://www.cnblogs.com/TiredInkRaven/p/19054982
頁:
[1]