【笔记】【百人计划】图形2.5 BUMP图改进

一、凹凸贴图Bump Mapping

  • 把物体的细节分为三种尺度
    • 宏观尺度(覆盖很多像素)
      • 由几何图元来表示
    • 中观尺度(覆盖少量像素)
      • 细节复杂,无法使用单个三角形渲染,并且足够大
    • 微观尺度(可能覆盖小于一个像素)
      • 在着色模型当中表现,模拟物体表面微观几何形状的相互作用

凹凸映射是模拟中观尺度的常用方法之一,能够让观察者感知到比几何模型尺度更小的细节

基本思想:在纹理中把尺度细节相关的信息编码进去,在着色过程中用受到干扰的表面代替真实表面,就让表面看起来具有小尺度的细节。

总之,凹凸贴图是对物体表面贴图进行变化再进行光照计算的一种技术。(增加物体真实感,但不需要额外的几何复杂度)

  • 分类
    • 法线贴图
    • 视差贴图
    • 浮雕贴图

在这三种技术中都会用到法线(贴图)

二、法线贴图Normal Mapping

法线贴图是一张存有物体局部表面法线信息的贴图。

计算光照时,程序读取法线图,并获取当前着色点的法线信息,结合光照信息进行光照计算。

法线贴图一般由高模映射到对应的底模上来生成,但像金属、木头等细节丰富的物体,可借助程序化软件如:Photoshop,Substance Designer等生成对应法线贴图

切线空间

法线的储存一般放在模型的切线空间中

  • 切线空间
    • 物体表面切线、副切线、法线方向为基,组成的几何空间
  • 读取切线空间法线,需要将法线从切线空间转换到世界空间
image-20220731003919885

世界和切线空间转换

切线空间坐标系的正交基是世界空间下的顶点法线(N)、切线(T)、副切线(B),法线为z轴,切线为x轴,副切线为y轴

构建一个3x3的矩阵做空间向量的坐标系转换。
$$
TBN = \begin{bmatrix}T_x&B_x&N_x\
T_y&B_y&N_y\
T_z&B_z&N_z\
\end{bmatrix}\
\ \
TBN^{-1}=TBN^T=\begin{bmatrix}T_x&T_y&T_z\
B_x&B_y&B_z\
N_x&N_y&N_z\
\end{bmatrix}\
$$
想不清哪个是世界-切线,哪个是切线-世界,考虑一个单位阵,左乘矩阵,看看会变成什么就知道了。

  • 切线空间的好处
    • 切线空间记录的是相对的法线信息,对于一个物体表面记录的法线扰动,可以同样应用到球形物体上(植物的光照处理),但是模型空间记录法线就是绝对的,只能在该物体上用。
    • 方便制作UV动画,贴图采样变化一致
    • 法线纹理可重用
    • 便于计算储存,0-1的储存映射范围,知道两个可以计算另一个
  • Unity中法线贴图的压缩格式
    • 非移动平台,unity会把法线贴图转换成DXRT5nm格式,这种格式只有两个有效通道AG通道,可以节省空间
      • 在DXRT5nm格式中,AG通道分别储存对应法线的x,y分量,z分量需要通过一个简单的计算求得。
    • 移动平台,unity使用传统RGB通道
7416ead6-fc7e-4e52-9dba-b56d68996a2a 8b45fbb7-0987-4b01-9c95-c47166d160b4

三、视差贴图Parallax Mapping

法线贴图只能改变法线而改变光照,无法使模型表面产生遮挡效果

视差贴图Parallax Mapping是一种类似法线贴图的技术。它用于提高模型表面细节并赋予其遮挡关系,可以和法线贴图一起使用。

视差贴图需要引进一张新的贴图——高度图。高度图一般是用于顶点位移使用的(位移/置换贴图 Displacement mapping),但性能消耗高,需要大量三角形。视差贴图的核心是改变纹理坐标来改变遮挡关系,视差贴图就利用储存模型信息的高度图,利用模型表面高度信息来对纹理进行偏移

image-20220731010656743

在着色时,模型在切线空间下所有点都在切平面内(0.0),核心就是对于要计算的片元A时,真正应该计算的点是视线与物体的“实际”交点B点。

image-20220731011441775

要计算B点,就需要AB两点在平面上的UV偏差,为了简便,采取近似计算的方法,根据高(深)度图及切线空间下视角方向,近似求解偏移量,视角方向(v)与切平面的正切值与A点的高度值相乘来近似求解,并通过一个缩放值来控制。(有比较大的误差,必须要用这个scale来调整)
$$
d = \frac{v.xy}{v.z}\cdot ha\cdot scale
$$

陡峭视察映射Steep Parallax Mapping

陡峭视察映射也是近似,但更准确一些

陡峭视察映射将深度分为等距的若干层,从顶端开始采样,并且每次沿视角方向偏移一定值,若当前层深度大于采样出的深度,则停止检查并返回结果

(有点ray marching的感觉,那其实在优化上也可以借鉴一下分级采样?https://xzyw7.github.io/post/CbZTf-uM4/#real-time-global-illuminationscreen-space)

image-20220731012950141

也可以根据v和n的角度来对采样层数进行控制

四、浮雕贴图Relief Mapping

image-20220731014135800

视差贴图在使用较大的uv偏移时存在失真。

浮雕贴图更容易提供更多的深度,还可以做自阴影、AO效果

实现方法

浮雕映射一般采用射线步进、二分查找来决定uv偏移量

第一种使用射线步进来查找可能的交点(直接用二分查找可能漏掉较薄的区域导致结果不准确),确定交点位于哪一个步进内。之后在该步进内使用二分查找快速确定交点位置,最后返回结果,偏移贴图。

image-20220731014352296

  • 解决最后一步二分查找性能开销问题
    • 视差闭塞贴图(Parallax Occlusion Mapping)
    • 在步进后,分别对步进两端uv值采样,对结果插值,作为p点的结果(插值导致表面平滑效果更好)

作业

结合先行版基础渲染光照介绍(一)将本次课所讲的案例结合进先前的光照效果

这里就4个案例嘛

法线贴图

左一:standard shader

左二:custom shader,使用法线贴图

右一:custom shader,无法线贴图

image-20220801232026790

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct v2f {
float4 pos : SV_POSITION;
float3 normal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
float3 tangent : TEXCOORD3;
float3 bitangent : TEXCOORD4;


};

fixed3 normal = normalize(i.normal);
fixed3x3 TBN = fixed3x3(normalize(i.tangent),normalize(i.bitangent),normal);
TBN = transpose(TBN);//Unity shader的矩阵是行优先的,所以我们要取个转置;
fixed3 bump = normalize(UnpackNormal(tex2D(_Normal, i.uv)));
normal = normalize(mul(TBN,bump));

不想像入门精要那样传一整个矩阵,我们可以传递的变量也是有限的,甚至可以只传tangent,副切线用叉乘计算。甚至也可以用之前的ddx和ddy的trick来计算。

https://xzyw7.github.io/post/zezxM-QCJ/#ddxddy%E4%B8%8E%E6%B3%95%E7%BA%BF%E8%B4%B4%E5%9B%BE

(Tips:有注意到在learnopengl中有描述,在一些网格较大的时候,出现TBN不互相垂直的情况,可以用施密特正交化来解决。)

视差贴图

这个时候发现……狮子模型这个素材没有高度图……还得换个素材……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fixed3 LightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));//normalize(_WorldSpaceLightPos0.xyz);//
fixed3 ViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));//normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
float3 h = normalize(LightDir + ViewDir);


fixed3 normal = normalize(i.normal);
fixed3 bitangent = normalize(cross(normal,i.tangent.xyz) * i.tangent.w);
fixed3x3 TBN = fixed3x3(normalize(i.tangent.xyz),bitangent,normal);
TBN = transpose(TBN);

//视差贴图
float height = tex2D(_heightMap,i.uv).r;
float3 ViewDirTS = normalize(mul(transpose(TBN),ViewDir));
float2 offUV = ViewDirTS.xy/ViewDirTS.z * height * _heightScale;
i.uv -= offUV;

//法线贴图
fixed3 bump = normalize(UnpackNormal(tex2D(_Normal, i.uv)));
normal = normalize(mul(TBN,bump));

image-20220802000836213

视差贴图在视线接近垂直的时候效果还是很好的,但是正如learnopengl中所说,当从一个角度看过去的时候,会有一些问题产生(和法线贴图相似),陡峭的地方会产生不正确的结果。并且它的效果非常依赖于_heightScale这一参数

陡峭视差贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
float2 steepParallaxMapping (float2 uv, float3 viewDir) {
float numLayers = 20;
float layerHeight = 1.0/numLayers;
float2 deltaUV = 1.0/numLayers * viewDir.xy / viewDir.z * _heightScale;
float2 currentUV = uv;
float currentHeight = tex2D(_heightMap,uv).r;
float currentLayerHeight = 0.0;
while(currentLayerHeight < currentHeight)
{
currentUV -= deltaUV;
currentHeight = tex2Dlod(_heightMap, float4(currentUV,0.0,0.0)).r;
//currentHeight = tex2Dgrad(_heightMap, currentUV,0.0,0.0).r;
currentLayerHeight += layerHeight;
}
return currentUV;
}

中间一直出现的报错“unable to unroll loop”,给tex2D改成tex2Dlod或tex2Dgrad就好了

参考

https://zhuanlan.zhihu.com/p/391443312

https://zhuanlan.zhihu.com/p/144434084

https://stackoverflow.com/questions/57994423/why-i-cant-use-tex2d-inside-a-loop-in-unity-shaderlab

tex2D只能从“均匀控制流”调用,因为它必须通过计算“导数”来计算LOD。tex2Dlod没有,因为您提供了LOD。

tex2Dlod和tex2Dgrad都能指定纹理层,所以能够在循环中调用。

image-20220802161513119

浮雕贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
float2 steepParallaxMapping (float2 uv, float3 viewDir) {
float numLayers = 20;
float layerHeight = 1.0/numLayers;
float2 deltaUV = 1.0/numLayers * viewDir.xy / viewDir.z * _heightScale;
float2 currentUV = uv;
float currentHeight = tex2D(_heightMap,uv).r;
float currentLayerHeight = 0.0;
while(currentLayerHeight < currentHeight)
{
currentUV -= deltaUV;
currentHeight = tex2Dlod(_heightMap, float4(currentUV,0.0,0.0)).r;
currentLayerHeight += layerHeight;
}
float2 left = currentUV;
float2 right = currentUV+deltaUV;
float dist = 1;
float2 midpoint = (left+right)/2;
int i = 0;
while (i<10) {
midpoint = (left+right)/2;
currentHeight = tex2Dlod(_heightMap, float4(midpoint,0.0,0.0)).r;
currentLayerHeight = length(midpoint)/length(viewDir.xy) * viewDir.z;
if (currentLayerHeight < currentHeight) {
right = midpoint;
dist = currentHeight - currentLayerHeight;
} else if (currentLayerHeight > currentHeight) {
left = midpoint;
dist = -currentHeight + currentLayerHeight;
} else {
break;
}
i++;
}
return midpoint;

}

视察闭塞贴图

1
2
3
4
5
6
7
8
9
10
float2 left = currentUV;
float2 right = currentUV+deltaUV;

float afterDepth = currentHeight-currentLayerHeight;
float beforeDepth = tex2D(_heightMap, right).r -currentLayerHeight + layerHeight;

float weight = afterDepth / (afterDepth - beforeDepth);
float2 finalTexCoords = right * weight + left * (1.0 - weight);

return finalTexCoords;

POM是肉眼可见的效果不错(上:SPM,中:RPM,下:POM),RPM就不太能看得出变化了,但其实还是有的

image-20220802175923830 image-20220802180825768 image-20220802175853495

参考资料

[1] https://www.bilibili.com/video/BV1Ub4y1Z765 【技术美术百人计划】图形 2.5 BUMP图改进

[2] Unity Shader入门精要 p146-155

[3]
https://learnopengl-cn.github.io/05%20Advanced%20Lighting/05%20Parallax%20Mapping/