记录“第一次真正改 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 源码非常大,如果没有层级地图,搜索出来一堆同名文件时会很容易迷路。
可以先用下面这张简化地图理解 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.cppBuild.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 一层一层传下去。
这里可以添加自己想要的,和之前一样,先声明有这玩意。
先不写内容,只是显示出来debug一下这个声明是不是加对了吧..
good,可以显示!
第二步:实现串通文件
不需要一上来就做那么复杂,可以只实现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 改成红色,同时保留原光强
这条链路里,任何一个节点漏掉,最终效果都可能不生效。
常见断点如下:
| 漏掉的位置 | 可能现象 |
|---|---|
只加了 UPROPERTY | Details 能勾选,但画面完全不变 |
| 没复制到 SceneProxy | Component 有新值,但渲染线程不知道 |
没调用 MarkRenderStateDirty() | 勾选后不刷新,可能重启或重新加载后才变化 |
| C++ shader 参数没加字段 | HLSL 收不到值,或者编译报错 |
| HLSL 结构没同步 | shader 编译失败或数据错位 |
没拷贝到 FDeferredLightData | uniform 有值,但最终 pixel shader 的 LightData 没值 |
| Pixel Shader 调用位置不对 | 代码写了,但实际光照计算没有用到修改后的颜色 |
| 判断条件太宽 | 其他灯光也被影响 |
| 判断条件太窄 | Directional Light 没被命中 |
第三步:吃菌子吧!
知道在那实现以后就可以玩点有意思的了哈哈哈哈!
后话
在我的文章里骂了很多次vibe coding ,终于在一个评论里看到了我想表达的嘴替。就是这个意思。且不说需求能不能100%的理解,很多时候出现了偏差来排查是最讨厌的。不如自己干来的快..

