找回密码
 立即注册
查看: 827|回复: 20

深入浅出UE4 ILC

[复制链接]
发表于 2022-2-11 18:28 | 显示全部楼层 |阅读模式
UE4在移动端即使在没有TOD的前提下,可选的GI方案也非常有限,静态物体的GI可以使用2D Lightmap来表达,而场景中的动态物体(Stationary,Movable,也包括诸如粒子、带骨骼动画的模型)等大多会采用基于三阶球谐(SH3)的静态漫反射来表达——UE4中表达SH3光照名字叫间接光照缓存(IndirectLightingCache,以下简称ILC)。
ILC基于逐物体生效,当一个物体覆盖比较大的空间范围的时候,ILC所能给出的结果往往会和周围使用诸如2D Lightmap,VLM等逐像素方案比有颜色/亮度跳变。
本文旨在为UE4的ILC做一个大而全完整的介绍,所包含内容如下:

  • ILC的使用基础
  • ILC在Lightmass生成解析
  • ILC的存储与加载
  • ILC的数据更新
  • ILC与渲染
  • ILC总结与优化
ILC使用基础

ILC数据生成
UE4默认生成的是体积Lightmap(Volumetric Lightmap),需要手工设置Lightmass的相生成选项,开启ILC的生成。具体的需要在WorldSetting中的Lightmass选项卡的Volume Lighting Method中,选择 Sparse Volume Lighting Samples,操作如下图所示。


在开启ILC生成选项之后,在下次构建光照图的时候,就会同时生成ILC数据。场景中生成ILC数据之后,可以勾选编辑器窗口中的Show/Visualize/Volume Lighting Samples进行预览,使用ILC数据预览可以快速定位ILC的常见显示问题,如发黑,光照偏色等问题。编辑器预览入口如下图所示



下图这些灰白的小方块即是生成的ILC数据



ILC生成参数设置
Lightmass ILC和Lightmap一样,只会生成在LightmassImportanceVolume中生成。
UE4编辑器仅提供了一个会影响到ILC密度缩放的参数可以调节。编辑器设置在WorldSetting\Lightmass关项卡中,它表示ILC生成距离的缩放值,默认为1,当它大于1时,ILC生成的距离增大,从而数量减少,小于1时,ILC生成的距离减小,从而数量增加。


ILC生成参数细节则需要在配置文件(*Lightmass.ini)中进行手工配置。常用的参数,为方便理解,我为它们的含义加上了注释,具体如下所示:
SurfaceLightSampleSpacing=300       //物体表面生成的ILC采样点之间的距离
FirstSurfaceSampleLayerHeight=50    //物体表面第一层ILC采样点和表面之间的距离
SurfaceSampleLayerHeightSpacing=250 //每层ILC采样点之间的距离
NumSurfaceSampleLayers=2   //物体表面生成几层ILC采样点
//Character Indirect Detail Volume内的ILC采样点距离
DetailVolumeSampleSpacing=300  
//既不是物体表面,又不是Character Indirect Detial Volume内的其它地方ILC采样距离
VolumeLightSampleSpacing=3000
除了NumSurfaceSampleLayers之外,其它参数的单位都是厘米,所以ILC在物体表面和Character Indirect Detail Volume中的采样点距离是3米,而在这两者之外,同时在Lightmass Importance Volume之内则其密度为30米。Character Indirect Detail Volume一般来用来给不会生成ILC的地方手工放置,用于在该位置增加ILC的生成。

ILC的常见问题


静态模型支持ILC且生成静态阴影
UE4 4.25开始,开始移动端静态Mesh也可以使用强制使用ILC,只需将其Lightmap Type设为Force Volumetric即可。


这样的好处是静态Mesh既可以有静态阴影,又可以节省内存只使用ILC而不是Lightmap



漏光和漏阴影
漏光是固定采样距离的SH类GI的顽固问题,ILC对于小于采样距离(还记得默认距离是3米吗?)的薄墙来说也一样是室内漏内,室外漏阴影。所以当游戏角色走到昏暗的墙角,可能会突然亮起来,而当游戏角色走在明亮的墙边阴影里,也可能会变的比周围环境更黑。以下是ILC漏光情况的示意,在全封闭室内接受ILC光照的亮度和周围环境比太亮了。



ILC过渡
默认的,UE4开启了ILC过渡功能,物体在移动过程中,它的ILC不是立即调节到当前ILC亮度,而是慢慢从之前的ILC过渡到当前的ILC。ILC过渡示意如下图所示


UE4的ILC过渡速度是可配置的
//默认值800,速度越大过渡越快,反之则越慢,时间越久
r.Cache.SampleTransitionSpeed

ILC在Lightmass生成

文章余下部分是ILC的代码实现和算法分析。
ILC在Lightmass中的生成分为两步,如下所示



生成ILC放置点
在UE4中,ILC的放置点可以使用CharacterImportanceVolume进行手工补足,但大多数的ILC放置点由算法生成。生成ILC放置点的流程分三步:

  • 在场景物体表面的法线正方向上,均匀生成NumSurfaceSampleLayers(默认值为2)层,密度为SurfaceLightSampleSpacing(默认值为3米) * VolumeLightSamplePlacementScale(默认值为1)的ILC的放置点
  • 为CharacterImportanceVolume内均匀生成密度为DetailVolumeSampleSpacing(默认值为3米)* VolumeLightSamplePlacementScale(默认值为1)ILC放置点
3. 为LightmassImportanceVolume中不靠近物体表面和CharacterImportanceVolume的地方,均匀生成稀疏的ILC放置点,密度为VolumeLightSampleSpacing(默认值为30米)
其中,对于能够在场景物体表面正方向生成ILC,还有一些其它限制:

  • 此场景物体必须是静态、参与Lightmap生成的、投射阴影的,对于熟悉Lightmass的同学来说这很好理解,因为Lightmass中只导入了静态的场景物体,其它物体对于Lightmass来说不存在。
  • 只有物体表面在世界空间朝上的部分会参与ILC生成,这儿看起来UE4似乎是有一个假设,它的ILC更多的是提供给在可行走表面上的角色所使用而优化,这样可以使全场景生成更小的ILC数据。


代码实现如下
if(TriangleNormal.Z > 0.0f)
{
    Rasterizer.DrawTriangle(Vertices[0],Vertices[1],Vertices[2],
                               XYPositions[0],XYPositions[1],XYPositions[2],false);
}


  • 对于退化的三角形或Lightmap精度小于给定阈值的表面,则该三角形不生成ILC
if (TexelDensity <
     2.0f / FMath::Square(DynamicObjectSettings.SurfaceLightSampleSpacing)
{
     continue;
}


  • 对于给定的采样点,如果它周围(采样方向为球体而不只是正半球)有超过30%的采样点是物体的背面,则该采样点不生成ILC
const int32 NumBackfacingHits = ComputeNumBackfacingHits(SamplePosition,System,
                                                  CoherentRayCache,SceneBoundingRadius,UniformHemisphereSamples);
if (NumBackfacingHits < .3f * UniformHemisphereSamples.Num() * 2)
{
    //生成ILC采样点
}
在UE4.25之前,ILC数据生成在PersistentLevel里,这样ILC在关卡被加载之初,就一直常驻于内存中。这对于小型地图来说无伤大雅,但当游戏需要更大的世界,更大的地图的时候,它会同时带来内存的巨大增长和ILC查询插值性能的降低。所以4.25之后ILC数据被分割到各个StreamingLevel中,可以配合WorldComposition和LevelStreaming对ILC数据进行动态加载和卸载。

  • 对于物体表面附近那些ILC来说,它们会被放置到该物体所在的StreamingLevel中
  • 对于CharacterImportanceVolume所产生的ILC来说,Lightmass会从采样点世界空间向下打一条射线,返回第一个被接触到的物体所在的StreamingLevel中
  • 稀疏的ILC放置点会存在于当前的PersistentLevel中
关于ILC放置点生成的代码实现见FStaticLightingSystem::BeginCalculateVolumeSamples函数

计算放置点的ILC
在生成ILC采样点之后,需要计算每个放置点的ILC的3阶SH参数。这一部分的代码实现见FStaticLightingSystem::CalculateVolumeSampleIncidentRadiance函数中。其基本流程如下所示

  • 计算该采样点上下半球潜在的贡献光子的方向,存储以用于真正的光照计算
  • 计算该采样点可以接受到的直接光能量的SH参数
  • 计算该采样点上半球间接光照入射光能量的SH参数(FinalGather,AdaptiveSample加速)
  • 计算该采样点下半球间接光照入射光能量的SH参数(FinalGather,AdaptiveSample加速)
  • 叠加2,3,4步的SH参数,生成最终的高质量和低质量ILC
在计算潜在有贡献的光子时,使用的是Volumetric PhotonSegementMapping,它们和附着在物体表面的传统光子不同——它们是传统光子在逃离物体表面之后,在空中每过一段距离就会被记录一个当前的方向和光照值所生成的能量记录,在Lightmass中被叫做PhotonSegement。想象一下光线在空间以直线传播,被PhotonSegement均分为一段一段的小线段,所图所示:


关于Photonmapping光照评估算法的介绍在LightMass源码分析之光照评估一文中有详细介绍,这儿不再做重复分析。同时,关于球谐函数的理论也已有海量的文献可参考,这儿也不再做具体介绍。这儿贴一下UE4把光照投射到SH基函数用于计算其参数的二阶和三阶实现,可做为实用函数库使用。
/** Specialization for 2nd order to avoid expensive trig functions. */
template<>
inline TSHVector<2> TSHVector<2>::SHBasisFunction(const FVector& Vector)
{        
    TSHVector<2> Result;
    Result.V[0] = 0.282095f;
    Result.V[1] = -0.488603f * Vector.Y;
    Result.V[2] = 0.488603f * Vector.Z;
    Result.V[3] = -0.488603f * Vector.X;
    return Result;
}

/** Specialization for 3rd order to avoid expensive trig functions. */
template<>
inline TSHVector<3> TSHVector<3>::SHBasisFunction(const FVector& Vector)
{
    TSHVector<3> Result;
    Result.V[0] = 0.282095f;
    Result.V[1] = -0.488603f * Vector.Y;
    Result.V[2] = 0.488603f * Vector.Z;        
    Result.V[3] = -0.488603f * Vector.X;         
    FVector VectorSquared = Vector * Vector;        
    Result.V[4] = 1.092548f * Vector.X * Vector.Y;        
    Result.V[5] = -1.092548f * Vector.Y * Vector.Z;        
    Result.V[6] = 0.315392f * (3.0f * VectorSquared.Z - 1.0f);        
    Result.V[7] = -1.092548f * Vector.X * Vector.Z;        
    Result.V[8] = 0.546274f * (VectorSquared.X - VectorSquared.Y);        
    return Result;
}
通用的参数计算公式见TSHVector::SHBasisFunction实现。

高质量ILC和低质量ILC的区别
UE4中高质量ILC和低质量ILC这两个名字很容易让人迷惑,初看会以为高质量的ILC该是更高精度的数据,比如更高阶,数据位数更多,但事实情形是:低质量的ILC包含更多的光照信息。
高质量ILC = 上半球静态直接光照 + 下半球静态直接光照 + 间接入射光照 低质量ILC =  高质量ILC      + 上半球非方向动态直接光照 + 下半球非方向动态直接光照      + 上半球固态天光 + 下半球固态天光
这个命名实际上并不是说高质量ILC质量更高,精度更佳,而是表达了它用于高品质绘制的场合——天光和非方向性的动态光源都实时计算,而不是使用静态烘焙的低精度数据。

ILC的存储与加载

烘焙完成的ILC数据会被存放于Level/StreamingLevel的MapBuildData中。MapBuildData里除了保存ILC,它同时还包含了诸如IBL、Navmesh、VLM、2DLightmap、ShadowMask等数据。


在使用Develop或开启stat命令的Test包中,可以很方便的使用stat命令来查看当然加载的ILC数据所占的内存量。其命令为
Stat MapBuildData 展示界面如下



FPrecomputedLightVolumeData
从代码实现来看,ILC数据在UE4中的Holder是FRPrecomputedLightVolumeData,它在渲染的时候使用的是Octree的数据结构,但在序列化时所表现出来的结构为数组。这也意味着StreamingLevel每次StreamingIn(加载)的时候,都需要从数组去重新去构造Octree。
在Android支持SM5或IOS上支持MetalMRT的前提下,会同时支持保存高、低质量的ILC,否则只会存储低质量的ILC。保存低质量ILC实现代码如下所示
TArray<FVolumeLightingSample> LowQualitySamples;
if (!Ar.IsCooking() || Ar.CookingTarget()->SupportsFeature(ETargetPlatformFeatures::LowQualityLightmaps))
{        
    Volume.LowQualityLightmapOctree.FindAllElements(
        [&LowQualitySamples](const FVolumeLightingSample& Sample)
        {    LowQualitySamples.Add(Sample);});
}
Ar << LowQualitySamples;  
高质量ILC保存代码只是把LowQualityLightmapOctree换成了HighQualityLightmapOctree。
加载低质量ILC实现为先加载ILC Samples数组,然后构造Octree,完成ILC加载。代码及注释如下:
TArray<FVolumeLightingSample> LowQualitySamples;
if (Ar.UE4Ver() >= VER_UE4_VOLUME_SAMPLE_LOW_QUALITY_SUPPORT)
{     
    //加载ILC采样点SH参数,这儿用一个函数,是为了兼容2,3阶        
    LoadVolumeLightSamples(Ar, NumSHSamples, LowQualitySamples);
}  

if (FPlatformProperties::SupportsLowQualityLightmaps()
    && (GIsEditor || !AllowHighQualityLightmaps(GMaxRHIFeatureLevel)))
{     
    //把所有采样点SH加入Volume,构造Low Quality Octree        
    for(int32 SampleIndex = 0; SampleIndex < LowQualitySamples.Num(); SampleIndex++)        
    {                
        Volume.AddLowQualityLightingSample(LowQualitySamples[SampleIndex]);        
    }
}
关于ILC加载和卸载的详细实现,可以参见FArchive的一个全局的操作符重载:
FArchive& operator<<(FArchive& Ar,FPrecomputedLightVolumeData& Volume)

ILC的数据更新与绑定

ILC、游戏线程与渲染线程
ILC数据伴随着ULevel一起加载,在ULevel的数据进行渲染初始化的时候会真正的被加入渲染场景中。其关键函数及引用关系如下图所示:


UE4采用的是多线程架构,其游戏线程和渲染线程在几乎所有平台上都相互分离,游戏逻辑大多数时候所能访问到的是游戏线程所提供的接口和数据,而渲染线程对逻辑线程来说不可知。UWorld/ULevel是游戏线程(GameThread)中的游戏世界和关卡,FScene则是渲染线程中的游戏世界,渲染线程中的游戏世界是平坦结构,抛弃了关卡这一概念。上述的ILC数据加载是在数据IO线程和游戏线程完成的,IO线程和游戏线程完成了ILC数据的加载,才会最终把这些数据同步到渲染线程,供最终渲染场景使用。
对于PersistentLevel来说,在LoadLevel加载完成之后 ,会调用UWorld的InitWorld来进行世界的初始化;而对于StreamingLevel或其它动态加载的Level来说,当它被加入世界的时候会调用AddToWorld。无论哪种方式的关卡,在被真正加入到游戏世界(UWorld)时,都会使用InitializeRenderingResources进行渲染资源初始化,该函数会初始化关卡的渲染资源,这些渲染资源即包含ILC数据——FPrecomputedLightVolume的AddToScene。类似的,当关卡被卸载的时候,它会调用FPrecomputedLightVolume的RemoveFromScene把本关卡所携带的ILC数据从渲染线程中移除。
在FScene中,ILC所有已加载关卡的ILC数据被推入简单的TArray中备用:
TArray<const FPrecomputedLightVolume*> PrecomputedLightVolumes;

ILC与UPrimitiveComponent
一般情形之下,只有Mobility为Stationary和Movable的场景组件才会使用ILC数据(这儿说明一下,所有的SkeletalMesh都是天生的movable)。例外情况是第一节所述,4.25及之后的UE4版本,StaticMeshComponent的LightmapType被设为VolumetricLightmap的时候,也会使用ILC替代2DLightmap作为Diffuse GI。
FIndirectLightingCache类是在UE4中渲染线程里真正实现UPrimitiveComponent的ILC缓存管理、ILC生成、插值和更新的功能类。更新一下上一部分的静态图,如下所示:



FIndirectLightingCache中同时实现了VLM的Cache和ILC的Cache,代码逻辑块放在一起,初看起来会比较混乱,但ILC Cache不需要分割3D空间,所以它需要一个一维单调增长的Cache ID即可(见AllocateBlock)
if (Size == 1)
{
    OutMin = FIntVector(NextPointId, 0, 0);
    NextPointId++;        
    return true;  
}
默认的,ILC的采用点采样,它的采样点近似的位于包围盒的中心点,如下图所示的白色Cube,它的采样点即大致位于编辑器中本地坐标的原点附近。


为什么说是近似呢?是因为UE4做为ILC计算所使用的BoundBox做过一些变换和适当的放大,由连续函数变为阶梯函数。这样当物体的包围盒改变不大的时候,保持ILC/VLM的结果相对稳定,这对于SkeletalMesh来说,不至于因为动画所引起的包围盒频繁的变化而导致GI的剧烈变化,从而一直忽明忽暗地闪烁。
包围盒整理公式:


函数图像:


代码实现可参考CalculateBlockPositionAndSize函数。
ILC和PrimitiveComponent的关联只有在PrimitiveComponent以下状态发生改变的情形才会更新:


  • 当一个新的PrimitiveComponent被加入到渲染场景中(FScene)且它需要使用ILC进行GI表达时,引擎需要为它分配一个ILC Block用于插值生成真正用于渲染的SH参数集
  • 当一个PrimitiveComponent被从渲染场景中移除且它已经分配了ILC Block时,这时引擎需要把已经分配给它的ILC Block回收利用
  • 这个状态比较特殊,当一个PrimitiveComponent的Transform发生变化时,在UE4中相当于先把该Component从FScene中移除,再重新加入到FScene中(截止到UE 4.26),所以它也会触发ILC Block的回收和再分配

关于ILC Block的分配和回收工作,分别位于AllocatePrimitive和ReleasePrimitive函数中。
再看看分配的ILC Block数据结构长啥样
class FIndirectLightingCacheBlock
{
public:     
    FIntVector MinTexel;     
    int32 TexelSize;     
    FVector Min;     
    FVector Size;     
    bool bHasEverBeenUpdated;
};
从数据结构中可知,它记录的是ILC所采样的位置和是否已经被更新过(类中的其它属性用于VLM),由此可见,该Block尚未完成真正用于渲染的SH参数生成,它只是一个占位符,记录的是在将来进行真正的SH参数更新的依据和必要参数。

ILC SH参数的生成和数据绑定
对于ILC SH参数计算的基本流程如下所示



除最后一步之外,其功能均同样实现于FIndirectLightingCache中。遍历所有关卡的ILC Octree的查找和插值行为是一个计算量密集的操作,所以UE4也对这部分操作同时提供了单线程和多线程实现的两个版本。
把上述绿色部分流程看作计算ILC SH参数,那么该流程可简化为如下两部分:


加上多线程的实现,增长为三部分:


如上图所示,渲染线程等待ILC计算任务的时间可能会很长(黄块),这就说明一开始执行多线程运算就等着它完成虽然能提升一定的效率,但往往不是最优的(FrustumCull的多线程调度就是这么简单粗暴的一开始任务就开始等待~~)。更有效率的方式是:在多线程执行ILC SH参数计算任务的同时,渲染线程可以执行一些其它比较耗时的任务(诸如灯光的可见性运算、阴影depth map准备等),在这些耗时较多的任务的执行完成之后再去等待多线程执行的ILC任务,可能压根就已经不存在等待时间,或者至少等待时间也已经极大的缩短了。
优化之后的多线程ILC更新流程如下所示:


关于ILC计算的任务调度,可以参考StartUpdateCachePrimitivesTask/FinalizeCacheUpdates。
记得一开始我们在讨论ILC采样数据生成的时候,有提到过ILC每个采样点都有一个作用半径,在为UPrimitive进行ILC SH生成的时候,即会使用此作用半径做为Filter用于求解每个采样点的贡献权重,计算权重代码加了些注释,如下所示:
//物体位置到采样点位置的距离平方
const float DistanceSquared = (VolumeSample.Position - WorldPosition).SizeSquared();
//采样点作用半径平方
const float RadiusSquared = FMath::Square(VolumeSample.Radius);  
if (DistanceSquared < RadiusSquared)
{     
    const float InvRadiusSquared = 1.0f / RadiusSquared;     
    //采样点贡献权重和距离的平方成反比 weight = (R^2 - dist^2)/R^4     
    const float SampleWeight = (1.0f - DistanceSquared * InvRadiusSquared) * InvRadiusSquared;      
    AccumulatedWeight += SampleWeight;      
    AccumulatedIncidentRadiance += VolumeSample.Lighting * SampleWeight;
}
注意到这儿的SampleWeight总和加起来不为1,在大部分时候它会远小于1,这样就会导致插值出来的SH在亮度上较周围环境偏暗,针对这一情形,UE4在遍历查找完所有Octree之后,所生成的SH参数AccumulatedIncidentRadiance会除以 AccumulatedWeight对最终结果进行归一化,从而尽可能保证亮度和周围环境相一致。同时因为在OCtree的查找过程中只考虑到采样点的作用半径而缺乏必要的遮挡信息,这也是ILC会在室内墙角漏光和室外墙边漏阴影的根本来源。Octree中查找采样点及加权平均的详细实现代码参见InterpolateIncidentRadiancePoint函数
最后再看看UE4降低ILC SH的振铃效应,UE4的做法和平常所见使用类似低通滤波方式对SH进行最终处理不同,它的做法是在SH的暗部直接叠加一个最亮方向5%的能量进去,不太清楚这个做法的数学推导过程是如何的。
降低振铃效应的完整代码如下所示:
template <int32 SHOrder>
static void ReduceSHRinging(TSHVectorRGB<SHOrder>& IncidentRadiance)
{        
    const FVector BrightestDirection = IncidentRadiance.GetLuminance().GetMaximumDirection();        
    TSHVector<SHOrder> BrigthestDiffuseTransferSH = TSHVector<SHOrder>::CalcDiffuseTransfer(BrightestDirection);        
    FLinearColor BrightestLighting = Dot(IncidentRadiance, BrigthestDiffuseTransferSH);         
    TSHVector<SHOrder> OppositeDiffuseTransferSH = TSHVector<SHOrder>::CalcDiffuseTransfer(-BrightestDirection);        
    FLinearColor OppositeLighting = Dot(IncidentRadiance, OppositeDiffuseTransferSH);         
    // Try to maintain 5% of the brightest side on the opposite side        
    // This is necessary to reduce ringing artifacts when the SH contains mostly strong, directional lighting from one direction        
    FVector MinOppositeLighting = FVector(BrightestLighting) * .05f;        
    FVector NegativeAmount = (MinOppositeLighting - FVector(OppositeLighting)).ComponentMax(FVector(0));                
    IncidentRadiance.AddAmbient(FLinearColor(NegativeAmount) * TSHVector<SHOrder>::ConstantBasisIntegral);
}

ILC与渲染

渲染这部分可写的东西不多,先看下Uniform Buffer的定义,知道它的位置和定义后,方便自己在做优化的时候去修改。


可以看到ILC在渲染时使用的是3阶SH,参数类型为float(半透的单参数部分为half)。当你在的抠UniformBuffer内存的时候,大概在mobile端只需要留下红框里这4项,其余项都可以去掉。
再看看在mobilebasepasspixelshader.usf中如果使用ILC SH进行渲染,见ComputeIndirect函数(4.25之前直接放在Main函数中),核心代码就两行:
FThreeBandSHVector DiffuseTransferSH = CalcDiffuseTransferSH3(MaterialParameters.WorldNormal, 1);
half3 DiffuseGI = max(half3(0, 0, 0), DotSH3(PointIndirectLighting, DiffuseTransferSH));
第一行是从法线还原SH基函数并乘以对应各阶系数,第二行是利用SH的基本特性SH参数点积基底函数以求解Diffuse GI。

ILC总结与优化

ILC的不足之处

  • 最严重的问题是漏光/漏阴影
  • 不支持场景的动态破坏,不支持TOD
  • 对立体空间支持不佳,尤其是对于空中飞行的游戏或跳伞类游戏来说
  • 对于基于静态实例(ISM)和分层的静态实例(HISM,如植被)的支持不好,因为ILC是基于Component绑定,这就意味着所有的Instance会共享一份ILC SH,对于一定范围内合并的ISM或HISM,其结果必然是南辕北辙
  • 对使用ILC的物体体积有较严格要求,比如一个大房子肯定是不能使用一个ILC来表达GI的,甚至一辆小卡车使用单ILC来表达也不足以表现其GI变化的频率
ILC优化

  • 针对漏光问题,可以生成额外的数据并使用阴影检测手段来规避,也可以在离线生成ILC采样点的时候做文章(可参考FarCry3),对检测到光照断层的区域降低ILC的作用半径。
  • 对室外远离地面的空间,可以直接使用全局的SKY SH全类似于PostProcess Volume方式计算GI,只有在近地面或光线变化敏感的区域生成较密集的ILC采样点
  • 对于画质要求高的游戏来说,ISM/HISM可以考虑逐Instance生成ILC而不是全部共用
  • 对于画质要求高的游戏来说,中型物体需要使用ILC,也可以为一个物体生成多个ILC布点(类似COD)
  • 对内存紧张的情形来说,在稍微损失一些效果的前提下,ILC的SH参数总是可以压缩为2阶的,从32位浮点数压缩为16位半精度
  • 注意到ILC SH光照是在逐像素计算的,而对于没有Normalmap的模型来说,完全等价于在VS里计算ILC光照。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-2-11 18:29 | 显示全部楼层
貌似4.24版本vlm切到es3.1 preview的cpu插值还有点问题,插值出的结果y和w通道反了… 也不知道是不是我看错了[捂脸][捂脸][捂脸]
发表于 2022-2-11 18:35 | 显示全部楼层
发文章的频率变高了[调皮]
发表于 2022-2-11 18:37 | 显示全部楼层
话说4.26好像可以把sh直接存到3dtexture里了,如果走这个分支的话过渡会好很多
发表于 2022-2-11 18:40 | 显示全部楼层
那就vlm了[大笑]
发表于 2022-2-11 18:50 | 显示全部楼层
这个在4.18不是被提示说用vlm来取代了吗?为什么又要重新启用lic?
发表于 2022-2-11 18:54 | 显示全部楼层
因为手机端没有VLM,如果只是用VLM类型的数据,内存也吃不消。
发表于 2022-2-11 18:55 | 显示全部楼层
多谢解答[赞同]
发表于 2022-2-11 19:01 | 显示全部楼层
vlm的分布极其不合理,对level streaming支持也有问题
发表于 2022-2-11 19:02 | 显示全部楼层
去年给unity项目实现了一套像素级二阶sh算法 省掉600M lightmap,当时觉得好有创意啊 然后同事发我ue4说 你想的和ue vlm 如出一辙 只是自己想了些防止漏光的做法而已
[吃瓜]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 19:45 , Processed in 0.099964 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表