深入URP之Shader篇9: SimpleLit Shader分析(5)

1,187 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情

Simple Lit Forward Pass

本篇完结 Fragment shader 函数

在前两篇中,FS的数据已经全部准备好了,本篇就重点看怎么计算光照的,这个Simple Lit有多Simple。另外本篇的另一个重点就是Unity的GI的使用,包括BakedGI和RealtimeGI,这些在这个Simple Lit里面都有用到。 看下剩下要分析的FS代码:

    half4 color = UniversalFragmentBlinnPhong(inputData, diffuse, specular, smoothness, emission, alpha);
    color.rgb = MixFog(color.rgb, inputData.fogCoord);
    color.a = OutputAlpha(color.a, _Surface);

    return color;

计算光照的核心函数就是UniversalFragmentBlinnPhong,传入的参数除了InputData,还有之前计算出来的diffuse, specular, smoothness, emission和alpha。之后使用MixFog将光照计算出来的颜色和雾的颜色进行混合,最后使用OutputAlpha计算最终的alpha值。虽然FS里面代码没几行,但是调用的shader library中的代码有很多,本篇包含大量代码且不能省略。

使用BlinnPhong模型计算光照

half4 color = UniversalFragmentBlinnPhong(inputData, diffuse, specular, smoothness, emission, alpha);

前面准备了那么多数据,就是为了这一步计算光照,只调用了一个函数。这个函数里面计算了主光以及附加光的BlinnPhong光照,以及混合了realtime和Baked GI。为了方便查看,将代码分段贴出来说明:

half4 UniversalFragmentBlinnPhong(InputData inputData, half3 diffuse, half4 specularGloss, half smoothness, half3 emission, half alpha)
{    
    // To ensure backward compatibility we have to avoid using shadowMask input, as it is not present in older shaders
#if defined(SHADOWS_SHADOWMASK) && defined(LIGHTMAP_ON)
    half4 shadowMask = inputData.shadowMask;
#elif !defined (LIGHTMAP_ON)
    half4 shadowMask = unity_ProbesOcclusion;
#else
    half4 shadowMask = half4(1, 1, 1, 1);
#endif

首先这儿先计算shadowMask,这个和之前初始化InputData函数里面的SAMPLE_SHADOWMASK其实是一样的,之所以写成这样,是为了兼容老的shader。

    Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask);

这个GetMainLight会返回主光源的属性,填充到Light结构中并返回,如果提供了shadow相关的参数,还会计算阴影衰减:

Light GetMainLight()
{
    Light light;
    light.direction = _MainLightPosition.xyz;
    light.distanceAttenuation = unity_LightData.z; // unity_LightData.z is 1 when not culled by the culling mask, otherwise 0.
    light.shadowAttenuation = 1.0;
    light.color = _MainLightColor.rgb;

    return light;
}

Light GetMainLight(float4 shadowCoord, float3 positionWS, half4 shadowMask)
{
    Light light = GetMainLight();
    light.shadowAttenuation = MainLightShadow(shadowCoord, positionWS, shadowMask, _MainLightOcclusionProbes);
    return light;
}

这儿的distanceAttenuation含义是光源强度随着距离衰减,但主光源是平行光,没有衰减,所以这儿表示光源是否被剔除。 计算阴影衰减使用了MainLightShadow方法:

half MainLightShadow(float4 shadowCoord, float3 positionWS, half4 shadowMask, half4 occlusionProbeChannels)
{
    half realtimeShadow = MainLightRealtimeShadow(shadowCoord);

#ifdef CALCULATE_BAKED_SHADOWS
    half bakedShadow = BakedShadow(shadowMask, occlusionProbeChannels);
#else
    half bakedShadow = 1.0h;
#endif

#ifdef MAIN_LIGHT_CALCULATE_SHADOWS
    half shadowFade = GetShadowFade(positionWS);
#else
    half shadowFade = 1.0h;
#endif

#if defined(_MAIN_LIGHT_SHADOWS_CASCADE) && defined(CALCULATE_BAKED_SHADOWS)
    // shadowCoord.w represents shadow cascade index
    // in case we are out of shadow cascade we need to set shadow fade to 1.0 for correct blending
    // it is needed when realtime shadows gets cut to early during fade and causes disconnect between baked shadow
    shadowFade = shadowCoord.w == 4 ? 1.0h : shadowFade;
#endif

    return MixRealtimeAndBakedShadows(realtimeShadow, bakedShadow, shadowFade);
}

这个函数首先调用MainLightRealtimeShadow计算了主光的实时阴影。这个函数就不再深入了,其内部会采样深度贴图并比较shadow coord.z,如果是软阴影还会进行PCFF过滤,等专门写shadow篇的时候再深入研究。这儿返回的shadow数值是一个0~1的浮点数,这个数会乘到光源颜色上,1的时候表示没有阴影,0就是全黑(硬阴影),之间的数就是软阴影。之后涉及到BakedShadow的处理,以及混合实时阴影和烘焙阴影,这关系到各种关键字,底层逻辑是Unity的混合光照,这儿就先不看了。下面回到UniversalFragmentBlinnPhong代码:

    #if defined(_SCREEN_SPACE_OCCLUSION)
        AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(inputData.normalizedScreenSpaceUV);
        mainLight.color *= aoFactor.directAmbientOcclusion;
        inputData.bakedGI *= aoFactor.indirectAmbientOcclusion;
    #endif

这儿应用了SSAO去调节颜色。SSAO是作为URP的一个Render Feature引入的,有一个专门的SSAO.hlsl。SSAO是一个后期处理,最终结果输出到一张名字为_ScreenSpaceOcclusionTexture的RT。而这里的GetScreenSpaceAmbientOcclusion就是采样了这张贴图,然后使用得到的参数来调节color。这么说应该使用的是上一帧的AO。简单看了一下SSAO相关代码做的分析,以前我也做过SSAO,不过完全是当后期做了,AO值直接用来改变屏幕像素颜色了,看这儿的做法似乎更科学。不过这不愧是Unity官方自带的Render Feature,可以直接在URP的Shader Library中插入代码使用,自己搞的SSAO就比较麻烦了。


    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);

这函数名字,混合realtime和bakedGI,看一下内容:

void MixRealtimeAndBakedGI(inout Light light, half3 normalWS, inout half3 bakedGI)
{
#if defined(LIGHTMAP_ON) && defined(_MIXED_LIGHTING_SUBTRACTIVE)
    bakedGI = SubtractDirectMainLightFromLightmap(light, normalWS, bakedGI);
#endif
}

其实是Mixed Lights的其中一种Lighting Mode: Subtractive。其实它做的事情不像名字和文档说的那么简单,需要单独深入分析。当然每一种Mixed Light的Lighting Mode都有大量的细节,需要专门研究,其实这几种混合模式对于怎么使用lightmap和realtime lighting/shadow给出了Unity的答案。


    half3 attenuatedLightColor = mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation);

将上面计算好的主光源的衰减,主光阴影的值全部乘到主光颜色上,供后面计算使用。

    half3 diffuseColor = inputData.bakedGI + LightingLambert(attenuatedLightColor, mainLight.direction, inputData.normalWS);
    half3 specularColor = LightingSpecular(attenuatedLightColor, mainLight.direction, inputData.normalWS, inputData.viewDirectionWS, specularGloss, smoothness);

终于,计算主光的diffuse和specular颜色。具体的计算很简单,就是Blinn-Phong。diffuse这儿还加上了inputData.bakedGI。bakedGI的计算上篇说了,就是采样lightmap或SH。


#ifdef _ADDITIONAL_LIGHTS
    uint pixelLightCount = GetAdditionalLightsCount();
    for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
    {
        Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask);
        #if defined(_SCREEN_SPACE_OCCLUSION)
            light.color *= aoFactor.directAmbientOcclusion;
        #endif
        half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
        diffuseColor += LightingLambert(attenuatedLightColor, light.direction, inputData.normalWS);
        specularColor += LightingSpecular(attenuatedLightColor, light.direction, inputData.normalWS, inputData.viewDirectionWS, specularGloss, smoothness);
    }
#endif

附加光的逻辑和主光一样,不用多解释了。在一个pass里面将所有的光源都计算了,就是URP单pass光照的核心。


#ifdef _ADDITIONAL_LIGHTS_VERTEX
    diffuseColor += inputData.vertexLighting;
#endif

如果附加光使用的是顶点光照,那么这儿直接加到diffuse上,可以看到顶点光照是没有高光的。


    half3 finalColor = diffuseColor * diffuse + emission;

#if defined(_SPECGLOSSMAP) || defined(_SPECULAR_COLOR)
    finalColor += specularColor;
#endif

    return half4(finalColor, alpha);
}

最终,将diffuse, emission和specualr都加一起,带上前面计算的alpha (上一篇说了),返回这个看似简单,但里面深不可测的UniversalFragmentBlinnPhong函数。再提一下这个高光,必须满足相应的关键字才会输出,否则就不使用了,那么前面的计算,应该也会被优化掉吧。我以前是做手游和H5的,shader都是提供源码入包,由gles的驱动载入时compile的,那些驱动也许没这么强的优化能力,但是Unity使用的shader compiler已经强大了很多,应该会先由这些compiler进行预先处理优化,我们就不用再担心shader没优化掉不用的东西了。希望如此吧。这个真的要研究又是一个很大的课题了。

Output Alpha

雾应该说过,忽略,说一下最最后一步,输出alpha:

half OutputAlpha(half outputAlpha, half surfaceType = 0.0)
{
    return surfaceType == 1 ? outputAlpha : 1.0;
}

alpha还是那个alpha,只不过如果表面类型是不透明,就直接设置为1

本篇小结

终于,最终分析完了Simple Lit Shader,这真是简约不简单。不简单的部分都挖坑了等着以后再独立分析。因为不可能在一个shader的分析过程中将所有技术点都研究透,否则弄着弄着就回不去了。另外一个原因是最终的shader是各个技术的集合处,整体分析之后可以先知道有哪些东西,大概怎么应用的,至于原理不是一个shader能体现出来的,而且也不仅仅存在于shader中,所以Inside URP系列会继续专注于Uniy URP的实现原理,从shader,代码等各方面去研究如GI, Bake, Lighting, Shadow, Postprocessing等各种技术。