unity中混合光照与阴影的探索

阅读本文前最好先看一下Unity custom shader中调用内置Lightmap和Light Probes

我们知道,unity中的Lighting Mode分为三种,baked indirect,subtractive和shadowmask三种。

baked indirect模式混合了实时直接光和烘焙间接光,并提供了实时阴影。由于光照比较真实,阴影精确度比较可信,所以非常适合中端硬件。
subtractive模式提供直接和间接光的烘焙,直接实时阴影只受一盏方向光的影响。由于它并不能提供非常真实的光照结果,所以风格化的渲染或者低端硬件比较适合这种模式。
shadowmask模式混合了实时直接光和烘焙间接光,它能烘焙物体的阴影并能自动混合实时阴影。这种模式看起来最为真实但也最消耗资源,不过可以在Quality Settings进行调整。高中端设备适合这种模式。

有了初步的印象,我们来看看具体使用时的不同之处

subtractive
shadowmask

从生成的文件来看,shadowmask模式比subtractive模式多了一张贴图,这张贴图保存了静态物体的阴影烘焙信息。那么subtractive模式没有这张贴图是不是意味着没有阴影信息呢?其实不是的,subtractive模式中静态物体的阴影也烘焙了,并且保存在了光照贴图中,也就是说阴影信息其实是shadowmask模式额外多用了一张贴图来保存,并不是说subtractive模式阴影信息没了。

subtractive
shadowmask

那么在shadowmask模式下有了这张阴影贴图后我们该如何去使用呢?看下面的代码

float directAtten = SHADOW_ATTENUATION(i);

float viewZ = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
float shadowFadeDistance = UnityComputeShadowFadeDistance(i.worldPos, viewZ);
float shadowFade = UnityComputeShadowFade(shadowFadeDistance);

float bakedAtten = UnitySampleBakedOcclusion(i.uvLM,i.worldPos);
float atten = UnityMixRealtimeAndBakedShadows(directAtten, bakedAtten, shadowFade);
col.rgb *= atten;
col.rgb += lm;

首先我们需要UnitySampleBakedOcclusion这个方法来获取烘焙阴影,它需要光照贴图的uv和世界坐标来进行采样,采样完毕所得到的阴影需要再扔进UnityMixRealtimeAndBakedShadows这个方法,同时扔进实时阴影和一个距离来进行混合,使得烘焙和实时的阴影不至于泾渭分明。
现在我们来详细看看这两个方法在干什么,UnitySampleBakedOcclusion的源码如下

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

    #else

        //In forward dynamic objects can only get baked occlusion from LPPV, light probe occlusion is done on the CPU by attenuating the light color.
        fixed atten = 1.0f;
        #if defined(UNITY_INSTANCING_ENABLED) && defined(UNITY_USE_SHCOEFFS_ARRAYS)
            // ...unless we are doing instancing, and the attenuation is packed into SHC array's .w component.
            atten = unity_SHC.w;
        #endif

        #if UNITY_LIGHT_PROBE_PROXY_VOLUME && !defined(LIGHTMAP_ON) && !UNITY_STANDARD_SIMPLE
            fixed4 rawOcclusionMask = atten.xxxx;
            if (unity_ProbeVolumeParams.x == 1.0)
                rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
            return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
        #endif

        return atten;
    #endif
}

首先可以知道,我们要用UnitySampleBakedOcclusion不得不声明一下SHADOWS_SHADOWMASK,否则进入else逻辑就不读取阴影贴图了(这时的阴影就要去读取光照探针中的数据了,传入的世界坐标worldPos这时就有用了)。。。 然后主要的代码就涉及到如果有光照贴图的话,去采样unity_ShadowMask贴图。采样出来的rawOcclusionMask记录的是该像素,被灯光影响的情况,比如采样的是(1,1,0,1)那么表示这个像素,被0号和1、3号灯照射到了,但是2号灯,不能照射到,这是原始的遮挡信息。 unity_OcclusionMaskSelector就是说,unity会自动的进行筛选最强的灯(像是(0,1,0,0),不可能出现(1,1,0,0)这种有最强的两盏灯),这个unity_OcclusionMaskSelector就记录对应的灯的编号而已。dot就是过滤出来真正需要的信息,比如说现在rawOcclusionMask是(0.5,0.1,0,0.8),unity_OcclusionMaskSelector是(0,1,0,0),那么过滤出来就是0.1,rawOcclusionMask的g通道的值被保留下来了。

UnityMixRealtimeAndBakedShadows的源码如下

half UnityMixRealtimeAndBakedShadows(half realtimeShadowAttenuation, half bakedShadowAttenuation, half fade)
{
    // -- Static objects --
    // FWD BASE PASS
    // ShadowMask mode          = LIGHTMAP_ON + SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK
    // Subtractive mode         = LIGHTMAP_ON + LIGHTMAP_SHADOW_MIXING
    // Pure realtime direct lit = LIGHTMAP_ON

    // FWD ADD PASS
    // ShadowMask mode          = SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = SHADOWS_SHADOWMASK
    // Pure realtime direct lit = LIGHTMAP_ON

    // DEFERRED LIGHTING PASS
    // ShadowMask mode          = LIGHTMAP_ON + SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK
    // Pure realtime direct lit = LIGHTMAP_ON

    // -- Dynamic objects --
    // FWD BASE PASS + FWD ADD ASS
    // ShadowMask mode          = LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = N/A
    // Subtractive mode         = LIGHTMAP_SHADOW_MIXING (only matter for LPPV. Light probes occlusion being done on CPU)
    // Pure realtime direct lit = N/A

    // DEFERRED LIGHTING PASS
    // ShadowMask mode          = SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = SHADOWS_SHADOWMASK
    // Pure realtime direct lit = N/A

    #if !defined(SHADOWS_DEPTH) && !defined(SHADOWS_SCREEN) && !defined(SHADOWS_CUBE)
        #if defined(LIGHTMAP_ON) && defined (LIGHTMAP_SHADOW_MIXING) && !defined (SHADOWS_SHADOWMASK)
            //In subtractive mode when there is no shadow we kill the light contribution as direct as been baked in the lightmap.
            return 0.0;
        #else
            return bakedShadowAttenuation;
        #endif
    #endif

    #if (SHADER_TARGET <= 20) || UNITY_STANDARD_SIMPLE
        //no fading nor blending on SM 2.0 because of instruction count limit.
        #if defined(SHADOWS_SHADOWMASK) || defined(LIGHTMAP_SHADOW_MIXING)
            return min(realtimeShadowAttenuation, bakedShadowAttenuation);
        #else
            return realtimeShadowAttenuation;
        #endif
    #endif

    #if defined(LIGHTMAP_SHADOW_MIXING)
        //Subtractive or shadowmask mode
        realtimeShadowAttenuation = saturate(realtimeShadowAttenuation + fade);
        return min(realtimeShadowAttenuation, bakedShadowAttenuation);
    #endif

    //In distance shadowmask or realtime shadow fadeout we lerp toward the baked shadows (bakedShadowAttenuation will be 1 if no baked shadows)
    return lerp(realtimeShadowAttenuation, bakedShadowAttenuation, fade);
}

相对于上一个方法,这个理解起来比较简单,根据不同情况返回实时阴影或者烘焙阴影,或者通过距离(点到摄像头的距离)来进行烘焙阴影与实时阴影的混合。

现在来看这个方法里的fade参数,通过代码可以看出我们需要调用UnityComputeShadowFadeDistanceUnityComputeShadowFade来计算得到最终的fade,那么就来看看这两个方法在干什么吧。

UnityComputeShadowFadeDistance源码如下

float UnityComputeShadowFadeDistance(float3 wpos, float z)
{
    float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
    return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
}

这里unity_ShadowFadeCenterAndType包含了阴影中心位置(xyz分量保存)和阴影的类型(w分量保存),由世界坐标wpos计算得到与阴影中心位置的距离sphereDist,按照阴影类型在视线距离zsphereDist之间做lerp操作来得到阴影fade的距离。不过我谷歌了一下,这个阴影类型w分量不是0就是1,也就是说最终结果返回的不是z就是sphereDist

UnityComputeShadowFade源码如下

half UnityComputeShadowFade(float fadeDist)
{
    return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
}

上面计算得到的阴影fade的距离还需要进一步的被矫正,所以需要把计算得到的距离扔进UnityComputeShadowFade方法,而_LightShadowDataz分量包含着scale信息,w分量包含着offset信息,计算一下后将结果限制在0-1之间,就是最终阴影fade的距离了。

最后,我们把UnityMixRealtimeAndBakedShadows计算得出的阴影乘上输出的颜色,再加上(shadowmask模式下烘焙出来的光照贴图比较暗,加比较能还原真实结果。而subtractive模式下是乘上去比较能还原结果,个人觉得这里是加还是乘以美术导向为准)光照贴图就好了。

这里要注意,shadowmask有两种模式,distance shadowmask和shadowmask。Distance shadowmask模式使用时第一眼看上去和baked indirect模式一样,阴影都是实时计算的。然而,还是会有一张阴影贴图生成,这张贴图中的阴影是当超出场景设置中的阴影距离时使用的,而在这个距离内都会使用实时阴影,所以这个模式会是最为昂贵的模式。我们之前代码里已经用上了UnityMixRealtimeAndBakedShadows这个方法,所以可以提供实时和烘焙阴影的过渡。

PS. 只有直接光的情况下UNITY_LIGHT_ATTENUATION就是UNITY_SHADOW_ATTENUATION,而UNITY_SHADOW_ATTENUATION就是SHADOW_ATTENUATION

#   define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#ifdef DIRECTIONAL
#    define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif

最后,我们来明确下各个模式下对动静态物体的影响。

shadowmask

distance shadowmask模式

  • 动态物体被混合光照到会接受到:
    • 实时直接光
    • 烘焙间接光,通过光照探针
    • 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
    • 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
    • 超出阴影距离后使用了光照探针的静态物体所产生的烘焙阴影
  • 静态物体被混合光照到会接受到:
    • 实时直接光
    • 烘焙间接光,通过光照贴图
    • 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
    • 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
    • 超出阴影距离后静态物体所产生的烘焙阴影,通过shadowmask贴图实现

shadowmask模式

  • 动态物体被混合光照到会接受到:
    • 实时直接光
    • 烘焙间接光,通过光照探针
    • 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
    • 超出阴影距离后使用了光照探针的静态物体所产生的烘焙阴影
  • 静态物体被混合光照到会接受到:
    • 实时直接光
    • 烘焙间接光,通过光照贴图
    • 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
    • 超出阴影距离后使用了阴影贴图的静态物体所产生的烘焙阴影
subtractive

subtractive模式

  • 动态物体被混合光照到会接受到:
    • 实时直接光
    • 烘焙间接光,通过光照探针
    • 在阴影距离内动态物体由主光源投出的实时阴影,阴影源自shadowmap技术
    • 使用了光照探针的静态物体所产生的阴影
  • 静态物体被混合光照到会接受到:
    • 烘焙直接光,通过光照贴图
    • 烘焙间接光,通过光照贴图
    • 静态物体产生的烘焙阴影,通过光照贴图
    • 在阴影距离内动态物体由主光源投出的实时阴影,阴影源自shadowmap技术
baked indirect

baked indirect模式

  • 动态物体被混合光照到会接受到:
    • 实时直接光
    • 烘焙间接光,通过光照探针
    • 在阴影距离内动态物体投出的阴影,阴影源自shadowmap技术
    • 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
  • 静态物体被混合光照到会接受到:
    • 实时直接光
    • 烘焙间接光,通过光照贴图
    • 静态物体产生的实时阴影,阴影源自shadowmap技术
    • 在阴影距离内动态物体产生的实时阴影,阴影源自shadowmap技术

项目地址

参考
unity中UnityGlobalIllumination.cginc的分析

Mixed Lighting Lightmap & Shader In Unity | Unity中混合光照Lightmap研究

Rendering 17 Mixed Lighting

Unity 阴影淡入淡出效果中Shader常量 unity_ShadowFadeCenterAndType和_LightShadowData的问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容