视差映射Parallax Mapping
- 主要为了赋予模型表面遮挡关系的细节。引入了一张高度图
- 可以和法线贴图一起使用,来产生一些真实的效果
- 高度图一般视为顶点位移来使用,此时需要三角形足够多,模型足够精细,否则看起来会有块状
- 如果在有限的三角形面的情况下,怎么办?这就用到了视差映射技术
- 视差映射技术:
- 核心:改变纹理坐标
- 利用模型表面高度信息来对纹理进行偏移(例如:低位置的信息被高位置的信息遮挡掉了,所以会采样更高的信息)
具体实现
- 视差映射的具体算法:如何在知道A的uv值的情况下,算出B的uv值
- 知道AB两者的偏移量即可
- 偏移量的获得:用近似的方法去求解
- 首先拿A的高度信息进行采样,得到物体表面距离水平面(0.0)的深度值Ha。
- 用深度值Ha和视线的三角关系算出物体上等比的偏移方向,算出近似的B点
- 可以看到图中近似点B和实际点B还是有挺大差距的,所以模拟度比较低
- 得到偏移之后B点的uv,再去对法线贴图进行采样、计算时,就不会采样A点了,而是B点
- 理解:视差贴图是如何产生遮挡效果的
- 当视线看到的是A点这样深度比较大的,那么视差贴图计算出的偏移值也是非常大的,这样A点最终被渲染出来的机会就比较小(偏移后就被采样到其他点上了)
- 当视线看到B点这样深度比较小的点,计算出来的偏移就比较下,甚至原来点的附近,所以被采样的机会就比较大
- 深度大的点很容易被深度小的点覆盖掉,这样就会表现出遮挡的效果
陡视差映射 Steep Parallax Mapping
- 基本思想:
- 将物体表面分为若干层,从最顶端开始采样,每次沿着视角方向偏移一定的值
- 如果当前采样的层数,大于实际采样的深度,就停止采样。(例如图中D点,采样到0.75层,实际是0.5,就停止采样,返回偏移坐标)
- 还原陡视差映射的算法:
- 首先对A点采样,得到深度大约为0.8的位置,而其对应视线深度为0.0,不符合,继续采样
- 采样B点,深度为1,视线深度为0.25,不符合,继续采样
- 采样C点,深度大约为0.8,视线深度为0.5,不符合,继续采样
- 采样D点,采样深度为0.75,视线深度为0.5,符合上述的条件,认为是比较合理的一个偏移点,就返回结果。
- 陡视差的问题:
- 在于分层机制,如果分层多,性能开销就会大;分层小,渲染锯齿就比较明显。
- 可以根据视角和法线的角度进行限定
- 锯齿问题会在浮雕贴图上做改善
浮雕映射 Relief Mapping
原理
- 可以更精确的计算uv偏移量、提供更多的深度、还可以做自阴影以及闭塞效果
- 例如下图:可以看到浮雕的凹凸深度明显更大,且凹凸有自阴影效果
- 浮雕映射一般用射线步进和二分查找来决定uv偏移量
- 第一步:射线步进部分,和视差贴图一样
- 后边:二分查找部分:通过射线步进找到合适的步进后,在此步进内使用二分查找来找到精确的偏移值
- 为什么不直接使用二分查找?
- 会产生比较大的误差
- 下图为例
- 如果直接使用二分查找,在深度0和1的中间的1点,进一步为2点 -> 3点 ->Q点。但我们要的结果是P点,可以看到结果很明显是错误的
视差闭塞贴图(POM = Parallax Occlusion Mapping)
- 相对于浮雕贴图,不用之处在于最后一步
- 浮雕贴图是在确认最后步进之后进行二分查找
- 视差闭塞贴图是在最后步进的两端uv值进行采样,采样之后再对这两个结果进行插值,插值的结果作为P点最终的偏移值
- 优点:
- 相对于浮雕映射,性能更好(最后只做插值,而浮雕要做二分查找)
- 相对于陡视差贴图,精确性更好
- 要求:
- 因为最后要做插值,所以要求表面是相对比较平滑/连续的,如果有莫名的凸起结果可能会出错
效果对比:
上图分别是 原图 加入法线贴图 普通视差贴图 以及浮雕贴图
实现代码
urp中实现
Shader "Bump Mapping" { Properties { _MainTex ("Texture", 2D) = "white" {} [Toggle(_NORMALMAP)] _EnableBumpMap("Enable Normal/Bump Map", Float) = 0.0 _NormalMap("NormalMap",2D) = "bump" {} _NormalScale("NormalScale" ,Float) = 1 [Toggle(_HEIGHTMAP)] _EnableHeightMap("Enable Height Map",Float) = 0.0 [Toggle(_RELIEFMAP)] _EnableReliefMap("Enable Relief Map",Float) = 0.0 _HeightMap("HeigheMap",2D) = "white"{} _HeightScale("HeightScale",range(0,0.5)) = 0.005 _Smoothness("Smoothness",range(0,2)) = 0.5 _Metallic("Metallic",range(0,1)) = 0.2 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 HLSLINCLUDE // Material Keywords #pragma shader_feature _NORMALMAP #pragma shader_feature _HEIGHTMAP #pragma shader_feature _RELIEFMAP //#pragma shader_feature _SPECULAR_COLOR #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" ENDHLSL Pass { Name "URPLighting" Tags { "LightMode" = "UniversalForward" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; float4 tangentOS : TANGENT; }; struct v2f { float4 positionCS : SV_POSITION; float3 normalWS:TEXCOORD0; float3 viewDirWS:TEXCOORD1; // Note this macro is using TEXCOORD2 DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 2); float4 uv : TEXCOORD3; #if defined(_HEIGHTMAP) || defined (_NORMALMAP) float4 tangentWS:TEXCOORD4; #endif #ifdef _HEIGHTMAP float4 uv2 : TEXCOORD5; #endif }; sampler2D _MainTex; float4 _MainTex_ST; float _Smoothness; sampler2D _NormalMap; float4 _NormalMap_ST; float _Metallic; float _NormalScale; sampler2D _HeightMap; float4 _HeightMap_ST; float _HeightScale; v2f vert(appdata v) { v2f o; VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz); o.positionCS = positionInputs.positionCS; o.viewDirWS = GetWorldSpaceViewDir(positionInputs.positionWS); o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex); VertexNormalInputs normalInputs = GetVertexNormalInputs(v.normalOS, v.tangentOS); o.normalWS = normalInputs.normalWS; #if defined (_HEIGHTMAP) || defined (_NORMALMAP) real sign = v.tangentOS.w * GetOddNegativeScale(); half4 tangentWS = half4(normalInputs.tangentWS.xyz, sign); o.tangentWS = tangentWS; #endif #ifdef _NORMALMAP o.uv.zw = TRANSFORM_TEX(v.uv, _NormalMap); #endif #ifdef _HEIGHTMAP o.uv2.xy = TRANSFORM_TEX(v.uv, _HeightMap); #endif OUTPUT_SH(normalInputs.normalWS.xyz, o.vertexSH); return o; } //视差映射 float2 ParallaxMapping(float2 Huv, real3 viewDirTS) { float height = tex2D(_HeightMap, Huv).r; float2 offuv = viewDirTS.xy / viewDirTS.z * height * _HeightScale; return offuv; } //陡峭视差映射 float2 SteepParallaxMapping(float2 uv, real3 viewDirTS) { float numLayers = 20.0; float layerHeight = 1.0 / numLayers; float currentLayerHeight = 0.0; float2 offlayerUV = viewDirTS.xy / viewDirTS.z * _HeightScale; float2 Stepping = offlayerUV / numLayers; float2 currentUV = uv; float2 AddUV = float2(0, 0); float currentHeightMapValue = tex2D(_HeightMap, currentUV + AddUV).r; for (int i = 0; i < numLayers; i++) { if (currentLayerHeight > currentHeightMapValue) { return AddUV; } AddUV += Stepping; currentHeightMapValue = tex2D(_HeightMap, currentUV + AddUV).r; currentLayerHeight += layerHeight; } return AddUV; } //浮雕贴图 float2 ReliefMapping(float2 uv, real3 viewDirTS) { float2 offlayerUV = viewDirTS.xy / viewDirTS.z * _HeightScale; float RayNumber = 20; float layerHeight = 1.0 / RayNumber; float2 SteppingUV = offlayerUV / RayNumber; float offlayerUVL = length(offlayerUV); float currentLayerHeight = 0; float2 offuv= float2(0,0); for (int i = 0; i < RayNumber; i++) { offuv += SteppingUV; float currentHeight = tex2D(_HeightMap, uv + offuv).r; currentLayerHeight += layerHeight; if (currentHeight < currentLayerHeight) { break; } } float2 T0 = uv-SteppingUV, T1 = uv + offuv; for (int j = 0;j<20;j++) { float2 P0 = (T0 + T1) / 2; float P0Height = tex2D(_HeightMap, P0).r; float P0LayerHeight = length(P0) / offlayerUVL; if (P0Height < P0LayerHeight) { T0 = P0; } else { T1= P0; } } return (T0 + T1) / 2 - uv; } half4 frag(v2f i) : SV_Target { #if defined(_HEIGHTMAP) || defined (_NORMALMAP) float sgn = i.tangentWS.w; float3 bitangent = sgn * cross(i.normalWS.xyz, i.tangentWS.xyz); half3x3 T2W = half3x3(i.tangentWS.xyz, bitangent.xyz, i.normalWS.xyz); #endif float3 viewDirWS = normalize(i.viewDirWS); //视差映射 #ifdef _HEIGHTMAP real3 viewDirTS = normalize(TransformWorldToTangent(-viewDirWS.xyz,T2W)); float2 offuv = float2(0,0); #ifdef _RELIEFMAP offuv = ReliefMapping( i.uv2.xy, viewDirTS); //陡峭视差映射 #else offuv = ParallaxMapping( i.uv2.xy, viewDirTS); //普通视差映射 #endif i.uv.xy += offuv; i.uv.zw += offuv; #endif // sample the texture half4 col = tex2D(_MainTex, i.uv.xy); // URP 光照 SurfaceData surfaceData = (SurfaceData)0; surfaceData.albedo = col; surfaceData.alpha = col.a; surfaceData.smoothness = _Smoothness; surfaceData.metallic = _Metallic; surfaceData.occlusion = 1; InputData inputData = (InputData)0; inputData.viewDirectionWS = viewDirWS; //法线映射 #ifdef _NORMALMAP float3 normalTS = UnpackNormalScale(tex2D(_NormalMap,i.uv.zw), _NormalScale); i.normalWS = TransformTangentToWorld(normalTS, T2W); #endif inputData.bakedGI = SAMPLE_GI(i.lightmapUV, i.vertexSH, i.normalWS); inputData.normalWS = i.normalWS; half4 color = UniversalFragmentPBR(inputData, surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.occlusion, surfaceData.emission, surfaceData.alpha); return color; } ENDHLSL } } }
build-in实现
Shader "Unlit/UnitLM" { Properties { _MainTex ("MainTex", 2D) = "white" {} _NormalTex ("NormalTex", 2D) = "bump" {} _HeightTex ("HeightTex", 2D) = "white" {} _Cubemap ("Cubemap", 2D) = "_Skybox" {} _MainCol ("MainCol", color) = (0.5,0.5,0.5,1.0) _Diffuse ("Diffuse", color) = (1,1,1,1) _Specular ("Specular", color) = (1,1,1,1) _Gloss ("Gloss", range(1,255)) = 50 _HeightScale ("HeightScale", range(0,0.15)) = 0.5 _NormalScale ("NormalScale", range(0,1)) = 1 } SubShader { Tags { "RenderType"="Opaque" } Pass { Name "StudyLM" Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "AutoLight.cginc" #include "Lighting.cginc" #pragma multi_compile_fwdbase_fullshadows #pragma target 3.0 struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TEXCOORD0; float2 texcoords : TEXCOORD1; }; struct v2f { float4 posCS : SV_POSITION; float4 posWS : TEXCOORD0; float3 normalDirWS : TEXCOORD1; float3 tangentDirWS : TEXCOORD2; float3 bitangentDirWS : TEXCOORD3; float2 uv0 : TEXCOORD4; }; uniform sampler2D _MainTex; uniform float4 _MainTex_ST; uniform sampler2D _NormalTex; uniform float4 _NormalTex_ST; uniform sampler2D _HeightTex; uniform sampler2D _Cubemap; uniform half4 _MainCol; uniform half4 _Diffuse; uniform half4 _Specular; uniform half _Gloss; uniform half _HeightScale; uniform half _NormalScale; v2f vert (a2v v) { v2f o; o.posCS = UnityObjectToClipPos(v.vertex); o.posWS = mul(unity_ObjectToWorld, v.vertex); o.normalDirWS = normalize(UnityObjectToWorldNormal(v.normal)); o.tangentDirWS = normalize(mul(unity_ObjectToWorld, v.tangent).xyz); //切线 o.bitangentDirWS = normalize(cross(o.normalDirWS,o.tangentDirWS) * v.tangent.w); //副切线 //o.uv0 = v.texcoords; o.uv0.xy = TRANSFORM_TEX(v.texcoords, _MainTex); return o; } // 视差映射 float2 ParallaxMapping(float2 texcoords, float3 viewDirTS) { // 高度采样 float height = tex2D(_HeightTex, texcoords).r; // 视点方向越接近法线 UV偏移越小 float2 offuv = viewDirTS.xy / viewDirTS.z * height * _HeightScale; return texcoords - offuv; } // 陡峭视差映射 float2 SPM(float2 texCoords, float3 viewDirTS) { // 高度层数 /* trick--分层次数由视点方向与法向夹角来决定,当视点的方向和法线方向越靠近时, 那么采样需要的偏离也越小,那么就可以采用较少的高度分层,反之则需要更多的分层。*/ float minLayers = 20; float maxLayers = 100; float numLayers = lerp(maxLayers, minLayers, abs(dot(float3(0.0,0.0,1.0), viewDirTS))); // 每层高度 float layerHeight = 1.0 / numLayers; // 当前层级高度 float currentLayerHeight = 0.0; // 视点方向偏移量 float2 offsetuv = viewDirTS.xy / viewDirTS.z * _HeightScale; // 每层高度偏移量 float2 deltaTexCoords = offsetuv / numLayers; // 当前 UV float2 currentTexcoords = texCoords; // 当前UV位置高度图的值 float currentHeightMapValue = tex2D(_HeightTex, currentTexcoords).r; while(currentLayerHeight < currentHeightMapValue) { // 按高度层级进行UV偏移 currentTexcoords += deltaTexCoords; // 从高度贴图采样获取当前UV位置的高度 currentHeightMapValue = tex2Dlod(_HeightTex, float4(currentTexcoords, 0, 0)).r; // 采样点高度 currentLayerHeight += layerHeight; } return currentTexcoords; } // 视差遮蔽映射 float2 Custom_SPM(float2 texCoords, float3 viewDirTS) { // 高度层数 /* trick--分层次数由视点方向与法向夹角来决定,当视点的方向和法线方向越靠近时, 那么采样需要的偏离也越小,那么就可以采用较少的高度分层,反之则需要更多的分层。*/ float minLayers = 20; float maxLayers = 100; float numLayers = lerp(maxLayers, minLayers, abs(dot(float3(0.0,0.0,1.0), viewDirTS))); // 每层高度 float layerHeight = 1.0 / numLayers; // 当前层级高度 float currentLayerHeight = 0.0; // 视点方向偏移量 float2 offsetuv = viewDirTS.xy / viewDirTS.z * _HeightScale; // 每层高度偏移量 float2 deltaTexCoords = offsetuv / numLayers; // 当前 UV float2 currentTexcoords = texCoords; // 当前UV位置高度图的值 float currentHeightMapValue = tex2D(_HeightTex, currentTexcoords).r; while(currentLayerHeight < currentHeightMapValue) { // 按高度层级进行UV偏移 currentTexcoords += deltaTexCoords; // 从高度贴图采样获取当前UV位置的高度 currentHeightMapValue = tex2Dlod(_HeightTex, float4(currentTexcoords, 0, 0)).r; // 采样点高度 currentLayerHeight += layerHeight; } // 前一个采样点 float2 preTexcoords = currentTexcoords - deltaTexCoords; // 线性插值 float afterHeight = currentHeightMapValue - currentLayerHeight; float beforeHeight = tex2D(_HeightTex, preTexcoords).r - (currentLayerHeight - layerHeight); float weight = afterHeight / (afterHeight - beforeHeight); float2 finalTexcoords = preTexcoords * weight + currentTexcoords * (1.0 - weight); return finalTexcoords; } float4 frag (v2f i) : SV_Target { // 准备向量 half3x3 TBN = half3x3(i.tangentDirWS, i.bitangentDirWS, i.normalDirWS); half3 viewDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); half3 viewDirTS = normalize(mul(TBN, viewDirWS)); // 视差映射 float2 pm_uv = Custom_SPM(i.uv0, viewDirTS); half3 normalDirTS = UnpackNormal(tex2D(_NormalTex, pm_uv)).xyz; half3 normalDirWS = normalize(lerp(i.normalDirWS,mul(normalDirTS, TBN),_NormalScale)); half3 lightDirWS = normalize(_WorldSpaceLightPos0.xyz); half3 halfDirWS = normalize(lightDirWS + viewDirWS); half3 reflectDirWS = normalize(reflect(-lightDirWS, normalDirWS)); // 准备向量积 half NdotL = dot(normalDirWS, lightDirWS); half NdotH = dot(normalDirWS, halfDirWS); half VdotR = dot(viewDirWS, reflectDirWS); // 光照模型 // 环境光 half4 MainTex = tex2D(_MainTex, pm_uv) * _MainCol; half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse * MainTex.rgb; half3 diffuse = lerp(ambient * _Diffuse.rgb * MainTex.rgb, max(0, NdotL) * _LightColor0.rgb * _Diffuse.rgb * MainTex, _Gloss / 255); // blinnphong half3 specular = pow(max(0, NdotH), _Gloss) * _LightColor0.rgb * _Specular.rgb; // phong //specular = pow(max(0, VdotR), _Gloss) * _LightColor0.rgb * _Specular.rgb; specular = lerp(diffuse * specular, specular, _Gloss / 255); half3 BlinnPhong = ambient + diffuse + specular; float3 finalRGB = BlinnPhong; return float4(finalRGB,1.0); } ENDCG } } }