深入URP之Shader篇8: SimpleLit Shader分析(4)

Simple Lit Forward Pass

本篇继续 Fragment shader 函数

InitializeInputData

InputData inputData;
InitializeInputData(input, normalTS, inputData);

InputData是URP ShaderLibrary的Input.hlsl中定义的一个结构体:

struct InputData
{
    
    
    float3  positionWS;
    half3   normalWS;
    half3   viewDirectionWS;
    float4  shadowCoord;
    half    fogCoord;
    half3   vertexLighting;
    half3   bakedGI;
    float2  normalizedScreenSpaceUV;
    half4   shadowMask;
};

这个结构体包含了光照计算所需要的所有输入参数,有些参数是直接从Varying获取,有些是进一步计算得到。这个结构体大量用于URP的各个Shader中。而InitializeInputData的作用就是设置这个结构体的各个成员值,这个函数名也是URP的一个惯例,很多shader中都会有这个函数,当然函数参数可能不一样,但是作用是一样的。我们看下SimpleLit Shader的这个函数吧。

void InitializeInputData(Varyings input, half3 normalTS, out InputData inputData)
{
    
    
    inputData.positionWS = input.posWS;

#ifdef _NORMALMAP
    half3 viewDirWS = half3(input.normal.w, input.tangent.w, input.bitangent.w);
    inputData.normalWS = TransformTangentToWorld(normalTS,
        half3x3(input.tangent.xyz, input.bitangent.xyz, input.normal.xyz));
#else
    half3 viewDirWS = input.viewDir;
    inputData.normalWS = input.normal;
#endif

    inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
    viewDirWS = SafeNormalize(viewDirWS);

    inputData.viewDirectionWS = viewDirWS;

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    inputData.shadowCoord = input.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
#else
    inputData.shadowCoord = float4(0, 0, 0, 0);
#endif

    inputData.fogCoord = input.fogFactorAndVertexLight.x;
    inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;
    inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS);
    inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);
    inputData.shadowMask = SAMPLE_SHADOWMASK(input.lightmapUV);
}
  • positionWS直接从Varyings input得到
  • viewDirWSnormalWS则是根据是否使用法线贴图分别设置。如果使用了法线贴图,回忆一下Vertex Shader中输出到Varying的操作,viewDirWS是存放在NTB三个向量的w中的。而normalWS是将法线贴图中存放的切线空间法线,也就是传入的normalTS,从切线空间变换到世界空间得到。重点来了,这儿使用的矩阵3x3矩阵是half3x3(input.tangent.xyz, input.bitangent.xyz, input.normal.xyz), 这样构造出来的矩阵的3个行分别是TBN三个向量,而关于空间变换矩阵有一个公共性质就是矩阵中包含了变换后的坐标轴,比如在切线空间中,三个坐标轴TBN分别是x,y,z轴单位向量,通过切线空间到世界空间的变换矩阵会分别变换成世界空间的TBN,也就是我们这儿的3个参数,那么以这三个世界空间向量组成的矩阵就是从切线空间变换到世界空间的矩阵。这儿3个向量是按行填入矩阵的,所以矩阵的3行就分别是3个轴,这样的矩阵如果想让任意向量从切线空间变换到世界空间,需要使用行向量在左乘矩阵的方式,这也正是TransformTangentToWorld的做法:
real3 TransformTangentToWorld(real3 dirTS, real3x3 tangentToWorld)
{
    
    
    // Note matrix is in row major convention with left multiplication as it is build on the fly
    return mul(dirTS, tangentToWorld);
}

当然构建矩阵的时候也可以构建为列主的矩阵,但是这样比较麻烦,没有直接填充向量这么方便。

  • 计算好viewDirWSnormalWS后还要分别归一化,但是用了两个不同的函数,有什么区别呢?
    normalWS使用的是NormalizeNormalPerPixel方法:
real3 NormalizeNormalPerPixel(real3 normalWS)
{
    
    
    #if defined(SHADER_QUALITY_HIGH) || defined(_NORMALMAP)
        return normalize(normalWS);
    #else
        return normalWS;
    #endif
}

根据是否使用高质量shader,或者是否使用了法线贴图,来决定是否执行归一化法线。
viewDirWS使用的是SafeNormalize方法,这是SRP Core中的:

// Normalize that account for vectors with zero length
real3 SafeNormalize(float3 inVec)
{
    
    
    real dp3 = max(FLT_MIN, dot(inVec, inVec));
    return inVec * rsqrt(dp3);
}

所谓Safe,就是检查了一下向量的长度的平方,是否为0,如果小于FLT_MIN则取FLT_MIN,即最小的正32位浮点数。rsqrt这个hlsl函数计算平方根的倒数,和原向量相乘就做了归一化。这个函数可以避免向量长度过小引起的除0错误。

  • 之后计算shadow coord,如果已经在顶点shader里面计算了直接从varying获取,或者如果定义了MAIN_LIGHT_CALCULATE_SHADOWS则使用TransformWorldToShadowCoord将世界空间位置变换到主光源空间,否则就是不使用阴影直接填充0。
  • fogCoord和vertex lighting都是直接从varying获取,之前在VS里面计算好了。
  • bakedGI根据是否使用lightmap从lightmap或SH中获取全局光照的颜色值:
// We either sample GI from baked lightmap or from probes.
// If lightmap: sampleData.xy = lightmapUV
// If probe: sampleData.xyz = L2 SH terms
#if defined(LIGHTMAP_ON)
#define SAMPLE_GI(lmName, shName, normalWSName) SampleLightmap(lmName, normalWSName)
#else
#define SAMPLE_GI(lmName, shName, normalWSName) SampleSHPixel(shName, normalWSName)
#endif
  • normalizedScreenSpaceUV 是归一化的屏幕空间UV,这是用于采样屏幕空间的uv坐标,比如SSAO会用到。
  • shadowMask是Mixed Lights的shadow Mask Lighting Mode相关的。在这种模式下面,会采样一张shadow mask贴图来获取物体上一个点最多和4盏灯的遮挡关系,以决定该点是否能被某个灯照射到。
#if defined(SHADOWS_SHADOWMASK) && defined(LIGHTMAP_ON)
    #define SAMPLE_SHADOWMASK(uv) SAMPLE_TEXTURE2D_LIGHTMAP(SHADOWMASK_NAME, SHADOWMASK_SAMPLER_NAME, uv SHADOWMASK_SAMPLE_EXTRA_ARGS);
#elif !defined (LIGHTMAP_ON)
    #define SAMPLE_SHADOWMASK(uv) unity_ProbesOcclusion;
#else
    #define SAMPLE_SHADOWMASK(uv) half4(1, 1, 1, 1);
#endif

从上面的关键字可知,必须开启lightmap且开启shadowMask才会从贴图采样,否则会直接使用unity_ProbesOcclusion,这是保存在Light Probe中的光源遮蔽数据。如果不使用shadow mask模式,那么返回的mask就是1,表示没有阴影。

本篇小结

本篇分析了InputData这个结构的所有成员,这个结构是几乎所有的URP lit shader都会用到。这些成员会在最终计算光照,混合实时光照和Baked GI时用到。下一篇是SimpleLit的最后一篇,看看所有这些光照是如何完成的。

猜你喜欢

转载自blog.csdn.net/n5/article/details/128248882