PA5 Bumpiness
Shader 与渲染算法 - 表面与光照 - 凹凸
Last updated
Shader 与渲染算法 - 表面与光照 - 凹凸
Last updated
Bump Mapping 本质上是一种法线扰动技术,它通过定义切线方向(tangent direction)和副法线方向(binormal),进而生成新的法线方向来模拟细微的凹凸效果。由此避免实际改变几何体的形状,增加模型的顶点数。
注:"binormal" 叫做 “副法线” 或 “副切线” 都行,“副切线” 更符合直觉一些。但是为了保持中英逻辑一致,本文中译作 “副法线”。
properties: 添加 _HeightMap 纹理(高程图,无需 scale or offset);
fragment shader: 在 shader 开始调用自定义的 normal 处理函数,生成凹凸表面;
制作自定义的 normal 处理函数:
对于 height map 纹理上的每一个 fragment,通过在 U 和 V 方向上分别取两个采样点,计算变化率(du 和 dv),生成 tangent vectors(切线向量)。比如 u1 - u2 对应 U 坐标方向切线;
将两个方向的 tangent vectors 进行 cross product(叉积)转化为垂直于表面的 normal 向量。
Bump mapping 产生了大量的运算(纹理采样 & 差分运算)。因为 normal 往往是相同的,所以希望仅运算一次,并且将运算结果存储在材质中。
使用 3D 软件创建纹理,将法线信息编码为 RGB 值,导入 Unity(需将 Texture Type 设置为 “Normal Map”,并勾选 Create from Grayscale,设置 “Bumpiness” 值);
在 Unity 中解码,即将 normal map 中保存的 RGB 信息还原为法线向量坐标。
无需对原有逻辑进行大幅修改,仅需动动手指即可对 Bump 超级加倍 🤌。
升级相关配置(主要是修改 uv 取值逻辑):
添加 detail texture 相关 properties 及相关控制变量;
Interpolators structure:uv 分量从 float2 类型变为 float4 类型;
vertex shader:对 uv 的前后两组分量分别实现纹理顶点转换;
InitializeFragmentNormal 函数里添加细节纹理的法线解码逻辑(使用 UnpackScaleNormal
函数),并将 main normal 与 detail normal 的结果叠加(使用 Whiteout blending
方法)。
将 bump mapping 从平面扩展到其他复杂的 3D 模型时,需要一个局部坐标系统来计算表面法线。对于之前简单的平面来说,法线、切线和副法线方向是相对固定的,平面环境天然提供了 bump mapping 所需的 X-Y-Z 本地向量空间。
但对于复杂的网格模型,表面的方向会不断变化,因此就需要一种方法在每个顶点或像素附近定义一个一致的坐标系。
这个自定义的各像素本地空间,就是 Tangent Space,切线空间。
这一节将首先创建切线空间并将其可视化,然后将切线空间导入 shader,使得可以用于 bump mapping。
使用 mikktspace 算法。根据 “是否在 vertex shader 阶段插值计算第三个向量 binormal vector”,给出条件编译的两种方式。
组件
struct VertexData:获取顶点 tangent vector(切线向量)信息;
struct Interpolators:使用条件编译添加插值器(选择顶点阶段插值,则为 binormal 也添加一个插值器);
vertex shader:
该阶段不插值:分别处理 i.tangent 的 xyz 与 w 分量;
该阶段插值:处理 i.tangent 的 xyz 分量,调用自定义 CreateBinormal 函数生成第三个向量 binormal vector;
⚠️:i.tangent 的 xyz 分量需转换为世界空间;w 分量作为标识位不参与转换,单独用作 CreateBinormal 函数的参数;
fragment shader:调用 InitializeFragmentNormal 函数,生成 Bump 后的新 normal。
Anki
Reference
GAMES101 - Lecture 9: Shading3 (Texture Mapping cont.)
其中 ,。
因为 (delta)是相邻两个采样点(1 pixel)的距离,因此 _HeightMap_TexelSize.x
和 _HeightMap_TexelSize.y
分别就是 U 和 V 方向上的 值。
其公式表达为: 。
其中 为两个采样点之间的距离,在这里是一个像素大小。
如上图,斜率为 。
通过线性代数 cross product 相关知识可知,因为 ,所以可以直接写得该式。
i.normal.z = sqrt(1 - saturate(dot(i.normal.xy, i.normal.xy)))
是根据 x 和 y 分量计算 z 值。
因为法线是单位向量,其长度为 1,因此有 ,于是 。
fragment shader:albedo 变量为漫反射中的材质 项(值来自 uv rgb 和 tint)。将主纹理的的 uv 采样修改为 uv 的 xy 分量,并与细节纹理叠加;
在 UnpackScaleNormal 函数中,z 向量是经由 公式得出,因此向量已经是单位向量,归一化后,x 和 y 分量的大小是经过缩放的,无法直接反映法线的实际变化率。所以,这里需要将 x 和 y 分量还原,通过除以 z 分量来得到未归一化前的偏导数(平行于表面的变化率)。
假设有一个非常平坦的 main normal vector ,以及一个陡峭的 detail normal vector ,whiteout blending 算法将其计算为 ,然后进行归一化操作。
首先容易得到法线向量 ,此时只需要一个额外的向量,就可以通过这两个向量,叉积得到第三个向量。
可以得到的第二个向量是切线向量 ,它是 mesh 的顶点数据的一部分,它由物体表面法线定义,与 U 坐标方向匹配(point to the right)。
第三个叉积出来的向量 ,称为 bitangent 或 binormal(副法线),与 V 坐标方向匹配(pointing forward)。
因为 ,叉积结果指向背面(backwards),需要乘以 -1。这个修正因子作为 的第四个分量存储。
如果此时对 (binormal)进行镜像,那么 (tangent normal) 就没有被处理,会出现错误的空间结果。而选择对 进行镜像,被计算出的 作为后置的计算步骤,自然也是正确的。