MIT 许可证 NuGet

首页 / 功能 / 光照与阴影

光照与阴影

一个方向性太阳、通过 KHR_lights_punctual 导入的点光源,以及来自天空本身的 环境光照。光照状态每帧在 Rendering/SceneLighting.cs 中组装一次,所有通道从 同一个缓冲区读取,因此阴影、GI 和天空不可能在太阳的位置上产生分歧。

光源是一个列表,每帧烘焙一次

SceneLighting 持有一个普通的 List<LightSource> 外加一个恒定环境光项。每帧一次 Bake 把该列表转换成每个通道都读取的 GPU 结构。 引擎中没有任何其他东西保留自己的光照状态副本。

Lighting.Ambient = new Vector4(0.12f, 0.13f, 0.16f, 1f);

Lighting.Add(new LightSource
{
    Kind = LightKind.Spot,
    Name = "porch",
    Position = new Vector3(24f, 6f, 7.5f),
    Direction = Vector3.Normalize(new Vector3(0f, -1f, -0.4f)),
    Color = new Vector4(1f, 0.86f, 0.66f, 1f),
    Intensity = 40f,
    Range = 24f,
    InnerConeAngle = 0.25f,
    OuterConeAngle = 0.55f,
    CastShadows = true,
    Priority = 10
});
成员说明
KindDirectional、Point 或 Spot。
IsOpen默认 true。关闭一个光源会把它留在列表里,但排除在烘焙之外。
Color, Intensity线性颜色和一个标量;强度以引擎单位计,而不是坎德拉。
Position, DirectionDirection 默认直朝下。Position 对方向光无效。
Range点光源和聚光灯的衰减距离。
InnerConeAngle, OuterConeAngle聚光锥,以弧度计。
CastShadows请求唯一的点光源阴影槽。每帧只有一个光源能拿到它。
Priority列表超出 GPU 预算时的决胜依据。

八个光源,以及第九个的遭遇

SceneLightParams.MaxLights 是 8。这是硬性 GPU 限制, 不是软目标:uniform 缓冲区有八个槽。当场景里更多时, Bake 按 Priority 排序并取前八个, 用相机位置按距离打破平局。

这会在某一天让你惊讶一次

第九个光源不会警告、抛错或变暗——它只是不存在。如果一个光源重要, 就给它高的 Priority。这是引擎不适合由几十个小型局部 光源照明的场景的主要原因;固定预算下的前向着色正是所做出的取舍。

从 glTF 文件导入的光源通过 Model.ImportedPunctualLights 到达,并由模型自身追加, 这意味着一个携带十二个光源的资产自己就会耗尽全部预算。 Model.LightIntensityScale 和 RenderQuality.KhrLightIntensityScale 的存在就是为了 把它们的度量级量级拉回引擎范围。

级联阴影贴图

三个太阳级联和一个点光源槽共享一张正方形深度图集,每个象限是边长 一半的瓦片。四个槽、一张纹理、一个绑定——这正是阴影通道也是唯一 一个设置自己视口的通道的原因。

旋钮含义生效时机
ShadowsEnabled总开关;关闭则完全跳过该通道。运行时
ShadowAtlasSize图集边长,D32 浮点。默认 2048。初始化
ShadowCascadeCount级联数量,钳制到 2–3。初始化
ShadowDistance太阳投影的最远距离,受相机远平面钳制。默认 40。运行时
CascadeSplitLambda均匀到对数的分割混合,默认 0.6。运行时
ShadowNormalOffset以阴影纹素计的法线偏移偏差;0 表示禁用。运行时
ShadowSoftnessTexels八点 Vogel 圆盘的半径,以纹素计。运行时
ShadowContactHardening在接触点附近收窄滤波。默认关闭。运行时
ShadowStrength缩放完全阴影化表面的暗度。运行时
ShadowCasterHeight光视锥在级联上方回拉多远,以免高投影体被裁剪。运行时
ShadowDepthBias恒定深度偏差,以深度单位计。初始化
ShadowSlopeScaledDepthBias按斜率缩放的深度偏差。初始化
ShadowCulling逐象限的光空间投影体剔除。运行时
ShadowAtlasReuse跳过重新渲染矩阵未变化的象限。运行时

两个深度偏差被标记为初始化而非运行时是有原因的:D3D12 和 Vulkan 在 创建管线状态时读取它们,因此启动后再赋值只会改变属性而别无他用。 请通过应用构造函数里的 RenderQuality.DefaultShadowDepthBias 设置它们。 相比之下,法线偏移偏差走 uniform 缓冲区,每帧都可以变化—— 这就是它成为首选旋钮的原因。

级联稳定性

缓慢移动的太阳是让阴影边缘爬行的经典方式:每一帧级联矩阵都轻微 变化,每一帧深度纹素都落在新位置,眼睛把由此产生的闪烁读作噪点。 有两个机制来抑制它。

  • 角度量化。 光照方向被吸附到由 ShadowLightAngleStep(默认 0.25)控制的网格上, 因此微小的太阳移动根本不会产生矩阵变化。
  • 纹素吸附。 光空间平移被舍入到整数阴影纹素,尺寸设定使级联 矩阵在 ShadowTargetStableFrames 帧内保持逐位一致—— 默认八帧。

CascadedShadow 把结果暴露出来供检查: MatricesStable 告诉你保证当前是否成立, EffectiveLightAngleStep 是实际生效的步长, Epoch 在图集内容失效时递增。参考应用把三者都显示 在屏幕上,因为「为什么我的阴影在爬行」可以通过读取它们来回答。

成员它告诉你什么
MaxCascades / SlotCount3 和 4——三个太阳级联加一个聚光槽。
CascadeViewProj, SpotViewProj当前烘焙进图集的矩阵。
CascadeSplits, ActiveCascadeCount本帧分割落在哪里。
SunActive, SpotActive每种阴影当前是否在渲染。
CullingActive光空间投影体剔除是否运行过。
GetAtlasViewport某个槽的象限矩形,如果你在调试图集的话。

已知缺口

  • 部分 点光源阴影。 一个槽、一个光源。有两个投影聚光灯的场景 只会得到一个阴影。
  • 缺失 点光源阴影立方体。 点光源槽是单一投影,因此无法进行 全向阴影投影。
  • 缺失 烘焙光照贴图。 所有光照都是动态的。这对变化的场景 是特性,对静态场景是代价。

想了解间接光而非直接光,请继续阅读 全局光照与环境光遮蔽。