持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情
Simple Lit Forward Pass
Fragment shader 函数
上篇中分析了Simple Lit Forward Pass的Vertex Shader都计算和输出了什么数据,本篇就看看在Fragment shader中是怎么使用这些数据计算最终的光照颜色的。 由于代码较短,就都贴出来方便查看:
half4 LitPassFragmentSimple(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = input.uv;
half4 diffuseAlpha = SampleAlbedoAlpha(uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap));
half3 diffuse = diffuseAlpha.rgb * _BaseColor.rgb;
half alpha = diffuseAlpha.a * _BaseColor.a;
AlphaDiscard(alpha, _Cutoff);
#ifdef _ALPHAPREMULTIPLY_ON
diffuse *= alpha;
#endif
half3 normalTS = SampleNormal(uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap));
half3 emission = SampleEmission(uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));
half4 specular = SampleSpecularSmoothness(uv, alpha, _SpecColor, TEXTURE2D_ARGS(_SpecGlossMap, sampler_SpecGlossMap));
half smoothness = specular.a;
InputData inputData;
InitializeInputData(input, normalTS, inputData);
half4 color = UniversalFragmentBlinnPhong(inputData, diffuse, specular, smoothness, emission, alpha);
color.rgb = MixFog(color.rgb, inputData.fogCoord);
color.a = OutputAlpha(color.a, _Surface);
return color;
}
首先进行的是Alpha Test
之前在分析Depth Only Pass的时候就看过alpha test的处理,和这儿有些类似,但是由于Depth only pass更加通用,所以会在使用albedo贴图alpha的时候做一些判断,在某些情况下这个贴图的alpha是不用来做alpha test的(贴图的alpha通道存储的是smoothness, glossiness这样的值),因此就不使用它作为alpha test的参数。而Simple Lit这儿就简单了,因为这个shader里面不使用那些关键字对应的功能,因此也无需判断那些关键字。
注意这儿采样贴图使用的函数名是SampleAlbedoAlpha,这个函数之前已经看过,就是使用SAMPLE_TEXTURE2D采样一张2D贴图,采样出来的结果是一个half4。你可以理解为这个half4包含了rgb反射率(albedo)和alpha。即不是简单的将贴图中的texel看成是一个颜色。diffuseAlpha.rgb和_BaseColor.rgb相乘作为最终的diffuse系数(其实仍然是反射率),而diffuseAlpha.a和_BaseColor.a相乘得到最终的alpha,使用AlphaDiscard函数进行Alpha Test。
void AlphaDiscard(real alpha, real cutoff, real offset = 0.0h)
{
#ifdef _ALPHATEST_ON
clip(alpha - cutoff + offset);
#endif
}
最终调用的是hlsl的clip函数,clip函数当接受的参数值小于0时丢弃这个片段。这儿的offset一般都不会设置,就是0,可以忽略,而alpha和cutoff比较,如果alpha < cutoff则片段没通过alpha test被丢弃。所以cutoff设置的是可通过alpha test的最小的alpha值,大于或等于cutoff的alpha值是通过alpha test的。
预乘alpha
还记得一开始我们看unlit shader的属性时说的BaseShaderGUI吗?预乘alpha开启的条件其实就是blend mode为Premultiply:
case BlendMode.Premultiply:
material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
material.EnableKeyword("_ALPHAPREMULTIPLY_ON");
此时,srcBlend为one,而dstBlend为1-srcAlpha,即blend的公式为:
finalColor = srcColor + (1-srcAlpha)*dstColor
和标准的alpha blend模式相比,srcColor在混合时不需要再乘以alpha,因为alpha已经预先乘上去了:
#ifdef _ALPHAPREMULTIPLY_ON
diffuse *= alpha;
#endif
这两种做法有何区别?很显然,这儿只有diffuse使用了alpha,而如果在blend时使用alpha是对最终颜色乘以alpha,所以这种做法得到的颜色应该更亮一些。因为可以认为乘alpha是减淡颜色。再多说一句预乘alpha这个技术,早期经常用在2d游戏渲染半透明sprite,在载入sprite时即预乘alpha(或是在导入资源时处理sprite图片),这样在渲染时就可以少一个乘法,性能提高了而效果是一样的,当然了这种做法时alpha值是不能变的,不能做fade in/fade out。
采样法线贴图
half3 normalTS = SampleNormal(uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap));
SampleNormal函数如下:
half3 SampleNormal(float2 uv, TEXTURE2D_PARAM(bumpMap, sampler_bumpMap), half scale = 1.0h)
{
#ifdef _NORMALMAP
half4 n = SAMPLE_TEXTURE2D(bumpMap, sampler_bumpMap, uv);
#if BUMP_SCALE_NOT_SUPPORTED
return UnpackNormal(n);
#else
return UnpackNormalScale(n, scale);
#endif
#else
return half3(0.0h, 0.0h, 1.0h);
#endif
}
如果定义了相应的关键字,则采样法线贴图,且根据是否使用了bump scale,使用不同的解码函数。这两个unpcak函数在SRP Core的Packing.hlsl中,涉及到法线贴图相关的编码方式,这儿不细说,也许会有一篇单独分析法线贴图会说。一般来说,法线贴图会保存切线空间的法线,因此这儿的变量名为normalTS。
采样自发光贴图
half3 emission = SampleEmission(uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));
half3 SampleEmission(float2 uv, half3 emissionColor, TEXTURE2D_PARAM(emissionMap, sampler_emissionMap))
{
#ifndef _EMISSION
return 0;
#else
return SAMPLE_TEXTURE2D(emissionMap, sampler_emissionMap, uv).rgb * emissionColor;
#endif
}
就是采样一张自发光贴图再乘以一个自发光颜色。
采样高光贴图以及获取smoothness
half4 specular = SampleSpecularSmoothness(uv, alpha, _SpecColor, TEXTURE2D_ARGS(_SpecGlossMap, sampler_SpecGlossMap));
half smoothness = specular.a;
SampleSpecularSmoothness函数在SimpleLitInput.hlsl中:
TEXTURE2D(_SpecGlossMap); SAMPLER(sampler_SpecGlossMap);
half4 SampleSpecularSmoothness(half2 uv, half alpha, half4 specColor, TEXTURE2D_PARAM(specMap, sampler_specMap))
{
half4 specularSmoothness = half4(0.0h, 0.0h, 0.0h, 1.0h);
#ifdef _SPECGLOSSMAP
specularSmoothness = SAMPLE_TEXTURE2D(specMap, sampler_specMap, uv) * specColor;
#elif defined(_SPECULAR_COLOR)
specularSmoothness = specColor;
#endif
#ifdef _GLOSSINESS_FROM_BASE_ALPHA
specularSmoothness.a = exp2(10 * alpha + 1);
#else
specularSmoothness.a = exp2(10 * specularSmoothness.a + 1);
#endif
return specularSmoothness;
}
根据不同的关键字,从高光贴图中采样出高光颜色或者直接使用材质定义的高光色。注意这儿的_GLOSSINESS_FROM_BASE_ALPHA,前面alpha test那儿说过,这个关键字表示base贴图的alpha存储的是glossiness,否则就使用高光贴图的alpha作为glossiness使用。这儿使用exp2解码smoothness,说明贴图中存储的是log空间。
待续
后面还有两篇讲这个Fragment Shader,这个Shader虽然不复杂,光照计算也只是传统的BlinnPhong,但是涉及到非常多的Unity光照系统的东西以及URP的惯例用法,所以内容较多,咱们慢慢来。