在Demo实装光照烘焙与探针

  欢迎参与讨论,转载请注明出处。

前言

  Demo目前的实时光影虽已完成,但考虑到不同的配置设备,还是得做出不同档次的光影方案。那么烘焙光照(Lightmapping)与光照探针(Light Probes)就免不了了。本文将结合项目实际需求,讲述遇到的问题及解决方案。项目引擎版本为2019.4,渲染管线为URP。

初步的烘焙

  首先要做的自然是对Shader增加光照烘焙与探针的支持,照抄URP的SimpleLit Shader即可。大致要点如下:

  • Shader添加multi_compile:LIGHTMAP_ON_MIXED_LIGHTING_SUBTRACTIVE,这表示Shader会参与光照贴图与混合光照
  • 顶点着色器参数添加half2 lightmapUV : TEXCOORD1;,这是光照贴图的UV
  • 片元着色器参数添加DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 1);,这是URP自带的宏,根据LIGHTMAP_ON决定配置光照烘焙或探针的参数(lightmapUV or vertexSH),最后的参数1决定是第几个TEXCOORD
  • 在顶点着色器调用OUTPUT_LIGHTMAP_UV(lightmapUV, lightmapScaleOffset, OUT)OUTPUT_SH(normalWS, OUT)宏,它们将根据情况配置lightmapUV与vertexSH
  • 在片元着色器对光照贴图或探针进行取色(SAMPLE_GI(lmName, shName, normalWSName)),最后将之加入到着色环节即可
  • 若是想要烘焙模式下也能接受实时阴影,记得调用MixRealtimeAndBakedGI函数

  总的来说都封装好了,照着拼凑而已。那么事不宜迟,直接按照默认的烘焙配置整个看看,记得要将GameObject的Static里的Contribute GI勾选方可参与烘焙:
0
  看着似乎还不错,那么对比下实时看看吧:
1
  这么一看还是有不少差距的,必须要让烘焙与实时的效果高度接近才行呐——

ShadowMask

  经过与烘焙设置一番斗智斗勇后,我发现我要的仅仅是让阴影烘焙,以节省阴影的运算罢了。什么全局光照、烘焙自带的着色等等都是不需要的。为此我尝试过不少骚操作:生成光照贴图后进行二值化处理、直接在Shader对烘焙色进行处理等……可惜这些方案都只是治标不治本,要么在流程上繁琐,要么性能不佳,要么无法应对所有情况。最终我把目光放在了烘焙三模式之一的ShadowMask,它将单独生成阴影贴图,那么若是我只用它,抛弃光照贴图,便可达到目的了。
  不幸的是,URP并没有支持ShadowMask,官网显示仍处于In research状态。幸好网上有其他人做了实现ShadowMask的教程,顺便也学习了一波可编程渲染管线(SRP)的基础知识。经过研究发现,ShadowMask的添加并不复杂,甚至可以说是URP主动将之关闭了(严重怀疑是故意拖到后面做,显得有活干)。当然这么干了之后就表示需要维护自己的URP版本了,顺便将之开源了。
  SRP本质上是开放了一个可供用户定制的表层,多数核心功能还是封装好的。ShadowMask也不例外,其生成附属于烘焙模块。我们要做的只是添加一些设置,以及相应的Shader支持罢了:

1
2
3
4
5
6
7
8
// 添加Shadowmask字段以让界面开启选项
mixedLightingModes = SupportedRenderingFeatures.LightmapMixedBakeModes.Subtractive | SupportedRenderingFeatures.LightmapMixedBakeModes.IndirectOnly | SupportedRenderingFeatures.LightmapMixedBakeModes.Shadowmask
// 添加Shader关键字,以决定是否启用功能
public static readonly string MixedLightingShadowmask = "SHADOWS_SHADOWMASK";
// 根据条件设置相应的Shader关键字
CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.MixedLightingShadowmask, renderingData.lightData.supportsMixedLighting &&m_MixedLightingSetup == MixedLightingSetup.ShadowMask);

  Shader方面要做的调整也不多,URP本身自带ShadowMask的贴图变量TEXTURE2D(unity_ShadowMask);,其UV与光照贴图一致,复用即可。记得在Shader添加multi_compile SHADOWS_SHADOWMASK以判别是否处于ShadowMask模式下。

1
2
3
4
5
6
7
8
9
10
#if defined(SHADOWS_SHADOWMASK) && defined(LIGHTMAP_ON)
// ShadowMask贴图只有R通道
half shadowMask = SAMPLE_TEXTURE2D(unity_ShadowMask, samplerunity_ShadowMask, lightmapUV).r;
// 不超过光照Strength值
shadowMask = LerpWhiteTo(shadowMask, GetMainLightShadowStrength());
// 加入到阴影着色中
light.shadowAttenuation = min(light.shadowAttenuation, shadowMask);
#endif

  大致要做的事情就这么多,烘焙设置除阴影方面外,能怎么快就怎么设置(反正也用不上光照贴图了),一般来说需要注意的有Bounces要设为1,不然阴影会不完整。Flitering将对阴影贴图做边缘柔和处理,Lightmap ResolutionLightmap Size决定阴影质量,参考如下:
2
  另外需要注意的是,阴影精度很大程度上取决于模型的大小,因为一个模型只能有一张光照/阴影贴图,在贴图大小定死上限的前提下,模型越大贴图的解析度自然越低。那么来看看效果吧,图一为实时,图二为烘焙
3
4
  效果可以说是高度接近了,干掉了光照贴图后着色变得完全一致,阴影贴图在合理的设置下也达到了高度接近实时的效果。坡肥!

光照探针

  现在虽然实现了高度接近实时的阴影烘焙,但显而易见,当人物走向阴影处便会是这样的结果:
5
  在某些游戏也许不太理会这种现象,但这也太捞了,光照探针便是为了解决这个问题而生的。通过在场景布置探针,将会根据动态对象附近的探针取色决定明暗度:
6
  光照探针如果要手动布置那实在是太麻烦了,于是我使用了这个插件,通过简单的设置暴力的去平铺一波:
12
  根据官方文档说法,探针数量与性能成反比(但越多越精确)。但此插件平铺并不会把探针置于模型内部,以及对比了下《使命召唤手游》的光照探针,感觉还行:
13
  Shader方面没什么要改的,在URP获取光照函数GetMainLight()本身自带了对光照探针的着色处理(附加在light.distanceAttenuation中),由于不需要用到全局光照,之前的OUTPUT_SH之类的都可以删了。当然有个现象需要注意下:
7
  可以看到在暗处时实在是太黑了(也许是放弃了全局光照导致),于是我们加个约束,将暗值约束在光照Strength到1:

1
2
3
#if !defined(LIGHTMAP_ON) && defined(_MIXED_LIGHTING_SUBTRACTIVE)
mainLight.distanceAttenuation = lerp(GetMainLightShadowStrength(), 1, saturate(mainLight.distanceAttenuation));
#endif

8
  很好,这下可以说是大功告成了!

后记

  最后演示下不同光影品质下的差别吧,分别为低、中、高:
9
10
11
  话虽如此,可我发现目前直接把高品质光影扔到iPhone8下居然稳定59帧,太强了……
14