实时渲染 凹凸贴图及改进
总览
凹凸贴图(Bump Mapping)思想最早是由图形学届大牛中的大牛 Jim Blinn 提出,Normal Mapping,Parallax Mapping,Parallax Occulision Mapping,Relief Mapping 等都是基于凹凸贴图衍生出的
除开displacement mapping,其他几种算法都是不改变物体表面的 几何法线,只修改照明方程中使用的法线值
Bump Mapping
-
原理
改变表面光照方程的法线,而不是表面的几何法线,对每个待渲染的像素在计算照明之前都要加上一个从高度图中找到的扰动,来模拟凹凸不平的视觉特征
-
对于高度图,每个像素值代表一个高度,白色表示高高度区域,黑色是低高度的区域
-
计算阶段
PS阶段
Displacement Mapping
-
原理
Displacement Mapping的每一个纹素中存储了一个向量,每个向量代表对应顶点的位移,不过这些纹素并不和像素一一对应而是和顶点对应。因此,纹素个数和网格的顶点个数是相同的
-
计算阶段
在VS阶段,获取每个顶点处的Displacement纹素的偏移向量,在局部空间进行偏移后再转入世界空间
Normal Mapping
-
原理
Normal Map 直接将正确的Normal 向量保存到一张纹理,在使用时直接从贴图中取出
-
计算阶段
PS
Parallax Mapping
-
Parallax Mapping是基于Bump Mapping的改进,他的性能更好。因为在Bump Mapping中,每个待渲染的像素都需要加上一个高度扰动,这导致有n个顶点就需要计算n次,而Parallax Mapping可以以非常少的计算次数来实现和Bump Mapping差不多的效果。如下图所示,有1000个顶点,但只计算了6次(两个三角形)
-
思想
修改纹理坐标使一个fragment的表面看起来比实际的更高或者更低,所有这些都根据观察方向和高度贴图。在A位置上的fragment不再使用点A的纹理坐标而是使用点B的-
如下图所示,红线代表高度贴图中的数值表达,向量\(\large \overrightarrow{V}\)代表观察方向。如果平面没有凸起,应采样点A;若出现凸起,因为点B挡住点A,而应采样点B
-
-
计算方法
将fragment到观察者的向量\(\large \overrightarrow{V}\)转换到切线空间中,经缩放的\(\large \overrightarrow{V}\)的x和y元素将于表面的切线和副切线向量对齐。由于切线和副切线向量与表面纹理坐标的方向相同,我们可以用\(\large \overrightarrow{V}\)的x和y元素作为纹理坐标的偏移量,这样就不用考虑表面的方向
基础的Parallax Mapping
-
原理:由于没有具体的几何信息,无法精确计算观察方向和交点 B ,也就无法精确得到 B 的投影。因此只能采用近似求解
如下图所示,取一向量\(\large \overrightarrow{P}\)和\(\large \overrightarrow{V}\)的方向一致,且大小是A点的高度(高度贴图),\(\large \overrightarrow{P}\)的投影值即为AB的UV偏移值
-
缺点
- 点B是粗略估算得到的。当表面的高度变化很快的时候,看起来就不会真实,因为向量\(\large \overrightarrow{P}\)最终不会和B接近
- 当表面被任意旋转以后很难指出从\(\large \overrightarrow{P}\)获取哪一个坐标
- 视差映射只有在高度相对平滑,并且不存在复杂细节时,才能得到不错的结果。如果观察向量和表面法线夹角过大会出现严重错误的结果
- 点B是粗略估算得到的。当表面的高度变化很快的时候,看起来就不会真实,因为向量\(\large \overrightarrow{P}\)最终不会和B接近
-
伪代码实现
因为TBN的TB轴以物体表面所建,N垂直于TB,因此以观察向量的z分量作为N轴方向,xy作为TB轴方向
float2 PS(float2 uv, float3 viewDirection) { // 采样高度贴图求得height // 求uv offset float2 offset = viewDirection.xy / viewDirection.z * (scale * height); return uv + offset; }
-
为什么需要 viewDirection.xy / viewDirection.z?
viewDirection.xy其实就是在求uv offset
当观察向量和多边形平面几乎垂直时求得的值是远远大于平行时求得的值,因此当有角度时我们会采取缩放观察向量,以增加uv offset,从而获得更强的视觉感;但不除以viewDirection.z也是可以的,根据效果来选用即可 -
scale是干嘛的?
scale用于控制视差映射效果的幅度
-
Steep Parallax Mapping
-
原理
将高度平均分为 n 层,从0层开始采样高度图,每一次会沿着V的方向偏移纹理坐标,如果采样的深度小于当前层的深度,停止检查并使用最后一次采样的纹理坐标作为结果- 如下图所示,深度被分割成8个层,每层的高度值是0.125。每层的纹理坐标偏移是V.xy/V.z * scale/numLayers。从顶层黄色方块的位置开始检查
- 层的深度为0,高度图深度H(T0)大约为0.75。采样到的深度大于层的深度,所以开始下一次迭代。
- 沿着V方向偏移纹理坐标,选定下一层。层深度为0.125,高度图深度H(T1)大约为0.625。采样到的深度大于层的深度,所以开始下一次迭代。
- 沿着V方向偏移纹理坐标,选定下一层。层深度为0.25,高度图深度H(T2)大约为0.4。采样到的深度大于层的深度,所以开始下一次迭代。
- 沿着V方向偏移纹理坐标,选定下一层。层深度为0.375,高度图深度H(T3)大约为0.2。采样到的深度小于层的深度,所以向量V上的当前点在表面之下。我们找到了纹理坐标Tp=T3是实际交点的近似点
- 如下图所示,深度被分割成8个层,每层的高度值是0.125。每层的纹理坐标偏移是V.xy/V.z * scale/numLayers。从顶层黄色方块的位置开始检查
-
伪代码实现
float2 ps(float uv, float3 viewDirection) { // 层数 float layers = 5; // 每层高度 float layerHeight = 1.f / layers; // 当前层高度 float currLayerHeight = 0.f; // 视点方向偏移总量 float2 p = viewDirection.xy / viewDirection.z * heightScale; // 每层高度偏移量 float deltaUV = p / layers; // 当前uv float2 currentUV = uv; // 采样高度贴图求得height float height = SAMPLE(HeightTex, currentUV).r; while(currLayerHeight < height) { // 按高度层级进行 UV 偏移 currentUV -= deltaUV; // 采样高度贴图求得height height = SAMPLE(HeightTex, currentUV).r; //采样点层的高度 currentLayerHeight += layerHeight; } return currentUV; }
Parallax Occlusion Mapping
-
原理
Parallax Occlusion Mapping 和 Steep Parallax Mapping类似,不同点在于是在确定的前后层间进行lerp。Parallax Occlusion Mapping性能比Steep Parallax Mapping好,但效果比Relief Mapping差
-
步骤
-
nextHeight = H(T3) - currentLayerHeight
-
prevHeight = H(T2) - (currentLayerHeight - layerHeight)
-
weight = nextHeight / (nextHeight - prevHeight)
-
Tp = T(T2) weight + T(T3) (1.0 - weight)
-
-
伪代码实现
float2 ps(float uv, float3 viewDirection) { // 层数 float layers = 5; // 每层高度 float layerHeight = 1.f / layers; // 当前层高度 float currLayerHeight = 0.f; // 视点方向偏移总量 float2 p = viewDirection.xy / viewDirection.z * heightScale; // 每层高度偏移量 float deltaUV = p / layers; // 当前uv float2 currentUV = uv; // 采样高度贴图求得height float height = SAMPLE(HeightTex, currentUV).r; while(currLayerHeight < height) { // 按高度层级进行 UV 偏移 currentUV -= deltaUV; // 采样高度贴图求得height height = SAMPLE(HeightTex, currentUV).r; //采样点层的高度 currentLayerHeight += layerHeight; } // 前一个uv float2 prevUV = currentUV + deltaUV; // height lerp // 从上到下,下一个点 float nextHeight = height - currentLayerHeight; // 上一个点 float preHeight = texture(depthMap, prevUV).r - (currentLayerHeight - layerHeight); float wieght = nextHeight / (nextHeight - preHeight); float currentUV = preUV * wieght + (1 - weight) * currentUV; return currentUV; }
Relief Mapping
-
Parallax Mapping 是针对 Normal Mapping 的改进,利用 HeightMap 进行了近似的 Texture Offset。而
Relief Mapping 是精确的 Texture Offset -
思想:Relief Mapping基于Steep Parallax Mapping,先进行Steep Parallax Mapping,可以得到准确交点的前后两个层,和对应的深度值,最后在两层间使用二分法进行迭代查找
-
优点
- 相较于 Parallax Mapping,Relief Mapping可以实现更深的凹凸深度
-
步骤
- 在陡峭视差映射之后,我们知道交点肯定在T2和T3之间
- 设每次迭代时的纹理坐标变化量ST,它的初始值等于向量V在穿过一个层的深度时的XY分量
- 设每次迭代时的深度值变化量SH,它的初始值等于一个层的深度
- 把ST和SH都除以2
- 把纹理坐标T3沿着反方向偏移ST,把层深度沿反方向偏移SH,得到此次迭代的纹理坐标T4和层深度H(T4)
- 采样高度图,把ST和SH都除以2
- 如果高度图中的深度值大于当前迭代层的深度H(T4),则将当前迭代层的深度增加SH,迭代的纹理坐标沿着V的方向增加ST
- 如果高度图中的深度值小于当前迭代层的深度H(T4),则将当前迭代层的深度减少SH,迭代的纹理坐标沿着V的相反方向增加ST
- 循环,继续二分搜索,直到规定的次数
- 最后一步得到的纹理坐标就是浮雕视差映射的结果
-
伪代码实现
float2 ps(float uv, float3 viewDirection) { // 层数 float layers = 5; // 每层高度 float layerHeight = 1.f / layers; // 当前层高度 float currLayerHeight = 0.f; // 视点方向偏移总量 float2 p = viewDirection.xy / viewDirection.z * heightScale; // 每层高度偏移量 float deltaUV = p / layers; // 当前uv float2 currentUV = uv; // 采样高度贴图求得height while(currLayerHeight < height) { // 按高度层级进行 UV 偏移 currentUV -= deltaUV; // 采样高度贴图求得height //采样点层的高度 currentLayerHeight += layerHeight; } // 二分UV float2 halfDeltaTexUV = deltaUV / 2; currentUV -= halfDeltaTexUV; // 二分高度 float halfLayerHeight = layerHeight / 2; currentLayerHeight -= halfLayerHeight; int numSearches = 5; // 5次效果就很不错了 for(int i = 0; i < numSearches; i++) { halfDeltaTexUV = halfDeltaTexCoords / 2; halfLayerHeight = halfLayerHeight / 2; // 采样高度图 currentHeightMapValue = tex2D(heightMap, currentUV).r; if(currentHeightMapValue > currentLayerHeight) { currentUV -= halfDeltaTexUV; currentLayerHeight += halfLayerHeight; } else { currentUV += halfDeltaTexUV; currentLayerHeight -= halfLayerHeight; } } return currentUV; }
reference
[Game-Programmer-Study-Notes/README.md at master · QianMo/Game-Programmer-Study-Notes (github.com)](https://github.com/QianMo/Game-Programmer-Study-Notes/blob/master/Content/《Real-Time Rendering 3rd》读书笔记/Content/BlogPost05/README.md)
Parallax Mapping视差映射:模拟冰块 - 知乎 (zhihu.com)
[渲染 - 译] GLSL 中的视差遮蔽映射(Parallax Occlusion Mapping in GLSL) - 迷途吧 - SegmentFault 思否