3D数学-基础纹理

3D数学-基础纹理

好记性不如烂笔头啊,还是记录一下!


概述

纹理最初的目的就是使用一张图片来控制模型的外观。使用纹理映射(texture mapping)技术,我们可以把一张图“黏”在模型表面,逐纹素(texel)(纹素的名字是为了和像素进行区分)地控制模型的颜色。

在美术人员建模的时候,通常会在建模软件中利用纹理展开技术把纹理映射坐标(texture——mapping coordinates)存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。通常,这些坐标使用一个二维变量 ( u , v ) (u,v) (u,v)来表示,其中 u u u是横向坐标,而 v v v是纵向坐标。因此,纹理映射坐标被称为UV坐标,如图:

![avatar][image1]

尽管纹理的大小可以是多种多样的,可以是256*256或者1024*1024,但顶点的UV坐标的范围通常都被归一化到 [ 0 , 1 ] [0,1] [0,1]范围内。


漫反射纹理

漫反射纹理(Diffuse Map)是最基础的一种纹理。其实就是一张覆盖物体的图像,让我们能够逐片段索引其独立的颜色值。在光照场景中,它通常只是用采样到的颜色值取代光照模型中的漫反射部分的颜色值。回忆一下基础光照中的光照模型:

c d i f f u s e = ( c l i g h t ⋅ m d i f f u s e ) ⋅ m a x ( 0 , n ⋅ l ) c_{diffuse}=(c_{light} \cdot m_{diffuse}) \cdot max(0, n \cdot l) cdiffuse=(clightmdiffuse)max(0,nl)

如果对这个公式有疑问可以参考《3D数学-基础光照》

采样的颜色值替换 m d i f f u s e m_{diffuse} mdiffuse部分,漫反射纹理通常是纹理是什么样子,渲染到物体上就是什么样子,例如:

![avatar][image2]

渲染出来会是这样:

![avatar][image3]


镜面光纹理

大家可能会注意到,应用了镜面高光后看起来会有点奇怪,因为这张图片中有两种材质,木材和钢材,但是木头不应该有这么强的镜面高光的。所以我们想让物体的某些部分以不同的强度显示镜面高光,我们就可以使用一张专门用于镜面高光的纹理贴图。我们可以使用一张黑白纹理来定义物体每个部分的镜面光强度,例如上面渲染的木箱它的镜面光纹理(Specular Map)

![avatar][image4]

镜面高光的强度可以通过图像每个像素的亮度来获取。镜面光纹理上的每个像素可以由一个颜色向量来表示。比如黑色代表颜色向量 < 0 , 0 , 0 > <0, 0, 0> <0,0,0>、灰色代表颜色向量 < 0.5 , 0.5 , 0.5 > <0.5, 0.5, 0.5> <0.5,0.5,0.5>,那么我们用光照模型计算出来的高光部分 c s p e c u l a r c_{specular} cspecular就可以点乘这个向量,这样就可以方便的控制不同部分的反光强度。在镜面光纹理(Specular Map)中,一个像素越白,说明说明物体表面的镜面光强度越大,经过镜面光纹理的后的渲染,如图:

![avatar][image5]

经过镜面光纹理的处理,看起来更逼真一些,但是感觉面还是平平的缺少一些细节。


法线纹理

现实中的物体表面并非平坦的,想提升一个表面的细节主要有两种方式。第一种通过增加顶点来提升面数来增加一个表面的细节,这种方式表现力强,但是比较耗费性能。另一种方法就是使用凹凸映射(bump mapping),给模型提供更多的细节表现。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”

在光照模型的中,影响光照强度的是辐照度(irradiance),而辐照度(irradiance)通常是由光源方向 l l l和表面法线 n n n点积来计算。我们通常无法改变光的强度,但是我们可以改变表面法线 n n n来控制一个表面的光照强度,表面只有一个法向量,使得这个平面被同样的一种辐照度(irradiance)照亮。如果每个fragment都有自己不同的法线会怎样,我们就可以根据表面细微的细节,来改变这些法线,这样在一个表面上就可以产生出表面并不平坦的错觉,如图:

![avatar][image6]

每个fragment都有自己不同的法线,也就可以看成这个表面是由很多微小的(垂直于法向量的)平面组成,物体经过光照模型计算后,表面细节会得到极大的提升,这种每个fragment使用各自的法线,替代一个面上所有fragment使用同一个法线的技术叫做法线纹理(normal mapping)凹凸映射(bump mapping),下图展示了没有使用法线纹理和使用了法线纹理的区别:

![avatar][image7]

可以看到细节获得了极大的提升,性能消耗确不大。因为我们只需要改变每个fragment的法线向量,不需要更改光照模型。现在我们是为每个fragment传递一个法线,不再使用插值的表面法线。这样光照使表面的每个fragment拥有了自己的细节。

为使法线纹理工作,我们需要为每个fragment提供一个法线。2D纹理不仅可以储存颜色和光照数据,还可以储存法线向量。这样我们可以从2D纹理中采样得到特定纹理的法线向量。像漫反射纹理(Diffuse Map)镜面光纹理(Specular Map)一样,我们可以使用一张2D纹理来储存法线数据。

由于法线向量是个几何工具,而纹理通常只用于存储颜色信息,用纹理存储法线向量不是非常直观。法线方向的分量范围在 [ − 1 , 1 ] [-1, 1] [1,1],而像素的分量范围为 [ 0 , 1 ] [0, 1] [0,1],因此我们需要做一个映射,通常使用的映射就是:

p i x e l = n o r m a l × 0.5 + 0.5 pixel = normal \times 0.5 + 0.5 pixel=normal×0.5+0.5

这就要求我们在对法线纹理采样后,还需要对结果进行一次反映射的过程来得到原先法线的方向。反映射的过程就是使用上面映射函数的逆函数:

n o r m a l = p i x e l × 2 − 1 normal = pixel \times 2 - 1 normal=pixel×21

将法线向量变换为这样的RGB颜色,我们就能把根据表面形状的fragment的法线保存在2D纹理中,例如上面渲染的砖块的例子:

![avatar][image8]

一般来说,法线纹理都会是这种偏蓝色调的纹理。这是因为所有发现的指向都偏向 z z z < 0 , 0 , 1 > <0, 0, 1> <0,0,1>,映射到像素即为 < 0.5 , 0.5 , 1 > <0.5, 0.5, 1> <0.5,0.5,1>,也就是浅蓝色。这些浅蓝色实际上说明fragment的大部分法线是和模型本身法线一样,不需要改变。法线向量从 z z z轴方向向其他方向轻微的偏移,颜色也就发生了变化,这样看起来编有了一种深度。然后我们就可以用采样的颜色值,反映射出对应的法线向量,然后进行光照计算,就可以得到上面渲染的表现效果。

这个方法看起来很完美,这样使用有很大的限制,如果法线纹理的所有向量都是基于 z z z < 0 , 0 , 1 > <0, 0, 1> <0,0,1>的偏移,那么必须模型表面的法向量必须是指向 z z z < 0 , 0 , 1 > <0, 0, 1> <0,0,1>。如果将表面旋转使得法向量指向 y y y < 0 , 1 , 0 > <0, 1, 0> <0,1,0>,计算出来的光照完全不对,如图:

![avatar][image9]

有一个解决方案是为每个表面制作一张单独的法线纹理。这样的话,如果一个立方体我们就需要6张法线纹理,如果一个模型上有无数朝向不同方向的面,这就不靠谱了。

另一个解决方案是,在一个不同的坐标空间中进行光照,这个坐标空间里,法线纹理向量总是指向这个坐标控件的正 z z z < 0 , 0 , 1 > <0, 0, 1> <0,0,1>方向。所有的光照向量都是相对于这个正 z z z < 0 , 0 , 1 > <0, 0, 1> <0,0,1>方向进行变换。这样我们就能始终使用同样的法线纹理,不管朝向问题。这就是切线空间(tangent space)

切线空间(tangent space)

切线空间(tangent space)也称为图像空间(image space),法线纹理中的法线向量都是定义在切线空间(tangent space)中,顶点法线永远指向 z z z < 0 , 0 , 1 > <0, 0, 1> <0,0,1>方向:

![avatar][image10]

然后我们需要确定切线(Tagent)方向:

![avatar][image11]

然而垂直与发现的切线有很多条,理论上哪一条都行。但我们需要保持连续一致性,以免衔接出现瑕疵。标准的做法是将切线方向和纹理空间对齐:

![avatar][image12]

定义一个空间坐标系需要三个基向量,因此我们还得计算副切线(Bitangent)

我们得到这三个基向量后就可以构建TBN矩阵T代表tangentB代表bitangentN代表normal),可以用这个矩阵把任意向量从切线空间(tangent space)转换到模型空间,然后我们只需要对TBN矩阵求逆就可以实现从模型空间转换到切线空间(tangent space)的变换矩阵。

现在我们现在已知法线(Normal),需要将切线(Tangent)副切线(Bitangent)对齐到纹理空间的 u u u轴和 v v v

![avatar][image13]

假设一个fragment进行采样:

![avatar][image14]

P 1 < U 1 , V 1 > P_{1}<U_{1}, V_{1}> P1<U1,V1>是切线空间中的TB平面上的一个坐标点

P 2 < U 2 , V 2 > P_{2}<U_{2}, V_{2}> P2<U2,V2>是切线空间中的TB平面上的一个坐标点

P 3 < U 3 , V 3 > P_{3}<U_{3}, V_{3}> P3<U3,V3>是切线空间中的TB平面上的一个坐标点

E 1 E_{1} E1是连接 P 1 P_{1} P1 P 2 P_{2} P2的的直线

E 2 E_{2} E2是连接 P 2 P_{2} P2 P 3 P_{3} P3的的直线

则有以下关系式:

{ Δ U 1 = ∣ U 2 − U 1 ∣ Δ V 1 = ∣ V 2 − V 1 ∣ Δ U 2 = ∣ U 3 − U 2 ∣ Δ V 2 = ∣ V 3 − V 2 ∣ \begin{cases} \Delta U_{1} = |U_{2} - U_{1}| \\[2ex] \Delta V_{1} = |V_{2} - V_{1}| \\[2ex] \Delta U_{2} = |U_{3} - U_{2}| \\[2ex] \Delta V_{2} = |V_{3} - V_{2}| \end{cases} ΔU1=U2U1ΔV1=V2V1ΔU2=U3U2ΔV2=V3V2

就可以得出一下关系:

{ E 1 = Δ U 1 T + Δ V 1 B E 2 = Δ U 2 T + Δ V 2 B \begin{cases} E_{1} = \Delta U_{1}T + \Delta V_{1}B \\[2ex] E_{2} = \Delta U_{2}T + \Delta V_{2}B \end{cases} E1=ΔU1T+ΔV1BE2=ΔU2T+ΔV2B

我们也可以写成这样:

{ ( E 1 x , E 1 y , E 1 z ) = Δ U 1 ( T x , T y , T z ) + Δ V 1 ( B x , B y , B z ) ( E 2 x , E 2 y , E 2 z ) = Δ U 2 ( T x , T y , T z ) + Δ V 2 ( B x , B y , B z ) \begin{cases} (E_{1x}, E_{1y}, E_{1z}) = \Delta U_{1}(T_{x}, T_{y}, T_{z}) + \Delta V_{1}(B_{x}, B_{y}, B_{z}) \\[2ex] (E_{2x}, E_{2y}, E_{2z}) = \Delta U_{2}(T_{x}, T_{y}, T_{z}) + \Delta V_{2}(B_{x}, B_{y}, B_{z}) \end{cases} (E1x,E1y,E1z)=ΔU1(Tx,Ty,Tz)+ΔV1(Bx,By,Bz)(E2x,E2y,E2z)=ΔU2(Tx,Ty,Tz)+ΔV2(Bx,By,Bz)

这样我们就可以方便的写成矩阵的形式:

[ E 1 x V 1 x E 1 y V 1 y E 1 z V 2 z ] = [ Δ U 1 Δ U 2 Δ V 1 Δ V 2 ] ⋅ [ T x B x T y B y T z B z ] \begin{bmatrix} E_{1x} & V_{1x} \\[2ex] E_{1y} & V_{1y} \\[2ex] E_{1z} & V_{2z} \end{bmatrix}= \begin{bmatrix} \Delta U_{1} & \Delta U_{2} \\[2ex] \Delta V_{1} & \Delta V_{2} \end{bmatrix} \cdot \begin{bmatrix} T_{x} & B_{x} \\[2ex] T_{y} & B_{y} \\[2ex] T_{z} & B_{z} \end{bmatrix} E1xE1yE1zV1xV1yV2z=[ΔU1ΔV1ΔU2ΔV2]TxTyTzBxByBz

然后我们可以进行变换,成为:

[ Δ U 1 Δ U 2 Δ V 1 Δ V 2 ] − 1 ⋅ [ E 1 x V 1 x E 1 y V 1 y E 1 z V 2 z ] = [ T x B x T y B y T z B z ] \begin{bmatrix} \Delta U_{1} & \Delta U_{2} \\[2ex] \Delta V_{1} & \Delta V_{2} \end{bmatrix}^{-1} \cdot \begin{bmatrix} E_{1x} & V_{1x} \\[2ex] E_{1y} & V_{1y} \\[2ex] E_{1z} & V_{2z} \end{bmatrix}= \begin{bmatrix} T_{x} & B_{x} \\[2ex] T_{y} & B_{y} \\[2ex] T_{z} & B_{z} \end{bmatrix} [ΔU1ΔV1ΔU2ΔV2]1E1xE1yE1zV1xV1yV2z=TxTyTzBxByBz

然后就可以计算出delta纹理坐标矩阵的逆矩阵,就可以计算出 T T T B B B,计算逆矩阵的方法这里就不详细介绍了,主要方式是计算矩阵的行列式,然后用1除以行列式再乘以它的伴随矩阵(Adjugate Matrix)

[ T x B x T y B y T z B z ] = 1 Δ U 1 Δ V 2 − Δ U 2 Δ V 1 [ Δ V 2 − Δ V 1 − Δ U 2 Δ U 1 ] ⋅ [ E 1 x V 1 x E 1 y V 1 y E 1 z V 2 z ] \begin{bmatrix} T_{x} & B_{x} \\[2ex] T_{y} & B_{y} \\[2ex] T_{z} & B_{z} \end{bmatrix}= \frac{1}{\Delta U_{1}\Delta V_{2}-\Delta U_{2}\Delta V_{1}} \begin{bmatrix} \Delta V_{2} & -\Delta V_{1} \\[2ex] -\Delta U_{2} & \Delta U_{1} \end{bmatrix} \cdot \begin{bmatrix} E_{1x} & V_{1x} \\[2ex] E_{1y} & V_{1y} \\[2ex] E_{1z} & V_{2z} \end{bmatrix} TxTyTzBxByBz=ΔU1ΔV2ΔU2ΔV11[ΔV2ΔU2ΔV1ΔU1]E1xE1yE1zV1xV1yV2z

这样我们就计算出了TBN矩阵,我们只需要得到TBN矩阵的逆矩阵就可是实现模型空间转换到切线空间(tangent space),在理想的情况下TBN矩阵是正交矩阵,我们就可以通过求转置矩阵来获得逆矩阵,即:

[ T x B x N x T y B y N y T z B z N z ] T = [ T x T y T z B x B y B z N x N y N z ] \begin{bmatrix} T_{x} & B_{x} & N_{x} \\[2ex] T_{y} & B_{y} & N_{y} \\[2ex] T_{z} & B_{z} & N_{z} \end{bmatrix}^{T}= \begin{bmatrix} T_{x} & T_{y} & T_{z} \\[2ex] B_{x} & B_{y} & B_{z} \\[2ex] N_{x} & N_{y} & N_{z} \end{bmatrix} TxTyTzBxByBzNxNyNzT=TxBxNxTyByNyTzBzNz

然而实际情况计算出的TBN矩阵往往不是正交矩阵,所以我们需要对这个矩阵进行格拉姆-施密特正交化过程(Gram-Schmidt process)来进行正交化:

![avatar][image15]

t o = n o r m a l i z e ( t − n × ( n ⋅ t ) b o = n × t o t_{o} = normalize(t - n \times (n \cdot t) \\[2ex] b_{o} = n \times t_{o} to=normalize(tn×(nt)bo=n×to

这样我们就得到了正交的TBN矩阵,一般来说有两种方式来使用它:

  1. 我们直接使用TBN矩阵,可以将切线坐标空间的向量转换到世界坐标空间。因此我们可以将从法线纹理中采样到的法线乘以TBN矩阵转换到世界空间中,这样法线、光照参数都在一个坐标系中,就可以进行光照模型的计算了。
  2. 我们也可以使用TBN的逆矩阵,可以将世界坐标空间的向量转换到切线坐标空间.我们使用这个矩阵将光照参数转换到切线空间中,然后就进行光照模型的计算了。

本节教程就到此结束,希望大家继续阅读我之后的教程。

谢谢大家,再见!


饮水思源

参考文献:

《3D游戏与图形学中的数学方法》
《Unity Shader 入门精要》
《法线贴图》
《法线贴图》