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;
}
其中,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;
}
这里面的unity_WorldTransformParams
却又位于URP的UnityInput.hlsl
中:
real4 unity_WorldTransformParams; // w is usually 1.0, or -1.0 for odd-negative scale transforms
回到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;
}
可见,必须激活关键字_ADDITIONAL_LIGHTS_VERTEX
,才会计算附加光的顶点光照。或依次获取所有的附加光,得到其颜色强度,然后使用LightingLambert
函数计算出一个漫反射光照颜色:
half3 LightingLambert(half3 lightColor, half3 lightDir, half3 normal)
{
half NdotL = saturate(dot(normal, lightDir));
return lightColor * NdotL;
}
所以,顶点光照计算的就是附加光的漫反射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
如果使用法线贴图,则会输出世界空间的法线,切线,和副切线。并且会把世界空间的视线方向夹带在这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
}
这个函数会根据shader quality以及是否使用法线贴图来决定是否要归一化法线,具体见上面的注释。
- 输出lightmap UV和SH
OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV);
OUTPUT_SH(output.normal.xyz, output.vertexSH);
这两个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
如果启用了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);
}
SH系数是通过light probe烘焙出来的低频球谐光照信息,使用SH函数可以用极小的代价去存储光照信息。具体也许会单独讲一篇。
- 输出雾参数和顶点光照
output.fogFactorAndVertexLight = half4(fogFactor, vertexLight);
将计算出来的雾参数和顶点光照颜色合并到一个向量中输出。 - 在顶点上计算阴影坐标
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
output.shadowCoord = GetShadowCoord(vertexInput);
#endif
必须是启用关键字REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR,这样就会在顶点级别计算阴影坐标然后varying插值,这样显然精度较低,但是会提高效率,具体什么情况会这样用后面再看。另外关于阴影肯定是要单独一篇或几篇分析的,所以这儿就不深入了。文章来源:https://www.toymoban.com/news/detail-430606.html
本篇小结
本篇分析了SimplLit Forward pass的 Vertex Shader 函数LitPassVertexSimple
,这个函数基本上就是为光照提供各种参数,只有附加光的漫反射有可能在这儿计算。这个函数考虑了有无NormalMap, LightMap以及顶点光照等各种情况。下篇我们就看一下这个simple lit到底在fragment shader中是怎么计算的。文章来源地址https://www.toymoban.com/news/detail-430606.html
到了这里,关于深入URP之Shader篇6: SimpleLit Shader分析(2) Vertex Shader的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!