持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
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得到viewDirWS
和normalWS
则是根据是否使用法线贴图分别设置。如果使用了法线贴图,回忆一下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);
}
当然构建矩阵的时候也可以构建为列主的矩阵,但是这样比较麻烦,没有直接填充向量这么方便。
- 计算好
viewDirWS
和normalWS
后还要分别归一化,但是用了两个不同的函数,有什么区别呢?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的最后一篇,看看所有这些光照是如何完成的。