深入URP之Shader篇6: SimpleLit Shader分析(2) Vertex Shader
创始人
2024-03-21 07:30:19

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_VERTEXuint 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);}
#endifreturn 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 _NORMALMAPoutput.normal = half4(normalInput.normalWS, viewDirWS.x);output.tangent = half4(normalInput.tangentWS, viewDirWS.y);output.bitangent = half4(normalInput.bitangentWS, viewDirWS.z);
#elseoutput.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;#elsereturn 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 Pipelinereal4 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 contributionreturn 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插值,这样显然精度较低,但是会提高效率,具体什么情况会这样用后面再看。另外关于阴影肯定是要单独一篇或几篇分析的,所以这儿就不深入了。

本篇小结

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

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...