MENU

文章目录

• 2026 年 06 月 05 日 • 已有 4 只咪围观过 • -学海无涯-

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

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


我选择切入的方向

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

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

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

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

项目本次是否处理原因
Directional Light目标明确,容易观察结果
PC Deferred RendererUE 桌面端常见路径,适合作为第一次练习(最近在移动端导出踩坑,累死我了)
Details 面板普通属性UPROPERTY 即可,不需要先写复杂 Detail Customization
Mobile Renderer移动端光照路径不同,容易扩大范围
Forward RendererForward 与 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();
};

这些宏不是装饰品。UCLASSUPROPERTYUFUNCTION 会被 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 ProcessEngine/Source/Runtime/Renderer/
RenderCore 层渲染基础设施、Shader 参数、Uniform Buffer 等Engine/Source/Runtime/RenderCore/
RHI 层抽象 DX12 / Vulkan / Metal 等图形 APIEngine/Source/Runtime/RHI/
Shader 层.ush / .usf 文件,最终 GPU 执行的 HLSLEngine/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 的最终光照像吃了菌子一样出现幻彩。

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

  1. 属性暴露:让 Details 面板能看到并编辑这个 bool。
  2. 数据传递:把 Component 上的 bool 传到渲染线程,再传到 shader 参数。
  3. 最终着色:在 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 ThreadUDirectionalLightComponentUObject,可被 Details 面板编辑,可序列化
Render ThreadFDirectionalLightSceneProxy / FLightSceneProxy渲染线程使用的轻量副本,不能随便读 UObject
Shader ParameterUniform / Structured 参数CPU 准备好后传给 GPU
GPU Shader.usf / .ush HLSL最终执行着色逻辑

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

  1. UObject 在游戏线程 / 编辑器世界,Shader 在 GPU 世界。
  2. 渲染线程通常使用 SceneProxy 保存渲染所需数据,避免直接访问 UObject。
  3. CPU 和 GPU 之间要通过明确的 shader 参数结构传值。
  4. 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 数据布局、对齐和跨语言传递。floatuint 更稳定。很多渲染代码里也会用:

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 LightSpot LightRect 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

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

常见断点如下:

漏掉的位置可能现象
只加了 UPROPERTYDetails 能勾选,但画面完全不变
没复制到 SceneProxyComponent 有新值,但渲染线程不知道
没调用 MarkRenderStateDirty()勾选后不刷新,可能重启或重新加载后才变化
C++ shader 参数没加字段HLSL 收不到值,或者编译报错
HLSL 结构没同步shader 编译失败或数据错位
没拷贝到 FDeferredLightDatauniform 有值,但最终 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
返回文章列表 打赏
本页链接的二维码
打赏二维码