【GAD翻译馆】Unity渲染教程(十七):混合光照

发表于2017-08-28
评论1 8.4k浏览

翻译:王成林(麦克斯韦的麦斯威尔审校:黄秀美(厚德载物)


只烘焙间接光

混合烘焙阴影和实时阴影

处理代码的变化和问题

支持消减光照(subtractivelighting


这是渲染系列教程的第17部分。在上一篇中,我们使用光照贴图添加支持了静态光照。在这一篇教程中我们将烘焙光照和实施光照的特征融合起来。

混合烘焙光照和实时光照

 

烘焙间接光

光照贴图可以使我们提前计算光照。这样以纹理内存为代价减少了GPU在实时中的工作量。此外,它还加入了间接光。但是如我们上次所见,它有一些限制。首先,高光不能被烘焙。其次,烘焙光只通过光照探头影响动态物体。最后,烘焙光不产生实时阴影。

你可以在下面的截图中看到完全实时光照和完全烘焙光照之间的区别。这是前一篇教程中的一个场景,唯一的不同是我将所有的球体都设置为动态并重新改变了一些球体的位置。其它一切都是静态的。这是使用前向渲染的方法。

完全实时和完全烘焙光照

 

我还没有调整光照探头,由于现在还没有那么多静态几何体,所以探头的位置不是很重要。现在的探头光照不是那么强烈,使我们在使用它时可以更容易地注意到它。


1.1 混合模式

烘焙光照有间接光而实时光照没有,因为间接光需要光照贴图。由于间接光可以为场景加入很大的真实感,如果我们可以将它和实时光照融合在一起就再好不过了。这是可以的,尽管这意味着着色的开销会增加。我们需要将混合光(Mixed Lighting光照模式(Lighting Mode设置为烘焙间接(Baked Indirect

混合光照,烘焙间接

我们已经在前一篇教程中切换到这个模式了,但是之前我们只使用了完全烘焙光照。结果,混合光照模式没有任何区别。为了使用混合光照,光照的模式必须要设置为混合

 

混合模式的主光源

在将主定向光改为混合光后,两件事会发生。首先,Unity会再次烘焙光照贴图。这一次光照贴图只会存储间接光,所以它会比之前的暗很多。

 

完全烘焙的光照贴图vs只有间接光的光照贴图

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

 

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

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


混合光可以在运行时调整吗?

是的,因为它们被用于实时光照。但是,它们的烘焙数据时静态的。所以在运行时你只能稍微调整光照,比如稍微调整它的强度。更大的变化会使人明显看出烘焙光照和实时光照之间的不同步。

 

1.2 更新我们的着色器

刚开始一切似乎正常运行。但是,定向光的阴影衰减发生了错误。我们通过极大降低阴影距离观察到阴影被剪掉了。 

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

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

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

1
2
3
4
5
6
7
8
struct Interpolators {
    
 
//  SHADOW_COORDS(5)
    UNITY_SHADOW_COORDS(5)
 
    
};

同样,TRANSFER_SHADOW应该替换为UNITY_TRANSFER_SHADOW

1
2
3
4
5
6
7
8
Interpolators MyVertexProgram (VertexData v) {
    
 
//  TRANSFER_SHADOW(i);
    UNITY_TRANSFER_SHADOW(i);
 
    

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

1
UNITY_TRANSFER_SHADOW(i, v.uv1);

这样会再次产生一个编译错误。这是因为在一些情况下UNITY_SHADOW_COORDS错误地创建了一个插值子,尽管实际上并不需要。在这种情况下,TRANSFER_SHADOW不会初始化它,因而导致错误。这个问题出现在5.6.0中,一直到5.6.22017.1.0beta版本中都有。

 

人们通常不会注意到这个问题,因为Unity的标准着色器使用UNITY_INITIALIZE_OUTPUT宏来完全地初始化它的插值子结构体。因为我们不使用这个宏,所以出现了问题。为了解决它,我们使用UNITY_INITIALIZE_OUTPUT宏来初始化我们的插值子。那样的话,我们的代码就可以编译成功了。

1
2
3
4
5
Interpolators MyVertexProgram (VertexData v) {
    Interpolators i;
    UNITY_INITIALIZE_OUTPUT(Interpolators, i);
    

UNITY_INITIALIZE_OUTPUT有什么作用?

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

1
2
3
4
5
6
7
8
9
10
11
// 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函数的深度处理阴影”。由于着色器编译器会随意地移动代码,这句话没有任何价值。如果这个特殊情况有任何原因的话,那么也很难发现,因为Unity的着色器代码非常混乱。我反正不知道。


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

1
2
3
4
5
6
7
8
9
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之后完成。

1
2
3
4
5
6
7
8
UnityLight CreateLight (Interpolators i) {
    
 
        UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
        attenuation = FadeShadows(i, attenuation);
 
    

但是只有当UNITY_LIGHT_ATTENUATION决定跳过衰减时。这是当HANDLE_SHADOW_BLENDING_IN_GIUnityShadowLibrary包含文件中有定义时才会发生。因此FadeShadows只有在HANDLE_SHADOWS_BLENDING_IN_GI被定义时才会工作。

1
2
3
4
5
6
7
8
9
10
11
12
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;

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

unitypackage


使用阴影蒙版(Shadowmask

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

Shadowmask模式

在这个模式中,混合光照的间接光和阴影衰减都存储在了光照贴图中。阴影被存储在一张额外的贴图(即阴影蒙版)上。当只有主定向光源时,所有被照亮的物体都会作为红色出现在阴影蒙版中。红色是因为阴影信息被存储在纹理的R通道中。事实上,贴图中至多可以储存四个光照的阴影,因为它只有四个通道。 

烘焙的强度以及阴影蒙版

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

没有烘焙阴影

 

2.1 对阴影蒙版取样

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

1
2
3
4
5
6
7
8
9
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以匹配当前正在被着色的光源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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函数为我们实现了这些。

1
2
3
4
5
6
        float bakedAttenuation =
            UnitySampleBakedOcclusion(i.lightmapUV, i.worldPos);
//      attenuation = saturate(attenuation shadowFade);
        attenuation = UnityMixRealtimeAndBakedShadows(
            attenuation, bakedAttenuation, shadowFade
        );

UnityMixRealtimeAndBakedShadows是如何工作的?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

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

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

实时阴影和阴影蒙版阴影

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

只有实时阴影衰减了

 

2.2 添加一个阴影蒙版G-缓存

现在阴影蒙版用于前向渲染,但是我们需要做一番工作使它也可用于延迟渲染。具体来说,当需要时,我们添加阴影蒙版信息作为一个额外的G-缓存。所以当SHADOWS_SHADOWMASK被定义时在我们的FragmentOutput结构体中添加另一个缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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-缓存,稍微有些复杂。并不是所有的平台都支持它。Unity只在有足够多的渲染目标可用时才支持阴影蒙版,因此我们也应该这样做。

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

我们只需在G-缓存中存储取样得到的阴影蒙版数据,因为现在我们没有一个确切的光照。为此我们可以使用UnityGetRawBakedOcclusions函数。它和UnitySampleBakedOcclusion相似,唯一不同在于它没有选择其中一个通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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代替它。

1
2
3
4
5
6
7
8
#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-缓存添加一个变量。

1
2
3
4
sampler2D _CameraGBufferTexture0;
sampler2D _CameraGBufferTexture1;
sampler2D _CameraGBufferTexture2;
sampler2D _CameraGBufferTexture4;

接下来,创建一个函数来得到适当的阴影衰减。如果我们有一个阴影蒙版,这可以通过对纹理取样然后和unity_OcclusionMaskSelector进行一次颜色饱和点乘来实现。这个变量是在UnityShaderVariables中定义的,包含了一个用于选择当前正在被渲染的光照通道的向量。

1
2
3
4
5
6
7
8
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中,即使当前光照没有实时阴影,我们在有阴影蒙版时也要衰减阴影,。

1
2
3
4
5
6
7
8
9
10
11
12
13
UnityLight CreateLight (float2 uv, float3 worldPos,
float viewZ) {
    
 
    #if defined(SHADOWS_SHADOWMASK)
        shadowed = true;
    #endif
 
    if (shadowed) {
        
    
 
    

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

1
2
3
4
5
6
7
8
9
10
11
    if (shadowed) {
        float shadowFadeDistance =
            UnityComputeShadowFadeDistance(worldPos, viewZ);
        float shadowFade = UnityComputeShadowFade
(shadowFadeDistance);
//      shadowAttenuation = saturate(shadowAttenuation
shadowFade);
        shadowAttenuation = UnityMixRealtimeAndBakedShadows(
            shadowAttenuation, GetShadowMaskAttenuation(uv),
shadowFade
        );
 
        
    

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

1
2
3
4
5
6
7
8
9
10
11
12
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了。 

距离阴影蒙版模式

 

距离阴影蒙版

Unity2017中,你使用哪个阴影蒙版模式是通过质量设置进行控制的。


当使用DistanceShadowmask模式时,所有物体都使用实时阴影。第一眼看去,好像和Baked Indirect模式完全一样。 

所有物体都有实时阴影

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

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

我们已经支持这个模式了,因为我们正在使用UnityMixRealtimeAndBakedShadows。为了正确地混合完全实时阴影和烘焙阴影,它像往常那样衰减实时阴影,然后取其和烘焙阴影的最小值。

 

2.5 多重光照 

因为阴影蒙版有四个通道,它可以最多同时支持4个光照体积重叠在一起。例如,在以下场景中有三个额外的聚光源。我调低了主光源的强度以使你更容易看到聚光源。

 

四个光源,都是混合光

主定向光源的阴影仍存储在R通道中。你还能够看到存储在G通道和B通道中的聚光源的阴影。最后一个聚光源的阴影存储在A通道中,我们看不到它。

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

5个重叠的光照,其中一个为完全烘焙光

 

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

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

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

Unity的标准着色器在5.6.2 2017.1. 0f1版本以前也存在这个问题。不过,这不是拥有光照贴图的引擎的一个内在限制。这是使用UNITY_LIGHT_ATTENUATION的新方法中的一个漏洞。Unity使用通过UNITY_SHADOW_COORDS定义的阴影插值子来存储定向阴影的屏幕空间坐标,或者其它拥有阴影蒙版的光源的光照贴图坐标。

使用阴影蒙版的定向光还需要光照贴图坐标。在前向基础通道中,这些坐标会被包含,因为LIGHTMAP_ON会在需要的时候被定义。然而,LIGHTMAP_ON在附加通道中永远不会被定义。这意味着附加定向光没有可用的光照贴图坐标。结果UNITY_LIGHT_ATTENUATION在这种情况下只会使用0,导致错误的光照贴图取样。

所以我们不能依靠UNITY_LIGHT_ATTENUATION额外获得使用阴影蒙版的定向光源。让我们能够轻松地分辨出这个情况。假设我们实际使用屏幕空间的定向阴影,在一些平台上不是这样。

1
2
3
4
5
6
7
8
9
10
#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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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函数进行我们自己控制的衰减。

1
2
3
4
5
6
7
float FadeShadows (Interpolators i, float attenuation) {
    #if HANDLE_SHADOWS_BLENDING_IN_GI || ADDITIONAL
_MASKED_DIRECTIONAL
_SHADOWS
        
    #endif
 
    return attenuation;

但是,这仍然不正确,因为我们为其输入了错误的衰减数据。我们必须绕开UNITY_LIGHT_ATTENUATION,只得到烘焙后的衰减,在这个情况中我们可以使用SHADOW_ATTENUATION宏。

1
2
3
4
5
6
7
8
9
10
11
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;

两个定向光源正确的衰减

 

是否可以完全依赖UNITY_LIGHT_ATTENUATION

很长一段时间内宏代码都很稳定。对于自定义着色器,最好使用Unity的光照配置。这种情况在5.6.0中发生了变化,其中一个新的方法强行使用一个旧的宏结构。理想状态下,他们会尽快修复这个宏,这样我们就可以继续使用了。否则,我会修改这篇教程不再使用它。

unitypackage


消减阴影

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

消减模式

在切换到消减模式后,场景会亮很多。这是由于静态物体现在同时使用完全烘焙的光照贴图和定向光源。和前面一样,动态物体仍然使用光照探头和定向光源。

静态物体受到两次光照

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

 

3.1 消减光照

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

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

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

1
2
3
4
5
6
7
8
9
10
11
#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

在做其他事情之前,我们必须去除掉双重阴影。为此我们可以关闭动态光照,就像我们对延迟通道所做的那样。

1
2
3
4
5
6
7
8
9
10
11
12
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;

静态物体只有烘焙光

 

3.2 为烘焙光打阴影

为了应用消减阴影,我们创建一个函数以在需要的时候调整间接光。通常它不会做任何事。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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中将代码复制过来。

1
2
3
4
5
6
7
8
void ApplySubtractiveLighting (
    Interpolators i, inout UnityIndirect indirectLight
) {
    #if SUBTRACTIVE_LIGHTING
        UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
        attenuation = FadeShadows(i, attenuation);
    #endif

下一步,我们要计算出如果使用实时光照的话我们可以接收到多少光。我们假设该信息和烘焙在光照贴图中的信息相吻合。由于光照贴图只包含漫射光,我们只需计算定向光的兰伯特项。

1
2
3
4
5
6
#if SUBTRACTIVE_LIGHTING
    UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
    attenuation = FadeShadows(i, attenuation);
 
    float ndotl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz));
#endif

为了达到阴影光照的强度,我们必须将兰伯特项乘以衰减。但是我们已经有了完全不含阴影的烘焙光照。因此我们估算一下有多少光被阴影挡住了。

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

通过从烘焙光中减去该估值,我们最终得到了调整好的光照。

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

减去后得到的光照

无论在什么环境光场景中,这总会产生纯黑色阴影。为了更好地符合场景的需要,我们可以使用我们的消减阴影颜色,可以通过unity_ShadowColor实现。阴影区域不应比这个颜色更暗,不过它们可以更亮些。所以我们取计算出的光照和阴影颜色的最大值。

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

我们还要考虑到阴影强度被设置为小于1这个情况。为了应用阴影强度,在有阴影和无阴影光照之间基于_LightShadowDataX分量做插值。

1
2
3
4
subtractedLight = max(subtractedLight, unity_ShadowColor.rgb);
subtractedLight =
    lerp(subtractedLight, indirectLight.diffuse,
_LightShadowData.x);
indirectLight.diffuse = subtractedLight;

有颜色的阴影

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

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

正确的消减阴影

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

其它光照错误的消减

下一篇教程将探讨实时全局光照。将会在20177月发布。

unitypackage

PDF


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: