目录

ILC

代码分析

BeginCalculateVolumeSamples

ProcessPixel

CalculateVolumeSampleIncidentRadiance

高质量ILC和低质量ILC的区别

GatherVolumeImportancePhotonDirections

FPrecomputedLightVolumeData

预计算光体积

包围盒

关联ILC和PrimitiveComponent

ILC Block的分配和回收

采样

振铃效应

ILC与渲染

ILC的不足之处

VLM


ILC

ILC基于逐物体生效

静态物体的GI可以使用2D Lightmap来表达,而场景中的动态物体(Stationary,Movable,也包括诸如粒子、带骨骼动画的模型)等大多会采用基于三阶球谐(SH3)的静态漫反射来表达

indirect light跟cache,前者表明这个方案是用于计算间接光的,每个probe中存储的是对应位置向着各个方向输出的Radiance信息。Cache则意味着数据是带有缓存的,通常计算场景中各个位置的probe的Radiance数据需要较高的消耗,但是如果将这些结算得到的数据缓存下来,只有在失效(比如物件发生变化或者光照发生变化的时候)时才进行重新计算,就可以降低烘焙的时间消耗,提升开发效率,这就是Cache的意义。

ILC在Lightmass中的生成分为两步,如下所示

对于能够在场景物体表面正方向生成ILC,还有一些其它限制:

  • 此场景物体必须是静态、参与Lightmap生成的、投射阴影的,对于熟悉Lightmass的同学来说这很好理解,因为Lightmass中只导入了静态的场景物体,其它物体对于Lightmass来说不存在。

  • 只有物体表面在世界空间朝上的部分会参与ILC生成,这儿看起来UE4似乎是有一个假设,它的ILC更多的是提供给在可行走表面上的角色所使用而优化,这样可以使全场景生成更小的ILC数据。

在UE4.25之前,ILC数据生成在PersistentLevel里,这样ILC在关卡被加载之初,就一直常驻于内存中。这对于小型地图来说无伤大雅,但当游戏需要更大的世界,更大的地图的时候,它会同时带来内存的巨大增长和ILC查询插值性能的降低。所以4.25之后ILC数据被分割到各个StreamingLevel中,可以配合WorldComposition和LevelStreaming对ILC数据进行动态加载和卸载。

  • 对于物体表面附近那些ILC来说,它们会被放置到该物体所在的StreamingLevel中

  • 对于CharacterImportanceVolume所产生的ILC来说,Lightmass会从采样点世界空间向下打一条射线,返回第一个被接触到的物体所在的StreamingLevel中

  • 稀疏的ILC放置点会存在于当前的PersistentLevel中

SurfaceLightSampleSpacing=300 ;物体表面生成的ILC采样点之间的距离 
FirstSurfaceSampleLayerHeight=50 ;物体表面第一层ILC采样点和表面之间的距离 
SurfaceSampleLayerHeightSpacing=250 ;每层ILC采样点之间的距离 
NumSurfaceSampleLayers=2 ;物体表面生成几层ILC采样点 
DetailVolumeSampleSpacing=300 
VolumeLightSampleSpacing=3000 
;为LightmassImportanceVolume中不靠近物体表面和CharacterImportanceVolume的地方,均匀生成稀疏的ILC放置点

代码分析

BeginCalculateVolumeSamples

//计算ImportanceBounds
VolumeBounds = GetImportanceBounds(false);

//仅在volume有面积时放置样本
if (X > 0.0f && Y > 0.0f && Z > 0.0f)
{
    //估计Landscape surfaces附近的采样数
    SurfaceLightSampleSpacing平方
    CurrentMesh->GetTriangle();
    //判断点是否在ImportanceVolume
    if (IsPointInImportanceVolume(Vertices[0].WorldPosition))
    {
    //计算normal
    //采样数 += TotalArea / SurfaceLightSampleSpacing;
    }
    采样数 *= NumSurfaceSampleLayers;
    if (采样数过多)
    {
        增加SurfaceLightSampleSpacing(距离)以减少光照样本数
    }
    ……
    if(样本在阴影投射网格上)
    {
        // Rasterize all triangles in the mesh
        TriangleNormal = (2 - 0)×(1 - 0);
        TriangleArea = 0.5f * TriangleNormal.Size3();
        if (TriangleArea > DELTA)
        {
            //计算Triangle vertices in lightmap UV space
            //计算光照贴图空间中的面积-三角形覆盖的光照贴图纹理像素数
            LightmapTriangleArea=(0.x*(1.y-2.y)+1.x*(2.y-0.y) + 2.x*(0.y - 1.y))
            //计算纹素
            TexelDensity = LightmapTriangleArea / TriangleArea;
            //纹理像素密度小于每个由 SurfaceLightSampleSpacing 形成               的直角三角形面积的一个纹理像素
            //Lightmap精度小于给定阈值的表面
            if (TexelDensity < 2.0f / SurfaceLightSampleSpacing)
            {
            continue;
            }
        }  
        // Only rasterize upward facing triangles
        if(TriangleNormal.Z > 0.0f) 
        {
            Rasterizer.DrawTriangle();
        } 
    }
     ……
     ……
     //追踪一条射线以找到结束
     IntersectLightRay
     //将样本放置在相交级别,如果没有相交,则将其放置在持久级别
     LevelGuid
    //添加一个样本并设置其半径,使其影响触及 3d 网格上的对角线样本。
    ……
    // 在重要性体积内以统一的 3d 网格生成样本。 这些将用于不重要区域的低分辨率照明。
                    
}

ProcessPixel

// 仅将样本放置在场景边界内
if (System.IsPointInImportanceVolume(Vertex.WorldPosition))
{
    //为每一层放置一个样本
    for(每一层)
    {
        // Only place a sample if there isn't already one nearby
        if (!FindNearbyVolumeSample())
         {
             NumBackfacingHits = ComputeNumBackfacingHits();
             //对于给定的采样点,如果它周围(采样方向为球体
             //有超过30%的采样点是物体的背面,则该采样点不生成ILC
            if(NumBackfacingHits<.3f*UniformHemisphereSamples*2) 
            {
                //生成ILC采样点 
            } 
         }
    }

CalculateVolumeSampleIncidentRadiance

计算给定世界空间位置的入射辐射

计算放置点的ILC:在生成ILC采样点之后,需要计算每个放置点的ILC的3阶SH参数。

TArray<FVector4> UpperHemisphereImportancePhotonDirections;
TArray<FVector4> LowerHemisphereImportancePhotonDirections;
//收集体积重要性光子方向)(潜在)
GatherVolumeImportancePhotonDirections();
//直接光
CalculateApproximateDirectLighting();
//FinalGather,AdaptiveSample加速
//上半球间接光照 - 下半球间接光照
 Upper - lower
FFinalGatherSample3 *= IncomingRadianceAdaptive
//计算间接光
CombinedIndirectLighting = Upper + Lower
//high
CombinedHighQualitySample = UpperStaticDirectLighting + LowerStaticDirectLighting + CombinedIndirectLighting;
//low
//将点和点固定直接照明合成到低质量的体积样本中,因为我们不会动态应用它们
CombinedLowQualitySample = UpperStaticDirectLighting + UpperToggleableDirectLighting + LowerStaticDirectLighting + LowerToggleableDirectLighting + CombinedIndirectLighting;
//合成静止天光对低质量体积样本的贡献
CombinedLowQualitySample += StationarySkyLighting
  1. 计算该采样点上下半球潜在的贡献光子的方向,存储以用于真正的光照计算

  2. 计算该采样点可以接受到的直接光能量的SH参数

  3. 计算该采样点上半球间接光照入射光能量的SH参数(FinalGather,AdaptiveSample加速)

  4. 计算该采样点下半球间接光照入射光能量的SH参数(FinalGather,AdaptiveSample加速)

  5. 叠加2,3,4步的SH参数,生成最终的高质量和低质量ILC

高质量ILC和低质量ILC的区别

UE4中高质量ILC和低质量ILC这两个名字很容易让人迷惑

事实情形是:低质量的ILC包含更多的光照信息。

高质量ILC = 上半球静态直接光照 + 下半球静态直接光照 + 间接入射光照

低质量ILC = 高质量ILC + 上半球非方向动态直接光照 + 下半球非方向动态直接光照 + 上半球固态天光 + 下半球固态天光

这个命名实际上并不是说高质量ILC质量更高,精度更佳,而是表达了它用于高品质绘制的场合——天光和非方向性的动态光源都实时计算,而不是使用静态烘焙的低精度数据。

GatherVolumeImportancePhotonDirections

//要求indirectlightbounces>0 使用photonmapping
//使用bUsePhotonSegmentsForVolumeLighting
if(满足)
{
    // 收集附近的第一次反弹光子,它给出了第一次反弹入射辐射函数的估计值
    FindNearbyPhotonsInVolumeIterative();
    FirstPhotonDirections.
    SecondPhotonDirections.
    for (index < FoundPhotonSegments)
   {
       CurrentPhoton = FoundPhotonSegments[index];
       //计算方向 当前位置->光子源
       NewDirection=;
       //仅当方向在法线的半球时才使用
       if (Dot3(NewDirection,Normal) > 0.0f)
       {
          
       }   
   }

}

在计算潜在有贡献的光子:Volumetric PhotonSegementMapping

它们和附着在物体表面的传统光子不同,它们是传统光子在逃离物体表面之后,在空中每过一段距离就会被记录一个当前的方向和光照值所生成的能量记录,在Lightmass中被叫做PhotonSegement。

想象一下光线在空间以直线传播,被PhotonSegement均分为一段一段的小线段。

FPrecomputedLightVolumeData

ILC加载和卸载

AddToScene();

RemoveFromScene();

ILC数据在UE4中的Holder是FRPrecomputedLightVolumeData,它在渲染的时候使用的是Octree的数据结构,但在序列化时所表现出来的结构为数组。这也意味着StreamingLevel每次StreamingIn(加载)的时候,都需要从数组去重新去构造Octree。

TArray<FVolumeLightingSample> LowQualitySamples;

if (Supported low quality lightmaps in volume samples)
{
    //加载ILC采样点SH参数,这儿用一个函数,是为了兼容2,3阶 
    LoadVolumeLightSamples(LowQualitySamples);
}

if (SupportsLowQualityLightmaps())
{
    //把所有采样点SH加入Volume,构造Low Quality Octree    
     for( SampleIndex < LowQualitySamples.Num())
     {
         AddLowQualityLightingSample();
      }
}

预计算光体积

precomputed light volume:ILC数据伴随着ULevel一起加载,在ULevel的数据进行渲染初始化的时候会真正的被加入渲染场景中。

UE4采用的是多线程架构,其游戏线程和渲染线程在几乎所有平台上都相互分离。UWorld/ULevel是游戏线程(GameThread)中的游戏世界和关卡,FScene则是渲染线程中的游戏世界,渲染线程中的游戏世界是平坦结构,抛弃了关卡这一概念。上述的ILC数据加载是在数据IO线程和游戏线程完成的,IO线程和游戏线程完成了ILC数据的加载,才会最终把这些数据同步到渲染线程,供最终渲染场景使用。

只有Mobility为Stationary和Movable的场景组件才会使用ILC数据

FIndirectLightingCache中同时实现了VLM的Cache和ILC的Cache

ILC Cache不需要分割3D空间,所以它需要一个一维单调增长的Cache ID即可(见AllocateBlock)

bool AllocateBlock(int32 Size, FIntVector& OutMin)
{
    if (Size == 1)
    {    
        OutMin = FIntVector(NextPointId, 0, 0);
        NextPointId++;
        // 点样本不经过体积纹理,成功
        return true;
    }
    else
    {
        //在纹理中找一个足够大的表面的空闲区域。
        return AddElement(X, Y, Z, Size, Size, Size);
    }
}

包围盒

ILC的采用点采样,它的采样点近似的位于包围盒的中心点,如下图所示的白色Cube,它的采样点即大致位于编辑器中本地坐标的原点附近。

为什么说是近似呢?

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

包围盒整理公式:

CalculateBlockPositionAndSize()
{
    FVector RoundedBoundsSize;
    //查找表示边界大小所需的指数
    XYZ
    //向上舍入到下一个整数指数以提供稳定性
    XYZ
    //对于单个样本分配,use an effective texel size of 5
    EffectiveTexelSize = TexelSize > 2 ? TexelSize : 5;
    
}

关联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的分配和回收

在 FIndirectLightingCache 中存储分配的数据。

 
class FIndirectLightingCacheBlock 
{ 
public:    
    FIntVector MinTexel;     
    int32 TexelSize;     
    FVector Min;     
    FVector Size;     
    bool bHasEverBeenUpdated; 
}; 

它记录的是ILC所采样的位置和是否已经被更新过(类中的其它属性用于VLM),由此可见,该Block尚未完成真正用于渲染的SH参数生成,它只是一个占位符,记录的是在将来进行真正的SH参数更新的依据和必要参数。

采样

ILC每个采样点都有一个作用半径,在为UPrimitive进行ILC SH生成的时候,即会使用此作用半径做为Filter用于求解每个采样点的贡献权重

 //物体位置到采样点位置的距离平方 
DistanceSquared = (VolumeSample.Position - WorldPosition).SizeSquared(); 
//采样点作用半径平方 
RadiusSquared = FMath::Square(VolumeSample.Radius);  
if (DistanceSquared < RadiusSquared) 
{     
    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

振铃效应

SH的暗部直接叠加一个最亮方向5%的能量进去

ReduceSHRinging(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
    // 当 SH 包含来自一个方向的大部分强定向照明时,这是减少振铃伪影所必需的
    FVector MinOppositeLighting = FVector(BrightestLighting) * .05f;
    FVector NegativeAmount = (MinOppositeLighting - FVector(OppositeLighting)).ComponentMax(FVector(0));
    IncidentRadiance.AddAmbient(FLinearColor(NegativeAmount) * TSHVector<SHOrder>::ConstantBasisIntegral);
}

ILC与渲染

ILC在渲染时使用的是3阶SH,参数类型为float(半透的单参数部分为half)。

//法线还原SH基函数并乘以对应各阶系数
FThreeBandSHVector DiffuseTransferSH = CalcDiffuseTransferSH3(DiffuseDir, 1);

// Compute diffuse lighting which takes the normal 
//利用SH的基本特性SH参数点积基底函数以求解Diffuse GI。
half3 DiffuseGI = max(half3(0, 0, 0), DotSH3(PointIndirectLighting, DiffuseTransferSH));

ILC的不足之处

  • 最严重的问题是漏光/漏阴影

漏光是固定采样距离的SH类GI的顽固问题

UE4开启了ILC过渡功能,物体在移动过程中,它的ILC不是立即调节到当前ILC亮度,而是慢慢从之前的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光照。

VLM

Lightmass会生成表面光照贴图(lightmap),用于表现静态对象上的间接光照。但是,动态的对象(例如角色)也需要一种接受间接光照的方法。这种方法就是在构建时将所有点的预计算光照存储在名为 体积光照贴图 的空间中,然后在运行时用于动态Object的间接光照的插值。

VLM是按下列方式工作的:

  • Lightmass将光照样本放置在关卡中的各个位置,并在光照构建期间为它们计算间接光照。

  • 当需要渲染动态Object时,就将体积光照贴图内插到着色的每个像素,提供预计算的间接光照。

  • 如果没有构建的光照可用(也就是说Object是新的或者移动过多),就从 静态 Object的体积光照贴图将光照内插到每个像素,直至光照重构完成为止。

在放置了LightmassImportanceVolume的地方,会生成均匀分布的4x4x4个probe(这个数值可以通过 Volumetric Lightmap Detail Cell Size 进行调整),而在static物件周围会提高probe的密度(具体的密度提升逻辑估计跟前面ILC中设置Probe之间的间距逻辑实行同的。在UE4.25之后,已经可以通过将静态物体的Lightmap Type设为Force Volumetric来使用ILC/VLM数据:

间接光照缓存仅将光照样本放置在静态几何体表面上方。 体积光照贴图将样本高密度地放置在静态几何体周围,在间接光照变化最大的地方呈现更多细节。跟ILC不同,VLM除了在Static物体上方会摆放probe之外,在空间中也会摆放Probe,这样就避免了使用LightmassCharacterIndirectDetailVolume来处理无静态物件区域中动态物件的间接光效果异常的问题,相当于扩大了probe的范围,从而增加了动态物体被间接光覆盖的范围,此外需要补充一下,相对于一些实时方案中光照使用2阶球谐来表示(LPV),ILC跟VLM中存储的SH系数是三阶的共9个参数。

此外,光照插值基于渲染物体的像素(即每个像素的光照SH系数是不同的)进行,跟ILC基于probe插值(即每个物件会插值出一个对应于物件所在位置的probe,之后使用这个probe的数据进行所有像素的着色)不同,这就是为什么VLM可以实现fog光照的原因,也可以用于解决当物件较大的时候,ILC得到的光照效果会周边lightmap计算得到的光照效果存在较大跳变的问题。

相对于ILC而言,VLM有如下的一些不足:内存消耗增加了

ILC跟VLM的问题有:漏光,可以提升Probe密度或者增加墙体厚度

Logo

ModelScope旨在打造下一代开源的模型即服务共享平台,为泛AI开发者提供灵活、易用、低成本的一站式模型服务产品,让模型应用更简单!

更多推荐