本篇参考B站视频 “技术美术百人计划”·霜狼_may ;
视差云的实现 https://zhuanlan.zhihu.com/p/83355147
PBR白皮书https://zhuanlan.zhihu.com/p/53086060
本篇主要用于自我复习,如有疑问或发现有什么错误,请多指教~
本篇内容主要包括:BumpMapping介绍以及法线映射,视差映射,浮雕映射;
参考里有个视差云感兴趣的可以看下;
一.介绍
1.Bump Mapping(凹凸映射)是干啥的?
答:用来表现物体的细节效果,是模拟中观尺度的常用方法之一;
对于细节根据尺度,我们可以将其分为宏观,中观,与微观三种;
-
宏观尺度:
- 特征覆盖很多像素;
- 由顶点,三角形或者其他几何图元来表示
- 宏观尺度上建模的例子如创造三维角色时的四肢或头部;
-
中观尺度:
- 特征只覆盖几个像素;
- 描述介于微观尺度与宏观尺度间特征,包含细节较复杂,无法使用单个三角形进行渲染;
- 但细节又较大,能够让观察者看到几个像素以上的表面曲率变换;
- 如:人脸皱纹,肌肉细节,衣服褶皱
-
微观尺度:
- 特征小于一个像素;
- 微观尺度在着色模型中工作,着色模型通常在像素着色器中实现,使用纹理贴图作为参数;
- 着色模型模拟物体表面微观几何形状的相互作用;
- 如:有光泽物体在微观尺度下光滑,漫反射物体表面在微观尺度下粗糙;
下图是来自毛星云大佬的第四节PBR白皮书的关于尺度的划分:
这一部分可以稍微延伸到PBR的微平面理论:
微平面理论是将物体表面建模成做无数微观尺度上有随机朝向的理想镜面反射的小平面(microfacet)的理论。在实际的PBR 工作流中,这种物体表面的不规则性用粗糙度贴图或者高光度贴图来表示。
(出自毛星云大佬的【基于物理的渲染(PBR)白皮书】(一) 开篇:PBR核心知识体系总结与概览)
2.基本思想
纹理中把尺度细节相关的信息编码进去,着色过程中用稍微受到干扰的表面代替真实表面,使表面看起来具有小尺度的细节;
3.原理
- 凹凸贴图映射技术对物体表面贴图进行变化,然后再进行光照计算的一种技术;
- 如给法线分量添加噪音(法线贴图),在保存扰动值的纹理图中进行查找(视差映射贴图/浮雕贴图);
- 这是一种提升物体真实感的有效方法,但却不需要额外提升物体的几何复杂度,无需对物体增加顶点或者改变顶点位置,达成提升物体表面细节或不规则性方面效果;
下图的右侧为加入法线贴图的效果,左侧则是未加的效果;
4.Bump Mapping分类
主要有法线映射,视差映射,浮雕映射;
其中最常用的是法线映射,增加法线贴图后,对局部的物体表面进行法线扰动,进而改变明暗关系,从而达到增加表面细节的效果。
下图自左往右分别是只加颜色贴图,加入法线贴图(法线映射),加入高度图(视差映射)
二. 法线映射
1.原理
- 法线贴图:一张存有物体局部表面法线信息的一张贴图;
- 计算光照时,程序读取法线图,并获取到当前像素点的法线信息,结合光线信息进行光照计算,从而让物体表现更丰富的细节,随光照方向变换出现实时的变化;
- 法线贴图的做法一般是利用高模映射到低模上来生成,也可以借助一些程序化软件生成
(如SD的Normalsobel节点可以将灰度图转换为法线贴图,ShaderMap软件也可以通过一张图获取其他所需的贴图)
(也有可以自己手绘的选择);
下图是一张传统和次时代的制作流程对比的图,其中高模映射到低模的过程处于次时代中流程中的烘焙过程;
2.实现
- 法线的存储:一般放到模型的切线空间中;
- 切线空间:以切线(T),副切线(B)和法线(N)组成的几何空间,法线垂直于物体表面,切线与物体表面相切,副切线则是由这两个向量叉乘所获;
- 计算光照时,需要把光照运算的向量放到统一坐标系下。读取切线空间法线,需要世界坐标转切线空间的转换矩阵或切线空间转世界空间的转换矩阵,将向量统一到同一坐标系后再进行光照操作。
- 世界与切线空间的转换:需要转换的向量都是方向向量,故可构建3*3的TBN矩阵(其中法线方向为z轴,切线方向为x轴,副切线方向为y轴)作为空间向量的坐标系转换矩阵。逆矩阵为切线空间到世界空间的转换矩阵,由于TBN矩阵为正交矩阵,故他的逆矩阵为TBN的转置矩阵,根据矩阵的乘法规则,只要调换叉乘的前后顺序即可实现从世界空间转切线空间或从切线空间转世界空间;
-
使用切线空间的一些好处:
- 自由度高,若法线记录在模型空间下,则它为相对于模型的绝对法线信息,如球体的法线信息无法传递给环形物体;而切线空间下的是相对法线信息,是对当前物体法线的扰动;
- 方便UV动画:它对现有法线扰动,当UV在模型上进行偏移时可以实现物体表面凹凸效果偏移从而实现UV动画;
- 法线贴图重用:如立方体六个面可以使用一张法线贴图;
- 便于压缩:由于切线空间中,其法线始终垂直于表面向外,在归一化的法线向量里,其法线值始终处于0和1之间;故而可以使用(1-T2 -B2 )1/2推导出法线的值;而模型空间里,理论上法线的值可以为负,即可以从-1到1之间,就不能只通过B和T两个值来推导法线值,必须存储三个值,从而无法实现图像压缩;
3.Unity中法线贴图的压缩格式
-
Unity的非移动平台上,会将法线贴图转换成DXRT5nm格式;该格式仅有GA通道,可以节省空间。
-
移动平台则使用传统的RGB通道,DXRT5nm中,GA存储对应法线的x,y分量,z则是通过勾股定理同套公式获得;
-
在解码贴图时有两步操作:
- 先将其*2-1:原因为编码时,切线与副切线的值的取值范围为-1到1,即可能为正方向也可能为负方向,作为贴图的颜色通道只能存储0到1的值,故做了一个映射将-1到1变成0到1,解码时会进行反向操作;
- 还有就是normal.xy*Scale对法线的效果进行缩放;
三.视差映射
1.视差映射介绍
- 视差映射主要是为了赋予模型物体表面细节的遮挡关系,与法线映射类似是欺骗人眼的效果,它能与法线贴图一同使用,使得平面更显立体从而产生较真实的效果;
- 它引入一张灰度图作为高度图,高度图随一般用于顶点位移,但需要大量的面数才能取得不错效果,而视差映射则非如此,它可以很好解决在低面情况下的效果展现;
下图是视频中调整视差参数值的效果,参数值越高,效果越明显;
2.原理
核心技术在于改变纹理的坐标,通过存储模型的高度图对采样的纹理进行偏移,从而实现高的地方遮挡低的地方的效果;
如下图,蓝色水平线为我们的模型表面,我们要让人眼看见底下凹凸不平的效果。随着视线方向看去,在模型表面A点所表现的纹理应该是在B点深度下的采样效果。
因此我们需要知道B点的UV值,接着问题可以转化为:
已知A点的UV值以及视向量,要获取B点UV值,只需要求偏移值d即可。对应d我们可以利用相似三角形的相关知识来获得;
具体步骤如下:
- 第一步先采样高度图,因为使用在视差映射里的高度图(
其实应该说是深度图吧)离物体表面越近的越偏黑色即值偏向0,离物体表面越远的越偏白色即值偏向于1,故而使用1减去高度图的采样值即可获得高度图存储的高度信息; - 第二步,我们将视向量从世界空间转换为切线空间,然后利用高度信息及视向量间的三角关系,使用相似三角形的知识,获取近似的偏移d值,最终获得的近似B点如下图中的b;
- 明显看到b与B差距有点大,所以该法子比较适合深度差距较小的情况;
- 计算公式为:d=v.xy/v.zhaScale;
视向量的z方向为法线方向即垂直物体表面的方向,所以这里首先利用视向量v.xy/v.z获取一个横向/竖向的tan值,然后利用该值乘上高度HA获得横向的偏移值; - 代码为:
//CG的顶点着色器使用宏做法
// TANGENT_SPACE_ROTATION;
// o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
//CG的不用宏的转换
//float3x3 rotation = float3x3(v.tangent.xyz,o.bDir,v.normal.xyz); //构建TBN等效于宏TANGENT_SPACE_ROTATION
//o.vDir = mul(rotation, ObjSpaceViewDir(v.vertex));
//视频中的应该是HLSL
float height =1-tex2D(_HeightMao,i.uv2.xy).r;
real viewDirTS=TransformWorldToTangent(viewDirWS.xyz,T2W);
float2 offuv = viewDirTS.xy/viewDirTS.z*height*_HeughtScale;
i.uv.xy+=offuv;
i.uv.zw+=offuv;
在获得b点坐标后,我们在采样时就不会采样A而是b,由于深度大高度小的A很容易失去被采样的机会,失去表现机会故而产生遮挡;
三.陡峭视差映射
1.基本思想
同为近似值,将深度分为等距的若干层,然后从最顶端开始采样,每次沿视角方向偏移一定距离,若当前层深度大于采样深度,停止采样并返回结果;
2.算法步骤
我们设当前深度为depthcurrent,当前偏移值为offsetcurrent,根据视向量我们获取到一个tan比值即v.xy/v.z,由于递进的深度depthamount是我们设定的,图中为0.25,所以可以算出递进的偏移值offsetamount=depthamount*v.xy/v.z;
- 第一次,采样dephA=0.8左右,depthcurrent=0,depthcurrent<depthA不满足停止条件,depthcurrent+=0.25,offsetcurrent+=offsetamount;
- 第二次,采样dephB=1.0左右,depthcurrent=0.25,depthcurrent<depthB不满足停止条件,depthcurrent+=0.25,offsetcurrent+=offsetamount;
- 第三次,采样dephC=0.8左右,depthcurrent=0.5,depthcurrent<depthC不满足停止条件,depthcurrent+=0.25,offsetcurrent+=offsetamount;
- 第四次,采样dephD=0.5左右,depthcurrent=0.75,depthcurrent>depthD满足停止条件,返回offsetcurrent作为最终偏移值;
3.存在的问题
- 分层的层数分的越多,虽然会越偏向与实际值,但是性能消耗会变得更大;
- 分层的层数分的少,渲染出来锯齿感会比较明显;
- 改进方法可以利用vdotn(法线向量点乘视向量),对采样层数的最大与最小值的阈值进行限定;
三.浮雕映射(Relief Parallax Mapping, RPM)
1.与视差映射对比
相比视差映射,能够更精确的计算uv偏移量,同时提供更多深度,也可以做自阴影与闭塞的效果;
下图中,上面两个使用视差映射,下面两个使用浮雕映射;
2.浮雕映射的实现
采用射线步进和二分查找决定uv偏移量;
- 射线步进和视差映射一样;
- 二分查找则是通过射线步进找到合适步进后,在步进内使用二分查找找到精确的偏移值;
3.为何不直接使用二分查找的理由
直接二分查找可能会漏掉较薄的区域,产生较大的误差;
四.视差遮蔽映射Parallax Occlusion Mapping, POM)
1.视差遮蔽映射(POM)是陡峭视差映射的另一个改进版本。浮雕映射用了二分搜索法来提升结果精度,但是搜索降低程序性能。视差遮蔽映射旨在比浮雕映射更好的性能下得到比陡峭视差映射更好的效果。但是视差遮蔽映射的效果要比浮雕映射差一些。
2.与浮雕映射不同
区别在于最后一步,它不使用二分查找,而是对射线步进最后的两个端点的uv值进行采样,最后将两个采样结果进行插值,获得插值结果作为最终偏移值即下图中T0与Tp的距离,它是性价比较高的选择;
//顶点着色器中操作
o.vDir =ObjSpaceViewDir(v.vertex);
//片元着色器中操作
half3 nDirTS = UnpackNormal(tex2D(_NormalTex,i.uv));
float3x3 TBN = float3x3(i.tangent,i.bitangent,i.normal);
half3 nDir = normalize(lerp(i.normal,mul(nDirTS,TBN),_UseNormal));
float3 vDir = normalize(i.vDir);
float3 vDirTS = normalize(-mul(TBN,i.vDir));
//使用POM做视差
float3 depthanduv=float3(i.uv,0);//z分量存储现在的检测深度
float d = tex2D(_HeightTex,i.uv).r;
float HeightAmount=1/_HeightLayers;
float2 offsetamount=(vDirTS.xy*d *_HeightScale*HeightAmount/(vDirTS.z)); //递进偏移量
float depth_pre;
float3 depthanduv_pre;
while(d>depthanduv.z){
depthanduv_pre=depthanduv;
depthanduv=float3(depthanduv.xy+offsetamount,depthanduv.z);
depthanduv.z+=HeightAmount;
d = tex2Dlod(_HeightTex,float4(depthanduv.xy,0,0)).a;
}
float w =(depthanduv.z-d)/((depthanduv.z-d)+(d-depthanduv_pre.z));//通过深度的获取插值比
depthanduv.xy =lerp(depthanduv_pre.xy,depthanduv.xy,w);
float2 depthuv =lerp(i.uv, i.uv+offsetamount, _UseHeight);//_UseHeight为Toggle值判断是否开启
half3 var_MainTex = tex2D(_MainTex, depthuv);
上下两张gif分别对应加入法线前后,和加入POM视差遮蔽映射前后效果