欢迎光临
我们一直在努力

【UE】渲染框架学习路径-初次修改源码

【UE】渲染框架学习路径-初次修改源码

  • 我选择切入的方向
  • UE 源码的基础地图
  • UE 源码的七个基础层级
      • 1. 构建系统:决定“什么会被编译”
      • 2. 模块系统:UE 不是一个巨大的单体工程
      • 3. Runtime 和 Editor 分层:先判断功能在哪个世界生效
      • 4. UObject 反射系统:为什么 `UPROPERTY` 会出现在 Details 面板
      • 5. Gameplay 框架:World、Actor、Component 的关系
      • 6. 渲染框架:从组件到 GPU 不是一步到位
      • 7. 插件系统:插件也是模块,但更适合独立发布
  • 开始练习
    • 第一步:让 Details 面板显示开关
          • 为什么 Details 的值不能直接被 Shader 读取
    • 第二步:实现串通文件
          • 关键文件职责总览
        • 关键改动 1:Component 保存 Details 面板的开关
            • 位置
            • 示例代码
            • 它读取了什么
            • 它传给谁
        • 关键改动 2:从 Component 复制到 SceneProxy
            • 为什么需要 SceneProxy
            • 示例代码
            • 它读取了什么
            • 它传给谁
            • 为什么必须存在
        • 关键改动 3:Details 修改后刷新 SceneProxy
            • 它读取了什么
            • 它传给谁
            • 为什么必须存在
        • 关键改动 4:把开关写入 C++ 渲染参数
            • 为什么建议用 float,而不是 bool
            • 示例代码
            • 它读取了什么
            • 它传给谁
            • 为什么必须存在
        • 关键改动 5:把参数接到 HLSL 的 LightData
            • 示例:在 FDeferredLightData 中增加字段
            • 示例:从 uniform 拷贝到 LightData
            • 它读取了什么
            • 它传给谁
            • 为什么必须存在
        • 关键改动 6:在 Pixel Shader 中把灯变红
            • 建议写法
            • 为什么不直接写死 `float3(1, 0, 0)`
            • 为什么要判断 `!LightData.bRadialLight`
      • 完整数据链复盘
    • ![2026-06-04T08:40:27.jpg][7]
    • 第三步:吃菌子吧!
  • 后话

记录“第一次真正改 UE 源码”的学习笔记。我不一定完全是对的。纯流水账碎碎念。 后续改高度雾等等做了很多更复杂的事情了,但是那些不可能开源,所以想了想还是先分享我的起步吧~

本次练习的最小目标是:在 DirectionalLightComponent 的 Details 面板中增加一个布尔开关 UseYumiColorfulLight。当它被勾选时,这盏平行光在 PC Deferred Renderer 路径下可以蹦迪。


我选择切入的方向

我其实搜了很多教程,每个平台都尽可能找到相关搜索。很多 UE 源码教程会从“如何下载源码、生成项目文件、编译 Editor”开始,但编译成功之后,下一步呢?源码这么大,我到底该改哪里?

这一块我之前学AngelScript的时候已经很熟了,感觉是冗余信息

我觉得最合理的方式是先做一个最小闭环:只让一条数据链跑通,只在一个渲染路径里验证成功。

所以这次的范围应该明确收窄:

项目本次是否处理原因
Directional Light 目标明确,容易观察结果
PC Deferred Renderer UE 桌面端常见路径,适合作为第一次练习(最近在移动端导出踩坑,累死我了)
Details 面板普通属性 用 UPROPERTY 即可,不需要先写复杂 Detail Customization
Mobile Renderer 移动端光照路径不同,容易扩大范围
Forward Renderer Forward 与 Deferred 的灯光组织方式不同
Path Tracing 路径完全不同,不适合作为第一次练习,而且暂时没有项目需要
Static baked lighting 静态烘焙涉及 Lightmass / 烘焙数据,不适合最小闭环

第一次练习的验收标准应该非常简单:

勾选 UseYumiColorfulLight,平行光开始蹦迪。

只要这个闭环成立,已经摸到了 UE 源码修改里最重要的一条路:编辑器属性 → Runtime 组件 → SceneProxy → Shader 参数 → HLSL 着色。


UE 源码的基础地图

在改源码之前,最重要的不是背函数名,而是先知道“功能大概属于哪一层”。UE 源码非常大,如果没有层级地图,搜索出来一堆同名文件时会很容易迷路。 2026-06-04T08:35:54.png

可以先用下面这张简化地图理解 UE 源码结构:

UnrealEngine/
├─ Engine/
│ ├─ Source/
│ │ ├─ Runtime/ 游戏运行时核心代码
│ │ ├─ Editor/ 编辑器相关代码
│ │ ├─ Developer/ 开发工具、调试工具
│ │ ├─ Programs/ UBT、UHT、UnrealPak 等独立工具
│ │ └─ ThirdParty/ 第三方库
│ │
│ ├─ Plugins/ 引擎插件
│ ├─ Content/ 引擎资源
│ └─ Config/ 引擎配置

├─ Samples/
├─ Templates/
└─ FeaturePacks/

对源码学习来说,最核心的位置是:

Engine/Source/

以后查 Actor、Component、Renderer、RHI、Editor 工具、构建系统,大部分时间都会在这里面转。


UE 源码的七个基础层级

1. 构建系统:决定“什么会被编译”

这里在我之前记录自己添加函数的时候有项目说明,包括link的官方文档。这里不展开细说了。

UE 的 C++ 构建主要依赖 UnrealBuildTool,也就是 UBT。

可以先这样记:

.Target.cs = 我要编译什么目标?例如 Editor、Game、Server
.Build.cs = 这个模块依赖哪些模块?
.h / .cpp = 真实代码

一个普通模块大概长这样:

MyModule/
├─ MyModule.Build.cs
├─ Public/
│ └─ MyClass.h
└─ Private/
└─ MyClass.cpp

Build.cs 很重要,因为 UE 是模块化编译。如果在一个模块里调用了另一个模块的类型,但没有在 Build.cs 里声明依赖,就可能出现编译错误、链接错误,或者 IntelliSense 能找到但实际编不过的情况。

不过本次练习主要是修改 UE 已有模块,不是新建模块,所以 Build.cs 不是重点。只需要先知道:UE 编译不是普通 C++ 工程直觉,它背后有自己的模块和构建规则。


2. 模块系统:UE 不是一个巨大的单体工程

UE 源码不是所有代码混在一起,而是按模块组织。常见模块包括:

Core
CoreUObject
Engine
RenderCore
Renderer
RHI
Slate
UnrealEd
Landscape
Niagara
UMG

改源码时,本质上大多数情况是在改某个模块里的某个类。

例如要看灯光、雾、大气、材质、组件,通常会碰到:

Engine/Source/Runtime/Engine/
Engine/Source/Runtime/Renderer/
Engine/Source/Runtime/RenderCore/
Engine/Source/Runtime/RHI/
Engine/Shaders/

如果做编辑器工具、Details 面板定制、菜单、资产编辑器,则更可能进入:

Engine/Source/Editor/
Engine/Source/Developer/
Engine/Plugins/Editor/

所以看到一个需求时,先判断它属于哪个模块方向,搜索会快一点。效率就高了。我电脑不是很好,这对我来说很重要了。


3. Runtime 和 Editor 分层:先判断功能在哪个世界生效

这是初学 UE 源码时非常关键的一层。

Runtime = 游戏运行时也需要的代码
Editor = 只有编辑器里才需要的代码

例如这些通常偏 Runtime:

AActor
UObject
UStaticMeshComponent
UMaterial
ULightComponent
UDirectionalLightComponent

而这些通常偏 Editor:

Details 面板
Asset Editor
Content Browser
蓝图编辑器
材质编辑器界面
编辑器菜单栏

本次练习同时碰到了两边:

  • Details 面板显示一个 bool,属于编辑器可见的属性表现。
  • 这个 bool 对最终光照产生影响,属于 Runtime / Renderer / Shader 逻辑。

这里有一个重要判断:如果只是让普通属性显示在 Details 面板里,通常不需要改 Editor/DetailCustomizations。只要属性是 UPROPERTY(EditAnywhere…),UE 的 Details 面板就会自动显示。

只有当需要自定义复杂 UI,例如自定义按钮、特殊折叠组、联动控件、搜索逻辑、动态图表,才更可能进入 Detail Customization。


4. UObject 反射系统:为什么 UPROPERTY 会出现在 Details 面板

UE 和普通 C++ 最大的区别之一,是 UObject 反射系统。

普通 C++ 可能只是:

class MyClass
{
public:
float Value;
};

UE 里经常会写成:

UCLASS()
class UMyObject : public UObject
{
GENERATED_BODY()

public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Value;

UFUNCTION(BlueprintCallable)
void DoSomething();
};

这些宏不是装饰品。UCLASS、UPROPERTY、UFUNCTION 会被 UHT,也就是 Unreal Header Tool 解析并生成额外代码。

可以粗暴理解为:

UObject 系统 = 反射 + 序列化 + 蓝图暴露 + GC 垃圾回收 + 编辑器属性面板

所以当给 UDirectionalLightComponent 增加一个 UPROPERTY(EditAnywhere…) 时,它会被 UE 的反射系统识别,并自动显示到 Details 面板里。

但是注意:

Details 面板能显示,只代表这个值存在于 UObject / Component 上,不代表它已经进入渲染线程,更不代表 Shader 能读到。

这就是本次练习最核心的地方。


5. Gameplay 框架:World、Actor、Component 的关系

如果从游戏对象角度看,UE 常见关系可以简化成:

UGameInstance

UWorld

ULevel

AActor

UActorComponent / USceneComponent / UPrimitiveComponent

常见对象关系是:

World 里有很多 Actor
Actor 身上挂很多 Component
Component 负责具体能力

例如一个角色可能是:

ACharacter
├─ CapsuleComponent
├─ SkeletalMeshComponent
├─ CharacterMovementComponent
└─ Camera / SpringArm

灯光也类似。

在场景里看到的 Directional Light Actor,本质上会包含一个 UDirectionalLightComponent。真正与灯光参数、方向、颜色、强度相关的核心数据,一般都在 Component 上。

所以本次功能的数据起点应该选在:

UDirectionalLightComponent

而不是直接从某个 Editor UI 文件开始。


6. 渲染框架:从组件到 GPU 不是一步到位

UE 的渲染可以先按下面这个层级理解:

层级作用常见路径
Engine 层 场景对象、Actor、Component、灯光、材质等高层数据 Engine/Source/Runtime/Engine/
Renderer 层 组织具体渲染流程,例如 Deferred Lighting、Fog、Post Process Engine/Source/Runtime/Renderer/
RenderCore 层 渲染基础设施、Shader 参数、Uniform Buffer 等 Engine/Source/Runtime/RenderCore/
RHI 层 抽象 DX12 / Vulkan / Metal 等图形 API Engine/Source/Runtime/RHI/
Shader 层 .ush / .usf 文件,最终 GPU 执行的 HLSL Engine/Shaders/

如果之后要改雾、大气、后处理、灯光、shader 参数,这几个位置会反复出现。

本次练习会跨过多个层级:

UDirectionalLightComponent Engine 层,游戏线程 / 编辑器对象
FDirectionalLightSceneProxy 渲染线程代理
FLightRenderParameters C++ 渲染参数结构
DeferredLightUniforms Deferred 光照 shader 参数
FDeferredLightData HLSL 光照数据结构
DeferredLightPixelShaders.usf 最终像素着色逻辑

这也是为什么“只是给平行光加了一个功能”,却会改多个文件。


7. 插件系统:插件也是模块,但更适合独立发布

插件可以理解为带 .uplugin 描述文件的一组模块和资源。它可以独立启用、禁用、打包和发布。

一个插件大概是:

MyPlugin/
├─ MyPlugin.uplugin
├─ Source/
│ └─ MyPlugin/
│ ├─ MyPlugin.Build.cs
│ ├─ Public/
│ └─ Private/
├─ Content/
└─ Resources/

本次练习是直接修改引擎源码,不是写插件。它更适合训练“引擎内部数据链路”。如果未来要做可发布工具,则要再判断功能能不能做成插件。

有些功能适合插件,例如编辑器工具、资产处理、材质节点、蓝图节点、后处理材质模板。有些功能如果需要改 Renderer 内部 shader 参数和引擎结构体,可能就必须走源码修改,或者需要更复杂的插件扩展点。


开始练习

这次需求可以写成一句话:

在 UDirectionalLightComponent 上添加一个可勾选的 bool,当它开启时,把这盏 Directional Light 的最终光照像吃了菌子一样出现幻彩。

但从源码角度,它真正包含三件事:

  • 属性暴露:让 Details 面板能看到并编辑这个 bool。
  • 数据传递:把 Component 上的 bool 传到渲染线程,再传到 shader 参数。
  • 最终着色:在 Deferred Light Pixel Shader 中根据开关修改 LightData.Color。
  • 核心链路如下:

    Details Bool

    UDirectionalLightComponent

    FDirectionalLightSceneProxy / FLightSceneProxy

    FLightRenderParameters / FLightShaderParameters

    DeferredLightUniforms

    FDeferredLightData

    DeferredLightPixelShaders.usf

    Red Light

    这条链路非常重要。以后做很多引擎级渲染功能时,本质也是类似模式:

    美术可调参数 → C++ 组件 / 资源 → 渲染线程结构 → Shader 参数 → HLSL 实现


    第一步:让 Details 面板显示开关

    最开始可以在 DirectionalLightComponent.h 中声明一个属性。

    示例写法:

    // [Yumi] Expose a minimal test switch for the Directional Light source modification exercise.
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light", meta=(DisplayName="UseYumiColorfulLight"))
    uint32 bUseYumiColorfulLight : 1;

    这一步的作用是:

    让编辑器 Details 面板出现一个可勾选的属性。

    但是这一步只负责“看得见、能勾选”。它不会自动影响渲染。

    可以把它理解成:

    在遥控器上加了一个按钮,但按钮还没有接到电视机内部电路里。

    所以如果只加了这个属性,然后进入场景勾选它,画面没有任何变化,这是正常的。

    为什么 Details 的值不能直接被 Shader 读取

    UE 的编辑器对象、游戏线程对象、渲染线程对象和 GPU Shader 不是同一个世界。

    简化理解如下:

    世界代表对象特点
    Editor / Game Thread UDirectionalLightComponent UObject,可被 Details 面板编辑,可序列化
    Render Thread FDirectionalLightSceneProxy / FLightSceneProxy 渲染线程使用的轻量副本,不能随便读 UObject
    Shader Parameter Uniform / Structured 参数 CPU 准备好后传给 GPU
    GPU Shader .usf / .ush HLSL 最终执行着色逻辑

    Shader 不会直接去读 UDirectionalLightComponent::bUseYumiColorfulLight。原因包括:

  • UObject 在游戏线程 / 编辑器世界,Shader 在 GPU 世界。
  • 渲染线程通常使用 SceneProxy 保存渲染所需数据,避免直接访问 UObject。
  • CPU 和 GPU 之间要通过明确的 shader 参数结构传值。
  • HLSL 侧需要有对应字段,否则即使 C++ 有数据,shader 也不知道怎么读。
  • 所以这次练习我觉得最烦的就是把这个 bool 一层一层传下去。 2026-06-04T08:38:15.png 这里可以添加自己想要的,和之前一样,先声明有这玩意。 2026-06-04T08:38:29.png 先不写内容,只是显示出来debug一下这个声明是不是加对了吧… 2026-06-04T08:38:46.png good,可以显示! 2026-06-04T08:39:01.png


    第二步:实现串通文件

    不需要一上来就做那么复杂,可以只实现true的时候变红,代表联通了!

    关键文件职责总览

    具体文件名可能会因 UE 小版本和源码分支略有差异,所以不要死记行号。更可靠的方法是用符号搜索定位:

    UDirectionalLightComponent
    FDirectionalLightSceneProxy
    FLightSceneProxy
    FLightRenderParameters
    FLightShaderParameters
    DeferredLightUniforms
    FDeferredLightData
    SetupLightDataForStandardDeferred
    DeferredLightPixelShaders.usf
    DeferredLightingCommon.ush
    LightData.ush
    LightDataUniforms.ush

    本次修改大概率会涉及这些层:

    层级可能文件职责
    Component 属性层 DirectionalLightComponent.h 声明 Details 面板可见的 bool
    Component 刷新层 DirectionalLightComponent.cpp 当属性变化时刷新渲染状态
    SceneProxy 层 LightComponent.cpp / 相关 Light SceneProxy 定义 把 Component 数据复制到渲染线程代理
    C++ Shader 参数层 SceneManagement.h / LightRendering.cpp 增加并填充传给 shader 的参数
    Shader 参数声明层 LightShaderParameters.ush 等 让 HLSL 侧知道参数存在
    Deferred Light 数据层 LightData.ush / LightDataUniforms.ush / DeferredLightingCommon.ush 把 uniform 数据转成像素着色时使用的 FDeferredLightData
    最终着色层 DeferredLightPixelShaders.usf 根据开关修改 LightData.Color

    不需要迷信“必须改这几个精确文件”。正确思路是:

    数据链路经过哪里,就在哪里补字段;最终在哪里生效,就在哪里写逻辑。


    关键改动 1:Component 保存 Details 面板的开关
    位置

    通常在:

    Engine/Source/Runtime/Engine/Classes/Components/DirectionalLightComponent.h

    或者所在版本中 UDirectionalLightComponent 的实际定义位置。

    示例代码

    // [Yumi] Expose a minimal render test toggle on Directional Light.
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light", meta=(DisplayName="UseYumiColorfulLight"))
    uint32 bUseYumiColorfulLight : 1;

    它读取了什么

    它不读取别人,它是数据源。美术或者开发者在 Details 面板中勾选这个属性,值会保存在 UDirectionalLightComponent 上。

    它传给谁

    下一步需要在创建 SceneProxy 时,把它复制给渲染线程使用的 proxy。

    因为这是整个数据链的起点。如果 Component 上没有这个属性,Details 面板没有输入入口,后面的 shader 参数就没有来源。


    关键改动 2:从 Component 复制到 SceneProxy
    为什么需要 SceneProxy

    UE 渲染时不会让渲染线程随便读取 UObject。对于场景物体、灯光、Primitive 等,UE 通常会创建对应的 SceneProxy。

    可以这样理解:

    UDirectionalLightComponent = 游戏线程 / 编辑器中的真实对象
    FDirectionalLightSceneProxy = 渲染线程使用的影子副本

    如果只把 bool 放在 UDirectionalLightComponent 上,而没有复制到 SceneProxy,渲染线程就不知道这个开关存在。

    示例代码

    在 Directional Light 对应 SceneProxy 中增加成员:

    // [Yumi] Store the Details toggle on the render-thread light proxy.
    uint32 bUseYumiColorfulLight : 1;

    构造 proxy 时从 Component 复制:

    // [Yumi] Copy the component value when building the render-thread proxy.
    bUseYumiColorfulLight(Component->bUseYumiColorfulLight)

    它读取了什么

    读取:

    Component->bUseYumiColorfulLight

    它传给谁

    传给:

    FDirectionalLightSceneProxy / FLightSceneProxy

    也就是渲染线程使用的数据副本。

    为什么必须存在

    因为渲染线程和游戏线程是分开的。不能指望 shader 参数填充阶段直接去读 Details 面板上的 UObject 属性。


    关键改动 3:Details 修改后刷新 SceneProxy

    如果在 Details 面板里勾选了开关,但画面没有立刻变化,很可能是旧的 SceneProxy 还在被使用。

    这时需要在属性变化时标记渲染状态需要刷新:

    #if WITH_EDITOR
    void UDirectionalLightComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
    {
    const FName PropertyName = PropertyChangedEvent.Property
    ? PropertyChangedEvent.Property->GetFName()
    : NAME_None;

    Super::PostEditChangeProperty(PropertyChangedEvent);

    if (PropertyName == GET_MEMBER_NAME_CHECKED(UDirectionalLightComponent, bUseYumiColorfulLight))
    {
    // [Yumi] Rebuild the scene proxy so the Details checkbox reaches the render thread immediately.
    MarkRenderStateDirty();
    }
    }
    #endif

    注意:真实源码中可能已经有 PostEditChangeProperty。如果已有,不要重复定义一个同名函数,而是合并到已有逻辑里。

    它读取了什么

    读取当前变化的属性名:

    PropertyChangedEvent.Property->GetFName()

    判断是不是:

    bUseYumiColorfulLight

    它传给谁

    它不是直接传值,而是通知引擎:

    这个组件的渲染状态脏了,需要重建 / 更新渲染代理。

    为什么必须存在

    如果不调用 MarkRenderStateDirty(),可能出现:

    • Details 面板值已经变了;
    • Component 上的 bool 已经是新值;
    • 但渲染线程仍然使用旧 SceneProxy;
    • shader 参数仍然是旧值。

    所以它是保证编辑器中实时反馈的重要刷新点。


    关键改动 4:把开关写入 C++ 渲染参数

    SceneProxy 有了 bool 之后,还要把它写入真正传给 shader 的灯光参数结构。

    通常会涉及类似:

    FLightRenderParameters
    FLightShaderParameters
    DeferredLightUniforms

    这里的名字可能因版本略有差异,但职责是类似的:它们都是 C++ 到 shader 之间的参数搬运站。

    为什么建议用 float,而不是 bool

    Shader 参数里建议用:

    float YumiColorfulLightEnabled;

    而不是:

    bool bYumiColorfulLightEnabled;

    原因是 shader 参数涉及 CPU / GPU 数据布局、对齐和跨语言传递。float 或 uint 更稳定。很多渲染代码里也会用:

    0.0 = 关闭
    1.0 = 开启

    来表达开关。

    示例代码

    在渲染参数结构中增加字段:

    // [Yumi] Pass the Directional Light test toggle to the deferred light shader.
    float YumiColorfulLightEnabled = 0.0f;

    填充参数时:

    // [Yumi] Directional lights are the only proxy type that can enable this shader test.
    LightParameters.YumiColorfulLightEnabled = bUseYumiColorfulLight ? 1.0f : 0.0f;

    其他灯光类型保持默认:

    // [Yumi] Other light proxy types keep the default 0.0 value.

    它读取了什么

    读取 SceneProxy 上保存的:

    bUseYumiColorfulLight

    它传给谁

    传给渲染参数结构,例如:

    FLightRenderParameters
    FLightShaderParameters
    DeferredLightUniforms

    为什么必须存在

    SceneProxy 上有值,不代表 shader 参数里有值。需要明确把它放进 shader 参数结构,否则后面的 HLSL 仍然无法读取。


    关键改动 5:把参数接到 HLSL 的 LightData

    C++ 侧填了 shader 参数之后,HLSL 侧也必须有对应字段。

    这通常会涉及 .ush 文件。可以理解为:

    .cpp / .h = CPU 侧知道这个参数
    .ush / .usf = GPU Shader 侧知道这个参数

    如果 C++ 侧加了字段,但 HLSL 侧没同步,可能会出现:

    • shader 编译报错;
    • 参数结构不匹配;
    • 读不到字段;
    • 读到错位数据;
    • 功能没有效果。
    示例:在 FDeferredLightData 中增加字段

    struct FDeferredLightData
    {
    // existing fields…

    // [Yumi] 1.0 means this deferred directional light should be forced to red for the test.
    float YumiColorfulLightEnabled;
    };

    示例:从 uniform 拷贝到 LightData

    // [Yumi] Copy the deferred uniform value into the light data used by the pixel shader.
    Out.YumiColorfulLightEnabled = DeferredLightUniforms.YumiColorfulLightEnabled;

    它读取了什么

    读取 shader uniform 中的:

    DeferredLightUniforms.YumiColorfulLightEnabled

    它传给谁

    传给像素着色逻辑使用的:

    FDeferredLightData LightData

    为什么必须存在

    Deferred Light Pixel Shader 最后通常操作的是 LightData,不是最早的 C++ Component,也不是原始 uniform。中间这层如果不接上,数据链就会断。


    关键改动 6:在 Pixel Shader 中把灯变红

    最终真正改画面的地方通常在:

    DeferredLightPixelShaders.usf

    这里才是“功能生效”的地方。

    建议写法

    // [Yumi] Deferred pixel shader helper for the Directional Light test toggle.
    void ApplyYumiColorfulLight(inout FDeferredLightData LightData)
    {
    BRANCH
    if (!LightData.bRadialLight && LightData.YumiColorfulLightEnabled > 0.5f)
    {
    const float LightEnergy = max(max(LightData.Color.r, LightData.Color.g), LightData.Color.b);
    LightData.Color = float3(LightEnergy, 0.0f, 0.0f);
    }
    }

    然后在合适位置调用:

    // [Yumi] Apply after LightData is prepared and before lighting evaluation uses LightData.Color.
    ApplyYumiColorfulLight(LightData);

    为什么不直接写死 float3(1, 0, 0)

    如果直接写:

    LightData.Color = float3(1, 0, 0);

    会丢掉原始光强。这样不管原来的灯强还是弱,最后都变成同一个红色强度,测试结果会不够合理。

    更好的做法是保留原来的能量,只改色相:

    const float LightEnergy = max(max(LightData.Color.r, LightData.Color.g), LightData.Color.b);
    LightData.Color = float3(LightEnergy, 0.0f, 0.0f);

    这样效果是:

    原来很亮的光 → 很亮的红光
    原来很弱的光 → 很弱的红光

    这比写死一个颜色更符合“测试开关只改变颜色,不破坏强度关系”的原则。

    为什么要判断 !LightData.bRadialLight

    Point Light、Spot Light、Rect Light 等局部灯通常属于 radial / local light。Directional Light 不是 radial light。

    所以加上:

    !LightData.bRadialLight

    是为了避免这个测试逻辑影响其他灯光类型。

    不过更严谨地说,具体判断方式要以所在版本的 FDeferredLightData 字段为准。如果源码里有更明确的 light type 标识,也可以用更精确的判断。


    完整数据链复盘

    本次功能的数据链可以这样复盘:

    1. 用户在 Details 面板勾选 UseYumiColorfulLight

    2. UDirectionalLightComponent::bUseYumiColorfulLight 保存这个值

    3. 创建 / 刷新 FDirectionalLightSceneProxy 时,把 Component 上的 bool 复制过去

    4. SceneProxy 填充 FLightRenderParameters / FLightShaderParameters

    5. Deferred Light 渲染流程把参数写入 DeferredLightUniforms

    6. HLSL 把 DeferredLightUniforms 拷贝进 FDeferredLightData

    7. DeferredLightPixelShaders.usf 读取 LightData.YumiColorfulLightEnabled

    8. 如果开启,就把 LightData.Color 改成红色,同时保留原光强

    2026-06-04T08:40:13.png

    这条链路里,任何一个节点漏掉,最终效果都可能不生效。

    常见断点如下:

    漏掉的位置可能现象
    只加了 UPROPERTY Details 能勾选,但画面完全不变
    没复制到 SceneProxy Component 有新值,但渲染线程不知道
    没调用 MarkRenderStateDirty() 勾选后不刷新,可能重启或重新加载后才变化
    C++ shader 参数没加字段 HLSL 收不到值,或者编译报错
    HLSL 结构没同步 shader 编译失败或数据错位
    没拷贝到 FDeferredLightData uniform 有值,但最终 pixel shader 的 LightData 没值
    Pixel Shader 调用位置不对 代码写了,但实际光照计算没有用到修改后的颜色
    判断条件太宽 其他灯光也被影响
    判断条件太窄 Directional Light 没被命中

    2026-06-04T08:40:27.jpg

    第三步:吃菌子吧!

    知道在那实现以后就可以玩点有意思的了哈哈哈哈! 2026-06-04T08:40:36.png


    后话

    在我的文章里骂了很多次vibe coding ,终于在一个评论里看到了我想表达的嘴替。就是这个意思。且不说需求能不能100%的理解,很多时候出现了偏差来排查是最讨厌的。不如自己干来的快… 2026-06-04T08:40:52.png

    赞(0)
    未经允许不得转载:171主机测评 » 【UE】渲染框架学习路径-初次修改源码
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址