深入URP之Shader篇7: SimpleLit Shader分析(3)

1,322 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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的惯例用法,所以内容较多,咱们慢慢来。