您现在的位置是:首页 >技术交流 >unity GI Shader 实现网站首页技术交流

unity GI Shader 实现

暮志未晚Webgl 2023-06-25 15:48:51
简介unity GI Shader 实现

之前分享了一篇对unity全局光照的解析,里面提到了一些东西,需要在Shader内实现,在这一篇补上。
要实现对全局GI的shader实现,我们可以通过对unity内置的Lit进行解析查看。

烘焙的方式有很多种,选择合适的方式烘焙和使用合适类型的光源尤为重要。

首先,我们先实现一下最基础的烘焙光照显示,就是将Light Map贴图显示出来。需要设置的是,将需要烘焙的模型全设置成静态,光照设置为Baked,并且模型开启了第二套LightMap UV。
在这里插入图片描述
准备工作结束。

纯烘焙,无动态

光照全部为静态的,照明模式将不会起作用,因为它选择哪个烘焙都一样,将直接光,间接光和阴影都烘焙到Light Map上面。
那么开始写shader,首先要开启宏

#pragma multi_compile _ LIGHTMAP_ON //光照贴图支持

获取LightMap的UV

float2 staticLightmapUV : TEXCOORD1;

LightMapUV是第二套

output.staticLightmapUV = input.staticLightmapUV * unity_LightmapST.xy + unity_LightmapST.zw;

然后计算完UV偏移以后,传递到片元着色器
在片元着色器中,我们只需要和之前的SH进行一个宏的判断,用来对间接光漫反射进行修改。

#if defined(LIGHTMAP_ON)
    float4 encodedIrradiance = SAMPLE_TEXTURE2D(unity_Lightmap,samplerunity_Lightmap,input.staticLightmapUV);
    Irradiance = DecodeLightmap(encodedIrradiance, float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0.0h, 0.0h));
#else
 Irradiance = SampleSH(WorldNormal);//SH,Light Probe
#endif

首先是对宏进行判断,然后读取LightMap贴图,读取到的贴图是被编码过的,所以还需要一步解码,这个是unity内置的功能,所以可以直接使用内值的变量去解码。如果当前没有开启LIGHTMAP_ON,我们还可以按照之前的那种SH方法去获取间接光漫反射。

在这种模式下,动态模型接收的是Light Probe的漫反射光照,因为没有主光源,实时渲染时也没有主光源进行渲染。
在这里插入图片描述
接下来兼容一下Lightmap方向贴图。

#pragma multi_compile _ DIRLIGHTMAP_COMBINED //方向贴图

首先要设置宏,用来判断是否开启方向贴图。
然后再LightMap中判断方向贴图,去设置方向贴图

half3 Irradiance = half3(0,0,0);
#if defined(LIGHTMAP_ON)
    float4 encodedIrradiance = SAMPLE_TEXTURE2D(unity_Lightmap,samplerunity_Lightmap,input.staticLightmapUV);
    Irradiance = DecodeLightmap(encodedIrradiance, float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0.0h, 0.0h));
    #if defined(DIRLIGHTMAP_COMBINED)
        float4 direction = SAMPLE_TEXTURE2D(unity_LightmapInd,samplerunity_Lightmap,input.staticLightmapUV);
        half3 LightDir = direction * 2.0f - 1.0f;
        half halfLambert = dot(WorldNormal,LightDir) * 0.5 + 0.5;
        Irradiance = Irradiance * halfLambert / max(1e-4,direction.w);
    #endif
#else
 Irradiance = SampleSH(WorldNormal);//SH,Light Probe
#endif

注意,unity_Lightmap是内置LightMap,unity_LightmapInd是内置的方向贴图名称,samplerunity_Lightmap是公用的采样器。
那么,有个问题,我们有了光照的方向,法向以及视角方向,是不是可以计算LightMap模型的高光效果,答案是肯定的。不管是blinnPhong还是使用GGX高光数据都够,那肯定能直接计算出来。
以下是示例代码:

#if !defined(_DIRHIGHLIGHT_OFF)
    half BlinnPhong = pow(saturate(dot(WorldNormal,normalize(LightDir + ViewDir))),30); //计算出高光强度
    Irradiance = Irradiance + Irradiance * BlinnPhong; //高光和漫反射相加
#endif

特点:

  1. 场景中的光照全靠Light Map,Light Probe,Refletion Probe提供。没有实时光源,节约性能。
  2. 所有物体缺少动态高光,静态物体勉强可以使用方向贴图获取朝向计算,动态物体的话只能使用其它方式实现。
  3. 动态物体不能产生投影。

总结:纯烘焙模式,由于其性能优秀,一般会采用在移动端固定视角类型的游戏上,动态物体就使用其它方式比如全局设置一个光的朝向等方法实现。动态物体无阴影,一般采用假阴影,比如平面阴影那种,还是比较适合移动端。
使用推荐:移动端2.5D俯视角游戏。

使用Mixed混合光照烘焙

之前说过,使用了Mixed混合光源以后,会额外的生成一组阴影贴图,也就是静态阴影贴图,然后根据我们设置的光照模式,有不同的区别。

Subtrative模式

Mixed灯光对LightMap物体无效,通过light.distanceAttenuation来屏蔽Mixed光源,如果是使用LightMap的物体,这个值将为0,避免的重复着色。
在这个模式下,静态物体的直接光,间接光,阴影都被烘焙到LightMap和LightProbe上面,动态物体还是会被Mixed光源照射,进行实时计算。这种方式是一种非常合理的设置方式,静态物体固定,用LightMap计算即可,动态物体刚好使用Mixed光照实时渲染,漫反射使用LightProbe。但是还是有几个问题需要解决,如果能解决,这将适合大多数移动端平台性能最优方案:

  1. 动态物体无法投射阴影到静态物体身上。
    解决方案:
//SUBTRACTIVE模式下的混合光照,用于处理实时光照和光照贴图的混合
#if defined(LIGHTMAP_ON) && defined(_MIXED_LIGHTING_SUBTRACTIVE)
    #if defined(_MAIN_LIGHT_SHADOWS_SCREEN)
        float4 positionCS = TransformWorldToHClip(WorldPos);
        float4 ShadowCoord = ComputeScreenPos(positionCS);
    #else
        float4 ShadowCoord = TransformWorldToShadowCoord(WorldPos);
    #endif
    float4 ShadowMask = float4(1.0, 1.0, 1.0, 1.0);
    Light mainLight = GetMainLight(ShadowCoord, WorldPos, ShadowMask);
    Irradiance = SubtractDirectMainLightFromLightmap(mainLight, WorldNormal, Irradiance);
#endif

这种方式是在对间接光源的辐射度里面去处理,从名字SubtractDirectMainLightFromLightmap就可以看出,是从光照贴图冲减去主光源,属于trick的方式。
首先还是要获取到平衡光主光源,这种方式只适合平衡光主光源。
上面的宏_MIXED_LIGHTING_SUBTRACTIVE是在unity内不声明的,只需要判断即可。
对于其的实现原理,我们可以查看源码学习一下:
在这里插入图片描述
从效果上看,在移动端是可以接受的:
在这里插入图片描述
2. 动态物体的实时光照无法被静态物体遮挡。
解决方案:就是在获取实时光源时,通过宏进行区别。

//ShadowMask是用来处理静态投影和动态投影的结合
#if defined(SHADOWS_SHADOWMASK) && defined(LIGHTMAP_ON)
    half4 ShadowMask = SAMPLE_SHADOWMASK(input.staticLightmapUV);
#elif !defined(LIGHTMAP_ON)
    half4 ShadowMask = unity_ProbesOcclusion;
#else
    half4 ShadowMask = half4(1, 1, 1, 1);
#endif

以上是获取ShadowMask的方式,如果使用光照烘焙,则直接获取ShadowMask的贴图(如果有的情况下)。如果没有LIGHTMAP_ON,那就是实时渲染,则在Light Probe上面去获取遮挡关系,遮挡关系我们也可以通过编辑器查看:
在这里插入图片描述
设置显示全部Light Probe,然后显示遮挡,会发现在阴影处的Light Probe明显比如在亮光处的Light Probe要暗。
在这里插入图片描述
unity的内置处理是一个叫MainLightShadow的函数:
在这里插入图片描述
这种方式是在移动端平台最推荐的一种方式,静态物体单纯的从Light Map上面获取直接光,间接光和阴影,能够实时从动态物体上面获取实时阴影进行混合。动态物体能够实时计算直接光,并且能够通过烘焙的Light Probe实时更新被场景遮挡的关系,性能和效果达到的一种均衡。
还有一些问题:角色被阴影遮挡没有实时遮挡那么明确,实时渲染阴影和烘焙阴影之间融合的也不好。如果移动平台的话,勉强够用。毕竟其它方式更耗。

ShadowMask模式

如果目标是主机或者pc,那就不用这么拮据了,可以使用ShadowMask模式,这种模式和上面的Subtrative的区别在于,烘焙时不会将阴影直接烘焙到LightMap上面,而是单独生成一份ShadowMask贴图,用于阴影遮挡关系。这样,就不用使用那种trick的方式计算阴影,而是直接两个阴影直接融合,并且动态物体身上也能够获取到烘焙的阴影的影响。但是你会发现,烘焙出来的LightMap的体积会增加一倍,这也算一个缺点。
在这里插入图片描述
你会发现每个贴图都有对应的一张shadowmask,ShadowMask模式下还有两种模式,之前也说过,
在这里插入图片描述
在质量里面可以设置ShadowMask的模式,ShadowMask就是静态物体纯用shadowmask贴图渲染阴影,也能和动态物体很好的融合。但是Distance Shadowmask,它会渲染两遍,也就是实时阴影和shadowmask阴影都渲染,最后再来一个融合。
在这里插入图片描述
上图为Distance Shadowmask的渲染阴影的批次。
在这里插入图片描述
上图为shadowmask模式的批次。明显的看出少了几十个pass。所以,还是那句话,更好的效果需要更多的性能。
使用Distance Shadowmask,你的静态物体会进行两次阴影渲染(shadowmask贴图的和实时渲染)最后进行融合的,而且还需要shadowCasterPass渲染。
而Shadowmask,相对于Subtrative模式,解决了动态阴影和静态阴影融合的问题,代价是lightmap体积翻倍。

Baked Indirect模式

这个模式,就是将间接光以及烘焙光都烘焙到LightMap上面,直接光还是实时渲染的。我们只需要按照前面,在间接光里面去获取LightMap去替换SH即可。

实时渲染LightMap

Realtime Lighting和Mixed Lighting是可以共用的,实时的主要解决实时光源的漫反射弹射效果,而烘焙的,可以直接处理Baked光源以及区域光Area Light。
在这里插入图片描述
如何混合呢,我们可以看到unity内置的代码,它兼容了动态LightMap和静态LightMap,通过宏,进行判断你的lightmap是否需要计算。它们之前没有重叠的部分,因为动态LightMap是存储的动态光照的漫反射信息,而静态LightMap是存储的静态光照的漫反射信息。所以它们之间相加即可。我们之前也计算了静态LightMap,那么动态LightMap的UV如何获取。

首先要获取到dynamicLightmapUV,这个需要第三套uv。

float2 dynamicLightmapUV : TEXCOORD2;

顶点着色器计算UV

output.dynamicLightmapUV = input.dynamicLightmapUV * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;

获取动态光照LightMap:

float4 encodedIrradiance = SAMPLE_TEXTURE2D(unity_DynamicLightmap,samplerunity_DynamicLightmap,input.dynamicLightmapUV);
Irradiance = DecodeLightmap(encodedIrradiance, float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0.0h, 0.0h));

这样就自己实现,如果没有自定义的要求,我推荐,还是直接使用SampleLightmap函数就行了。

这种双管齐开的方式,是最好的效果,推荐最高的效果上开启即可,毕竟用了两套烘焙系统,即耗性能又耗内存,但是效果会好很多,因为你的动态光源和静态光源都得到间接光漫反射的计算,让效果更真实。
推荐:主机和电脑高画质开启。

插件推荐

Bakery 替换内置的烘焙GI,效率还高
Magic Lightmap Switcher 可以实现LightMap贴图的切换
Magic Light Probes 可以帮你轻松快速的布置光照探测器LightProbe

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。