视差映射

发布时间 2023-03-25 17:21:13作者: kayiko

视差映射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
        }
    }
}
View Code

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
        }
    }
}
View Code