.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}简直了，让自己完全没时间摸鱼我愿称作高级牛马的自我修养——自己做皮鞭踩坑都是我自己遇到的，如果你没有遇到或者遇到别的可以飞书填写表单。可以帮你安装Hooks，爱发电购买玉米粒(6.6R)就可以！这篇文章记录一次从 0 到 1 搭建「VSCode Codex 执行完成后自动发送飞书单聊通知」的完整过程。最终效果是：在 VSCode 中使用 Codex 执行任务，任务结束后，飞书会自动收到一条来自应用机器人的单聊消息。消息中可以包含执行时间、电脑名、项目路径、模型名、Hook 事件类型，以及 Codex 最后一条回复。1. 总体思路与方案选择1.1 最终目标想实现的链路很简单：Codex 执行结束 ↓ Codex Stop Hook 触发 ↓ 本地 Python 脚本执行 ↓ 获取飞书 tenant_access_token ↓ 调用飞书发送消息接口 ↓ 飞书应用机器人单聊通知我最终飞书收到的消息类似：✅ Codex 执行完啦！ 时间：2026-06-18 16:09:09 电脑：DESKTOP-XXXXXXX 项目：D:\YourProject 模型：gpt-5.5 事件：Stop 最后回复： Hi. What do you want to work on?1.2 为什么使用 Codex Stop HookCodex Hooks 是 Codex 提供的生命周期扩展机制，可以在特定事件发生时执行本地脚本。这里最适合使用的是 Stop 事件，因为它会在 Codex 当前回合停止时触发。官方文档中提到，Hook 配置可以放在：~/.codex/hooks.json <repo>/.codex/hooks.json对于本文场景，我使用的是用户全局配置：C:\Users\YourName\.codex\hooks.json这样所有项目都可以复用同一套通知逻辑。1.3 为什么不用群机器人 Webhook一开始我考虑过飞书「自定义机器人 Webhook」。Webhook 方案很简单：Codex Hook → 调用 Webhook → 飞书群收到消息但自定义机器人更偏向群聊通知，如果目标是「机器人直接单聊发给我」，更适合使用飞书开放平台的应用机器人能力。所以最后采用的是：飞书自建应用机器人 + tenant_access_token + open_id + 发送消息 API这样可以让应用机器人直接给指定用户发送单聊消息。1.4 最终文件结构最后本地会有两个关键文件：C:\Users\YourName\.codex │ ├─ hooks.json │ └─ hooks └─ feishu_codex_done.py其中：hooks.json负责告诉 Codex：当 Stop 事件触发时，运行哪个命令。feishu_codex_done.py负责调用飞书接口，发送单聊通知。2. 飞书开放平台配置2.1 创建自建应用进入飞书开放平台，创建一个自建应用，例如：应用名称：Codex 应用类型：企业自建应用创建完成后，可以在「凭证与基础信息」中看到：App ID App Secret这里要注意：项目说明App ID应用 ID，可以写入环境变量App Secret应用密钥，相当于密码，不要公开open_id用户在当前应用下的身份 ID，后面需要获取如果 App Secret 曾经出现在截图、日志、博客草稿中，建议立刻重置。2.2 添加机器人能力在应用后台左侧找到：添加应用能力 → 机器人添加完成后，可以在飞书客户端中看到这个应用机器人。此时它已经具备作为机器人出现的基础能力，但还不能保证可以主动发消息。2.3 开通发送消息权限进入：权限管理 → 开通权限搜索：im:message需要确认至少有这个权限：以应用的身份发消息 im:message:send_as_bot如果已经显示「已添加」，就不需要重复添加。2.4 发布应用版本权限或机器人能力发生变更后，需要重新发布应用。路径一般是：应用发布 → 版本管理与发布 → 创建版本版本说明可以写：添加机器人发送消息权限如果页面提示「本次发布免审核」，保存并提交发布后即可使用。3. 获取接收人的 open_id3.1 为什么不用邮箱飞书发送消息接口支持多种 receive_id_type，例如：open_id user_id union_id email chat_id我这里不想用邮箱，而是希望机器人直接发给当前飞书用户，所以选择：receive_id_type = open_id3.2 open_id 的坑最开始我误用了 API 调试页面中的示例 open_id，结果发送失败，返回类似：{ "code": 99992351, "msg": "The request you send is not a valid {open_id} or not exists", "field_violations": [ { "field": "receive_id", "description": "id not exist" } ] }这个错误的意思是：receive_id 不是当前应用下真实存在的 open_id注意，open_id 是和应用相关的。不要随便复制别的页面、别的应用、示例代码里的 ou_xxx。3.3 正确获取 open_id可以通过飞书开放平台的 API 调试台获取自己的 open_id。常见方式是：进入发送消息相关接口 ↓ 根据排查建议进入获取 open_id 的 API 调试台 ↓ 选择当前应用 ↓ 选择成员为自己 ↓ 调用接口 ↓ 复制返回的 open_id正确的 open_id 通常长这样：ou_xxxxxxxxxxxxxxxxxxxxx3.4 在 API 调试台测试发送消息在「发送消息」接口中测试：POST /open-apis/im/v1/messages查询参数：receive_id_type = open_id请求头：Authorization = Bearer tenant_access_token请求体示例：{ "receive_id": "ou_xxxxxxxxxxxxxxxxxxxxx", "msg_type": "text", "content": "{\"text\":\"✅ Codex 单聊通知测试成功！\"}", "uuid": "codex-test-001" }这里有一个很容易踩的坑：content 必须是字符串，而不是普通 JSON 对象。正确："content": "{\"text\":\"hello\"}"错误："content": { "text": "hello" }测试成功后，飞书单聊中会收到应用机器人发来的消息。4. 本地环境准备4.1 配置环境变量本地脚本需要使用四个环境变量：FEISHU_APP_ID FEISHU_APP_SECRET FEISHU_RECEIVE_ID FEISHU_RECEIVE_ID_TYPEPowerShell 中执行：setx FEISHU_APP_ID "cli_xxxxxxxxxxxxxxxxx" setx FEISHU_APP_SECRET "你的 App Secret" setx FEISHU_RECEIVE_ID "ou_xxxxxxxxxxxxxxxxxxxxx" setx FEISHU_RECEIVE_ID_TYPE "open_id"执行后会看到：成功: 指定的值已得到保存。4.2 setx 的坑setx 写入的是后续进程的环境变量，当前 PowerShell 窗口不一定能立刻读取到。所以执行完 setx 后，需要：关闭当前 PowerShell 重新打开 PowerShell 重启 VSCode否则脚本或 Codex 可能读不到环境变量。4.3 安装 Python一开始本机没有可用的 Python，运行下面命令都失败：python --version python3 --version py -3 --version错误类似：Python was not found; run without arguments to install from the Microsoft Store...可以使用 winget 安装 Python：winget install --id Python.Python.3.12 -e安装完成后，关闭 PowerShell，重新打开，再检查：python --version成功时会显示类似：Python 3.12.104.4 Windows Store Python 别名问题如果安装后仍然提示 Microsoft Store，可以检查：Windows 设置 → 应用 → 高级应用设置 → 应用执行别名关闭：python.exe python3.exe5. 编写并测试 Python 通知脚本5.1 创建脚本目录创建 Hook 脚本目录：mkdir "$env:USERPROFILE\.codex\hooks" -Force创建 Python 脚本：notepad "$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"5.2 完整 Python 脚本写入下面代码（这里是最简单的，只有干完了和时间项目的信息，可以根据自己的需要拓展）：import datetime import json import os import sys import traceback import urllib.error import urllib.request APP_ID = os.environ.get("FEISHU_APP_ID", "").strip() APP_SECRET = os.environ.get("FEISHU_APP_SECRET", "").strip() RECEIVE_ID = os.environ.get("FEISHU_RECEIVE_ID", "").strip() RECEIVE_ID_TYPE = os.environ.get("FEISHU_RECEIVE_ID_TYPE", "open_id").strip() LOG_PATH = os.path.join( os.path.expanduser("~"), ".codex", "hooks", "feishu_codex_done.log", ) def safe_unicode(text): if text is None: return "" if not isinstance(text, str): text = str(text) return text.encode("utf-8", "replace").decode("utf-8", "replace") def cut(text, max_len=2000): text = safe_unicode(text) if not text: return "" return text if len(text) <= max_len else text[:max_len] + "..." def log(message): try: os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True) now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") message = safe_unicode(message) with open(LOG_PATH, "a", encoding="utf-8", errors="replace") as f: f.write(f"[{now}] {message}\n") except Exception: pass def post_json(url, payload, headers=None): data = json.dumps(payload, ensure_ascii=False).encode("utf-8", "replace") req = urllib.request.Request( url, data=data, headers={ "Content-Type": "application/json; charset=utf-8", **(headers or {}), }, method="POST", ) try: with urllib.request.urlopen(req, timeout=20) as resp: raw = resp.read().decode("utf-8", errors="replace") log(f"POST OK url={url} status={resp.status} body={cut(raw, 3000)}") return json.loads(raw) except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", errors="replace") log(f"POST HTTP ERROR url={url} status={e.code} body={cut(raw, 3000)}") raise except Exception: log("POST EXCEPTION:\n" + traceback.format_exc()) raise def require_feishu_ok(name, result): if not isinstance(result, dict): raise RuntimeError(f"{name} 失败：返回不是 JSON：{result}") if result.get("code") != 0: raise RuntimeError(f"{name} 失败：{result}") def get_tenant_access_token(): result = post_json( "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", { "app_id": APP_ID, "app_secret": APP_SECRET, }, ) require_feishu_ok("获取 tenant_access_token", result) return result["tenant_access_token"] def send_feishu_message(token, text): text = safe_unicode(text) result = post_json( f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={RECEIVE_ID_TYPE}", { "receive_id": RECEIVE_ID, "msg_type": "text", "content": json.dumps({"text": text}, ensure_ascii=False), }, headers={ "Authorization": f"Bearer {token}", }, ) require_feishu_ok("发送飞书消息", result) return result def read_hook_stdin(): try: raw_stdin = sys.stdin.read() log(f"HOOK RAW STDIN: {cut(raw_stdin, 3000)}") if not raw_stdin.strip(): return {} return json.loads(raw_stdin) except Exception: log("HOOK STDIN JSON PARSE FAILED:\n" + traceback.format_exc()) return {} def main(): try: hook_data = read_hook_stdin() if not APP_ID or not APP_SECRET or not RECEIVE_ID: log( "MISSING ENV: " f"APP_ID={bool(APP_ID)}, " f"APP_SECRET={bool(APP_SECRET)}, " f"RECEIVE_ID={bool(RECEIVE_ID)}, " f"RECEIVE_ID_TYPE={RECEIVE_ID_TYPE}" ) print(json.dumps({"continue": True}, ensure_ascii=False)) return cwd = safe_unicode(hook_data.get("cwd", "")) now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") text = ( "尊敬的老大！小的干完了！！！🙇🏻‍♂️🙇🏻‍♂️🙇🏻‍♂️\n\n" f"时间：{now}\n" f"项目：{cwd if cwd else '未知'}" ) token = get_tenant_access_token() result = send_feishu_message(token, text) log(f"FEISHU SEND SUCCESS: {result}") except Exception: log("FEISHU NOTIFY FAILED:\n" + traceback.format_exc()) print(json.dumps({"continue": True}, ensure_ascii=False)) if __name__ == "__main__": main()5.3 手动测试脚本不要直接运行：python "$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"这样可能看起来「没反应」，因为脚本会等待 stdin。正确测试方式是传入一个空 JSON：echo {} | python "$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"如果飞书收到消息，说明脚本已经跑通。5.4 模拟 Codex Hook 输入可以用下面命令模拟 Codex 传入的数据：'{"cwd":"D:\\TestProject","model":"codex","hook_event_name":"Stop","last_assistant_message":"Test from local PowerShell"}' | python "$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"这样飞书通知里就会出现模拟项目路径、模型名和最后回复。6. 配置 Codex Stop Hook6.1 创建 hooks.json创建或打开 Hook 配置文件：notepad "$env:USERPROFILE\.codex\hooks.json"写入：{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "python \"C:\\Users\\YourName\\.codex\\hooks\\feishu_codex_done.py\"", "timeout": 15, "statusMessage": "Sending Feishu notification" } ] } ] } }6.2 为什么建议写绝对路径一开始我尝试写："command": "python \"%USERPROFILE%\\.codex\\hooks\\feishu_codex_done.py\""但在 Codex Hook 里，%USERPROFILE% 不一定会像普通 cmd 那样展开。最后改成绝对路径后稳定成功："command": "python \"C:\\Users\\YourName\\.codex\\hooks\\feishu_codex_done.py\""6.3 在 VSCode 中确认 Hook重启 VSCode 后，在 Codex 输入框中输入：/hooks如果配置被读取，会出现类似：Stop command: python "C:\Users\YourName\.codex\hooks\feishu_codex_done.py" timeout: 15 statusMessage: Sending Feishu notification如果 Hook 没有被信任，需要在这个界面里启用或信任。6.4 测试 Codex 自动通知让 Codex 执行一个很小的任务：请回复一句 hello，不要修改文件。等待 Codex 结束后，飞书应该自动收到一条通知。这一步成功后，说明完整链路已经跑通：Codex Stop Hook → Python 脚本 → 飞书应用机器人 → 单聊通知7. 踩坑复盘与安全收尾7.1 这次踩过的坑问题原因解决方式自定义机器人不能单聊Webhook 更适合群聊通知改用应用机器人发送消息 APIid not existopen_id 不是当前应用下真实 ID通过 API 调试台重新获取权限已添加但不能发权限变更后未重新发布重新发布应用版本content invalidcontent 写成了 JSON 对象改成 JSON 字符串python 不可用本机未安装 Python 或被 Store 别名拦截安装 Python 并检查执行别名手动运行脚本没反应脚本等待 stdin使用 `echo {}python ...`PowerShell 脚本乱码中文编码和引号复制问题改用 Python 版脚本Hook 没触发Hook 未信任或路径未展开/hooks 确认，改用绝对路径7.2 App Secret 安全处理如果 App Secret 曾经出现在截图、日志或文章草稿里，建议立即去飞书开放平台重置。重置后重新写入环境变量：setx FEISHU_APP_SECRET "新的 App Secret"然后重启 VSCode。7.3 不要提交的内容不要把下面内容提交到 Git：真实 App Secret 真实 open_id 真实项目路径 包含密钥的截图如果要做成团队可复用工具，建议把敏感信息放在：系统环境变量 .env 文件 凭据管理器 CI/CD Secret7.4 后续优化方向当前版本只实现了「执行完成通知」，后续可以继续扩展：通知中增加任务耗时。通知中区分成功 / 失败。通知中列出修改过的文件。通知里只显示项目名，而不是完整路径。不同项目发送到不同飞书会话。把脚本封装成一个可复用小工具。加入消息卡片，让通知更好看。8. 官方文档索引8.1 Codex 相关Codex Hooks 官方文档Codex IDE Extension Features8.2 飞书机器人与消息飞书机器人概述自定义机器人使用指南发送消息 API消息常见问题8.3 飞书鉴权与用户身份自建应用获取 tenant_access_token如何获取 Open ID用户身份概述发布与审核自建应用结论最终稳定方案是：Codex Stop Hook + Python 本地脚本 + 飞书应用机器人单聊 API这套方案跑通后非常实用。Codex 在 VSCode 里执行任务时，我不用一直盯着窗口。只要它停下来，飞书就会给我发消息。这次踩坑最多的地方不是代码本身，而是：飞书 open_id 权限发布 Python 环境 Codex Hook 路径把这些点处理好以后，整个链路其实很清晰，也很适合继续扩展成个人自动化通知系统。后话后来我又迭代了，现在是这样的。可以简单指令执行“修改”，“回退”等功能。
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}记录“第一次真正改 UE 源码”的学习笔记。我不一定完全是对的。纯流水账碎碎念。后续改高度雾等等做了很多更复杂的事情了，但是那些不可能开源，所以想了想还是先分享我的起步吧~本次练习的最小目标是：在 DirectionalLightComponent 的 Details 面板中增加一个布尔开关 UseYumiColorfulLight。当它被勾选时，这盏平行光在 PC Deferred Renderer 路径下可以蹦迪。我选择切入的方向我其实搜了很多教程，每个平台都尽可能找到相关搜索。很多 UE 源码教程会从“如何下载源码、生成项目文件、编译 Editor”开始，但编译成功之后，下一步呢？源码这么大，我到底该改哪里？这一块我之前学AngelScript的时候已经很熟了，感觉是冗余信息我觉得最合理的方式是先做一个最小闭环：只让一条数据链跑通，只在一个渲染路径里验证成功。所以这次的范围应该明确收窄：项目本次是否处理原因Directional Light是目标明确，容易观察结果PC Deferred Renderer是UE 桌面端常见路径，适合作为第一次练习（最近在移动端导出踩坑，累死我了）Details 面板普通属性是用 UPROPERTY 即可，不需要先写复杂 Detail CustomizationMobile 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 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 的最终光照像吃了菌子一样出现幻彩。但从源码角度，它真正包含三件事：属性暴露：让 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 ThreadUDirectionalLightComponentUObject，可被 Details 面板编辑，可序列化Render ThreadFDirectionalLightSceneProxy / FLightSceneProxy渲染线程使用的轻量副本，不能随便读 UObjectShader ParameterUniform / Structured 参数CPU 准备好后传给 GPUGPU 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 面板可见的 boolComponent 刷新层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为什么需要 SceneProxyUE 渲染时不会让渲染线程随便读取 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，而不是 boolShader 参数里建议用：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 的 LightDataC++ 侧填了 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.bRadialLightPoint 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 改成红色，同时保留原光强这条链路里，任何一个节点漏掉，最终效果都可能不生效。常见断点如下：漏掉的位置可能现象只加了 UPROPERTYDetails 能勾选，但画面完全不变没复制到 SceneProxyComponent 有新值，但渲染线程不知道没调用 MarkRenderStateDirty()勾选后不刷新，可能重启或重新加载后才变化C++ shader 参数没加字段HLSL 收不到值，或者编译报错HLSL 结构没同步shader 编译失败或数据错位没拷贝到 FDeferredLightDatauniform 有值，但最终 pixel shader 的 LightData 没值Pixel Shader 调用位置不对代码写了，但实际光照计算没有用到修改后的颜色判断条件太宽其他灯光也被影响判断条件太窄Directional Light 没被命中第三步：吃菌子吧！知道在那实现以后就可以玩点有意思的了哈哈哈哈！后话在我的文章里骂了很多次vibe coding ，终于在一个评论里看到了我想表达的嘴替。就是这个意思。且不说需求能不能100%的理解，很多时候出现了偏差来排查是最讨厌的。不如自己干来的快..
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}HLSL Material CreatorHLSL Material Creator is an Unreal Engine editor plugin for creating materials from HLSL Custom node code.FeaturesCreate Unreal Engine materials from HLSL Custom node code.Supports Unlit Surface materials.Supports Post Process materials.Auto-generates material inputs for supported HLSL declarations.Supports float, float1, float2, float3, float4, and texture2d inputs.Supports selected special inputs such as Time, World Position, Object Position WS, Texture Coordinate, and Post Process scene textures.Supported Unreal Engine VersionsUnreal Engine 5.5Unreal Engine 5.6Unreal Engine 5.7Supported PlatformWindowsWin64DependenciesThis plugin has no third-party dependencies.InstallationDownload the plugin package for your Unreal Engine version.Copy the HLSLMaterialCreator folder into your project's Plugins folder.Restart Unreal Engine.Enable the plugin from Edit > Plugins.Restart Unreal Engine again if required.NotesThis is an editor-only plugin. It is intended to help artists and technical artists create Unreal Engine materials from HLSL Custom node code more efficiently.
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}前言我觉得网上有很多关于 Vibe Coding 的内容，标题往往都很夸张..我用 AI 做了一个项目，赚了多少钱。普通人靠 AI 开发软件，实现副业变现。不会写代码，也能做出商业产品。这些说法当然很吸引人，让我也很好奇，五一花了点时间学习了一下。但以我自己的体验来看，至少在现阶段，我并不觉得它适合大多数人直接拿来做很商业化的项目。不太可能做到我做了一个某团，之类的。它确实很强，但强的地方不一定是“帮你立刻赚钱”。对我来说，它可以帮我做出完全符合自己需求的轻量工具。以前为了一个很小的功能，可能要找各种App，研究苹果快捷指令，或者下载一些功能复杂但并不真正适合自己的软件。用到最后，要么功能不够，要么操作太重，要么数据不在自己手里。要不就是要开会员，我真的交了很多智商税..现在不一样了。我自己能做的话，它不需要服务所有人，只要 100% 符合我自己的要求就够了。叠个甲：这就有点像魔兽插件。反正我也不卖钱，也不打算服务别人，自己用得舒服就行。所以如果你期待的是“如何用 AI 做出一个爆款产品”，那这篇可能不太符合你的预期。我的分享更偏向*普通人如何用 Vibe Coding 做出真正适合自己的个人工具。冷知识，不喜欢可以关闭该网页。制作链路这段时间，我大概做了六七个小工具。其中我需求最强的有两个：一个是个人生活管理和打卡 App。 一个是记录冰箱食材的工具。生活管理那个涉及比较多隐私，就不展开了。这里主要讲冰箱食材管理这个项目。这个工具的需求其实很简单：我想知道自己买了什么食材、花了多少钱、什么时候买的、什么时候过期、哪些东西快要吃完、哪些东西已经放太久了。听起来很小，但对我来说非常实用。因为现实生活里，家庭冰箱管理是一件很容易失控的事情。买菜的时候觉得自己很有计划，过几天就忘了冰箱里到底有什么。或者你买了菜，你妈不知道，第二天也买了一样的菜..等想起来的时候，菜已经坏了，肉已经冻了很久，调料也不知道开封多久了。所以我真正想解决的不是“记录食材”本身，而是让自己对生活里的消耗有感知。第一个版本：用有道云表格记录最早的时候，我只是用有道云的表格来记录。记录内容包括：买了什么消耗了什么花了多少钱什么时间购买什么时间过期这个方法很原始，但也能用。问题是，手动维护太麻烦了。每次买完东西都要打开表格、找到对应位置、手动输入信息。消耗食材的时候，也要再回去修改。更麻烦的是，表格本身不会主动提醒我哪些食材快过期。所以这个阶段，我其实只是把“冰箱记忆”从脑子里搬到了表格里，但它还没有真正变成一个工具。后来我发现，其实有道云也可以申请 API。理论上，可以通过 API 读取和处理表格数据，再做一些自动总结和提醒。但实际体验下来，在当时的开发环境和能力范围内，它能做的事情比较有限。比较适合做一些简单的数据总结、过期提醒，再复杂一点就会变得不太顺手。如果对这个方向感兴趣，可以去看官方文档。但对我来说，它并不是最理想的路线。第二个版本：做成微信小程序后来我尝试把它做成微信小程序。这个版本我做了一半，还给我妈试用了一下。微信小程序的好处是入口方便，手机上打开很自然。对于家庭成员来说，它也比网页、表格更容易理解。然后微信小程序做了一半，还给我妈试了一下，如果是免费的其实上能做的不多，而且数据是手机本地的，如果有两个手机，或者一个手机，又要电脑编辑。这个云是要钱的..这时候我想到，我自己就有服务器啊，为什么要再买个云。第三个版本：挂在自己的服务器上于是我做了第三个版本：网页端模拟 App。我把它部署在自己的服务器上，通过浏览器访问。没有对外开放接口，也不是公开产品。访问需要输入特定网址，还需要账号和密码。这个方案对我来说几乎是最合适的。只要有浏览器，所有设备都能用。 手机可以用，电脑可以用，平板也可以用。 数据存在自己的服务器上，不依赖某个 App，也不用额外买云服务。反正...家里人记录能有多少数据，这个项目完结在服务器上加上函数包也就1MB..这也是我对 Vibe Coding 最大的感受之一：它最适合解决那些“太个人化、太具体、太不值得商业化”的需求。这些需求找不到完美的现成软件，因为它们太私人了。 但正因为私人，它们对自己才最有价值。碎碎念怎么用很多人刚开始用 AI 做东西时，会直接说：帮我做一个管理冰箱食材的 App。AI 当然可以生成一个页面。 它可能会有新增食材、删除食材、分类展示、过期时间这些基础功能。看起来好像也能用。但问题是，它很可能不是你真正想要的东西。因为“管理冰箱食材”只是一个抽象需求。它没有说明你的生活方式，也没有说明你的操作习惯，更没有说明你到底希望这个工具解决什么问题。你是想记录购买时间？ 还是想提醒过期？ 是按食材分类，还是按冰箱位置分类？ 要不要记录价格？ 要不要记录消耗速度？ 要不要多人共用？ 要不要支持手机和电脑同步？ 要不要有家庭成员权限？ 过期提醒是提前一天，还是提前三天？这些问题，AI 不会天然知道。所以我一直觉得，AI 降低的是实现成本，但不会降低思考成本。甚至可以说，AI 让“思考能力”变得更重要了。以前你不会写代码，很多想法无法实现。 现在 AI 可以帮你写代码，但前提是你得知道自己到底要什么。如果你自己都说不清需求，AI 生成出来的东西也只会像抽盲盒。你可能不断觉得“不对”“不好用”“不是这种感觉”，但又说不出来到底哪里不对。最后不是节省时间，而是浪费更多时间。这和工作里对接需求其实很像。一个逻辑很差的 PM，会让整个团队都很痛苦。（我TMD曾经深受其害） 他说了很多话，但你听不懂他到底要什么。 他说“这里感觉不对”，但讲不清为什么不对。 他说“能不能高级一点”，但不知道高级具体指什么。和 AI 沟通也是一样的。你不能把自己变成那种只会说“没那种感觉”的抽象甲方。操作逻辑比如我不会只对 AI 说：这个不好使，优化一下这种说法太模糊了。我会尽量说得更具体：请优化什么功能：功能缺点1；操作逻辑缺点1；具体php哪里逻辑需要修改点1；这样 AI 就知道我要改什么、为什么改、改完应该是什么效果。AI 的效率很大程度上取决于描述能力。你不一定要成为专业工程师，但至少要逐渐理解自己正在做的东西。完全靠抽盲盒，是不靠谱的。去理解这次做这些小工具，也让我改变了对技术学习的看法。以前我总觉得，做一个应用之前，必须先系统学完很多东西：前端框架、数据库、接口、状态管理、部署、权限、数据结构……这些当然重要。但如果一直停留在学习阶段，可能永远不会真的开始做。Vibe Coding 给了我一个新的入口：先做出来，再理解它。 先遇到问题，再学习解决问题所需要的知识。这不意味着基础不重要。而是说，项目本身可以成为学习的路径。当我真的需要保存数据时，我自然会去理解本地存储和数据库。 当我真的需要多设备同步时，我自然会去理解服务器和接口。 当我真的需要统计图表时，我自然会去理解数据结构和聚合逻辑。 当我真的遇到页面不刷新的问题时，我自然会去理解状态管理。 当我真的需要登录保护时，我自然会去理解权限和认证。这种学习方式更具体，也更有反馈。它不是为了学而学，而是每一个知识点都对应着一个真实问题。结语所以，如果你问我 Vibe Coding 到底有没有用，我的答案是：有用，非常有用。但它不是许愿机。也不是印钞机..也祝你Coding顺利！
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}现在很多材质ai已经可以写了，但是拿到hlsl以后还需要去创建材质，添加custom，手动添加材质，然后加各种参数，填写默认值，还是很麻烦这些步骤本身并不难，却很消耗耐心。尤其当你需要反复验证效果、频繁替换代码，加一个input，删两个input的时候，这种身心上的成本会比想象中更重。所以我花了两个晚上的时间，写了这个小脚本。为什么做这个功能我一直觉得，AI 帮我们写出一段代码，和这段代码真正进入生产流程，中间其实隔着很长一段距离。尤其是材质系统相关的内容，它并不是“代码能跑”就结束了。 它还要能被整理、能被复用、能被其他人理解，也要能以更稳定的形式进入项目。 如果每一次都要从头手工搭建一遍材质节点和参数，那前面节省下来的时间，很容易又在后面被一点点消耗掉。所以这个脚本想解决的，并不是“如何生成 HLSL”，而是另一个更实际的问题：如何让 HLSL 更顺畅地进入 Unreal 的材质工作流。把这一步补上之后，AI、现成的 shader 逻辑、或者自己手写的 HLSL，才算真正开始变得好用。关于这次演示用到的 HLSL这次演示里的 HLSL，可以在 Shadertoy 找到对应的 GLSL。 而 GLSL 转 HLSL 这件事，其实在 GitHub上已经有比较成熟的方案了。就我自己的体验来说，在“语法映射”和“结构转换”这个层面上，这类现成方案往往比 AI 更稳定，也更可控。上不了Git，B站上搜“ue shadertoy”也有相当多的教程。AI 的优势更适合用在辅助理解这些环节 ，但如果是偏工程化、偏规则映射的工作，成熟方案通常还是更可靠一些。它真的有概率会瞎改里面逻辑。我并不是在否定 AI。恰恰相反，我觉得 AI 已经非常有用了。 只是它更适合作为工具链中的一环，而不是替代一切的答案。真的，不要太迷信Vibe Coding...一些AI写材质的想法这里和具体用哪个模型关系不大。 我更想表达的是AI 是否真的能帮到你，很大程度上取决于你自己现在站在哪个阶段。我经历过几个阶段，我觉得使用ai会出现两种情况。第一种：你已经有一定的 HLSL 基础如果你对 HLSL 已经有一定理解，并且脑子里很清楚自己想实现什么效果、需要哪些逻辑、哪里应该怎么拆，那么 AI 大概率会成为一个很好的加速器。AI去实现逻辑是非常好的。它可以帮你省去很多重复劳动，比如补函数、整理结构、快速试不同写法，或者把一个已经成型的思路更快地展开。 我自己是从 2023 年开始接触 HLSL 的。 在 CSDN 上还能看到我当时纯手写的一些 Groom 材质内容。现在回头看，那时的实现当然还很简单，但它对我来说很重要，因为很多理解都是从那个阶段一点点建立起来的。 后面我也写过一些后处理相关的内容，其中一部分已经开源，另外一些还没有整理出来。第二种：你只是想靠 AI 直接抽出结果另一种情况就在一些情况下很常见了。 你并没有真正理解代码，但你希望通过不断描述效果，让 AI 直接给你抽出一个满意的结果。这当然不是不可以，只是它很像抽卡。因为 AI 对“逻辑”的理解，通常远远强于它对“美术效果描述”的理解。 你说“我想要一种更柔一点、轻一点、空气感更强的边缘变化”，对人类美术来说这可能是很自然的表达； 但对 AI 来说，这样的描述往往并不稳定，它更容易抓住结构性的指令，而不是审美里的微妙差异。所以最后经常会变成： ..你可能要抽很多次，才接近你想要的东西。也可能抽很久，仍然得不到真正符合预期的结果。如果在这个过程中你又完全看不懂代码，那会很被动。因为你既无法判断它到底写得对不对，也无法自己做最后那一步修正。所以我觉得如果想认真把 AI 用进 shader 工作流里，至少先学一点基础语法。不用一开始就学得很深。 先能看懂输入输出，能理解函数结构，知道一个效果大概是怎么被组织出来的，这就已经很有帮助了。我是纯美术出身，大学连数学课都没认真上过几节。 但我后来还是一点点把这些东西学下来了。 所以如果你现在也正站在这个门口，不用太怀疑自己。 我可以学会，你也可以。（说实话，有那个时间等抽卡，教程已经看完了..）如果你想直接看 C++ 的实现如果你更关心这个工具本身是怎么落地的，尤其是如何直接通过 C++ 去生成对应结构，可以看我的上一篇博客。也可以看我AngelScript那一篇。只是AS不能直接在默认创建的C++项目里面用，比较麻烦。这里的h和cpp我发到我的git了，之前有权限的朋友可以直接获取。如果你打开是404，说明你没有权限目前支持的参数写法现在这个 HLSL 解析方式，支持三种输入习惯。第一种比较简单： 只写参数名，不写默认值。 这种情况下，生成出来的默认值会是 0。第二种是把默认值也直接写进注释里。 这样在生成材质参数时，就可以自动带上预设值，省掉后续手动填写的步骤。格式如下：// @input {数据类型} {参数名字} = {默认值}第三种是自己会分析有没有应该input的但是你前面漏写了。视频演示
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}碎碎念：一般网上用rgba来控制uv的方案好像多一点，但是我感觉色环直接上下左右拉好像更直观更爽一点，网上没有看到类似方案，于是我自己写了一个。主要参考的是ronja的方案：HSV Color Space主要思路：这个颜色拆成 色相 Hue 和 饱和度 Saturation。Hue 决定往哪个方向偏移，Saturation 决定偏移有多。然后用这个结果去改 UV，最后再用新的 UV 去采样 MainTex。HSV 里 Hue 本来就可以看成一个圆周角度，而 Saturation 可以理解成离灰色中心有多远HLSL:float3 C = max(ControlColor, 0.0); float maxC = max(C.r, max(C.g, C.b)); if (maxC > 0.00001) { C /= maxC; } float cMax = max(C.r, max(C.g, C.b)); float cMin = min(C.r, min(C.g, C.b)); float delta = cMax - cMin; float sat = 0.0; if (cMax > 0.00001) { sat = delta / cMax; } float hue = 0.0; if (delta > 0.00001) { if (cMax == C.r) { hue = (C.g - C.b) / delta; if (hue < 0.0) { hue += 6.0; } } else if (cMax == C.g) { hue = (C.b - C.r) / delta + 2.0; } else { hue = (C.r - C.g) / delta + 4.0; } hue /= 6.0; } float angle = hue * 6.28318530718 + AngleOffsetDeg * 0.01745329252; float2 dir = float2(cos(angle), -sin(angle)); float sat01 = saturate((sat - MinSat) / max(0.00001, (1.0 - MinSat))); float mag = OffsetStrength * pow(sat01, SatPower); float2 newUV = UV - dir * mag; newUV = saturate(newUV); float3 texColor = Texture2DSample(MainTex, MainTexSampler, newUV).rgb; return texColor;
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}还是有一些项目需要维护蓝图，之前已经写的太复杂了，我不想再花很多时间再写一遍as，虽然有ai辅助，但是现在盲目相信ai还是不行的，需要自己理解。然后有一些接口就是没有，接口没有的话，as和bp都是处理不了的。我搞了一整天要崩溃了，也不想总麻烦同事...自己写吧。然后我发现写结接口是最简单的，编译引擎又翻车了，但是这次不是编译本身，是P4V的问题..我查了很多资料以后有一个最简单的流程：这是总体官方文档，需要知道的内容这个链接也link了其他部分，基本能知道所有的信息。之前的文章有人私信我说打不开，epic的文档404一般换地区就可以解决..实在不行换版本，除了新功能其实文章内容都不是差非常多。我是c++项目，什么都没调整过的原生项目长这样:然后手动加这两个文件，按照格式规范书写内容，这里尝试了一个最简单的，其他静态函数的内容官方文档写的蛮仔细了..这里是官网链接https://dev.epicgames.com/documentation/en-us/unreal-engine/blueprint-function-libraries-in-unreal-engine这里是我简陋的测试代码：cpp：#include "Test55_CFunctionLibrary.h" #include "CoreMinimal.h" // 添加此行以确保FString已定义 FString UTest55_CFunctionLibrary::HelloYumi() { return TEXT("Hello from Yumi's VS"); }h：//这里不用写系统应该会自己创建，这个意思是这个头文件只允许被编译器包含一次。 #pragma once //把“蓝图函数库”这个父类请进来 #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" //反插一次，这样才能显示 #include "Test55_CFunctionLibrary.generated.h" UCLASS() class TEST55_C_API UTest55_CFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "Test55_C") static FString HelloYumi(); };然后就有了，就这么简单^ ^
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}项目主要是移动端，恶补ing..总览与适用范围1. 总览UE 移动端渲染的整体框架UE 的移动端渲染可以理解为三层选择：平台/图形 API（Android / iOS；Vulkan / OpenGL ES / Metal）→ 渲染器路径（Mobile Forward / Mobile Deferred）→ 具体功能开关（阴影、反射、后处理等）。在 UE 5.5 的“移动侧”更新中，官方明确强调 Mobile Forward 增强（包括矩形区域光、胶囊阴影、体积雾、Niagara 粒子灯、以及移动端 SSR 等）。 1从“渲染管线”角度，移动端一帧通常仍然遵循：可见性（InitViews/剔除）→ 阴影深度（ShadowDepths）→（可选）深度预通过（PrePass）→ BasePass（输出颜色/或写入移动 GBuffer）→ 光照/阴影合成 → 半透明 → 后处理。移动端的具体 pass 结构，和 Mobile Forward / Mobile Deferred 的分叉，在 UE 的移动渲染分析文章中有较清晰的 RenderDoc 截帧与流程拆解（FMobileSceneRenderer）。 2移动端“延迟着色”的引入，本质上是为了把“每个物体都要按灯算一遍”的成本，部分转换为“先写 GBuffer、再按屏幕算光”的成本，从而更适合“灯多/局部灯多”的场景；但它也会显著增加 Render Target、带宽与内存压力。 3 2 4与 PC/主机渲染相比的关键差异移动端相比 PC/主机，差异不在“概念”，而在“默认策略 + 可用硬件特性 + 可持续功耗”。1) 硬件架构更偏 TBDR（Tile-Based Deferred Rendering）/片上 tile memory，强调“减少系统内存读写、减少过度绘制、减少不必要的 pass 依赖”。 5 2) 带宽与功耗是长期瓶颈：移动 GPU 想一直维持高性能更难，通常需要你主动把“每像素的读写/采样/分支/后处理”压下去，否则很快触发热降频。 6 7 8 3) 功能层面：PC/主机常见的“高端动态光影与反射体系”（例如大规模动态阴影、复杂 GI/反射）在移动端要么是阉割版、要么是高端机/实验性路径。UE 5.5 对移动端新增特性明显集中在“Mobile Forward 画质补齐 + 移动 SSR 打通”。 1 9 10 4) Shader 与 RT（Render Target）策略更保守：移动端的“每像素 texture bandwidth”很宝贵（含 GBuffer 写入、后处理读写、阴影/反射采样）。Arm 的优化建议里直接把“纹理带宽”作为 fragment shader 关键瓶颈之一。 8注：原 Deep Research 在线图像轮播占位符已移除；Markdown 导出不支持该交互组件。为什么移动端在灯光、阴影、反射上限制更多移动端“贵”的根因，通常不是某个功能“算不出来”，而是算得出来但算不久/算不稳/算不齐：带宽（Bandwidth）： 灯光/反射/阴影经常意味着额外 RT、额外采样、额外读写（ShadowMap、GBuffer、SceneColor/Depth/Normal）。纹理读写一多，fragment shader 就容易“喂不饱”，性能直接被带宽卡死。 8 5Overdraw（过度绘制）： 透明、粒子、UI、复杂半透明叠加，会把“每像素的着色次数”乘上去；Forward 路径下尤其典型，因为光照计算往往发生在物体被绘制时。Apple 直接把“最小化 overdraw、减少不必要依赖与 load/store”作为性能优化核心主题之一。 5 2Shader 复杂度与纹理采样： 高光/法线/清漆/多层材质/SSR/PPR 都会明显增加 ALU 和采样，且采样常常就是带宽。 8 2分辨率与后处理： 大多数后处理按像素成本线性或近似线性随分辨率增长；SSR、Bloom、TAA、色调映射都属于“屏幕空间”类，分辨率一上去，成本就非常直接。 2 3 11热降频与电池： Android 官方把“热状态”视为应用潜在性能上限，并建议监控 thermal headroom 并动态下调工作负载。长时间运行（例如 10–20 分钟）比短跑 benchmark 更能暴露“灯光/反射太重导致掉帧”的真实问题。 6 7影响移动端画面与性能的核心瓶颈（你在项目里应该怎么理解）带宽：像素越多、RT 越多、采样越多 → 越容易被卡。 8Overdraw：透明/粒子/叠层 UI/大量重叠几何 → “同一像素重复算”。 5 2Shader 复杂度：材质指令数、分支、函数、清漆/复杂 BRDF、SSR/PPR → “每次算得更贵”。 8 11 2纹理采样：法线、粗糙、金属、AO、反射 cubemap、阴影贴图、后处理链 → “更贵 + 更吃带宽”。 8 3分辨率：屏幕空间技术成本与像素数强相关。 3 11后处理：MobileHDR 打开后，后处理链条通常明显更重，但也能显著提升观感（Bloom、Tonemapper、Color Grading 等）。 2 1热降频/电池：持续高功耗 = 掉频 = 帧率/稳定性下降。 6 72. 说明适用范围引擎版本与“你应该默认相信什么”以 UE 5.5 的官方发布描述为“功能引入依据”（例如移动 SSR、矩形区域光等），并以 5.5.4 Hotfix 作为版本落点：5.5.4 属于 hotfix，主要是修复类发布。 1 12由于部分移动功能和“渲染路径/设备 profile/图形 API”强绑定，任何“是否可用”都必须拆到 Mobile Forward vs Mobile Deferred、以及 Vulkan vs OpenGL ES vs Metal 来谈。 3 4 13目标平台与图形 APIAndroid：Vulkan / OpenGL ES（GLES）都需要考虑，但 Google 明确把 Vulkan 作为 Android 的主要低层图形 API，并说明 OpenGL ES 仍支持但不再积极发展新特性；同时 Vulkan 的优势包括更低 CPU 开销、更现代的特性（含 bindless、光追等）。 14 7iOS：图形 API 为 Metal；移动端渲染架构高度 TBDR，强调减少系统内存流量与 overdraw。 5UE 在移动端提供 Mobile Forward / Mobile Deferred，并且还提供“桌面渲染器上移动端”的实验路径（通常是 Android Vulkan / iOS 高端机）。 1 3“正式可用 / 实验性 / 不推荐 / 兼容性差”的标记规则为方便你在项目落地做决策，下文统一使用四级标记：官方支持（可量产）：功能在 UE 5.5 的官方移动更新中被明确列入移动渲染能力，或在移动相关文档/配置体系中长期稳定存在，且不依赖“桌面渲染器跑在手机上”。 1 3理论可行但不推荐：能跑，但成本或风险大，常见于“多一次整场景渲染”“重后处理链”“重阴影/重反射”类。 3 11 15某些设备可跑但不稳定：通常与高端机、特定 GPU/驱动、特定 API（多为 Vulkan/Metal）绑定；或者 UE 明确标为 Experimental（例如移动端 Lumen）。 9 10 3基本不适合量产：依赖 SM6 / 桌面级渲染器 / 大量 RT 与带宽，且在移动端缺乏官方“稳定覆盖”的预期。以 Android 为例，Arm 甚至指出 UE 默认并不官方支持 Android SM6，因此诸多桌面级特性（如 Nanite/VSM 路线）天然不该作为常规移动量产依赖。 16 17移动端灯光体系3. 把“灯光”拆开逐项讲（重点）先给你一个“移动端灯光”的底层心智模型移动端灯光要先分清两件事：1) 光照来自哪里：静态（烘焙）：Lightmass → Lightmap（含方向性可选）+（可选）Volumetric Lightmap（给动态物体采样间接光）。 18 19 20 21动态（实时）：Directional 的 CSM 是移动端最常见的动态阴影方案；局部灯（点/聚光）通常受数量限制，且阴影支持更苛刻。 2 21 22 32) 你用哪条移动渲染路径：Mobile Forward：更“省 RT/GBuffer”，但局部灯数量与特性约束更明显；UE 5.5 在 Mobile Forward 上加了很多画质特性补齐（矩形区域光、胶囊阴影等）。 1 2Mobile Deferred：更容易承载“多灯/更复杂反射/更接近桌面 blending 的反射捕捉器”等，但 GBuffer 与带宽成本更高，并且在 OpenGL ES 上可能需要额外开关（r.Mobile.AllowDeferredShadingOpenGL）。 3 4 13关键落地建议： 你在移动端做灯光，不是“能不能做”，而是“把实时部分压缩到极少数关键灯 + 把大部分质量放到烘焙/材质/后处理补偿”。各类灯光与移动端支持：工程化对照表下面表格以“默认移动渲染器（Mobile Forward / Mobile Deferred）”为主，不把“桌面渲染器跑在手机上”当作常规量产方案（会在替代方案/风险里单列）。 1 3 "Mobile Rendering and Shading Modes for Unreal Engine")表内“量产级别”统一用： S（官方支持可量产） / A（能跑但谨慎） / B（部分设备/不稳定） / C（基本不量产）灯光类型量产级别是否支持支持条件（常见硬门槛）渲染原理概述（移动端视角）性能成本主要来自哪里常见画面问题推荐替代方案低/中/高端设备建议Directional Light（平行光）S支持常用于“主光 +（可选）CSM 动态阴影”；动态阴影通常用 CSM；级联数在移动端有上限与项目设置限制。 2 "剖析虚幻渲染体系（12）- 移动端专题 Part 1（UE移动端渲染）") 22 "Use Cascaded ShadowsUnreal Engine 4.27 Documentation")移动端通常“最多 1 个关键方向光”（太阳），配合 CSM 做近距离实时阴影；其余间接光依赖 Lightmap/Skylight/反射。 2 3ShadowDepths（渲染阴影贴图）：级联数 × 阴影分辨率 × 投影到阴影里的三角形数；带宽：采样 ShadowMap；shader：每像素阴影过滤。 15 2 22阴影锯齿/闪烁、Peter-panning（阴影飘）、阴影漏光、远处阴影突然消失（距离/级联配置）。 22 "Use Cascaded ShadowsUnreal Engine 4.27 Documentation") 23远景用烘焙阴影+近景 CSM；对不重要物体关闭接收 CSM（Receive Mobile CSMShadows）；用 Modulated Shadows（仅 Stationary 方向光）做“便宜阴影”。 22 2低端：1 级联/低距离/少投射体；中端：2 级联默认；高端：可试 3–4 级联但要控距离与投射集合。 22 2Point Light（点光）S/A（无阴影时可量产）支持（但限制多）移动端局部灯数量通常受“每物体/每像素最多影响灯数”限制，常见上限 4；默认不建议开点光阴影。 21 24 2Forward：多为“附加局部灯”在 BasePass/ForwardLighting 里做简化计算；Deferred：可承担更多点光（历史上可到 8 movable 点光的演示）。 3 2GPU/Shader：每像素对每盏灯计算；overdraw：forward 下被遮挡的像素也可能算；CPU：灯影响的物体列表/批次。 15 25 2光照“断层”（超过灯数上限后灯不生效）、亮斑不稳定、不同机型衰减/高光差异。 21 24 26用 Emissive 假光、用 Light Function/IES（若目标路径支持）做“少灯更像”；把点光改成聚光（阴影成本通常更低）。 1 15 24低端：尽量 0–1 个近景点光；中端：控制在 2–4 且无阴影；高端：可考虑移动 Deferred + 更多点光但要控半径与叠加区。 21 3 25Spot Light（聚光）S/A（有阴影时谨慎）支持UE 在移动端支持聚光照明；移动端“可动聚光阴影”历史上默认关闭，需要显式开启相关 cvar/项目设置；并受“可见阴影聚光数上限”约束。 3 2 13Spot shadow 通常用 2D shadow map；在移动端实现会把聚光阴影与 CSM 共享采样/图集等资源以省开销。 2 3ShadowDepths：额外 shadow map 渲染；带宽：额外阴影采样；shader：聚光衰减+阴影过滤；CPU：受影响物体/批次。 15 3 2设备上不出阴影（开关/Quality/Permutation 问题）、阴影糊/抖、阴影被 atlas 挤压导致分辨率变化。 3 2 23用无阴影聚光 + 烘焙阴影贴图/Decal/顶点 AO；用少量“关键聚光”强调主体，其余用 Emissive/反射捕捉器补观感。 2 1 3低端：基本不做聚光阴影；中端：允许 1–2 个关键聚光（尽量不投影或低分辨率）；高端：可做少量聚光阴影但要做设备分级。 3 6 15Sky Light（天光）S支持常与 Reflection Capture/Reflection Environment 共同决定环境镜面；Stationary/Static 更常见，移动端通常依赖捕捉的 cubemap。 3 23Skylight 贡献 IBL（漫反射环境光 + 镜面环境反射）；移动端反射多来自 cubemap（Skylight/Reflection Capture），并可能混合 Lightmap。 3 2带宽/采样：环境 cubemap 采样；shader：BRDF 计算；内存：cubemap 分辨率/数量。 2 3“画面发灰/没方向感”、室内漏光（Skylight 过强）、反射不对（捕捉器布置/材质粗糙度问题）。 27 2 28用烘焙 AO/顶点 AO 强化接触；用分区 Reflection Capture；室内用局部 cubemap/盒体捕捉器。 27 2 3低端：Skylight 静态 + 低分辨率反射；中端：合理分区捕捉；高端：可配合移动 SSR 做局部提升。 3 1 2Rect Light / Area Light（矩形区域光）S（5.5 起明确支持）/A（需谨慎验证）支持（UE 5.5 明确列为 Mobile Forward 新增能力）UE 5.5 官方说明 Mobile Forward 支持“rectangular area lights”。注意：这不等价于“桌面端所有 area light 特性全量”，通常仍需做设备验证与材质/阴影策略收敛。 1多数实时引擎里的“Rect Light”本质是对面光源的近似（影响高光形状/衰减），而非真正面面积积分；移动端会更偏近似以控成本。 1 3shader：更复杂的高光形状/采样；若叠加阴影/体积雾，成本迅速上升；overdraw：forward 下被遮挡像素也可能算。 25 5 1画面“高光很怪/过亮”，不同机型高光一致性差；在低端机上可能造成明显掉帧或被 device profile 直接降级。 26 6 1如果目标只是“窗户亮边/软高光”：用 emissive + 反射 cubemap；用条形灯贴图（Decal）或高光假表面。 2 1低端：尽量不用；中端：只放 1–2 个关键 rect light；高端：可用但要严格 profiling。 6 15 1Emissive 作为假光照S支持（材质级）需要材质管线允许 Emissive；移动端常用 Unlit/Emissive“画亮”，并用后处理 Bloom 提升“像在发光”。 2 1不参与真实光照传递，但可以“直接把颜色加到最终结果”并配合 Bloom/Color Grading 形成观感；也可做假高光/边缘光。 2 1很便宜：主要是材质自身的 ALU/采样；若 Bloom/TAA 开启则把成本转移到后处理。 3 6“像贴图在发光但不照亮场景”、夜景缺少间接光感觉；过强时会曝光/色调映射失控。 2 6用烘焙（把发光贡献 bake 进 lightmap）、用局部假光（少量点/聚光无阴影）、用反射捕捉器增强镜面。 18 3 2低/中/高端都建议大量使用（是移动端的“画质性价比核心”）。 2 6Static / Stationary / Movable：移动端支持与差异（你必须会的“落地规则”）Static：直接光 + 间接光都进烘焙（Lightmap），运行时最省 GPU，但灵活性最低；移动端想要稳定 30/60fps，Static 通常是基本盘。 18 29 2Stationary：一般用于“灯不动但要影响动态物体一些效果”的折中；它比 Static 复杂得多（需要把静态部分与动态部分组合），社区回答也强调 Stationary 的实现复杂度更高。 24 3Movable：完全实时，移动端最贵；如果再叠加阴影，则成本通常呈“乘法上升”。 15 6 2一个非常工程化的结论： 在移动端，你要把 Movable 当作“稀缺资源”。把“光会动”当作玩法需求，而不是默认美术需求。Lightmass / 烘焙 / Lightmap / Indirect Lighting / AO：移动端最核心的“主力方案”Lightmap 是移动端“长期稳定画质”的底座：UE 文档摘要明确指出 Lightmap 主要用于“静态网格的 baked/precomputed lighting”。 18 Lightmass（Epic Games 体系内的预计算光照工具）负责生成：直射阴影 + 反弹光（GI/Indirect）+ AO（可选）等静态光照信息。 29 18方向性 Lightmap（Use Lightmap Directionality）：移动端可以选择是否让 Lightmap携带方向性（允许结合每像素法线获得更立体的凹凸感）。官方移动性能建议摘要明确说：开启会更真实但更贵；关闭会更“平”但更便宜。 21 2Volumetric Lightmaps（给动态物体“采样间接光”）：UE 的发布日志介绍了：Volumetric Lightmaps 用砖块与采样点覆盖 Lightmass Importance Volume，并在 GPU 上插值到任意位置，从而让角色/粒子等动态物体能获得低成本的预计算光照“落地感”。 19社区在“移动端不工作”的排查中也确认 Volumetric Lightmaps 在移动端可用，并解释了常见误解：它采样的是间接光颜色而不是静态阴影投影；如果你想让静态墙体阴影影响动态物体，需要用动态 CSM 阴影（例如把投影阴影的墙变成 Movable 以走 CSM）。 20 22压缩与内存：Lightmap 的压缩开关会显著影响包体与运行时内存。CPU Lightmass 文档摘要直接指出：禁用 lightmap 压缩可减少压缩伪影，但会让内存与磁盘体积增加约 4×。 18动态光、逐像素光、逐顶点光、附加光数量限制（移动端“最容易踩坑”的那条线）移动端的局部灯（点/聚光）通常有“数量上限”与“每物体可影响灯数上限”。典型案例：项目设置里很多人看到“最多 4 个 Movable 点/聚光影响”的限制，并在论坛询问是否能强推更高，说明这个限制在真实项目里非常常见。 21 24 在 UE 的移动端渲染拆解中也明确提到：Forward 路径下局部灯总数通常不能超过 4。 2为什么移动端局部灯贵：从底层解释Forward 下：每个像素在绘制每个物体时就要把“会影响它的灯”算一遍；如果还有 overdraw，被遮挡像素也会算，浪费非常直接。 25 5 2Deferred 下：把光照从“物体空间”转移到“屏幕空间”，可以更好地处理多灯（尤其是很多小灯），但以 GBuffer 的额外 RT/带宽为代价。 3 2 4移动端阴影类型与支持情况：CSM、距离场阴影、接触阴影、VSM 等这里先给“结论式地图”，后面在支持矩阵里再汇总：CSM（Cascaded Shadow Maps）：移动端方向光动态阴影主力。移动端可调“级联数上限”，默认常为 2；并且“Far Cascades + Dynamic Cascades 的总数不能超过移动端 MaxCascades 设置”。 22 2按物体接收 CSM 的开关：receive_mobile_csm_shadows（移动端专用）可以让某些组件不接收 CSM，从而减少着色成本。 22Spotlight 阴影（移动端）：历史上“可动聚光阴影”需要显式开启，且受可见数量/分辨率/atlas 约束。 3 2Point Light 阴影（移动端）：在移动端渲染拆解中给出很直接的原因：点光阴影需要立方体阴影贴图，而“一次 pass 渲染立方体阴影”的技术需要桌面级特性（例如几何着色器/SM5 能力），因此移动端默认不支持/不推荐。 2胶囊阴影（Capsule Shadows）：UE 5.5 官方明确把它列为 Mobile Forward 新增支持能力之一（常用于角色、性能相对可控）。 1虚拟阴影贴图 VSM（Virtual Shadow Maps）：它是面向“电影级资产、开放世界、大量动态阴影与 Nanite”设计的桌面体系；且往往与更高 Shader Model/桌面渲染路径绑定。移动量产中通常不把它当默认选项，而是视作“桌面渲染器/高端实验路径”的内容。 16 17移动端反射体系4. 把“反射”拆开逐项讲（重点）先把“移动端反射”分成三类输入源移动端反射几乎总是以下三类混合（区别在“真实度 vs 成本 vs 覆盖范围”）：1) 反射捕捉器（Reflection Capture）/反射环境（Reflection Environment）：离线/静态 cubemap 为主，成本可控，是量产基石。 27 2 3 2) 屏幕空间反射（SSR / 移动端 SSR / PPR）：只反射“屏幕里看得到的东西”，质量提升明显但有缺失与稳定性问题；UE 5.5 明确宣布移动端 SSR 在 Mobile Forward 与 Deferred 均可用。 1 3 11 3) 真实平面反射（Planar Reflection）/ SceneCapture 类：画质强但通常意味着“再渲染一次场景”或额外 RT，移动端很容易爆。 3 15 28反射方案对照表（支持、原理、代价、风险、项目适配）同样用 S/A/B/C 四级标记。反射方案量产级别是否支持支持条件（常见硬门槛）原理（移动端视角）主要性能消耗点画质优缺点兼容性风险替代方案推荐项目类型Reflection Capture（反射捕捉器）S支持放置 Sphere/Box Capture；移动端可启用“捕捉器压缩”以节省内存/带宽（默认可用 ETC2 等）。 2 28预先捕捉环境为 cubemap，运行时按材质粗糙度/法线做 IBL 镜面反射；移动端常为“每物体取最近捕捉器”，可能缺少视差校正。 27 2 3内存：cubemap 分辨率×数量；带宽：像素采样 cubemap；加载：反射贴图驻留。 2 8优点：稳定、覆盖全局、成本可控；缺点：与真实几何不一致、动态物体不更新、室内外过渡需要精心布置。 27 2不同机型纹理格式/压缩差异、某些 GPU（如 Mali + OpenGL）可能出现反射异常案例需要专项测试。 26 14结合 Skylight；增加 capture 分区；在关键平面叠加移动 SSR/PPR。 3 1全类型项目（写实/二次元/休闲）都应作为默认底座。 1 2Sphere / Box Reflection CaptureS支持Sphere 更适合局部、Box 更适合室内盒状空间；需要按房间/走廊分区布置，避免跨区污染。 27 2同上，但用不同形状定义影响范围；移动端由于可能缺少视差校正，Box 的“空间感”不一定等于桌面。 27 3同 Reflection Capture（内存/带宽/采样）。 8 2常见问题：墙角反射“漂”、房间切换时反射跳变、金属过强导致“像镜子贴图”。 27 2 28设备 profile 造成分辨率/压缩差；capture 过多导致内存上涨。 2 18用更少 capture + 更好的材质粗糙度分布；关键镜面平面用 PPR/Planar。 11 1 28室内、角色换装大厅、赛车车库等“局部环境反射强”的场景。 27 2Reflection Environment（反射环境）S支持依赖 Reflection Capture 与 Skylight；移动 deferred 可支持更高级的“按像素 blending”。 3 2把捕捉器反射作为环境镜面项参与 BRDF；高端路径可做多 cubemap 混合提高过渡质量。 3主要还是 cubemap 的内存/带宽。 8 2优点：稳定；缺点：动态遮挡/动态物体反射缺失。 27 3Forward vs Deferred 的一致性差；OpenGL ES 下高级 blending/数组纹理能力可能受限。 3 13 14用移动 SSR 做局部补足；对二次元项目可用“高光+环境贴图”解决大多数需求。 1 2大多数项目默认开启（写实越依赖）。 1 2Skylight 对反射的影响S支持Skylight 捕捉/指定 cubemap；移动 deferred 明确支持“Skylight support for reflection”。 3 2Skylight 的 cubemap 是反射的重要来源，尤其当场景 capture 不够密时。 3 27cubemap 采样与 BRDF；间接影响曝光/色调映射。 2 6室内泛蓝/泛灰、反射过统一、金属“脏”。 2 27设备差异相对小，但需要做室内外分区与不同 capture。 27 2室内用 Box Capture 强化；写实项目配合轻量 SSR。 1 11全类型项目。 2SSR（移动端 SSR）A（高端机量产可用，需分级）UE 5.5 起支持UE 5.5 官方确认“SSR now work in both Mobile Forward and Deferred”；通常要求 Mobile HDR，且移动端 SSR 很多实现依赖 TAA 稳定画面。 1 30 2屏幕空间基于 SceneColor/Depth（以及可能的法线/粗糙）追踪反射；移动端常用更受限/更高性价比的形式（例如 PPR）。 11 3 2带宽：读 SceneColor/Depth/可能的中间 buffer；shader/compute：追踪/投影/修补孔洞；分辨率：越高越贵。 11 3 8优点：细节真实、接触反射自然；缺点：屏幕外缺失、边缘断裂、法线粗糙导致噪点/闪烁。 11 31不同 GPU 对精度/半精度、纹理格式、TAA 链路影响大；需要 device profile 分级与真机覆盖。 6 2 26反射捕捉器 + 关键平面 PPR；对低端直接关闭 SSR，用材质/后处理假高光。 2 11 1写实室内/车漆/水面等“需要接触反射”的项目（高端机优先）。 1 3Planar Reflection（真实平面反射：再渲染一次）B/C理论可行但很贵平面反射通常意味着“按反射视角再渲染一次场景”；在移动端容易直接把 GPU/带宽打满。 3 15 28用反射相机渲染反射纹理，再投射到平面；画质接近镜子。 3最贵点：额外一次（或近似一次）场景渲染；额外 RT；额外带宽；额外过度绘制。 3 5优点：镜面正确且屏幕外也有；缺点：成本爆、容易掉帧/发热。 3 6机型差异极大；一旦动态物体多就更难控。 6 15用 PPR/移动 SSR 替代；或用静态 cubemap 伪反射。 11 2 3仅极少数“镜子是核心玩法/卖点”的高端机项目或 Demo。 6 3Pixel Projected Reflection（PPR）/ MobilePPRA（强烈建议在“平面反射需求”里优先于 Planar）支持（移动友好反射）需要 Mobile HDR；通过 cvar 控制 r.Mobile.PlanarReflectionMode 与 r.Mobile.PixelProjectedReflectionQuality；并依赖特定 Planar Reflection Mode（MobilePPR 等）。 3 2 28PPR 是受限的 SSR：把反射限定在“平面/解析反射区域”，用“反向投影/数据散射”替代昂贵的逐像素 ray-march，获得更好的性能/质量比。 11 3 2主要成本：额外中间 buffer + 投影 pass + 反射 pass；仍是屏幕空间，成本随分辨率上升。 11 3 8优点：比全 SSR 更稳、更便宜；缺点：只适合平面、对粗糙/非平面有限制、仍会有缺失/孔洞。 11 3依赖 Mobile HDR 与设备性能分级；项目设置/模式不对时“看起来像没反射”。 28 2 6反射捕捉器作为全局底，PPR 只用在水面/地面/镜面地砖等关键平面。 11 27 2写实室内地面、水面、车展台等“可用平面近似”的项目。 3 11Scene Capture Cube / 2D（动态捕捉）C基本不量产（移动端动态更新非常贵）本质是额外相机渲染；Cube 等于 6 次（或近似 6 次）视角渲染。 3 15运行时更新反射贴图/RT。超高 GPU + 带宽：多次渲染 + RT；还可能引入同步/依赖。 5 15优点：动态真实；缺点：基本不可控。兼容性不仅是机型，还常常是“发热后必崩”。 6 15改为离线 bake cubemap；或用反射捕捉器静态。 27 2仅 Demo/少量展示镜面物体的高端体验。Cubemap 伪反射（材质采样）S支持只要材质允许采样 cubemap；常用于“车漆/金属/玻璃”的稳定反射观感。 2 27把环境 cubemap 当作镜面近似输入，结合粗糙度/法线做 BRDF。 2 3便宜：主要是 cubemap 采样；成本随采样次数/LOD 变化。 8 2容易“假”，反射与场景不一致；但可通过粗糙度与艺术调参掩盖。 27 2风险小、兼容性好。与反射捕捉器/Skylight 一起使用。 27 2全类型项目（尤其二次元/风格化）。Lumen / 硬件光追 / 软件光追 / RT Reflections 与移动端B/C（视为实验/高端特化）UE 文档摘要：移动端 Lumen 为 Experimental（高端 Android）“Using Lumen on Mobile”摘要表述：UE 5.4+ 在高端 Android 设备上提供 Lumen 实验支持；“Lumen Technical Details”摘要也写到 Lumen 支持 Android Vulkan 并可用于 Mobile Renderer。 9 10Lumen 是动态 GI/反射体系，通常依赖更高计算/带宽与更复杂的场景表示；移动端仅在特定高端条件下尝试。 9 10很昂贵：计算 + 带宽 + 内存；且对稳定帧率/热降频非常敏感。 6 8 9画质上限高，但移动端往往要降质量/降距离；也可能遇到“看起来没 GI/没反射”的配置坑。 9兼容性风险大（设备、驱动、API、机型分级）。 9 6 26量产建议：以烘焙 GI + 反射捕捉器 +（可选）PPR/SSR 为主。 2 1 3仅高端写实、且愿意做严格设备白名单/动态调度的项目。 6 9“哪些反射方案适合量产、哪些只适合演示”的一句话结论量产默认组合：Reflection Capture（分区） + Skylight + 材质正确的粗糙度/金属度分布。 27 2 3高端机增强：在关键平面启用 PPR/移动 SSR 做接触反射提升（并用 device profile 分级）。 1 11 2演示/非量产常见坑：动态 SceneCapture（尤其 Cube）、全场景 Planar Reflection、多处镜面大面积实时反射。 3 15 6材质与观感补偿5. 材质与灯光/反射的关系（反射重点）BaseColor / Metallic / Roughness / Specular / Normal / AO：对移动端光照的实际影响移动端的光照与反射很大一部分由“材质参数”决定，尤其是：Metallic 与 Roughness：决定你看见的是“漫反射”为主还是“镜面反射”为主，以及反射的清晰度/能量分配。反射捕捉器/Skylight/SSR 的观感差异，往往首先是粗糙度分布差异。 2 3 27Normal：决定高光与反射的细节破碎度；移动端若关闭方向性 Lightmap，会让法线在“纯烘焙环境光”下显得更平。 21 2AO：在移动端常被用作“廉价的接触感与间接遮蔽补偿”；因为真实 SSAO/GTAO 要额外屏幕空间计算。 3 2Emissive：不照亮环境，但可与 Bloom/色调映射结合，成为移动端“灯光感”的性价比核心。 1 2为什么很多“反射问题”本质上是材质问题（重点）移动端最常见的“反射看起来不对”，通常不是反射系统坏了，而是：1) 粗糙度过低/过统一： 把大量表面做成“接近 0 粗糙度”的镜面，会把反射捕捉器的缺陷、SSR 的缺失边界、cubemap 的不一致全部暴露。先把 roughness 做出合理分布，反射质量会“立刻像是提升了一档”。（这是工程经验，但它与“捕捉器反射是近似、屏幕空间反射有缺失”这两个事实强相关。） 27 11 2 2) 法线尺度/细节频率不对： 过强法线会导致高光碎裂、SSR/PPR 更噪；过弱法线让反射像贴图。 11 2 3) 金属度误用（把非金属当金属）： 常见于新手：把塑料/木头做成金属，导致过强镜面能量，反射“假得很明显”。（PBR 误用会把移动端近似反射的缺陷放大。） 2 27 4) 材质路径不兼容导致“移动端 fallback”： 移动端材质有功能限制（例如纹理采样器数量、部分表达式/着色模型不可用），一旦触发 fallback，观感可能与 PC 预览差很多。移动渲染分析里明确提到 ES3.1 下纹理采样器数量有限、可用着色模型更少。 2移动端材质性能：指令数、采样数、分支、透明、双面、Clear Coat 等移动端成本一般优先看“fragment shader 是否被带宽喂不饱”：Arm 的 fragment shader 优化建议明确指出：纹理会消耗大量内存带宽，带宽过量会导致 fragment shader 无法获得足够数据。 8透明/Masked 材质在移动端非常贵（overdraw + late-z + blending），移动渲染分析也强调透明/Masked “极其耗性能，建议尽量不透明”。 2 5采样器数量：移动端硬件限制会让你在复杂材质上更容易踩到上限（例如 ES3.1 常见限制 16 个采样器）。 2常见工程策略： 用“Fully Rough / 降精度 / 合并贴图通道 / 减少层数 / 避免依赖纹理读取（dependent texture read）”来换稳定帧率。 2 8 5PBR 在移动端的常见误区（专门列出）误区：把“写实”理解成“到处镜面”。正确做法是粗糙度做分布、金属度做物理正确，否则反射系统的近似会被放大成“假”。 27 2误区：用大量实时灯去补材质问题。移动端局部灯昂贵且受上限约束，先把材质、烘焙与反射捕捉器做对，再用少量灯点睛。 21 15 2误区：透明材质想要像桌面一样“又便宜又好”。透明在移动端基本是“成本炸弹”，且反射/灯光模型可能受限（例如需要特殊的 Lighting Mode 才能在某些平台生效）。 28 2 5如何用材质假装更高级的灯光或反射效果用 Emissive + Fresnel 做边缘光/假高光；再用 Bloom 把“亮”扩散成“在发光”。 1 2用“局部环境贴图（局部 cubemap）”叠加在关键材质上（例如车漆/玻璃），绕开 SSR 的缺失与捕捉器过渡问题。 27 2对二次元/风格化：把“高光形状”当作美术贴图/曲线控制，而不是追求物理正确反射，这在移动端通常更稳定、更便宜。 2 86. 后处理与观感补偿Bloom、Tonemapper、Exposure、Color Grading、AA、屏幕百分比：如何影响灯光/反射观感UE 5.5 官方移动更新中提到 Mobile Forward 新增的“更高画质特性”，很多都需要配合 Mobile HDR/后处理链才能真正体现在观感上（例如 Bloom 对 Emissive 的“发光感”贡献非常大）。 1 2PPR/移动 SSR 这类屏幕空间反射，本质上与 SceneColor/Depth 强耦合，后处理链（尤其 TAA）会影响稳定性与噪点/闪烁。社区关于移动屏幕空间反射的讨论也明确提到“移动 SSR 需要 TAA”的设定提示。 30 11 3曝光（Auto Exposure）/色调映射（Tonemapper）会决定高光是否“糊成一团”或“亮不起来”；移动端若关闭某些后处理，会直接改变灯光层次感。 2 6哪些后处理移动端性价比高，哪些代价大（工程口径）高性价比（常用）：Bloom（适度）、Color Grading（LUT/轻量参数）、基础 Tonemapper。它们对“氛围感、发光感、整体色彩统一性”的提升很直接。 1 2代价偏大（谨慎）：屏幕空间 AO（尤其 GTAO）、SSR/PPR（取决于质量档位与分辨率）、复杂 DOF、复杂体积效果叠加。移动端渲染分析明确指出：移动设备因 dependent texture read、带宽与 RT 解析等限制，后处理在移动上更耗甚至会“卡住渲染管线”。 2 5 8体积雾（Volumetric Fog）：UE 5.5 提到 Mobile Forward 支持体积雾，但它通常不应在低端机默认开启，而应做分级。 1 6当不能上真实反射/动态光时，如何补偿用烘焙 GI（Lightmass）解决“暗部发黑/没反弹光”的大问题。 18 29 2用反射捕捉器分区 + 材质合理粗糙度，解决“金属像塑料/反射空”的大问题。 27 2用 Emissive + Bloom/Color Grading，解决“灯光不够戏剧化/不够亮”的问题。 1 2用 PPR 在少数关键平面上提升“接触反射”，而不是全场景 Planar 反射。 11 3 6性能成本框架与支持矩阵7. 讲性能成本时的统一框架（含分级与底层原因）统一成本拆解：你在 Profiling 时要对应到“哪一类瓶颈”CPU 开销（偏 CPU 瓶颈）典型来源：Draw call 提交与状态切换、灯光影响物体列表构建、阴影投射体遍历。Arm 的 Vulkan vs OpenGL ES 对比强调：Vulkan 更低 CPU overhead，且多线程更友好；这在“多灯、多阴影、多 pass”项目里尤其关键。 7 14动态灯光阴影的 CPU 成本往往与“灯数 × 受影响物体数 × 物体分裂程度”相关；论坛给出公式化描述：CPU 侧 ~ NumberOfLights NumberOfMeshesAffected NumberOfSeparateObjects。 15GPU 计算开销（偏算力瓶颈）典型来源：逐像素光照（多灯叠加）、阴影过滤、SSR/PPR 追踪或投影计算。 25 11 2带宽开销（偏带宽瓶颈）典型来源：多 Render Target（GBuffer/后处理链）、高分辨率 ShadowMap、SceneColor/Depth 多次读写。Apple 强调“尽量减少系统内存流量”；Android AGI 文档也把 texture memory bandwidth 作为可观测瓶颈。 5 8 14Render Target / 深度 / GBuffer / Shadow Map（偏带宽 + 内存）Mobile Deferred 的 GBuffer 会增加 RT 数量与读写。 2 3ShadowMap（CSM/Spot shadow）对 RT 与带宽同样敏感，且阴影深度 pass 还会额外消耗几何处理。 15 22Shader Complexity（偏算力 + 带宽）Arm 指出纹理带宽与 fragment shader 性能强相关；移动渲染分析也强调 dependent texture read、采样器限制、半精度/全精度选择会直接影响性能与瑕疵风险。 8 2Overdraw（偏 overdraw 瓶颈）透明、粒子、UI 会直接把片段着色次数倍增；Apple 把“最小化 overdraw”作为核心最佳实践之一。 5 2纹理内存与反射贴图内存（偏内存瓶颈）Reflection Capture cubemap 数量与分辨率直接影响内存；Lightmap 压缩开关影响包体与运行时内存（禁用压缩可增到约 4×）。 2 18分辨率变化对反射/阴影成本的影响SSR/PPR/后处理按像素成本增长；CSM 的阴影分辨率与级联数增加也会增长。 11 22 2热降频与长时间运行Android 官方指出设备只能维持高性能有限时间，建议监控 thermal headroom 并主动降负载；这意味着你必须把“高端灯光/反射”当作“可动态降级”的质量档，而不是固定开关。 6 14功能成本分级（相对概念 + 为什么如此）说明：以下分级是“在移动端普遍场景下”的相对概念；真实成本仍取决于分辨率、材质复杂度、场景几何、灯影响范围、设备 GPU 与驱动。 6 8 15很便宜：Emissive（不带复杂采样）、静态反射捕捉器采样（少量）、静态 Lightmap（压缩开启、分辨率合理）。 2 18 27较便宜：1 盏无阴影 Directional（仅直接光）、少量无阴影局部灯（≤4，半径小）、基础 Bloom/Color Grading。 21 1 2中等：Directional + 1–2 级联 CSM（距离短）、少量聚光阴影（数量极少）、PPR 低质量档（关键平面）、移动 Deferred（但控制 GBuffer 与后处理）。 22 3 11 13昂贵：3–4 级联 CSM、多个阴影聚光、移动 SSR（较高质量/高分辨率）、体积雾叠加、多 RT 后处理链。 22 1 11 6很昂贵：真实 Planar 反射（再渲染一次）、动态 SceneCapture（特别是 Cube）、移动端 Lumen（实验/高端）、桌面渲染器跑在手机上（实验路径）。 3 9 168. 支持矩阵（灯光/阴影/反射/后处理汇总）说明：矩阵以“移动渲染器（Mobile Forward/Deferred）”为主要对象；“Desktop Renderer on Mobile”单独列为实验路径。 1 3功能名移动端是否支持是否推荐量产主要代价（偏向）主要风险推荐替代方案适合项目类型低/中/高端建议平行光 Directional支持推荐CSM/阴影贴图（GPU+带宽）阴影锯齿/漏光/距离断层烘焙远景 + 近景 CSM；关闭部分物体接收 CSM全类型低端 1 级联；中端 2 级联；高端 3–4 但控距离 22 2CSM（移动）支持推荐（但要控）ShadowDepths（GPU）+ 采样（带宽）cascades 超限/距离不当降级级联数/距离；烘焙阴影写实/中重度项目低端短距离低级联 22 2Receive Mobile CSMShadows（按组件）支持强烈推荐（优化手段）降低 shading cost（省 GPU）关闭后“看起来没影”只关次要物体全类型全档位建议用 22点光 Point（无阴影）支持（受上限）推荐（少量）每像素多灯（GPU/overdraw）超上限灯不生效Emissive 假光；改聚光多数项目低端尽量不用；中端≤4 21 2点光阴影通常不支持/不推荐不推荐立方体阴影贴图（极贵）基本不可控用方向光 CSM/聚光阴影/烘焙少数 Demo不建议 2 15聚光 Spot（无阴影）支持（受上限）推荐（少量）每像素光照（GPU）超上限/Shader permutationEmissive/Decal 假投影室内/舞台光低端少用 2 21聚光阴影（Movable）支持（需开启）谨慎ShadowDepths + 采样（GPU+带宽）配置/质量档导致不出阴影降阴影数/分辨率；改烘焙写实重点灯高端机分级 3 2Skylight支持推荐cubemap 采样（带宽/内存）室内漏光/发灰分区 capture；烘焙 AO全类型全档位建议 3 27Rect Light（5.5 Mobile Forward 新增）支持（5.5 明确）中高端谨慎量产shader 更重（GPU）设备差异/发热掉帧emissive + cubemap写实/展示低端避免 1 6Capsule Shadows（5.5 Mobile Forward 新增）支持（5.5 明确）推荐（角色类）较省（相对）形体逼真度有限贴花/顶点 AO角色主导项目低中端也可 1Lightmass Lightmap（静态）支持强烈推荐包体/内存（带宽/内存）UV/压缩伪影/漏光合理密度；必要时关压缩（代价大）全类型全档位核心 18 29Use Lightmap Directionality支持按项目取舍更贵但更立体（GPU）低端压力关闭方向性 + 材质补偿风格化/低端低端可关 21 2Volumetric Lightmaps支持推荐（有动态角色时）内存/插值（中等）误解导致“看似没效果”配合 CSM/烘焙角色/动态物体中高端更适配 19 20Reflection Capture（含压缩）支持强烈推荐cubemap 内存/带宽布置不当跳变分区布置/压缩全类型全档位核心 2 28Mobile SSR（5.5 打通）支持（5.5）高端分级推荐带宽+计算（昂贵）缺失/闪烁/精度差PPR/反射捕捉器写实/车漆/水低端关 1 11 30PPR（Pixel Projected Reflection）支持推荐用于平面中等到昂贵（屏幕空间）开关/模式不对无效capture + 仅关键平面 PPR水面/地面中高端 3 11 28Planar Reflection（真实再渲染）理论可行不推荐很昂贵（额外渲染）掉帧/发热PPR/SSRDemo基本避免 3 6后处理链（Mobile HDR）支持推荐但要克制带宽/RT（中等到昂贵）低端掉帧只保留 Bloom/ColorGrading全类型低端精简 2 5Lumen（移动端）Experimental不推荐常规量产很昂贵（全链路）兼容性与稳定性风险大烘焙 + capture + PPR高端写实特化白名单+动态降级 9 6Desktop Renderer on Mobile（Vulkan Desktop 等）Experimental不推荐很昂贵/不为移动优化包体/兼容/性能风险用 Mobile Forward/DeferredDemo/实验极少数高端 3 16项目决策、排错、优化与学习路径9. 项目决策树（简版，但可直接落地）写实项目目标：稳定 30fps（广覆盖） 选择：Mobile Forward + 静态 Lightmass（高质量烘焙）+ Reflection Capture 分区 + 少量无阴影局部灯 + 近景短距离 1–2 级联 CSM。 2 22 18目标：45–60fps（中高端） 在上面基础上：关键平面加 PPR；或在高端机开移动 SSR（必须 device profile 分级 + TAA 与质量档联动）。 1 11 6二次元/风格化项目方向：减少真实动态光依赖，把“体积感/高光形状/边缘光”交给材质与后处理（Emissive + Fresnel + Bloom）。灯光：1 个 Directional（可无阴影或短距离阴影）+ 合理 Skylight + 捕捉器；局部灯尽量少、尽量不投影。 2 1俯视角项目俯视角天然利好：遮挡关系稳定、镜头不易贴地，CSM 距离可压得很短；反射多用 capture 即可。 22 27开放世界项目关键矛盾：阴影覆盖距离与稳定性。建议：Directional + 分级 CSM（低端短距离、少级联；高端更远一些但控投射集合）；远景依赖烘焙/贴图阴影；尽量避免多处实时反射。 22 15 6室内场景项目“反射捕捉器分区”是底座：Box Capture 按房间/走廊布置；关键地面/水面用 PPR。 27 11 2竞速/射击/卡牌/休闲分类（快速策略）竞速（车漆反射敏感）：capture + 高端 SSR/PPR 分级；局部灯少而精。 1 11射击（性能/稳定优先）：Directional + CSM 短距离；大量 emissive/贴花/后处理做氛围。 22 2卡牌/休闲（成本最敏感）：尽量 Unlit/简化 Lit；以烘焙与材质假光为主。 2 810. 排错手册（≥30 条，表格）说明：下表覆盖灯光、阴影、反射、材质、配置、平台差异与驱动差异。验证步骤优先用“真机 + profile + 逐项关开 + 设备分级”。 6 2 26 28 22现象根因归类如何验证如何修复如何避免再次出现手机上反射几乎没有/很弱配置/反射捕捉器缺失场景里是否放了 Reflection Capture；材质金属/粗糙是否合理布置 Sphere/Box Capture；合理粗糙度；启用 capture 压缩制作规范：每个室内区必须有 capture；材质审核粗糙度分布 27 2预览有反射，打包到 Android 没反射平台差异/配置真机查 Mobile HDR、Planar Reflection Mode、相关 cvar开启 Mobile HDR；设置 MobilePPR；设置 r.Mobile.PixelProjectedReflectionQuality>0用 device profile 固化开关；真机回归测试 28 2 3PPR/Planar Reflection 在 iOS/Android ES3.1 不工作配置/材质模式检查材质透明 Lighting Mode；检查 Mobile HDR调整水材质 Lighting Mode（必要时用更昂贵的模式）；确认 Mobile HDR建立“水材质移动端模板”并锁定参数 28 6反射边缘断裂/屏幕外缺失功能限制（SSR/PPR）观察物体离开屏幕后反射是否消失用 capture 兜底；限制 SSR 只用于关键区域；提高粗糙度掩盖默认反射以 capture 为主，SSR/PPR 只加分 11 27某些 Mali + OpenGL 机型反射异常/破碎驱动/GPU 差异只在特定 GPU/特定 API 复现；切 Vulkan 对比优先 Vulkan；对该机型降级反射（关闭 SSR/降低 cubemap 分辨率）建立 GPU 白名单/黑名单；上线前覆盖 Mali/Adreno/Apple 26 14 6烘焙后物体发黑资源/Lightmap UV/密度Lightmap Density/Lighting Static Mesh Info 检查修 UV2；提高关键物体 Lightmap 分辨率；检查光照重要体积资源规范：UV2 无重叠、合理留白、密度分级 18 32烘焙阴影有明显块状压缩伪影压缩设置/带宽取舍对比开启/关闭 Compress Lightmaps关键场景可禁用压缩（代价：内存/包体约 4×）；或提高分辨率+优化 UV默认开启压缩；只对极少数关键地图禁用并单独评估 18动态角色在静态阴影下“漂浮”功能限制（静态阴影不投到 Movable）查看 Volumetric Lightmap 是否开启；角色是否只采样间接光用 Volumetric Lightmaps；或用 CSM 给角色接收/投射近景阴影标准做法：动态角色必须用 VLM + 近景 CSM 19 20 22Volumetric Lightmap 看起来“没效果”常见误解观察间接光颜色是否变化，而不是静态阴影投影正确认知：VLM 提供间接光；静态阴影不影响 Movable培训规范：区分“间接光采样”与“阴影投射” 20 19动态光在手机上不生效配置/灯数上限检查是否超过移动端局部灯上限；查看 Project Settings 动态灯数降低局部灯数量；缩小半径；改用 emissive/烘焙灯光规范：局部灯必须有预算表与半径上限 21 2点光/聚光超过 4 个时部分灯“消失”功能限制（上限）把灯逐个禁用，观察是否轮换控制在上限内；改用移动 deferred（高端）；合并灯/用 emissive设计阶段就锁定“每屏幕最大灯数” 21 2阴影锯齿严重分辨率/级联/滤波调整 CSM 级联数、距离、分辨率；看 aliasing缩短阴影距离；提高关键级联分辨率；减少级联数但提高近景质量根据镜头设定“阴影距离预算” 22 23阴影闪烁/爬行精度/级联切换/偏移观察相机移动时阴影边缘抖动调整 shadow bias；优化级联分布；减少远距离阴影统一项目 shadow bias 标准；锁定移动端质量档 22 2远处阴影突然消失配置（CSM distance）对比不同 shadow distance（Mobile）增加动态阴影距离（谨慎）；或改为烘焙远景阴影镜头设计阶段定“阴影最远可见距离” 23 22Far CSM 不生效配置（MaxCascades）检查 Num Dynamic Cascades + Far Cascades 是否超 MaxCascades提高移动端 MaxCascades 或减少总 cascades统一 shadow cascade 预算与上限 22某些物体接收阴影成本过高过度接收 CSM开启/关闭 receive_mobile_csm_shadows 对比 GPU对小物体/远景关闭接收 CSM资源导入规则：小物体默认不接收 CSM 22聚光阴影在移动端不出配置（Permutation/开关）检查是否开启 movable spotlight shadow 相关设置开启相应 cvar/项目设置；控制可见数量建立“移动端灯光开关清单”并锁版本 3 2水面反射在编辑器有，手机上没有配置/材质/模式检查 Mobile HDR、Planar Reflection Mode、材质透明 Lighting Mode按移动端要求设置；必要时改材质 Lighting Mode移动水材质模板 + 真机验证 28 2透明材质反射表现差功能限制/透明成本对比不透明版本；查看透明 Lighting Mode尽量用不透明假玻璃；必要时使用更贵的透明模式并限用UI/特效规范：透明层数预算、限制大面积透明 28 5 2Android 与 iOS 观感差别大平台差异（TBDR/精度/纹理格式）同一场景跨平台截图对比；检查半精度模拟统一色调映射与后处理；对 Android 做 device profile 分级建立“Android Vulkan/Android GLES/iOS Metal”对照测试矩阵 5 14 2Mobile Previewer 与真机不一致工具限制以真机为准；对比 Mobile Previewer“指定 device profile”用 UE 5.5 的 Android profile capture/半精度模拟功能辅助，但最终以真机验收流程上规定“预览仅用于迭代，验收必须真机” 1开启 SSR 后明显掉帧带宽/屏幕空间计算GPU profile 看后处理/反射 pass降 SSR 质量/分辨率；只在高端机开启SSR 默认只在高端档位启用 11 6 1SSR 噪点/闪烁严重TAA/精度/粗糙度观察是否随帧抖动；检查是否启用 TAA启用 TAA（若移动 SSR 依赖）；提高 roughness；降低强镜面面积材质规范：移动端避免大面积粗糙度=0 的镜面 30 11 27PPR 看起来“像没开”配置/模式检查 r.Mobile.PlanarReflectionMode 与 r.Mobile.PixelProjectedReflectionQuality设置正确模式（MobilePPR 等），并确保 Mobile HDR 开启把相关 cvar 固化到 DefaultEngine.ini + device profile 3 2 28反射 capture 过多导致内存上涨/加载慢内存瓶颈统计 capture 数量与分辨率；查看纹理内存合并 capture 区域；降低分辨率；开启压缩制作规范：capture 必须有“最大数量/分辨率”限制 2 8包体变大（Lightmap/反射贴图）资源/压缩策略对比关压缩/高分辨率 lightmap 的差异开启 lightmap 压缩；控制 lightmap 密度；压缩 capture资源审查：Lightmap Density + lightmap 统计 18 32发热后帧率明显下降热降频记录运行时间与 thermal headroom；观察掉帧与温度相关性动态降级：降分辨率/关 SSR/降阴影距离/降特效接入 Android Thermal API 思路做质量自适应（至少做“长跑测试”） 6 14Vulkan 比 OpenGL ES 更不稳定（闪退）驱动/设备差同机型切换 API 对比对问题机型回退 GLES；或做 Vulkan 白名单设备分级：Vulkan 默认、但保留 GLES fallback（或反之） 14 6OpenGL ES 画质功能缺失API 能力差异检查是否使用了需要 Vulkan/Metal 的特性对 GLES 降级：关移动 deferred/关高级反射从立项就决定 GLES 目标与功能上限 14 13 2移动 deferred 在 OpenGL 预览不工作工具/预览限制观察 Mobile Preview（OpenGL）是否无法显示 deferred不用 OpenGL 预览判定 deferred；以真机为准；需要时开启 r.Mobile.AllowDeferredShadingOpenGLQA 流程：deferred 必须 Vulkan/Metal 真机验证 4 13GI/间接光“看起来没了”配置/项目状态检查是否关闭 static lighting；检查烘焙是否有效重建光照；确认 lightmap/VLM；不要误指望移动端实时 GI项目策略：移动端 GI 默认走烘焙；实时 GI 仅实验高端 18 9 6场景太灰/没对比Skylight/曝光/色调映射调整曝光、Skylight 强度、Color Grading用局部对比 LUT；加 AO/顶点 AO；减少 Skylight 漏光美术规范：室内外分区 capture + 曝光策略 2 6 27高光异常（某些机型特别亮/特别暗）精度/驱动差异/材质同材质在不同 GPU 对比；切半精度/全精度对关键材质启用全精度；降低高光能量；做机型分级关键材质必须跨 Mali/Adreno/Apple 回归 2 6 26动态阴影不显示（打包后）配置/质量档检查 Directional 是否 Movable；检查动态阴影距离设置正确 mobility 与距离；检查 device profile 是否把阴影质量设为 0出包前真机 checklist：阴影开关、质量档、profile 全检查 23 22 6阴影成本突然升高投射体过多/级联过大GPU profile 看 ShadowDepths；统计投射网格减少投射体、简化阴影网格、缩短距离阴影投射体规范：树叶/小件默认不投射 15 22多灯导致 CPU 也卡CPU 侧灯-物体组合爆炸观察 RenderThread/GameThread 与灯相关开销减灯、减受影响物体、减对象分裂；批处理灯预算不仅看 GPU，也要看 CPU 组合规模 15 7反射 capture 切换时“跳变”明显布置问题走到 capture 边界观察跳变调整 capture 影响范围与重叠策略；移动 deferred 提升 blending（高端）关卡规范：capture 分区必须覆盖且边界在不显眼处 27 3车漆反射像“贴纸”材质粗糙/法线/捕捉器降低镜面强度、调整 roughness 纹理；检查 capture做真实 roughness 分布；加局部 capture；高端加 SSR车漆材质作为专项：必须真机标定 27 1 6夜景灯光“像没有照亮”功能限制（发光不照场景）关掉 post，看 emissive 是否只是贴图亮用烘焙把灯光贡献 bake 进 lightmap；少量真实灯补关键区域夜景策略：静态 GI + emissive + bloom 优先 18 1 2Lumen 在移动端无效/不一致实验性/兼容性检查是否 Android Vulkan 高端机；是否开启实验路径回退烘焙方案；只在白名单启用；提供开关不把 Lumen 作为移动端默认依赖 9 611. 优化清单（上线前“TA 检查表”）技术美术上线前检查项（建议你做成 Jira/飞书 checklist）渲染路径：Mobile Forward/Deferred 是否符合项目定位；Android 是否 Vulkan 为默认；OpenGL ES 是否作为 fallback。 14 13 1灯光预算：方向光是否唯一主光；CSM 级联数、距离、投射体集合是否在预算内。 22 15点/聚光数量是否不超过移动端上限；是否有“同屏最大影响灯数”规范。 21 2阴影预算：哪些物体必须接收 CSM，哪些必须关闭 receive_mobile_csm_shadows。 22反射预算：Reflection Capture 是否按室内外/房间分区；数量与分辨率是否受控；是否启用压缩。 28 27 8SSR/PPR 是否仅高端档位启用，且有 TAA/分辨率联动策略。 1 30 11烘焙与 Lightmap：Lightmap UV2 是否合格；密度是否分级；是否存在“禁用压缩导致内存暴涨”。 18 32材质规范：透明/Masked 是否严格限制层数与覆盖面积；采样器数量是否逼近上限；是否大量 dependent texture read。 2 8 5长时间稳定性：至少做 20–30 分钟真机“长跑”，记录帧率曲线与热状态；必要时做动态降级。 6 14设备分级建议（工程做法）低端：关 SSR/PPR、短距离 CSM、少局部灯、强烘焙、材质 Fully Rough 倾向。 2 22中端：少量 PPR（关键平面）、2 级联 CSM、局部灯 ≤4（无阴影）。 11 21 22高端：可尝试移动 SSR（谨慎）、更多后处理、少量聚光阴影，但必须受热策略约束。 1 6性能 profiling 流程（推荐顺序）1) 先确定是 CPU bound 还是 GPU bound（并锁定代表性场景）。 21 6 2) GPU bound：先看 ShadowDepths / 后处理 / 透明过度绘制 / SSR(PPR) / BasePass。 15 2 11 3) CPU bound：看 draw call、灯影响组合、阴影投射体遍历、PSO/管线创建（尤其 Vulkan）。 7 1 15 4) 热问题：用 Android Thermal API/头寸指标确认是否“热导致掉帧”。 612. 学习路径（入门→进阶→高级→项目实战）入门阶段（你应该在 1–2 周内掌握）必学知识点：Mobile Forward vs Mobile Deferred 的区别（为什么 deferred 更适合多灯，但更吃 RT/带宽）。 2 4Lightmap/Lightmass 的基本工作流：UV2、密度、重建光照、压缩影响。 18 32Reflection Capture 的布置与材质粗糙度对反射观感的决定性影响。 27 2容易混淆概念：“反射捕捉器”≠“实时反射”；“Emissive 发光”≠“照亮环境”。 27 1练习：做一个室内房间：只用静态光照 + capture 分区，目标是在低端机保持稳定帧率。 达到水平：能把“画面黑/灰/反射假”通过烘焙与材质解决到“可上线质量”。 29 2进阶阶段（你开始能做“分级与取舍”）必学知识点：CSM 级联数、距离、投射体与性能的关系；会用 receive_mobile_csm_shadows 做局部优化。 22局部灯上限与“为什么移动端多灯贵”的底层原因。 21 25 5PPR 基本原理与启用条件（Mobile HDR、PlanarReflectionMode、质量档）。 3 11 28练习：做一条走廊：2–3 个局部灯（无阴影）+ 1 个主方向光（短距离 CSM）+ 走廊分区 capture + 地面 PPR。 达到水平：能稳定完成“中端机 45fps/高端机 60fps”的画质分级策略。 6 14高级阶段（你能从“管线与硬件”解释并解决问题）必学知识点：带宽、RT load/store、overdraw、texture sampling 的硬件视角（TBDR、tile memory）。 5 8Vulkan vs OpenGL ES 的 CPU overhead 与稳定性取舍，如何做设备白名单/黑名单。 14 7 6真机热稳定：学会用 thermal headroom 判断“为什么 10 分钟后掉帧”。 6练习：做一套 device profile：低/中/高三档，动态调整阴影距离、SSR 开关、分辨率比例。 达到水平：能指出问题更偏：算力/带宽/内存/overdraw/shader，并给出可执行的降级路径。 6 8项目实战阶段（你能带团队上线）必学知识点：“灯光与反射预算表”制度化：每关/每镜头/每玩法场景有明确预算。 15 6资产制作规范：Lightmap UV、材质层数、透明叠层、capture 布置规范。 32 2 27回归测试体系：跨 GPU/跨 API/长跑热测试。 6 26 14练习：选一个真实关卡，做“从 PC 观感迁移到移动端”的完整落地：保留核心卖点，明确砍掉哪些特性。 9 1 达到水平：能在立项阶段给出“可上线的灯光与反射方案”，并在中后期把风险收敛到可控范围。移动端灯光与反射设计总原则1) 移动端永远先定“默认底座”：静态 Lightmass（含 VLM）+ 反射捕捉器分区 + 合理材质粗糙度，是最稳的量产骨架。 18 19 27 2 2) 把实时当作“高端增强层”，而不是默认层：移动 SSR/PPR、聚光阴影、体积雾、矩形区域光，都应做 device profile 分级，并默认可被热策略降级。 1 6 3 3) 每个“贵”都能追到一个底层原因：多灯 → forward 下 overdraw 乘法；deferred 下 GBuffer/带宽上升。 25 4阴影 → ShadowDepths 额外几何 + 采样带宽。 15 22反射 → cubemap 内存/带宽；SSR/PPR 读写 SceneColor/Depth。 8 11 274) 先修材质，再谈反射：一半以上的“反射问题”是粗糙度/金属度/法线/透明模式导致的“近似被放大”。 27 2 28 5) 用最少的阴影达成最大叙事：Directional CSM 只给玩家近景信息；远处阴影交给烘焙、贴图、AO、构图与色调。 22 18 2 6) 把“长时间稳定帧率”当成第一 KPI：移动端性能不是一次跑满，而是持续不掉；热降频是移动上线的真实敌人。 6 14 7) 对高端实验路径保持清醒： 移动端 Lumen、桌面渲染器跑手机、SM6 相关路线（Nanite/VSM 等）要么是 Experimental，要么依赖特定设备与修改；不要把它们当作移动量产的默认依赖。 9 16 17 8) 最终以真机为准：UE 5.5 虽增强了 Mobile Previewer，但移动端依旧是“设备/驱动/精度/格式”的组合问题；项目流程必须以真机覆盖与 device profile 分级收敛风险。 1 6 26重建参考链接Unreal Engine 5.5 is now available剖析虚幻渲染体系（12）- 移动端专题 Part 1（UE移动端渲染）Mobile Rendering and Shading Modes for Unreal EngineUsing the Mobile Deferred Shading Mode in Unreal EngineHarness Apple GPUs with Metal - WWDC20Thermal API | Android game developmentUse Vulkan for graphics | Android game developmentAndroid Game Development Kit overview (AGI / texture bandwidth / shader profiling)Using Lumen Global Illumination on Mobile in Unreal EngineLumen Technical Details in Unreal EngineMobile Rendering | Unreal Engine 4.27 Documentation5.5.4 Hotfix ReleasedUnreal Engine Console Variables ReferenceFeatures and APIs Overview | Android 15 graphicsGuidelines for creating game with dynamic lights only and good performanceMali and Unreal Engine’s Nanite: Enabling the future of mobile graphicsVirtual Shadow Maps in Unreal EngineCPU Lightmass Global Illumination in Unreal EngineVolumetric Lightmaps in Unreal EngineVolumetric Lightmaps not working on mobilePerformance Guidelines for Mobile Devices in Unreal EngineUse Cascaded Shadows | Unreal Engine 4.27 DocumentationLighting for Mobile Platforms | Unreal Engine 4.27 DocumentationHow to increase mobile Max Moveable Spotlight / Point Lights?What is the cost of dynamic point lights?Broken reflections on Android MaliReflections Captures in Unreal EngineRendering Settings in the Unreal Engine Project SettingsStationary Light Mobility in Unreal EngineScreen Space Reflections on mobile require TAA (forum discussion)Planar Reflections in Unreal EngineUnderstanding Lightmapping in Unreal Engineturn 占位符到链接映射turn27view0 → Unreal Engine 5.5 is now availableturn24view0 → 剖析虚幻渲染体系（12）- 移动端专题 Part 1（UE移动端渲染）turn23view0 → Mobile Rendering and Shading Modes for Unreal Engineturn0search27 → Using the Mobile Deferred Shading Mode in Unreal Engineturn17view3 → Harness Apple GPUs with Metal - WWDC20turn17view4 → Thermal API | Android game developmentturn20view2 → Use Vulkan for graphics | Android game developmentturn11search0 → Android Game Development Kit overview (AGI / texture bandwidth / shader profiling)turn12search0 → Using Lumen Global Illumination on Mobile in Unreal Engineturn12search1 → Lumen Technical Details in Unreal Engineturn11search25 → Android Game Development Kit overview (AGI / texture bandwidth / shader profiling)turn22view0 → Mobile Rendering | Unreal Engine 4.27 Documentationturn28search0 → 5.5.4 Hotfix Releasedturn13search0 → Mobile Rendering and Shading Modes for Unreal Engineturn7search23 → Using the Mobile Deferred Shading Mode in Unreal Engineturn25search6 → Unreal Engine Console Variables Referenceturn20view1 → Features and APIs Overview | Android 15 graphicsturn12search16 → Harness Apple GPUs with Metal - WWDC20turn13search3 → Mobile Rendering and Shading Modes for Unreal Engineturn10search25 → Guidelines for creating game with dynamic lights only and good performanceturn17view0 → Mali and Unreal Engine’s Nanite: Enabling the future of mobile graphicsturn12search15 → Mali and Unreal Engine’s Nanite: Enabling the future of mobile graphicsturn15search13 → Virtual Shadow Maps in Unreal Engineturn26search8 → CPU Lightmass Global Illumination in Unreal Engineturn26search4 → Volumetric Lightmaps in Unreal Engineturn26search0 → Volumetric Lightmaps not working on mobileturn26search1 → Performance Guidelines for Mobile Devices in Unreal Engineturn10search6 → Performance Guidelines for Mobile Devices in Unreal Engineturn10search7 → Use Cascaded Shadows | Unreal Engine 4.27 Documentationturn25search3 → Unreal Engine Console Variables Referenceturn10search3 → Use Cascaded Shadows | Unreal Engine 4.27 Documentationturn15search11 → Lighting for Mobile Platforms | Unreal Engine 4.27 Documentationturn10search33 → How to increase mobile Max Moveable Spotlight / Point Lights?turn14search10 → What is the cost of dynamic point lights?turn10search35 → Broken reflections on Android Maliturn0search32 → Reflections Captures in Unreal Engineturn14search1 → Rendering Settings in the Unreal Engine Project Settingsturn13search4 → Unreal Engine 5.5 is now availableturn26search24 → Stationary Light Mobility in Unreal Engineturn26search32 → CPU Lightmass Global Illumination in Unreal Engineturn26search12 → CPU Lightmass Global Illumination in Unreal Engineturn25search8 → Using the Mobile Deferred Shading Mode in Unreal Engineturn0search20 → Rendering Settings in the Unreal Engine Project Settingsturn14search9 → Screen Space Reflections on mobile require TAA (forum discussion)turn14search19 → Planar Reflections in Unreal Engineturn12search4 → Using Lumen Global Illumination on Mobile in Unreal Engineturn26search28 → Understanding Lightmapping in Unreal Engineturn10search13 → Thermal API | Android game developmentturn25search5 → Using the Mobile Deferred Shading Mode in Unreal Engineturn14search21 → Performance Guidelines for Mobile Devices in Unreal Engineturn11search8 → Thermal API | Android game developmentturn19search10 → Harness Apple GPUs with Metal - WWDC20
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}UETech【BP】在蓝图中Key关卡序列【AngelScript】学习路径与资料库【EUW】自动化导入DAZ资产并创建PBR材质思路VFXAssets【Groom】Xgen 到 Unreal Engine 添加自定义属性实测流程【HLSL】屏幕模糊-hlsl编写初体验【VDB】【Landscape】雪山场景摸鱼MayaQtpy【Qtpy】自动化清理maya文件，带材质abc导入unreal（maya插件制作初体验）Plugin三渲二 Pencil+ 4 for Maya 线稿渲染Stable Diffusion【Sora】【Hunyuan3d】写实模型ai转风格化资产探索depth-anything-v2 模型机制总结并测试ComfyUI-nunchaku 安装并测试Prompt【Prompt】HUMAN 3.0
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}以前都是处理资产标准化，遍历材质和相关param和refrence相关就可以，第一次接触Seq，还挺有意思。但是因为后续多半是使用As来编写逻辑了，不会使用蓝图了，所以这个文档多半不会再更新。学习思路和实际上的实现逻辑是接近的，一般节点和function能关联上，所以文档在Seq遍历的理解上上也有一定价值，作为一个存档吧。And，我的网课开始了，没精力了...Runtime 控制 vs Editor 编辑你对 Sequence 的需求通常分两类：Runtime（运行时）= “播放控制、动态替换绑定”常见任务：Play / Pause / Stop / Reverse / LoopJump/Seek 到某个时间或帧动态换绑定：同一段镜头，换角色/道具/相机（Rebinding）这类通常用 LevelSequenceActor / LevelSequencePlayer 完成。Editor（编辑器）= “改资产、做批处理”常见任务：批量遍历 Content Browser 里选中的多个 Level Sequence修复丢失引用（Binding 断了）批量加 Track / Section批量打 Key / 改曲线批量 Bake/导出 AnimSequence这类通常在 Editor Utility Widget（EUW） 或 Editor Utility Blueprint 里做，靠 Sequencer Scripting 提供的扩展库。1. Sequencer 的数据结构：你到底在改什么？把 Level Sequence 想成“时间轴资产”，它内部关键结构是一条链：LevelSequence（资产） → MovieScene（根时间轴容器） → Object Bindings（对象绑定：这条轨控制谁） → Tracks（轨道：控制什么类型数据，如 Transform / CameraCut / Property） → Sections（片段：轨道上的时间区间） → Channels（通道：Transform.X 等） → Keys（关键帧：时间+值）你做工具时，“编辑 Sequence”= 找到 Binding → 创建/修改 Track → 创建/修改 Section → 拿到 Channel → 写 Keys → 刷新 UI → 保存资产这一篇知乎说的还蛮清楚的。精髓就是这两张图。当然我在实战的时候发现其实也是变化比较多的，需要自己去官方文档一点点推理。比如这里设置相机就是很标准的上述流程，不管是蓝图还是As，都是这个层级这个套路。如果是As的话，大概是这样的，基本一样这里是修改相机Z轴，前面TargetZ已经定义过了： private bool TryUpdateCameraBindingTrackZ(FMovieSceneBindingProxy Binding, float TargetZ) { SetLogsText("[Step] Searching camera transform tracks"); TArray<UMovieSceneTrack> Tracks = Binding.FindTracksByType(UMovieScene3DTransformTrack::StaticClass()); SetLogsText(f"[Info] Transform track count: {Tracks.Num()}"); bool bUpdated = false; int UpdatedChannelCount = 0; for (auto Track : Tracks) { if (Track == nullptr) { continue; } TArray<UMovieSceneSection> Sections = Track.GetSections(); for (auto Section : Sections) { if (Section == nullptr) { continue; } TArray<UMovieSceneScriptingChannel> Channels = Section.GetAllChannels(); for (auto Channel : Channels) { if (Channel == nullptr) { continue; } if (Channel.ChannelName == n"Location.Z") { if (ApplyTargetZToScriptingChannel(Channel, TargetZ)) { bUpdated = true; UpdatedChannelCount++; } } } } }但是动画没有这个Channel。这一点感觉没有人特地说过..我做这个动画项目的时候搞了一下午才查到的..因为普通属性和动画的保存方式不一样。普通属性，比如位置、旋转、颜色，是这样：Track -> Section -> Channel这些值是 Sequencer 自己存的，所以能看到 Channel。动画，比如 Anim Sequence，是这样：Track -> Section -> 动画资源动画的骨骼关键帧保存在动画资源里，不在 Sequencer 的 Channel 里。最后实现是这样的。2. 开工前的准备：插件、蓝图库、节点搜索关键词2.1 建议启用的插件Sequencer Scripting（强烈建议）：提供 Sequence/Track/Section/Channel/Key 的脚本访问入口（Blueprint / Python 都能用）。Editor Scripting Utilities：EUW 常用（选资产、存资产、日志/工具链）。如果你发现“加轨/打 Key”相关节点搜不到，第一件事就是检查 Sequencer Scripting 是否启用。2.2 你会高频用到的“入口库”在蓝图里搜索这些关键词，能快速定位入口：LevelSequenceEditor（编辑器上下文）获取当前 Sequencer 打开的 Sequence获取选中的绑定（bindings）刷新 Sequencer UIMovieSceneExtensions（==结构编辑入口==）++这一块变化比较多，需要自己去理解++MovieSceneSequenceExtensions（从 Sequence 取 bindings、添加轨、设置绑定等）MovieSceneTrackExtensions（从 Track 取 sections 等）MovieSceneSectionExtensions、MovieSceneChannelExtensions（获取 channels / keys / 添加 key 等）SequencerTools（导出/烘焙一类的编辑器工具）例如 Export Anim Sequence3. Part A：运行时用蓝图“操作”Sequence（播放/跳转/换绑定）3.1 最常用：Create Level Sequence Player典型流程：1) Create Level Sequence Player（输入 Sequence 资产 + 播放设置） 2) 得到 LevelSequencePlayer 3) 调用 Play / Pause / Stop / SetPlaybackPosition 等适合“临时播放某段 Sequence”。3.2 跳转到某个时间/帧（Seek / Jump）如果你要求“镜头帧精确对齐”，尽量按 帧 来思考（FrameNumber / FrameRate）。只需要粗略跳转，可用秒。3.3 动态换绑定（Rebinding）典型场景：Sequence 里原本绑定的是一个关卡里的相机 A运行时你想用相机 B 来跑同一段 CameraCut / Transform或者角色是运行时 Spawn 的，你需要把轨道绑定到新角色实现思路：找到需要替换的 Binding（用名字/Tag 更稳）Override/Set Binding 为新 Actor（官方“Actor Rebinding”文档有完整流程）4. Part B：编辑器用蓝图“编辑资产”Sequence（EUW 批处理主战场）编辑器批处理的核心目标：“对资产本身做修改”，而不是“仅在运行时改播放对象”。 你会非常频繁遇到三件事：1) 获取要处理的 Level Sequence 资产 2) 修改 bindings/tracks/keys 3) 刷新 UI + 保存资产（不然你会怀疑人生）4.1 获取要处理的 Sequence：三种入口方式 A：从 Content Browser 选中资产开始（最常见）适合做“批量修改选中的多个 Sequence”。方式 B：获取当前 Sequencer 打开的 Sequence（对当前序列一键修复）适合做“我正在看这个序列，我要一键修复/一键加轨/一键打 key”。方式 C：扫描某个目录下所有 Sequence（全库升级/修复）适合做“我想把整个项目某类序列统一改一遍”。5. 关键能力地图：Bindings / Tracks / Sections / Channels / Keys下面这张“能力地图”建议你背下来：它决定了你写 EUW 是否能从 0 到 1。5.1 Bindings：对象绑定（这条轨控制谁）你常用的能力：列出 Sequence 的所有绑定（遍历）读取绑定的 Display Name / Binding ID获取这个绑定当前绑定了哪些对象（在编辑器里尤其关键）设置/替换绑定（修复引用、换角色、换相机）5.2 Tracks：轨道（控制什么）你常用的能力：给 Binding 添加某种 Track（Transform / Property / Parameter / CameraCut 等）读取 Track 下有哪些 Sections删除某些 Track 或合并结构（进阶）5.3 Sections：片段（时间区间）你常用的能力：设置 Section 的起止范围（start/end frame）获取 Section 的 Channels（例如 Transform 的 9 个 float 通道）5.4 Channels / Keys：曲线与关键帧（核心编辑能力）你常用的能力：在某个时间（帧）添加 key批量偏移/缩放时间批量改值删除 key（稀疏化、清理噪点）一旦你能稳定做到“拿到 Channel 并写 Key”，Sequencer 就变成了“可编程时间轴”。6. 实战 1：批量修复丢失引用（Missing Binding / Missing Actor）6.1 问题长什么样？你会看到：Sequence 里轨道还在（CameraCut、Transform 都在）但提示 “The object bound to this track is missing” 一类信息播放时镜头/动画不动 这通常说明：Binding 断了（原 Actor 被删/改名/换关卡/换层级）。6.2 工具算法（稳定版）输入： 一组 Level Sequence 资产 + 你的“匹配规则” 输出： 修复后的资产（binding 重新指向正确 Actor）流程：1) 对每个 Sequence：获取所有 bindings 2) 对每个 binding：检查“当前绑定对象列表”是否为空 3) 为空 → 说明断了 4) 用你的规则在关卡中找到目标 Actor（按 Name/Tag/映射表） 5) 将该 Actor 重新绑定到该 binding 6) 刷新 UI + 保存资产6.3 “匹配规则”怎么设计才不脆？推荐优先级（从稳到不稳）：1) Binding Tag / 自定义 Tag（最推荐） 2) Actor Tag（关卡里给 Actor 打 tag） 3) Actor Label（编辑器显示名） 4) Object Name（可读性差、容易变化） 5) “第几个 binding”（最不稳，尽量别用）最稳的做法： 在制作 Sequence 时就规定：每个关键对象必须有固定的 Binding Tag/命名约定。 这样修复工具才能 100% 自动化。7. 实战 2：批量添加 Track + Section（Transform / Property / Parameter）你做“批量加轨”通常分三类：7.1 给绑定对象加 Transform Track（最常见）适用：相机、道具、角色根节点、任何 Actor 级动画。步骤（概念）：1) 找到目标 binding 2) Add Track：3D Transform Track 3) Add Section：Transform Section 4) 设置 Section 范围（例如覆盖整个 Sequence） 5) 获取 channels（Location/Rotation/Scale）并打 key7.2 给绑定对象加 Property Track（关键：PropertyPath）适用：灯光强度、FOV、景深、蓝图变量等。官方有专门的 Blueprint snippet 讲“如何拿 property name/path、track class，并把轨道加到 object binding 上并 keying”。 它基本就是“Property Track 批处理”的标准模板。PropertyPath 怎么拿？常见思路（不同版本 UI 名称可能略有差异）：在 Sequencer 里手动加一次该属性轨，然后在轨道/属性上找 “Copy/Reference/Path” 类的菜单或者直接用“你知道的属性名 + 组件层级”拼接（不推荐，容易写错）或者把“可配置的 property path”做成 EUW 的可编辑字段（更像工具）7.3 参数轨（Material/Component 参数）适用：材质 Scalar/Vector 参数、组件材质索引等。社区经验：一些参数轨需要 cast 到特定 section（例如 MovieSceneParameterSection）后调用 “Add Scalar Parameter Key”。某些细节在蓝图中可能不可调用（需要 C++ 暴露），因此你做工具时要准备“降级方案”（例如改用 Python、或改为在 Sequencer 中预设模板轨后复制）。8. 实战 3：批量打 Key（写入曲线）与批量改 Key（偏移/缩放/删点）8.1 批量打 Key：通用思路1) 找到 Section 2) 拿到 Channel（Float/Bool/Enum/Vector 等） 3) 将时间（帧）转换到序列的 frame space 4) Add Key（Time, Value）三种“打 key”的高频模式：初始化：在 0 帧写入初始值（例如相机初始 Transform）固定节奏：每 N 帧写一组 key（例如每 10 帧采样一次）二段式：start/end 两个 key，靠曲线自动插值（常见镜头推拉）8.2 批量改 Key：偏移 / 缩放 / 改值时间偏移：所有 keys 的 time += Δ时间缩放：time = (time - pivot) * scale + pivot数值偏移：value += Δ（例如 FOV 全部 +5）数值缩放：value *= scale（例如强度统一乘 1.2）8.3 删除/稀疏化 Key（进阶）用途：降噪（比如动捕抖动）降低点数（性能/文件大小）清理误打 key（工具误操作）实现思路：Get Keys → 按规则筛选（例如每隔 N 个保留一个）→ 删除其余实战建议：给工具加 “Dry Run / 预览统计”，避免一键删爆。9. 实战 4：自动 Camera Cut（创建 Cut 轨 + 绑定相机）如果你做“模板镜头序列”或“自动生成预览 Sequence”，CameraCut 基本是必备。社区给过一个很实用的蓝图链路（UE5.2 起非常常用）：1) Add Possessable（Object to = CameraActor） 2) Add Track（type = Camera Cut Track） 3) Add Section 4) Cast to MovieSceneCameraCutSection 5) Set Camera Binding ID（Camera Binding ID 来自 Add Possessable 返回的 Binding ID）你可以把它封装成： “把当前选中的 CameraActor 自动接到当前 Sequence 的 CameraCut” 或 “创建一个新 Sequence 并自动生成 CameraCut + 镜头片段”10. 实战 5：Sequencer 驱动蓝图（Event Track / Director / Expose to Cinematics）10.1 Event Track：在某一帧触发事件最常见用途：镜头到某帧：开门/播音效/触发 VFX片段结束：切下一个 Sequence与 UI/剧情逻辑联动官方提供了“从 Sequencer 的 Event Track 触发 Level Blueprint 事件”的流程。10.2 Expose to Cinematics：让 Sequencer 直接关键帧控制蓝图变量你想在 Sequencer 里对蓝图变量打 key（比如 float 强度、bool 开关），通常需要把变量设置为“可被 Cinematics 暴露”。 这样变量才会出现在 Sequencer 的可添加轨道列表里。典型场景：控制材质参数（配合 MID）控制景深、后期参数（如果你把它们封装成蓝图变量）控制动画蓝图参数（例如表情强度、IK 权重）11. 实战 6：Bake / 导出 AnimSequence（编辑器脚本）如果你希望把 Sequencer 里的动画烘焙成 AnimSequence（类似 “Bake to Animation Sequence”），可以用 SequencerTools 的导出节点。常见批处理需求：一个 Sequence 里多个角色绑定：一键全部导出为 AnimSequence自动化管线：修改 Sequence 后自动导出供后续流程使用注意事项：这类节点多数是 Editor Scripting 范畴，不是运行时功能需要确保绑定对象上存在可导出的 SkeletalMeshComponent对 spawnables/动态生成对象的处理要特别小心（有些导出版本会“先 spawn 再导出”）12. Spawnable vs Possessable：工具开发绕不开的概念Possessable（占用型）绑定到关卡里已经存在的 Actor最常见：关卡相机、关卡灯光、场景道具Spawnable（生成型）Sequence 播放时由 Sequencer 负责 Spawn/Destroy常见：镜头用的临时道具、特效、一次性演员为什么这对工具很重要？修引用：Possessable 断了需要你在关卡里重新找 Actor；Spawnable 则可能根本不需要“找关卡 Actor”导出动画：Spawnable 可能需要先保证它能被正确 spawn，导出才有效重用模板：模板序列更推荐用 Spawnable（可移植），但也更容易遇到“运行时对象生命周期”的坑13. 时间系统：秒 / 帧 / DisplayRate / TickResolution（别把 Key 打偏了）Sequencer 里至少有两套“时间尺度”：Display Rate：你在 UI 里看到的帧率（例如 24/30/60）Tick Resolution：内部更细的时间分辨率（用于精确采样）工具开发时建议：1) 统一使用 FrameNumber（不要混用秒和帧） 2) 所有“外部输入时间”（比如你 UI 输入的秒）都先转换到 Sequence 的 frame space 3) 写 key 之前确认：该 Section 的起止范围覆盖了你要写的时间如果你做“按当前播放头帧打 key”的工具：需要从 Level Sequence Editor 获取当前时间/当前帧（不同版本的 API 名称略有差异，思路一致）。14. “看起来没生效”的 90% 根因：刷新 UI + 保存资产你在 EUW 里改完 Sequence 后，常见症状：你确定写了 key但 Sequencer 面板不显示变化或者重开编辑器后全没了解决方案几乎永远是这两步：1) 刷新 Sequencer UI有专门的蓝图 API：刷新在“下一 tick”生效2) 保存资产你改的是资产，不保存就不会落盘建议把这两步做成工具里的“统一收口”： 每次批处理结束都自动：Refresh + Save + 打日志15. 常见坑与排错清单（节点找不到/轨道缺失/变量不可关键帧）15.1 节点搜不到（最常见）先看插件：Sequencer Scripting 是否启用再看上下文：你是不是在 Runtime 蓝图里找 Editor-only 节点？15.2 Property Tracks 缺失 / 某些属性找不到原因通常是：属性不可被 Sequencer 动画化（或未暴露）你要控制的是组件/子对象属性，但轨道层级没选对蓝图变量没启用 “Expose to Cinematics”解决策略：先手动在 Sequencer 里加一次轨道，看它真实生成了什么轨（Track 类型 + 目标对象层级）再回到工具里按同样结构生成15.3 CameraCut 绑定不上最稳的方式：Set Camera Binding ID 的输入 Binding ID 直接来源于 “Add Possessable 返回的 Binding ID”15.4 修改生效但 UI 不更新刷新 Sequencer UI（next tick）若你在批量循环里频繁刷新，建议“最后统一刷新一次”，避免卡顿16. 工程化最佳实践：让你的批处理工具可维护、可升级1) 定位规则与编辑动作分离定位：如何找到 binding/actor（Tag、Name、映射表）动作：如何加轨/打 key/导出 分离后，你改定位规则不会牵动全部逻辑。2) 用 Binding Tag / Actor Tag 做稳定锚点不要依赖数组下标不要依赖临时对象名 否则序列一重排，你工具就炸。3) 加日志与统计每个 Sequence：修了几个 binding、加了几条轨、写了多少 keys错误记录：找不到 actor、property path 无效、cast 失败 批量跑全库没有日志 = 无法维护。4) 做 Dry Run（预演模式）先扫描不写入，只输出“将要改哪些”确认无误再执行写入 这能避免“一键毁库”。5) 封装版本差异点UE5.x 某些 API 会改名或迁移把“添加 spawnable/添加 subsequence/导出动画”等入口封装成单独函数 未来升级 UE 时只改一处。参考资料下面是你写工具时最该收藏的一组入口（官方/社区），方便你对照节点名与行为。Epic：Sequencer Scripting Foundations 自动化编辑 Level Sequence 的基础心智模型。Epic：Add Property Tracks and Keys to a Sequence in BPs Property Track + Key 的标准模板。Epic：Actor Rebinding in Blueprints with Sequencer 动态/修复绑定的官方说明。Epic：Trigger Level Blueprint Events from Sequencer Event Track 触发蓝图。Epic：Refresh Current Level Sequence 刷新 Sequencer UI。Epic：Export Anim Sequence 编辑器脚本导出 / 烘焙。Epic：Set Camera Binding ID CameraCut 绑定。论坛：CameraCut 绑定蓝图链路（UE5.2 经验贴） CameraCut 绑定蓝图链路参考。论坛：Component/Parameter 轨道加 Key（MovieSceneParameterSection 等） Component / Parameter 轨道加 Key 的讨论与实现经验。Tech Blog：Performance at Scale: Sequencer in Unreal Engine 4.26 Sequencer 性能与规模化，帮助理解大项目序列的性能特征。