翻译17 Mixed Lighting混合光照

只烘焙间接光
混合烘焙阴影和实时阴影
处理代码的变化和问题
支持消减光照(subtractivelighting)html

unity 5.6.6f2缓存

1 烘焙间接光

光照贴图能够提供预计算光照:以纹理内存为代价减小了GPU在实时中的工做量;还加入了间接光。函数

限制首先高光不能被烘焙其次烘焙光只经过光照探头影响动态物体最后烘焙光不产生实时阴影性能

你能够在下面的截图中看到彻底实时光照和彻底烘焙光照之间的区别。这是前一篇教程中的一个场景,惟一的不一样是我将全部的球体都设置为动态并从新改变了一些球体的位置。其它一切都是静态的。这是使用前向渲染的方法。优化

image image

彻底实时和彻底烘焙光照spa

1.1 混合模式

烘焙光照有间接光而没有实时光照,由于间接光须要光照贴图。因为间接光能够为场景加入很大的真实感,若是咱们能够将它和实时光照融合在一块儿就再好不过了。这是能够的,但也意味着着色的开销会增长。咱们须要将混合光(Mixed Lighting)的光照模式(Lighting Mode)设置为烘焙间接(Baked Indirect)。翻译

image

混合光照,烘焙间接3d

咱们已经在前一篇教程中切换到这个模式了,可是以前咱们只使用了彻底烘焙光照。虽然表现结果与彻底烘焙光照相同,混合光照模式没有任何区别。为了使用混合光照,光源的模式必需要设置为混合。代理

image

混合模式的主光源code

在将主定向光改成混合光后,两件事会发生:首先,Unity会再次烘焙光照贴图。这一次光照贴图只会存储间接光,因此它会比以前的暗不少。

image image

彻底烘焙的光照贴图 vs 只有间接光的光照贴图

另外,全部物体都会像主光源被设置为实时那样被照亮。只有一点不一样:光照贴图被用来为静态物体添加间接光,而不是球谐光或探头。动态物体的间接光仍要使用光照探头。

image

混合光照,实时直接光照烘焙间接光

咱们不须要改变咱们的着色器来支持这点,由于前向基础通道(forward base pass)已经融合了光照贴图数据和主定向光源。和往常同样,额外的光照会获得附加通道(additive pass)。当使用延迟渲染通道时,主光源也会获得一个通道。

混合光能够在运行时调整吗?
是的,由于它们被用于实时光照。可是,它们的烘焙数据时静态的。因此在运行时你只能稍微调整光照,好比稍微调整它的强度。更大的变化会令人明显看出烘焙光照和实时光照之间的不一样步。

1.2 更新着色器

刚开始一切彷佛正常运行。可是,定向光的阴影衰减发生了错误。咱们经过极大下降阴影距离观察到阴影被剪掉了。

image image

阴影衰减,标准着色器vs咱们的着色器

虽然Unity很长一段时间都有混合光照模式,但实际上它在Unity5中就不起做用了。Unity5.6中新加入了一个混合光照模式,即咱们如今使用的这个。当该新模式被加入时,UNITY_LIGHT_ATTENUATION宏下面的代码发生了变化。咱们在使用彻底烘焙光照或者实时光照时没有注意到这一点,可是咱们必须更新咱们的代码以适应混合光照的新方法。因为这是最近的一个巨大的变化,咱们必需要注意它所带来的问题。

咱们要改变的第一点是再也不使用SHADOW_COORDS宏来定义阴影坐标的插值(interpolater)。咱们必须使用新的UNITY_SHADOW_COORDS宏来代替它。

struct Interpolators
{
    …
    //SHADOW_COORDS(5)
    UNITY_SHADOW_COORDS(5)
    …
};

一样,TRANSFER_SHADOW应该替换为UNITY_TRANSFER_SHADOW

Interpolators MyVertexProgram (VertexData v)
{
    …
    //TRANSFER_SHADOW(i);
    UNITY_TRANSFER_SHADOW(i);
    …
}

然而,这会产生一个编译错误,由于该宏须要一个额外的参数。从Unity 5.6开始,只有定向阴影的屏幕空间坐标中被放入一个插值。点光源和聚光源的阴影坐标如今在片断程序(fragment program)中进行计算。有个新变化:在一些状况中光照贴图的坐标被用在阴影蒙版(shadow mask)中,咱们会在后面讲解这一点。为了该宏能正常工做,咱们必须为它提供第二个UV通道中的数据,其中包含光照贴图的坐标。

UNITY_TRANSFER_SHADOW(i, v.uv1);

这样会再次产生一个编译错误。这是由于在一些状况下UNITY_SHADOW_COORDS错误地建立了一个插值,尽管实际上并不须要。在这种状况下,TRANSFER_SHADOW不会初始化它,于是致使错误。这个问题出如今5.6.0中,一直到5.6.2和2017.1.0beta版本中都有。

人们一般不会注意到这个问题,由于Unity的标准着色器使用UNITY_INITIALIZE_OUTPUT宏来彻底地初始化它的插值结构体。由于咱们不使用这个宏,因此出现了问题。为了解决它,咱们使用UNITY_INITIALIZE_OUTPUT宏来初始化咱们的插值。

Interpolators MyVertexProgram (VertexData v)
{
    Interpolators i;
    UNITY_INITIALIZE_OUTPUT(Interpolators, i);
    …
}

UNITY_INITIALIZE_OUTPUT有什么做用?

它只是为变量分配数值0,将其转换为正确的类型。至少是当程序支持该宏时会这样,不然它不会作任何事。

// Initialize arbitrary structure with zero values.
// Not supported on some backends
// (e.g. Cg-based particularly with nested structs).
// hlsl2glsl would almost support it, except with structs that have
arrays
// -- so treat as not supported there either :(
#if defined(UNITY_COMPILER_HLSL) || defined(SHADER_API_PSSL) || \
defined(UNITY_COMPILER_HLSLCC)
    #define UNITY_INITIALIZE_OUTPUT(type,name) name = (type)0;
#else
    #define UNITY_INITIALIZE_OUTPUT(type,name)
#endif

一般咱们倾向于只使用显式赋值,不多使用这个初始化插值宏。

1.3 手动衰减阴影

如今咱们正确地使用了新的宏定义,可是主光源的阴影仍然没有按照它们应该的那样衰减。结果咱们发现当同时使用定向阴影和光照贴图时,UNITY_LIGHT_ATTENUATION不会对光源进行衰减。使用混合模式的主定向光源就会产生这个问题。因此咱们必须手动设置。

为何在这个例子中阴影没有衰减?
一、UNITY_LIGHT_ATTENUATION宏以前是独立使用的,可是自从Unity5. 6它开始和Unity的标准全局光照函数一同使用。咱们没有采用一样的方法,所以它不能正常工做。
二、至于为何要作这个改动,惟一的线索就是AutoLight中的一段注释:“为了性能的缘由以GI函数的深度处理阴影”。因为着色器编译器会随意地移动代码。

对于咱们的延迟光照着色器,咱们已经有了进行阴影衰减的代码。将相关代码片断从MyDeferredShading中复制到My Lighting中的一个新函数中。惟一实际的区别在于咱们必须使用视图向量和视图矩阵构建viewZ。咱们只须要Z份量,因此无需进行一次完整的矩阵乘法。

float FadeShadows (Interpolators i, float attenuation) {
    float viewZ =
        dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
    float shadowFadeDistance =
        UnityComputeShadowFadeDistance(i.worldPos, viewZ);
    float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
    attenuation = saturate(attenuation + shadowFade);
    return attenuation;
}

该手动衰减必须在使用了UNITY_LIGHT_ATTENUATION初始化完成以后。

UnityLight CreateLight (Interpolators i) {
    …
    UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
    attenuation = FadeShadows(i, attenuation);
    …
}

只有当HANDLE_SHADOW_BLENDING_IN_GI在UnityShadowLibrary.cginc文件中有定义时,FadeShadows才会开始计算。

float FadeShadows (Interpolators i, float attenuation) {
    #if HANDLE_SHADOWS_BLENDING_IN_GI
        // UNITY_LIGHT_ATTENUATION doesn't fade shadows for us.
        float viewZ = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
        float shadowFadeDistance = UnityComputeShadowFadeDistance(i.worldPos, viewZ);
        float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
        attenuation = saturate(attenuation + shadowFade);
    #endif
    return attenuation;
}

最后,咱们的阴影如它们应该的那样正常衰减了。

2 使用阴影蒙版(Shadowmask)

烘焙间接光的混合模式成本很高。它们须要实时光照外加间接光的光照贴图那么大的工做量。它和彻底烘焙光照相比最重要的是加入了实时阴影。幸运的是,有一个方法仍能够将阴影烘焙到光照贴图中,将其和实时阴影综合起来。为了开启这个功能,咱们将混合光照模式改成Shadowmask。

image

Shadowmask模式

在这个模式中,混合光照的间接光和阴影衰减都存储在了光照贴图中阴影被存储在一张额外的贴图(即阴影蒙版)。当只有主定向光源时,红色的阴影蒙版决定是否过滤被照亮的物体。红色是由于阴影信息被存储在纹理的R通道。事实上,贴图中至多能够储存四个光照的阴影,由于它只有四个通道。

image

烘焙的强度以及阴影蒙版

在Unity建立了阴影蒙版后,静态物体的阴影投射会消失。只有光照探头仍会处理它们。动态物体的阴影不受影响。

image

没有烘焙阴影

2.1 对阴影蒙版采样-Sampling the Shadowmask

为了从新获得烘焙阴影,咱们必须对阴影蒙版采样样。Unity的宏已经对点光源和聚光源进行了取样,不过咱们必须也要将它包含在咱们的FadeShadows函数中。为此咱们可使用UnityShadowLibrary中的UnitySampleBakedOcclusions函数。它须要光照贴图的UV坐标和世界位置做为输入参数。

float FadeShadows (Interpolators i, float attenuation)
{
    #if HANDLE_SHADOWS_BLENDING_IN_GI
        …
        float bakedAttenuation = UnitySampleBakedOcclusion(i.lightmapUV, i.worldPos);
        attenuation = saturate(attenuation + shadowFade);
    #endif
    return attenuation;
}

UnitySampleBakedOcclusion是什么样子的?
它使用光照贴图坐标对阴影蒙版取样,而后选择适当的通道。unity_OcclusionMaskSelector变量是一个含有一个份量的向量,该份量被设置为1以匹配当前正在被着色的光源。

fixed UnitySampleBakedOcclusion (float2 lightmapUV, float3 worldPos) {
    #if defined (SHADOWS_SHADOWMASK)
        #if defined(LIGHTMAP_ON)
            fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D_SAMPLER(
                unity_ShadowMask, unity_Lightmap, lightmapUV.xy
            );
        #else
            fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
        #endif
        return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
    #else
        return 1.0;
    #endif
}

该函数还处理了光照探头代理体积的衰减,可是咱们尚未支持这点因此我去掉了那部分的代码。这就是为何该函数有一个世界位置的参数。

当使用阴影蒙版时,UnitySampleBakedOcclusions提供给咱们烘焙阴影衰减,在其余状况下它的值都为1。如今咱们必须将它和咱们已经有的衰减综合起来而后对阴影进行衰减。UnityMixRealtimeAndBakedShadows函数为咱们实现了这些。

float bakedAttenuation = UnitySampleBakedOcclusion(i.lightmapUV, i.worldPos);
//attenuation = saturate(attenuation shadowFade);
attenuation = UnityMixRealtimeAndBakedShadows
(
    attenuation, bakedAttenuation, shadowFade
);

UnityMixRealtimeAndBakedShadows是如何工做的?

它也是UnityShadowLibrary中的一个函数。它还处理光照探头代理体积以及一些其余极端状况。那些状况和咱们无关,因此我删除了一些内容。

inline half UnityMixRealtimeAndBakedShadows (
    half realtimeShadowAttenuation, half bakedShadowAttenuation, half fade
) {
    #if !defined(SHADOWS_DEPTH) && !defined(SHADOWS_SCREEN) && \
        !defined(SHADOWS_CUBE)
        return bakedShadowAttenuation;
    #endif

    #if defined (SHADOWS_SHADOWMASK)
        #if defined (LIGHTMAP_SHADOW_MIXING)
            realtimeShadowAttenuation = saturate(realtimeShadowAttenuation + fade);
            return min(realtimeShadowAttenuation, bakedShadowAttenuation);
        #else
            return lerp(
                realtimeShadowAttenuation, bakedShadowAttenuation, fade
            );
        #endif
    #else //no shadowmask
        return saturate(realtimeShadowAttenuation + fade);
    #endif
}

若是没有动态阴影,那么结果将获得烘焙的衰减。这意味着动态物体没有阴影,以及被映射到光照贴图上的物体没有烘焙阴影。

当没有使用阴影蒙版时,它会进行原来的衰减。不然,它会根据咱们是否作了阴影混合进行表现,咱们后面再讲。如今,它只是在实时衰减和烘焙衰减之间进行一个插值

image

实时阴影和阴影蒙版阴影

如今静态物体有了实时阴影和烘焙阴影,且它们正确地混合。实时阴影的衰减仍然超过了阴影距离,可是烘焙阴影没有。

image

只有实时阴影衰减了

2.2 添加一个阴影蒙版G-Buffer

如今阴影蒙版可用于前向渲染路径,可是咱们须要使它也可用于延迟渲染:添加阴影蒙版信息做为一个额外的G-缓存。因此当SHADOWS_SHADOWMASK被定义时,在FragmentOutput结构体中添加一个缓存。

struct FragmentOutput {
    #if defined(DEFERRED_PASS)
        float4 gBuffer0 : SV_Target0;
        float4 gBuffer1 : SV_Target1;
        float4 gBuffer2 : SV_Target2;
        float4 gBuffer3 : SV_Target3;

        #if defined(SHADOWS_SHADOWMASK)
            float4 gBuffer4 : SV_Target4;
        #endif
    #else
        float4 color : SV_Target;
    #endif
};

添加的第五个G-缓存,会使显存增大,并非全部的平台(mobile)都支持它。Unity只在有足够多的渲染目标可用时才支持阴影蒙版,所以咱们也应该这样作。

#if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
    float4 gBuffer4 : SV_Target4;
#endif

咱们只需在G-缓存中存储采样获得的阴影蒙版数据,并且没有一个确切的光照,为此咱们可使用UnityGetRawBakedOcclusions函数,它与UnitySampleBakedOcclusion类似,惟一不一样在于它没有选择某个纹理通道。

FragmentOutput output;
#if defined(DEFERRED_PASS)
    #if !defined(UNITY_HDR_ON)
        color.rgb = exp2(-color.rgb);
    #endif
    output.gBuffer0.rgb = albedo;
    output.gBuffer0.a = GetOcclusion(i);
    output.gBuffer1.rgb = specularTint;
    output.gBuffer1.a = GetSmoothness(i);
    output.gBuffer2 = float4(i.normal * 0.5 + 0.5, 1);
    output.gBuffer3 = color;
    #if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
        output.gBuffer4 = UnityGetRawBakedOcclusions(i.lightmapUV, i.worldPos.xyz);
    #endif
#else
    output.color = ApplyFog(color, i);
#endif

为了能够在没有光照贴图的时候也能成功编译,当光照贴图坐标不可用时咱们使用0代替它。

#if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
    float2 shadowUV = 0;
    #if defined(LIGHTMAP_ON)
        shadowUV = i.lightmapUV;
    #endif
    output.gBuffer4 = UnityGetRawBakedOcclusions(shadowUV, i.worldPos.xyz);
#endif

2.3 使用阴影蒙版G-缓存

调整MyDeferredShading延迟渲染着色器。

第一步先添加额外的一个G-buffer变量。

sampler2D _CameraGBufferTexture0;
sampler2D _CameraGBufferTexture1;
sampler2D _CameraGBufferTexture2;
sampler2D _CameraGBufferTexture4;

第二步,建立一个函数来获得适当的阴影衰减。若是有了阴影蒙版,可经过对纹理采样而后和unity_OcclusionMaskSelector进行一次颜色饱和点乘。这个变量是在UnityShaderVariables.cginc中定义的,包含了一个用于选择当前正在被渲染的光照通道的向量。

float GetShadowMaskAttenuation (float2 uv)
{
    float attenuation = 1;
    #if defined (SHADOWS_SHADOWMASK)
        float4 mask = tex2D(_CameraGBufferTexture4, uv);
        attenuation = saturate(dot(mask, unity_OcclusionMaskSelector));
    #endif
    return attenuation;
}

在CreateLight中,即便当前光照没有实时阴影,咱们在有阴影蒙版时也要衰减阴影。

UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
    …
    #if defined(SHADOWS_SHADOWMASK)
        shadowed = true;
    #endif
    if (shadowed) {
        …
    }
    …
}

为了正确地包含烘焙阴影,再次使用UnityMixRealtimeAndBakedShadows代替以前的衰减计算。

if (shadowed) 
{
    float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
    float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
//  shadowAttenuation = saturate(shadowAttenuation + shadowFade);
    shadowAttenuation = UnityMixRealtimeAndBakedShadows(
        shadowAttenuation, GetShadowMaskAttenuation(uv), shadowFade
    );
    …
}

如今也可使用自定义的延迟光照着色器获得正确的烘焙阴影了。例外,即当咱们的优化分支被使用时会跳过阴影混合。该捷径在阴影蒙版被使用时不可用。

if (shadowed) {
    …
    #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
        #if !defined(SHADOWS_SHADOWMASK)
            UNITY_BRANCH
            if (shadowFade > 0.99) {
                shadowAttenuation = 1;
            }
        #endif
    #endif
}

2.4 阴影蒙版-距离模式 DIstance Shadowmask

虽然使用阴影蒙版模式咱们能够获得不错的静态物体的烘焙阴影,动态物体却不能从中获利。动态物体只能接收到实时阴影以及光照探头数据。若是咱们但愿获得动态物体的阴影,那么静态物体必须也要投射实时阴影。这里的混合光照模式咱们要用到距离阴影蒙版(Distance Shadowmask)了。

image

距离阴影蒙版模式

在2017及以上,使用哪一个阴影蒙版模式是经过质量设置进行控制

当使用DistanceShadowmask模式时,全部物体都使用实时阴影。第一眼看去,好像和Baked Indirect模式彻底同样。

image

全部物体都有实时阴影

不过这里仍有一个阴影蒙版。在这个模式中,烘焙阴影和光照探头的使用超出了阴影距离。所以该模式是成本最高的模式,在阴影距离范围内等价于烘焙间接模式,超出该范围则等价于阴影蒙版模式。

近处为实时阴影,远处为阴影蒙版和探头

2.3节中已经支持这个模式了,由于咱们正在使用UnityMixRealtimeAndBakedShadows。为了正确地混合彻底实时阴影和烘焙阴影,它像往常那样衰减实时阴影,而后取其和烘焙阴影的最小值。

2.5 多重光照

由于阴影蒙版有四个通道,它能够最多同时支持4个光照体积重叠在一块儿

image image

四个光源,都是混合光

主方向光源的阴影仍存储在R通道中。你还可以看到存储在G通道和B通道中的聚光源的阴影,最后一个聚光源的阴影存储在A通道中。

当光照体积不重叠时,它们使用相同的通道来存储它们的阴影数据。因此你能够有任意多个混合光照。可是你必须确保至多四个光照体积彼此重叠若是有太多个混合光影响同一篇区域,那么一些就会改回到彻底烘焙模式。为了说明这一点,下面这张截图显示的是在多加入一个聚光源之后的光照贴图。你能够在强度贴图中清楚地看到其中一个已经变成了烘焙光。

image image

5个重叠的光照,其中一个为彻底烘焙光

2.6 支持多个有蒙版的定向光

不幸的是,阴影蒙版只有当包含至多一个混合模式的方向光源存在时才能正常工做。对于额外的方向光,阴影衰减会发生错误,至少是在使用前向渲染通道时。延迟渲染倒没有问题。

image

两个方向光源产生错误的衰减

这是使用UNITY_LIGHT_ATTENUATION的新方法中的一个漏洞:Unity使用经过UNITY_SHADOW_COORDS定义的阴影插值来存储方向阴影的屏幕空间坐标,或者其它拥有阴影蒙版的光源的光照贴图坐标。

使用阴影蒙版的方向光还须要光照贴图坐标。在forward-render中,这些坐标会被包含,由于LIGHTMAP_ON会在须要的时候被定义。然而,LIGHTMAP_ON在additional-pass中永远不会被定义。这意味着多余的方向光没有可用的光照贴图坐标。结果UNITY_LIGHT_ATTENUATION在这种状况下只会使用0,致使错误的光照贴图采样

因此咱们不能依靠UNITY_LIGHT_ATTENUATION额外得到使用阴影蒙版的方向光源。用屏幕空间的方向阴影

#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    …
#endif
#if !defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
    #if defined(SHADOWS_SHADOWMASK) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
        #define ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS 1
    #endif
#endif

接下来,对那些额外有蒙版的定向阴影,咱们也要包含光照贴图坐标。

struct Interpolators {
    …
    #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
        float2 lightmapUV : TEXCOORD6;
    #endif
};
…
Interpolators MyVertexProgram (VertexData v) {
    …
    #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
        i.lightmapUV = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
    #endif
    …
}

当光照贴图坐标可用时,咱们能够再次使用FadeShadows函数进行咱们本身控制的衰减。

float FadeShadows (Interpolators i, float attenuation) 
{
    #if HANDLE_SHADOWS_BLENDING_IN_GI || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
        …
    #endif
    return attenuation;
}

可是,这仍然不正确,由于咱们为其输入了错误的衰减数据。咱们必须绕开UNITY_LIGHT_ATTENUATION,只获得烘焙后的衰减,在这个状况中咱们可使用SHADOW_ATTENUATION宏。

float FadeShadows (Interpolators i, float attenuation) 
{
    #if HANDLE_SHADOWS_BLENDING_IN_GI || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
        //UNITY_LIGHT_ATTENUATION doesn't fade shadows for us.
        #if ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
            attenuation = SHADOW_ATTENUATION(i);
        #endif
        …
    #endif
    return attenuation;
}

image

两个定向光源正确的衰减

3 消减阴影-Subtractive Shadows

混合光照很好,可是它不像彻底烘焙光照那样成本低廉。若是以低性能硬件为目标,那么混合光照不太可行。烘焙光照会管用,可是事实上你也许须要动态物体对静态物体投射阴影。那样的话,你可使用消减混合光照模式

image

消减模式

在切换到消减模式后,场景会亮不少。这是因为静态物体如今同时使用彻底烘焙的光照贴图和方向光源。这是由于动态物体仍然会同时使用光照探头和方向光源。

image

静态物体受到两次光照

消减模式只可用于前向渲染当使用延迟渲染路径时,相关的物体会回到前向渲染路径,就像透明物体那样。

3.1 消减光照

在消减模式中,静态物体经过光照贴图被照亮,同时还将动态阴影考虑在内。这是经过下降光照贴图在阴影区域的强度来实现的。为此,着色器须要使用光照贴图和实时阴影。它还须要使用实时光照来计算出要将光照贴图调暗多少。这就是为何咱们在切换到这个模式后获得了双重光照。

消减光照是一个近似,只在一个单必定向光下起做用,所以它只支持主方向光的阴影。另外,咱们必须以某种方式了解在动态着色区域内间接光的环境是什么。因为咱们使用的是一个彻底烘焙的光照贴图,咱们没有这个信息。Unity没有包含一个额外的只有间接光的光照贴图,而是使用了一个统一的颜色对环境光取近似值。即实时阴影颜色(Realtime Shadow Color),你能够在混合光照选项中调整它。

在着色器中,咱们知道当LIGHTMAP_ONSHADOWS_SCREEN,和LIGHTMAP_SHADOW_MIXING关键词被定义而SHADOWS_SHADOWMASK没有被定义时咱们应该使用消减光照。若是这样的话咱们定义SUBTRACTIVE_LIGHTING,以便更容易使用它。

#if !defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
    #if defined(SHADOWS_SHADOWMASK) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
        #define ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS 1
    #endif
#endif

#if defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
    #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK)
        #define SUBTRACTIVE_LIGHTING 1
    #endif
#endif
在作其余事情以前,咱们必须去除掉双重阴影。为此咱们能够关闭动态光照,就像咱们对延迟通道所作的那样。
UnityLight CreateLight (Interpolators i)
{
    UnityLight light;

    #if defined(DEFERRED_PASS) || SUBTRACTIVE_LIGHTING
        light.dir = float3(0, 1, 0);
        light.color = 0;
    #else
        …
    #endif

    return light;
}

image

静态物体只有烘焙光

3.2 为烘焙光打阴影

为了应用消减阴影,咱们建立一个函数以在须要的时候调整间接光。一般它不会作任何事。

void ApplySubtractiveLighting (
    Interpolators i, inout UnityIndirect indirectLight
) {}

咱们在获取光照贴图数据后要调用该函数。

UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
    …

    #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
        #if defined(LIGHTMAP_ON)
            indirectLight.diffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));

            #if defined(DIRLIGHTMAP_COMBINED)
                …
            #endif

            ApplySubtractiveLighting(i, indirectLight);
        #else
            indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
        #endif
        …
    #endif

    return indirectLight;
}

若是有消减光照,那么咱们必须获取阴影衰减。咱们能够简单地从CreateLight中将代码复制过来。

void ApplySubtractiveLighting (
    Interpolators i, inout UnityIndirect indirectLight
) {
    #if SUBTRACTIVE_LIGHTING
        UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
        attenuation = FadeShadows(i, attenuation);
    #endif
}

下一步,咱们要计算出若是使用实时光照的话咱们能够接收到多少光。咱们假设该信息和烘焙在光照贴图中的信息相吻合。因为光照贴图只包含漫射光,咱们只需计算定向光的Lambert。

#if SUBTRACTIVE_LIGHTING
    UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
    attenuation = FadeShadows(i, attenuation);
    float ndotl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz));
#endif

为了达到阴影光照的强度,咱们必须将兰伯特项乘以衰减。可是咱们已经有了彻底不含阴影的烘焙光照。所以咱们估算一下有多少光被阴影挡住了。

float ndotl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz));
float3 shadowedLightEstimate = ndotl * (1 - attenuation) * _LightColor0.rgb;

经过从烘焙光中减去该估值,咱们最终获得了调整好的光照。

float3 shadowedLightEstimate = ndotl * (1 - attenuation) * _LightColor0.rgb;
float3 subtractedLight = indirectLight.diffuse – shadowedLightEstimate;
indirectLight.diffuse = subtractedLight;

image

减去后获得的光照

不管在什么环境光场景中,这总会产生纯黑色阴影。为了更好地符合场景的须要,咱们可使用咱们的消减阴影颜色,能够经过unity_ShadowColor实现。阴影区域不该比这个颜色更暗,不过它们能够更亮些。因此咱们取计算出的光照和阴影颜色的最大值。

float3 subtractedLight = indirectLight.diffuse - shadowedLightEstimate;
subtractedLight = max(subtractedLight, unity_ShadowColor.rgb);
indirectLight.diffuse = subtractedLight;

咱们还要考虑到阴影强度被设置为小于1这个状况。为了应用阴影强度,在有阴影和无阴影光照之间基于_LightShadowData的X份量作插值。

subtractedLight = max(subtractedLight, unity_ShadowColor.rgb);
subtractedLight = lerp(subtractedLight, indirectLight.diffuse, _LightShadowData.x);
indirectLight.diffuse = subtractedLight;

image

有颜色的阴影

由于咱们的场景的环境强度(ambient intensity)被设置为0,因此默认的阴影颜色和场景不太搭配。可是能够很轻松地发现消减阴影,所以我没有调整它。还有一点很是明显,即阴影颜色如今覆盖了全部的烘焙阴影,而实际不该该这样。它应该只影响那些接收动态阴影的区域,不该该使烘焙阴影变亮。为此,使用消减光照和烘焙光照的最小值。

//indirectLight.diffuse = subtractedLight;
indirectLight.diffuse = min(subtractedLight, indirectLight.diffuse);

image

正确的消减阴影

如今只要咱们使用适当的阴影颜色,咱们就会获得正确的消减阴影。可是记住这只是一个近似,并且它不太适用于多重光照。例如,其它的烘焙光会产生错误的阴影。

image

多重光照错误的消减