fwalker 发表于 2023-1-18 16:05

Unity实现预积分皮肤次表面散射

摘要

上一节实现了Burley Normalized Diffusion,这一节来实现一下性能较优的预积分皮肤方案。
下面我来简单看一下次表面散射的理论知识。Burley Normalized Diffusion和Pre Integrated Subsurface Scattering方案都离不开 Diffusion Profiles。Diffusion Profiles描述了光线是如何进行次表面散射的,通常会用一些曲线来做近似,如高斯等。



可视化的diffusion profile


理论

通常计算次表面散射,需要着色点对多个邻近的像素进行采样,然后根据Diffusion Profiles R(r)求出权重,再求出次表面散射的结果。
而预积分的方案是根据以下观察做出了近似:基色相同的平面下几乎观察不出次表面散射的现象(类似在PS上相同颜色怎么高斯模糊后结果也是一样。),当平面弯曲时,入射辐照度将随着NL的减少而减少,会造成特别的漫反射衰减。然而,随着次表面散射有效地“模糊”入射辐照度,衰减的外观将会改变。



平坦情况观察不出SSS现象



弯曲时会造成特别的漫反射衰减

基于以上观察,预积分的方案是取一个圆,然后通过积分入射辐照度乘以漫射剖面来计算该圆上每个点的最终衰减:


现在来根据几何图看一下积分公式:


图中的公式中为单位圆2 r sin()的r被省略,R(d)是我们的Diffusion Profiles,d是P到K的距离。


cos项是光照的衰减(角度·L),分母是能量守恒归一化。具体推导可以参考大佬的文章:
那么计算出预积分结果后生成出2DLut表,我们运行只需要用N·L和曲率倒数1/r来采样这张Lut来近似次表面散射来代替diffuse。那么曲率怎来获取呢,下图给出了计算方法,但是推荐烘焙一张比较好。





预积分Lut

实现

这里R(d)函数我直接使用上一节的BurleyDiffusionProfile和参数。



Normalized Diffusion

            if (lut == null)
            {
                var previousRenderTexture = RenderTexture.active;
                Material lutMat = new Material(Shader.Find("Plpeline/SkinLut"));

                Vector3 sd = (Vector3)(Vector4)SkinRenderFeature.Debug.scatteringDistance;
                <span class="kt">var shapeParam = new Vector3(Mathf.Min(16777216, 1.0f / sd.x),
                  Mathf.Min(16777216, 1.0f / sd.y),
                  Mathf.Min(16777216, 1.0f / sd.z));
                float maxScatteringDistance = Mathf.Max(sd.x, sd.y, sd.z);
                float filterRadius = SkinRenderFeature.SampleBurleyDiffusionProfile(0.997f, maxScatteringDistance);

                lutMat.SetFloat("_MaxRadius", filterRadius);
                lutMat.SetVector("_ShapeParam", shapeParam);
                lut = new RenderTexture(lutSize, lutSize, 0, RenderTextureFormat.ARGB32, 0)
                {
                  graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.R16G16B16A16_SFloat,
                  filterMode = FilterMode.Bilinear
                };
                CommandBuffer cmd = new CommandBuffer();
                RenderTexture.active = lut;
                cmd.SetRenderTarget(lut);
                cmd.SetViewport(new Rect(0, 0, lutSize, lutSize));
                cmd.ClearRenderTarget(true, true, Color.clear);
                cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.identity);
                cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, lutMat, 0, 0);
                Graphics.ExecuteCommandBuffer(cmd);
                cmd.Release();
                Object.DestroyImmediate(lutMat);
                Texture2D result = new Texture2D(lutSize, lutSize, TextureFormat.RGBAHalf, false);
                result.ReadPixels(new Rect(0, 0, lutSize, lutSize), 0, 0, false);
                result.Apply(false);
                RenderTexture.active = previousRenderTexture;

               
                string path = (EditorUtility.SaveFilePanel("", "Assets", "skinLut", "exr"));
                if (!string.IsNullOrEmpty(path))
                {
                  Debug.Log("路径+" + path);
                  File.WriteAllBytes(path, result.EncodeToEXR());
                  AssetDatabase.Refresh();
                }
            }
预览DiffusionProfile Shader(参考HDRP)
Shader "Hidden/DrawDiffusionProfile"
{
    SubShader
    {
      Tags{ "RenderPipeline" = "UniversalPipeline" }
      Pass
      {
            Cull   Off
            ZTestAlways
            ZWrite Off
            BlendOff

            HLSLPROGRAM
            #pragma editor_sync_compilation
            #pragma target 4.5
            #pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch

            #pragma vertex Vert
            #pragma fragment Frag


      #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
      #include "SkinCommon.hlsl"

      float4 _ShapeParam;
      float _MaxRadius; // See 'DiffusionProfile'

      //-------------------------------------------------------------------------------------
      // Implementation
      //-------------------------------------------------------------------------------------

      struct Attributes
      {
            float3 vertex   : POSITION;
            float2 texcoord : TEXCOORD0;
      };

      struct Varyings
      {
            float4 vertex   : SV_POSITION;
            float2 texcoord : TEXCOORD0;
      };

      Varyings Vert(Attributes input)
      {
            Varyings output;
            // We still use the legacy matrices in the editor GUI
            output.vertex = mul(unity_MatrixVP, float4(input.vertex, 1));
            output.texcoord = input.texcoord.xy;
            return output;
      }

      float4 Frag(Varyings input) : SV_Target
      {

            // Profile display does not use premultiplied S.
            floatr = _MaxRadius * 0.5 * length(input.texcoord - 0.5); // (-0.25 * R, 0.25 * R)
            float3 S = _ShapeParam.rgb;
            float3 M;

            // Gamma in previews is weird...
            //S = S * S;
            M = EvalBurleyDiffusionProfile(r, S) / r; // Divide by 'r' since we are not integrating in polar coords
            return float4(sqrt(M), 1);
      }
      ENDHLSL
    }
    }
      Fallback Off
}
生成Lut的shader
Shader "Plpeline/SkinLut"
{
    SubShader
    {
      Pass
      {
            Cull   Off
            ZTestAlways
            ZWrite Off
            BlendOff

            HLSLPROGRAM
            #pragma editor_sync_compilation
            #pragma target 4.5
            #pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch

            #pragma vertex Vert
            #pragma fragment Frag


      #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
      #include "SkinCommon.hlsl"


      float4 _ShapeParam;
      float _MaxRadius; // See 'DiffusionProfile'



      struct Attributes
      {
            float4 vertex   : POSITION;
            float2 texcoord : TEXCOORD0;
      };

      struct Varyings
      {
            float4 vertex   : SV_POSITION;
            float2 texcoord : TEXCOORD0;
      };

      Varyings Vert(Attributes input)
      {
            Varyings output;
            output.vertex = TransformObjectToHClip(input.vertex.xyz);
            output.texcoord = input.texcoord.xy;
            return output;
      }
      

      float4 Frag(Varyings input) : SV_Target
      {
            float4 reslut = 1;
            float2 uv = input.texcoord;
            float cosTheta = uv.x *2 -1;
            float r = 1.0/(max(0.00001,uv.y));
            float rad2deg = 57.29578;
            float theta = acos(cosTheta) * rad2deg;
            float3 totalWeights = 0.0;
            float3 totalLight = 0.0;
            int sampleCount = 128;
            float sampleAngle = (theta - 90.0);
            int stepSize = 180.0 / sampleCount;
            float3 S = _ShapeParam.rgb;
            float deg2rad = (PI / 180.0);
            for (int i = 0; i < sampleCount; i++)
            {
                float diffuse = saturate(cos(sampleAngle * deg2rad));
                float dAngle = abs(theta - sampleAngle);
                float sampleDist = abs(2.0f * r * sin(dAngle * 0.5f * deg2rad));
                float3 weights = EvalBurleyDiffusionProfile(sampleDist, S);
                totalWeights += weights;
                totalLight += diffuse * weights;
                sampleAngle += stepSize;
            }
            reslut.xyz = totalLight.xyz / totalWeights.xyz;
            return reslut;
      }
      ENDHLSL
    }
    }
      Fallback Off
}



可以通过调整Diffuse Profile参数来获取不同散射效果







运行时
漫反射项
    float3 lutColor = SAMPLE_TEXTURE2D(_SkinLut, sampler_SkinLut, float2((nl * 0.5 + 0.5) * shadow, inverCurve));
    float3 diffR = kD * albedo * lutColor * lightColor;简单法线模糊一下
float blur = cuv * _NormalSSSBlur; // 次表面越强, 法线越糊
float3 diffuseNormal0 = UnpackNormalScale(SAMPLE_TEXTURE2D_LOD(_BumpMap, sampler_BumpMap, uv, blur),1.0);透射采用上一节的方案
// Ref: Steve McAuley - Energy-Conserving Wrapped Diffuse
real ComputeWrappedDiffuseLighting(real NdotL, real w)
{
    return saturate((NdotL + w) / ((1.0 + w) * (1.0 + w)));
}

// Computes the fraction of light passing through the object.
// Evaluate Int{0, inf}{2 * Pi * r * R(sqrt(r^2 + d^2))}, where R is the diffusion profile.
// Note: 'volumeAlbedo' should be premultiplied by 0.25.
// Ref: Approximate Reflectance Profiles for Efficient Subsurface Scattering by Pixar (BSSRDF only).
float3 ComputeTransmittanceDisney(float3 S, float3 volumeAlbedo, float thickness)
{
    // Thickness and SSS mask are decoupled for artists.
    // In theory, we should modify the thickness by the inverse of the mask scale of the profile.
    // thickness /= subsurfaceMask;

    float3 exp_13 = exp2(((LOG2_E * (-1.0 / 3.0)) * thickness) * S); // Exp[-S * t / 3]

    // Premultiply & optimize: T = (1/4 * A) * (e^(-S * t) + 3 * e^(-S * t / 3))
    return volumeAlbedo * (exp_13 * (exp_13 * exp_13 + 3));
}
    float3 diffT = WrappedDiffuseLighting(-nl, 0.0) * albedo * lightColor * shadow;
    if (isPunctualLight)
    {
      float thicknessInUnits = -nl;// (distFrontFaceToLight - distBackFaceToLight) /* * -NdotL */;
      float thicknessInMillimeters = thicknessInUnits * 1000.0 * _WorldScale;
      float3 S = _ShapeParamsAndMaxScatterDists.rgb;
      float dt = max(0, thicknessInMillimeters - thickness);
      float3 exp_13 = exp2(((LOG2_E * (-1.0 / 3.0)) * dt) * S); // Exp[-S * dt / 3]
      transmittance = transmittance * exp_13;
    }
    return diffR + diffT * transmittance;
高光使用普通的高光
效果

下面看一下只计算方向光的结果


曲率缩放值可以调节使次表面散射效果强弱



调节曲率系数



透射效果
页: [1]
查看完整版本: Unity实现预积分皮肤次表面散射