PA5 Bumpiness
Shader 与渲染算法 - 表面与光照 - 凹凸
1 原教旨主义:Bump Mapping

Bump Mapping 本质上是一种法线扰动技术,它通过定义切线方向(tangent direction)和副法线方向(binormal),进而生成新的法线方向来模拟细微的凹凸效果。由此避免实际改变几何体的形状,增加模型的顶点数。
注:"binormal" 叫做 “副法线” 或 “副切线” 都行,“副切线” 更符合直觉一些。但是为了保持中英逻辑一致,本文中译作 “副法线”。
1.1 有趣的想法 Fascinating Idea
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 向量。
1.2 CODING
Shader "Code/PA5_Bumpiness/BumpMapping" {
Properties {
_Tint ("Tint", Color) = (1, 1, 1, 1)
_MainTex ("Albedo", 2D) = "white" {}
[NoScaleOffset] _HeightMap ("Heights", 2D) = "gray" {}
[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.1
}
SubShader {
Pass {
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma target 3.0
#pragma multi_compile _ VERTEXLIGHT_ON
#pragma vertex Vert
#pragma fragment Frag
#define FORWARD_BASE_PASS
#include "BumpMapping_Light.cginc"
ENDCG
}
Pass {
Tags {
"LightMode" = "ForwardAdd"
}
Blend One One
ZWrite Off
CGPROGRAM
#pragma target 3.0
#pragma multi_compile_fwdadd
#pragma vertex Vert
#pragma fragment Frag
#include "BumpMapping_Light.cginc"
ENDCG
}
}
}
2 维新派:Normal Mapping

Bump mapping 产生了大量的运算(纹理采样 & 差分运算)。因为 normal 往往是相同的,所以希望仅运算一次,并且将运算结果存储在材质中。
2.1 粗糙的想法 Rough Idea
使用 3D 软件创建纹理,将法线信息编码为 RGB 值,导入 Unity(需将 Texture Type 设置为 “Normal Map”,并勾选 Create from Grayscale,设置 “Bumpiness” 值);
在 Unity 中解码,即将 normal map 中保存的 RGB 信息还原为法线向量坐标。
2.2 CODING
Properties {
_Tint ("Tint", Color) = (1, 1, 1, 1)
_MainTex ("Albedo", 2D) = "white" {}
[NoScaleOffset] _NormalMap ("Normals", 2D) = "bump" {}
_BumpScale ("Bump Scale", Float) = 1
[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.1
}
3 配置升级:Bump 叠 Bump
无需对原有逻辑进行大幅修改,仅需动动手指即可对 Bump 超级加倍 🤌。
3.1 粗糙的想法 Rough Idea
升级相关配置(主要是修改 uv 取值逻辑):
添加 detail texture 相关 properties 及相关控制变量;
Interpolators structure:uv 分量从 float2 类型变为 float4 类型;
vertex shader:对 uv 的前后两组分量分别实现纹理顶点转换;
fragment shader:albedo 变量为漫反射中的材质 项(值来自 uv rgb 和 tint)。将主纹理的的 uv 采样修改为 uv 的 xy 分量,并与细节纹理叠加;
InitializeFragmentNormal 函数里添加细节纹理的法线解码逻辑(使用
UnpackScaleNormal
函数),并将 main normal 与 detail normal 的结果叠加(使用Whiteout blending
方法)。
3.2 CODING
Properties {
_Tint ("Tint", Color) = (1, 1, 1, 1)
_MainTex ("Albedo", 2D) = "white" {}
[NoScaleOffset] _NormalMap ("Normals", 2D) = "bump" {}
_BumpScale ("Bump Scale", Float) = 1
[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.1
_DetailTex ("Detail Texture", 2D) = "gray" {}
[NoScaleOffset] _DetailNormalMap ("Detail Normals", 2D) = "bump" {}
_DetailBumpScale ("Detail Bump Scale", Float) = 1
}
4 处理非平面:从平面到更多 Mesh
将 bump mapping 从平面扩展到其他复杂的 3D 模型时,需要一个局部坐标系统来计算表面法线。对于之前简单的平面来说,法线、切线和副法线方向是相对固定的,平面环境天然提供了 bump mapping 所需的 X-Y-Z 本地向量空间。
但对于复杂的网格模型,表面的方向会不断变化,因此就需要一种方法在每个顶点或像素附近定义一个一致的坐标系。
这个自定义的各像素本地空间,就是 Tangent Space,切线空间。
这一节将首先创建切线空间并将其可视化,然后将切线空间导入 shader,使得可以用于 bump mapping。
4.1 破解之道:切线空间 Tangent Space
4.2 粗糙的想法 Rough Idea
使用 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。
4.3 CODING
// 可视化切线空间。normal-green, targent-red, binormal-blue.
using UnityEngine;
public class TangentSpace_Visualizer : MonoBehaviour {
// 调整可视化 lines 的长度和位置偏移(不会紧贴在表面)
public float offset = 0.01f;
public float scale = 0.1f;
void OnDrawGizmos () {
MeshFilter filter = GetComponent<MeshFilter>();
if (filter) {
Mesh mesh = filter.sharedMesh;
if (mesh) {
ShowTangentSpace(mesh);
}
}
}
void ShowTangentSpace (Mesh mesh) {
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
Vector4[] tangents = mesh.tangents;
for (int i = 0; i < vertices.Length; i++) {
ShowTangentSpace(
transform.TransformPoint(vertices[i]),
transform.TransformDirection(normals[i]),
transform.TransformDirection(tangents[i]),
tangents[i].w
);
}
}
void ShowTangentSpace (
Vector3 vertex, Vector3 normal, Vector3 tangent, float binormalSign
) {
vertex += normal * offset;
Gizmos.color = Color.green;
Gizmos.DrawLine(vertex, vertex + normal * scale);
Gizmos.color = Color.red;
Gizmos.DrawLine(vertex, vertex + tangent * scale);
Vector3 binormal = Vector3.Cross(normal, tangent) * binormalSign;
Gizmos.color = Color.blue;
Gizmos.DrawLine(vertex, vertex + binormal * scale);
}
}
APPENDIX
Anki
Reference
GAMES101 - Lecture 9: Shading3 (Texture Mapping cont.)
Last updated