• 深入URP之Shader篇6: SimpleLit Shader分析(2) Vertex Shader


    Simpe Lit Forward Pass

    Vertex Shader 函数

    看看在顶点shader中都计算了什么

    • 计算顶点坐标
      这个和之前一样:
      VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
      再复习一下,这个函数位于ShaderVariablesFunctions.hlsl中。
    • 计算法线和切线
      VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
      输入的是ObjectSpace(OS)的法线和切线,这个是Unity在导入时处理好的数据。具体计算如下:
    VertexNormalInputs GetVertexNormalInputs(float3 normalOS, float4 tangentOS)
    {
        VertexNormalInputs tbn;
    
        // mikkts space compliant. only normalize when extracting normal at frag.
        real sign = tangentOS.w * GetOddNegativeScale();
        tbn.normalWS = TransformObjectToWorldNormal(normalOS);
        tbn.tangentWS = TransformObjectToWorldDir(tangentOS.xyz);
        tbn.bitangentWS = cross(tbn.normalWS, tbn.tangentWS) * sign;
        return tbn;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    其中,GetOddNegativeScale函数位于SPR Core的SpaceTransforms.hlsl中:

    real GetOddNegativeScale()
    {
        // FIXME: We should be able to just return unity_WorldTransformParams.w, but it is not
        // properly set at the moment, when doing ray-tracing; once this has been fixed in cpp,
        // we can revert back to the former implementation.
        return unity_WorldTransformParams.w >= 0.0 ? 1.0 : -1.0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里面的unity_WorldTransformParams却又位于URP的UnityInput.hlsl中:

    real4 unity_WorldTransformParams; // w is usually 1.0, or -1.0 for odd-negative scale transforms
    
    • 1

    回到GetVertexNormalInputs中,首先计算world space的法线和切线没啥问题,只不过变换法线需要用逆转置矩阵因此使用一个特定的函数TransformObjectToWorldNormal,而变换切线使用普通的变换向量的函数TransformObjectToWorldDir即可,这两个函数自己写shader也经常用。而计算副切线是使用法线和切线的叉积,但是其结果需要校正符号。而这个符号是Object space切线的w和GetOddNegativeScale返回值的乘积。
    这个我暂时没弄清楚,按照之前掌握的知识,Object space切线的w的正负,是由DCC工具计算切线时的手向性决定的,而unity_WorldTransformParams的w按照unity的注释和奇数次的负缩放有关,这个很模糊。先留着等搞清楚再修改。
    当然如果只是学习怎么用,怎么在unity shader中计算切线,就照着这个来就行。

    • 顶点光照
      half3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS);
      调用VertexLighting函数,输入世界空间的位置和法线,计算逐顶点的光照颜色。
      VertexLighting函数在Lighting.hlsl中:
    half3 VertexLighting(float3 positionWS, half3 normalWS)
    {
        half3 vertexLightColor = half3(0.0, 0.0, 0.0);
    
    #ifdef _ADDITIONAL_LIGHTS_VERTEX
        uint lightsCount = GetAdditionalLightsCount();
        for (uint lightIndex = 0u; lightIndex < lightsCount; ++lightIndex)
        {
            Light light = GetAdditionalLight(lightIndex, positionWS);
            half3 lightColor = light.color * light.distanceAttenuation;
            vertexLightColor += LightingLambert(lightColor, light.direction, normalWS);
        }
    #endif
    
        return vertexLightColor;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可见,必须激活关键字_ADDITIONAL_LIGHTS_VERTEX,才会计算附加光的顶点光照。或依次获取所有的附加光,得到其颜色强度,然后使用LightingLambert函数计算出一个漫反射光照颜色:

    half3 LightingLambert(half3 lightColor, half3 lightDir, half3 normal)
    {
        half NdotL = saturate(dot(normal, lightDir));
        return lightColor * NdotL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    所以,顶点光照计算的就是附加光的漫反射lambert颜色,所有附加光都计算然后叠加到一起。

    • 雾的参数
      half fogFactor = ComputeFogFactor(vertexInput.positionCS.z);
      这个函数之前Unlit的时候看过,但是没多做解释。其实就是简单的深度雾,根据当前坐标的z值在clip和far之间的比例(统一到[0,1]之间)使用线性插值或者指数函数计算雾的颜色值。关于z值的计算回头统一有一篇集中分析。
    • 法线贴图的参数
    #ifdef _NORMALMAP
        output.normal = half4(normalInput.normalWS, viewDirWS.x);
        output.tangent = half4(normalInput.tangentWS, viewDirWS.y);
        output.bitangent = half4(normalInput.bitangentWS, viewDirWS.z);
    #else
        output.normal = NormalizeNormalPerVertex(normalInput.normalWS);
        output.viewDir = viewDirWS;
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果使用法线贴图,则会输出世界空间的法线,切线,和副切线。并且会把世界空间的视线方向夹带在这3个向量的w中,节省了一个输出向量(因为这些向量是varying,需要GPU去插值的,越少越好,而Vector4的插值和Vector3是一样的消耗,SIMD的原因)。如果不使用法线贴图,那么只要输出世界空间的法线和视线方向即可。注意这儿的法线使用了一个NormalizeNormalPerVertex函数处理:

    // A word on normalization of normals:
    // For better quality normals should be normalized before and after
    // interpolation.
    // 1) In vertex, skinning or blend shapes might vary significantly the lenght of normal.
    // 2) In fragment, because even outputting unit-length normals interpolation can make it non-unit.
    // 3) In fragment when using normal map, because mikktspace sets up non orthonormal basis.
    // However we will try to balance performance vs quality here as also let users configure that as
    // shader quality tiers.
    // Low Quality Tier: Normalize either per-vertex or per-pixel depending if normalmap is sampled.
    // Medium Quality Tier: Always normalize per-vertex. Normalize per-pixel only if using normal map
    // High Quality Tier: Normalize in both vertex and pixel shaders.
    real3 NormalizeNormalPerVertex(real3 normalWS)
    {
        #if defined(SHADER_QUALITY_LOW) && defined(_NORMALMAP)
            return normalWS;
        #else
            return normalize(normalWS);
        #endif
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这个函数会根据shader quality以及是否使用法线贴图来决定是否要归一化法线,具体见上面的注释。

    • 输出lightmap UV和SH
    OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV);
    OUTPUT_SH(output.normal.xyz, output.vertexSH);
    
    • 1
    • 2

    这两个OUTPUT是根据是否启用lightmap定义的宏,如下:

    #ifdef LIGHTMAP_ON
        #define DECLARE_LIGHTMAP_OR_SH(lmName, shName, index) float2 lmName : TEXCOORD##index
        #define OUTPUT_LIGHTMAP_UV(lightmapUV, lightmapScaleOffset, OUT) OUT.xy = lightmapUV.xy * lightmapScaleOffset.xy + lightmapScaleOffset.zw;
        #define OUTPUT_SH(normalWS, OUT)
    #else
        #define DECLARE_LIGHTMAP_OR_SH(lmName, shName, index) half3 shName : TEXCOORD##index
        #define OUTPUT_LIGHTMAP_UV(lightmapUV, lightmapScaleOffset, OUT)
        #define OUTPUT_SH(normalWS, OUT) OUT.xyz = SampleSHVertex(normalWS)
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果启用了lightmap,就会对lightmap UV进行变换并输出到output.lightmapUV中,否则啥也不干。对于球谐系数SH则是相反,使用lightmap就啥也不干,否则就会采样该顶点的球谐系数:

    // Samples SH L0, L1 and L2 terms
    half3 SampleSH(half3 normalWS)
    {
        // LPPV is not supported in Ligthweight Pipeline
        real4 SHCoefficients[7];
        SHCoefficients[0] = unity_SHAr;
        SHCoefficients[1] = unity_SHAg;
        SHCoefficients[2] = unity_SHAb;
        SHCoefficients[3] = unity_SHBr;
        SHCoefficients[4] = unity_SHBg;
        SHCoefficients[5] = unity_SHBb;
        SHCoefficients[6] = unity_SHC;
    
        return max(half3(0, 0, 0), SampleSH9(SHCoefficients, normalWS));
    }
    
    // SH Vertex Evaluation. Depending on target SH sampling might be
    // done completely per vertex or mixed with L2 term per vertex and L0, L1
    // per pixel. See SampleSHPixel
    half3 SampleSHVertex(half3 normalWS)
    {
    #if defined(EVALUATE_SH_VERTEX)
        return SampleSH(normalWS);
    #elif defined(EVALUATE_SH_MIXED)
        // no max since this is only L2 contribution
        return SHEvalLinearL2(normalWS, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);
    #endif
    
        // Fully per-pixel. Nothing to compute.
        return half3(0.0, 0.0, 0.0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    SH系数是通过light probe烘焙出来的低频球谐光照信息,使用SH函数可以用极小的代价去存储光照信息。具体也许会单独讲一篇。

    • 输出雾参数和顶点光照
      output.fogFactorAndVertexLight = half4(fogFactor, vertexLight);
      将计算出来的雾参数和顶点光照颜色合并到一个向量中输出。
    • 在顶点上计算阴影坐标
    #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
        output.shadowCoord = GetShadowCoord(vertexInput);
    #endif
    
    • 1
    • 2
    • 3

    必须是启用关键字REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR,这样就会在顶点级别计算阴影坐标然后varying插值,这样显然精度较低,但是会提高效率,具体什么情况会这样用后面再看。另外关于阴影肯定是要单独一篇或几篇分析的,所以这儿就不深入了。

    本篇小结

    本篇分析了SimplLit Forward pass的 Vertex Shader 函数LitPassVertexSimple,这个函数基本上就是为光照提供各种参数,只有附加光的漫反射有可能在这儿计算。这个函数考虑了有无NormalMap, LightMap以及顶点光照等各种情况。下篇我们就看一下这个simple lit到底在fragment shader中是怎么计算的。

  • 相关阅读:
    WPF使用HelixToolkit加载obj格式模型
    大数据Hadoop之——Hadoop 3.3.4 HA(高可用)原理与实现(QJM)
    centOS7管理开放防火墙端口
    【Leetcode】top 100 矩阵
    Spring和Spring Mvc整合详解
    谷歌紧急更新,Chrome 今年第二个零日漏洞曝光
    ThinkPHP5目录结构
    Sublime Text 常用且比较实用的插件
    数智化升级必答题,尚美数智如何成为酒店行业标准答案?
    Android studio在Ubuntu桌面上 创建桌面图标,以及导航栏图标
  • 原文地址:https://blog.csdn.net/n5/article/details/128198624