纯原生 C++ 程序编译后,很多类型信息可能彻底消失。
但 IL2CPP 为了在运行时支持:
-
反射;
-
序列化;
-
MonoBehaviour 类型;
-
泛型;
-
Unity 对象系统;
-
方法查找;
-
字段访问;
通常还需要 global-metadata.dat。
目标是理解游戏逻辑、类结构、Unity 方法或某个功能,Il2CppDumper 比继续硬读 FairGuardProtect.dll 更直接。
如果目标是理解保护器如何加载、检测或阻止分析,仍然需要研究 FairGuardProtect.dll。
dnSpy擅长什么
dnSpy主要用于分析托管程序集,例如:
Assembly-CSharp.dll
UnityEngine*.dll
其他 .NET DLL
对于传统 Unity Mono 游戏,它可以直接显示接近原始代码的 C# 方法体:
public void Update()
{
health -= damage;
}
还能:
- 搜索类和方法
- 查看 C# 伪源码
- 编辑 IL 指令或方法
- 调试托管代码
- 保存修改后的 .NET 程序集
为什么不适合直接分析当前游戏
当前目录有:
GameAssembly.dll
global-metadata.dat
Endfield_TBeta_OS_Data\il2cpp_data
说明主要游戏代码经过 IL2CPP 转换:
原始 C#
↓ IL2CPP构建
C++代码
↓ 本机编译
GameAssembly.dll
最终的游戏方法体已经变成 x64 机器码,
写的 C# 脚本最终如何被编译和执行 ,而不是整个游戏采用哪一种文件格式。Unity 的场景、纹理、模型、AssetBundle、引擎本体等,并不会因此全部变成另一种格式。编辑Unity Documentation+1
overflow-visible!
同一个 Unity Project
├─ Build Target
│ └─ Windows / Android / iOS / WebGL
│
└─ Scripting Backend
├─ Mono
└─ IL2CPP
overflow-visible!
FairGuardProtect.dll + 0x29C50
└─ 聚合多个检测结果
├─ PEB.BeingDebugged → 0x08
├─ ProcessDebugObjectHandle → 某一位,疑似 0x20
├─ 代码/系统调用桩检查
├─ 其他 NT 信息类调用
└─ flags != 0
├─ 设置 detected
└─ 调用后续 callback
PEB.BeingDebugged == 1 与 GS:[0x60] → PEB+2 的汇编已经形成闭环,因此 0x08 的静态意义基本可以定案。
但"本次一定是 ProcDump 将 BeingDebugged 改成 1"仍应写成:
本次 ProcDump 监控环境使进程处于 debugger-visible 状态;结合没有其他已知调试器,ProcDump 是最可能来源。
原因是 ProcDump 官方文档只明确规定 -t 是"进程终止时写 dump",没有承诺其内部监控机制在所有版本、参数组合下必然采用哪一种附加方式。当前官方版本是 ProcDump 12.01,文档更新于 2026 年 7 月 9 日;-r 使用 PSS clone 只是在取 dump 阶段降低停顿,也不能据此推断终止监控完全不建立调试关系。编辑Microsoft Learn
你看到的:
overflow-visible!
BeingDebugged = 1
NtGlobalFlag = 0
至少说明 dump 捕获时 PEB 确实已处于"被调试"状态。NtGlobalFlag=0 并不能否定后期附加,因为它与"进程创建时就在调试器下启动"不是同一个时间点。
聚合完成后的 flags
overflow-visible!
if (flags != 0)
对应的最终汇编位置,通常类似:
overflow-visible!
asm
test bl, bl
jz not_detected
或者:
overflow-visible!
asm
test eax, eax
je not_detected
也可能是栈变量:
overflow-visible!
asm
cmp byte ptr [rsp+XX], 0
你需要在 Ghidra 中记录以下三个信息:
overflow-visible!
聚合结束地址
├─ module RVA,例如 0x29D7A
├─ flags 最终存放位置
│ ├─ BL / EBX
│ ├─ AL / EAX
│ └─ [RSP+offset]
└─ 回调调用地址
不要优先使用普通 bp
普通 bp/bu 是软件断点,会把目标指令临时替换为 breakpoint instruction。微软文档明确说明软件断点会修改代码字节;而你研究的函数本身包含代码完整性检查,因此这可能产生新的变量。编辑Microsoft Learn+1
更适合的是执行型硬件断点:
overflow-visible!
sxe ld:FairGuardProtect.dll
g
模块加载并停下后:
overflow-visible!
lm m FairGuardProtect
? FairGuardProtect+0x29C50
然后在"最终测试 flags"的指令上设置:
overflow-visible!
ba e 1 FairGuardProtect+0x<聚合结束RVA>
ba e 1 使用处理器执行断点,不改写 FairGuard 的指令字节。ba 也可以附带自动命令,命中后读取寄存器、栈并继续。微软将其定义为 processor breakpoint;x64 支持 1、2、4、8 字节访问范围,执行断点大小必须为 1。编辑Microsoft Learn
例如 flags 在 BL:
overflow-visible!
ba e 1 FairGuardProtect+0x<地址> ".printf \"FG flags = 0x%02x\n\", @bl; r; kb; g"
如果 WinDbg 不接受 @bl,读取完整寄存器:
overflow-visible!
ba e 1 FairGuardProtect+0x<地址> ".printf \"FG RBX = %p\n\", @rbx; r; kb; g"
然后取:
overflow-visible!
flags = RBX & 0xFF
如果 flags 位于栈中:
overflow-visible!
ba e 1 FairGuardProtect+0x<地址> "db @rsp+0x<offset> L1; r; kb; g"
这里的关键不是断在函数入口,而是断在所有 OR 操作已经完成、尚未执行 callback 的位置。
WinDbg └─ 向 Windows 内核提出请求 ├─ 暂停目标线程 ├─ 读取目标虚拟内存 ├─ 读取 CPU 寄存器上下文 ├─ 接收异常和模块加载事件 └─ 恢复目标线程
FairGuard 并不是数据提供者。
Windows 的 DebugActiveProcess 机制要求调试器具有目标进程所需的访问权限;启用 SeDebugPrivilege 后,可以忽略目标进程通常的 DACL 限制来调试进程。编辑Microsoft Learn+2编辑Microsoft Learn+2
所以一旦附加成功:
overflow-visible!
WinDbg 读取某个地址
└─ Windows 检查调试器权限
└─ Windows 从目标进程的地址空间读取
目标进程不会收到:
overflow-visible!
"WinDbg 想读取 0x12345678,是否允许?"
这样的询问。
如果 WinDbg 已经合法取得足够权限,普通用户态 FairGuard 通常不能对 Windows 内核说:
"这一页内存禁止调试器读取。"
它能做的是改变游戏自身行为,例如:
overflow-visible!
发现调试器
└─ 立即清除敏感数据
├─ 退出
├─ 进入假代码路径
├─ 故意破坏状态
└─ 让调试器只看到经过伪装的内容
PEB在层级中的位置
overflow-visible!
Windows 内核
├─ 真正管理进程、线程、Handle、调试对象
│
└─ 为进程建立用户态地址空间
├─ 程序代码和 DLL
├─ Heap
├─ Stack
├─ TEB:每个线程一份
└─ PEB:每个进程一份
它位于:
overflow-visible!
目标进程自己的用户态虚拟地址空间
所以 FairGuard和 WinDbg看到的是同一块内存。
区别是:
FairGuard └─ 从自己的进程内部直接读取
PEB WinDbg └─ 请求 Windows 内核读取目标进程中的 PEB
x64 Windows下可以这样理解:
overflow-visible!
GS Base
└─ 当前线程 TEB 的起始虚拟地址
└─ TEB + 0x60
└─ 保存 PEB 指针
因此:
overflow-visible!
asm
mov rax, qword ptr gs:[60h]
实际含义是:
overflow-visible!
C
rax = CurrentThreadTEB->ProcessEnvironmentBlock;
此时 RAX 得到的是 PEB的运行时虚拟地址。
然后:
overflow-visible!
asm
cmp byte ptr [rax+2], 0
就是:
overflow-visible!
C
if (PEB->BeingDebugged != 0)
完整关系:
overflow-visible!
当前线程
└─ GS Base → 当前线程的 TEB
└─ TEB + 0x60 → PEB指针
└─ PEB + 0x02 → BeingDebugged
每个线程的 TEB地址不同:
overflow-visible!
Thread 1 → TEB A ┐
Thread 2 → TEB B ├→ 同一个 PEB
Thread 3 → TEB C ┘
因为 TEB描述线程,而 PEB描述整个进程。
frida_agent_main 是 Frida Agent 的典型入口点名称。Frida 官方资料明确展示过加载 frida-agent 后,通过 dlsym(..., "frida_agent_main") 找到并执行该入口。编辑Frida
因此保护器里出现这个字符串,更合理的解释是:
overflow-visible!
FairGuardProtect
└─ 动态插桩工具检测
└─ Frida 检测
└─ 搜索 frida_agent_main 这一特征
而不是:
overflow-visible!
RenderDoc
└─ 内部调用 Frida
RenderDoc 是一套独立的开源图形调试器,有自己的捕获、注入和图形 API Hook 实现。官方记录中描述的是 RenderDoc 注入目标进程后,直接 Hook Vulkan、D3D11 等图形 API,没有显示它依赖 Frida。编辑GitHub+1
MiniDumpWriteDump 是什么
这是 Windows 的 DbgHelp/DbgCore API:
overflow-visible!
C++
BOOL MiniDumpWriteDump(
HANDLE hProcess,
DWORD ProcessId,
HANDLE hFile,
MINIDUMP_TYPE DumpType,
...
);
它可以把目标进程的部分或全部信息写入转储文件,例如:
overflow-visible!
.dmp
├─ 已加载模块
├─ 线程列表
├─ 线程寄存器
├─ 线程调用栈
├─ PEB / TEB 等进程结构
├─ 指定范围的虚拟内存
└─ 完整进程内存,取决于 DumpType
微软文档明确说明,它只需要目标进程句柄,并要求该句柄具有 PROCESS_QUERY_INFORMATION、PROCESS_VM_READ 等权限;没有要求调用 DebugActiveProcess。编辑Microsoft Learn+1
为什么它不算调试器附加
真正的 Win32 调试附加是:
overflow-visible!
DebugActiveProcess(GamePID)
└─ Windows 建立调试关系
├─ 游戏产生调试事件
├─ 调试器调用 WaitForDebugEvent
├─ 异常交给调试器
├─ DLL 加载通知交给调试器
└─ 调试器调用 ContinueDebugEvent
DebugActiveProcess 会让调用者正式成为目标进程的调试器。编辑Microsoft Learn+1
而转储监视器做的是:
overflow-visible!
OpenProcess(GamePID)
└─ MiniDumpWriteDump(...)
它没有:
overflow-visible!
DebugActiveProcess
WaitForDebugEvent
ContinueDebugEvent
所以它没有建立 Windows 的调试事件通道。
更准确的术语是:
外部进程转储,而不是非调试附加。
修改源碼,重新編譯 DLL
這是最正常的方法:
overflow-visible!
Original.cpp
└─ 修改函數邏輯
└─ 重新編譯、鏈接
└─ 產生新的 Original.dll
例如原來:
overflow-visible!
C++
int Check()
{
return 0;
}
改成:
overflow-visible!
C++
int Check()
{
return 1;
}
這不是 Hook,而是直接產生一個新版 DLL。
適用條件:
overflow-visible!
└─ 你擁有源碼



3. 修改已經載入內存的原 DLL
程序正在運行時:
overflow-visible!
磁盤
└─ Original.dll
進程虛擬內存
└─ Original.dll 的已載入映像
可以直接修改內存中的 DLL 代碼或資料。
例如:
overflow-visible!
Original.dll!Check
原來:
xor eax, eax
ret
內存 Patch 後:
mov eax, 1
ret
通常流程:
overflow-visible!
找到 Original.dll 的載入基址
└─ 基址 + 函數 RVA
└─ 得到函數運行時地址
└─ VirtualProtect 改成可寫
└─ 寫入新機器碼
└─ FlushInstructionCache
└─ 恢復頁面權限
VirtualProtect 可以修改當前進程內存頁的保護屬性;修改可執行代碼後,Windows 文檔要求調用方處理指令快取一致性。编辑Microsoft Learn
這種修改通常只影響:
overflow-visible!
└─ 當前這一次運行
程序退出後,該進程的虛擬內存消失;磁盤上的原 DLL 一般仍未改變。
因此:
overflow-visible!
內存 Patch
├─ 當前進程有效
├─ 重啟後通常消失
└─ 每次啟動都要重新安裝 Patch
外部程序也可以使用 WriteProcessMemory 向另一個具有相應權限的進程地址空間寫入資料。编辑Microsoft Learn
你圈出的 FairGuardProtect.dll 是 Ghidra 项目数据库中导入的副本/分析对象,不是游戏原目录里正在使用的那份 DLL。
当前关系是:
游戏原文件
E:\EndF PC\endF Cli\...\FairGuardProtect.dll
↓ 导入
Ghidra 项目数据库
E:\EndF PC\RenderDocCaptures\FairGuardProtect Analysis.gpr/.rep
Ghidra 這個名字其實是縮寫兼諧音梗,不是一般英文單字,所以不能用一般拼字規則去推導發音 ------這是美國國家安全局(NSA)開發的逆向工程工具(reverse engineering framework),名字來源是:
1. 名字的雙重來源:
- 這個名字致敬了哥吉拉(Godzilla)電影宇宙裡的怪獸「基多拉」(King Ghidorah)------一隻三頭龍怪獸
- 同時也是個文字遊戲:"Ghidra" 可以拆解成暗示 "IDA" (業界最著名的逆向工程工具 IDA Pro)藏在字裡面------Gh-IDR-a,中間藏著 IDR,諧音 IDA,算是開發團隊對競爭對手工具的一個小玩笑/致敬
2. 發音為什麼是 "GEE" 不是 "GHEE" 或其他:
- 因為它源自 Ghidorah(基多拉)這個怪獸名字的前半段發音,英文裡 Ghidorah 唸作 /ɡɪˈdɔːrə/ 或類似音,Ghidra 保留了前面 "Ghi-" 的發音邏輯,唸作 /ɡiː/
- gh 這個組合在這裡單純讀 /g/(硬音),因為這是專有名詞借用發音,不是走一般英文的 gh 規則(你之前學過 gh 在 laugh 裡讀 /f/、在 though 裡不發音,但那些是英文本土/中古英文遺留的拼字,Ghidra 是現代人造的專有名詞,發音直接沿用日本怪獸片名字的音譯邏輯)
- 業界原本這類工具最知名的是商業軟體 IDA Pro(前面提過 Ghidra 名字裡藏著對它的致敬/玩笑),但 IDA Pro 授權費用高昂
- NSA 內部長期自行開發、使用這套工具,2019 年決定開源釋出,一方面提升自己在資安社群的形象和影響力,一方面也讓全球資安研究人員、學界、企業能免費使用這麼強大的工具,間接也讓更多人幫忙發現並回報這套工具本身的問題,形成社群共同維護的效果
現在的使用狀況:
Ghidra 開源後,已經被全球資安研究人員、CTF(資安競賽)玩家、學術界廣泛採用,是目前免費逆向工程工具裡最主流的選擇之一(跟開源的 radare2、商業的 IDA Pro 並列常見選項)。
if (名称比较结果 == 0)
在普通汇编知识中,== 0 与 != 0 的区别通常是:
JZ / JE 为零/相等时跳转
JNZ / JNE 非零/不相等时跳转
這套工具的實際層級大致是:
overflow-visible!
Substance Painter/Blender 插件
└─ UI、選擇資產、建立模型與材質
└─ RuriRipperPyBridge
├─ Unity YAML 解析
├─ Mesh/骨骼/材質資料轉換
└─ pythonnet / CoreCLR 橋接
└─ Ruri.RipperHook.dll
└─ 從遊戲安裝目錄讀取 CAB/AssetBundle
└─ 解析依賴閉包、輸出 YAML 與貼圖位元組
公開倉庫確實包含 Painter 插件、Unity YAML 解析、Mesh 解碼、材質轉換、Shader 接線等大量原始碼,而且使用 AGPL-3.0 授權。

只下載公開 GitHub 倉庫,得到的是:
overflow-visible!
公開部分
├─ 怎樣解析已經拿到的 YAML
├─ 怎樣解碼 Mesh
├─ 怎樣轉座標系
├─ 怎樣建立 Blender/Painter 資產
└─ 怎樣呼叫 Ruri.RipperHook.dll
缺失部分
└─ Ruri.RipperHook.dll 本身及特定遊戲 Hook
這就是他稱為「半開源」的原因。更精確的術語應該是:
開源前端依賴閉源或受限制的後端元件。
名稱雖然叫 Hook,但公開資訊顯示它更像是對 AssetRipper 資產解析流程的橋接、修補或遊戲特化模組。是否另有進程注入行為,要看到未公開 DLL 才能確認。

Patch Instruction后,Listing 左侧通常有紫色补丁标记;可以通过菜单查找已修改字节。- 项目可能创建恢复快照,但它主要用于崩溃恢复,不等同于可长期浏览的版本历史。
- 导出的 DLL 也不会携带一份"Ghidra 修改记录"。
查看当前有哪些字节被改过,可以尝试:
Search → For Matching Instructions
不过更可靠的是检查 Ghidra 的补丁表。在不同版本中通常位于:
Window → Bytes
或搜索 Memory Map 中的 byte modifications。你截图中 0x180027EA6 左侧的紫色图标就表示该位置有用户修改。
原来这里是 74 0F(JZ),现在游戏目录里的 DLL 已经是 75 0F(JNZ)。因此不是只在 Ghidra 项目里改了,OS.exe 旁边的实际 DLL 也已经换成导出版本。
模块列表:游戏进程当前实际加载的 DLL
目标列表:FairGuard 内置的待检测名称
這個倉庫是在直接修改 Blender 原始碼,製作一個 Blender 分支版本,不是普通外掛。
更準確地說,它是:
overflow-visible!
官方 Blender 5.2 LTS 原始碼
└─ bb-yi 的 Blender NPR Port
└─ 修改 Blender 核心與 EEVEE
├─ 新增 NPR 節點系統
├─ 新增全屏 Filter Materials
├─ 新增 Render Textures
├─ 新增螢幕空間 Outline
├─ 新增 Render Pass
├─ 修改 Shader Editor
└─ 加入若干 Goo Engine/NPR 節點

這張圖更準確的理解
可以這樣看:
└ 工程用途上
├ 它就是 toon face shadow texture
├ 用來控制二次元角色臉部受光/背光時的陰影形狀
└ 常跟角色朝向、主光方向、左右臉切換一起使用
└ 術語上
├ 嚴格說 :不一定是標準 signed distance field
├ 業界/教程口語上 :很多人還是會直接叫它 face SDF
└ 因為它最後也常被拿來做"像 SDF 一樣平滑可控的陰影分界"
如果按用途來分,最像的是:
└ 二次元臉部明暗控制圖
├ 常見叫法:face shadow map / toon face shadow map / face lightmap
在一些教程裡也會被叫:face SDF
不是我前面說的那種最純粹的「灰度距離場貼圖」。
Ghidra Listing 左侧显示的是"程序地址",对 PE 文件通常是按 Ghidra 当前 Image Base 计算后的虚拟地址 VA;RVA 需要再减掉 Image Base。 Ghidra 官方接口也明确区分了程序地址与 Image Base。编辑Ghidra+1
这个 DLL 很可能是:
overflow-visible!
Image Base = 0x180000000
所以:
overflow-visible!
Ghidra Listing 左侧地址:0x180027EA6
Image Base: 0x180000000
─────────────
RVA: 0x000027EA6
也就是:
overflow-visible!
RVA = Ghidra地址 - ImageBase
在 Ghidra 的 Listing 窗口 里按键盘 F,最常见含义是:
overflow-visible!
F
└─ Create Function
└─ 从当前光标地址开始创建一个函数
通常流程是:
overflow-visible!
找到疑似函数入口
├─ 如果那里还是 undefined bytes
│ └─ 先按 D:Disassemble
└─ 再把光标放在函数第一条指令
└─ 按 F:Create Function
Ghidra 会从当前地址沿控制流分析:
overflow-visible!
当前地址
├─ 顺序指令
├─ 条件跳转
├─ 无条件跳转
└─ RET
然后把可以归属于该子程序的指令标记为一个 Function。官方教程明确写的是:先用 D 反汇编,然后在函数顶部按 F 创建函数。编辑Ghidra
例如你来到:
overflow-visible!
asm
180781A0C push rbx
180781A0E sub rsp,20h
180781A12 mov rbx,rcx
...
180781A60 ret
在 180781A0C 按 F 后,Ghidra 可能创建:
overflow-visible!
C++
undefined8 FUN_180781a0c(...)
{
...
}
这并不是运行程序,也不是修改原始 DLL 指令。它主要修改的是 Ghidra 项目中的分析标记:
overflow-visible!
F 创建 Function
├─ 定义函数入口
├─ 定义函数范围
├─ 生成默认函数名
├─ 让 Decompiler 尝试生成伪代码
├─ 建立 callers / callees
└─ 改善参数、局部变量和栈分析
但 F 是上下文相关的。在数据区域,F 也可能用于在 float 与 double 数据类型之间循环;而工具栏中的字母 F 按钮表示"导航到下一个函数",那是工具栏按钮,不一定是键盘 F 的行为。编辑Ghidra+1
也就是把 R12 当前保存的 64 位数值压入栈顶 。它不跳转、不调用函数,也不修改 R12 本身;它只修改 RSP 和栈内存。x64 模式下,这种 PUSH 通常让 RSP 减少 8 字节。
不能理解成"把前面这个东西的地址压给后面"。更准确的是:
overflow-visible!
asm
PUSH R12
等价于:
overflow-visible!
C++
RSP -= 8;
*(uint64_t*)RSP = R12;
也就是说:
overflow-visible!
PUSH R12
├─ 读取 R12 当前保存的 64 位数值
├─ 栈顶 RSP 向下移动 8 字节
├─ 把这个数值写入新的栈顶
└─ 下一条仍然顺序执行
例如执行前:
overflow-visible!
R12 = 0x12345678
RSP = 0x1000
执行后:
overflow-visible!
R12 = 0x12345678 // 没变
RSP = 0x0FF8 // 减少 8
[RSP] = 0x12345678 // 栈中保存了一份
下一条 RIP = 0x180781A0E
在 64 位模式下,普通的 64 位 PUSH 会让 RSP 减少 8 字节,并把操作数写入栈顶。编辑Intel CDRD Public+1
它压入的是"值",不固定是地址
假设:
overflow-visible!
R12 = 0x000001E819782000
而这个数值恰好指向内存中的某个对象,那么可以说:
overflow-visible!
把 R12 中保存的地址值压栈
但假如:
overflow-visible!
R12 = 8
那么压入的就是整数 8。
因此规则是:
overflow-visible!
PUSH 寄存器
└─ 不关心寄存器中的值具有什么语义
├─ 可以是地址
├─ 可以是整数
├─ 可以是位标志
└─ 也可以是暂时生成的混淆数据
截图里的:
overflow-visible!
asm
PUSH R12=>DAT_1e819782
真正的 CPU 指令仍然只是:
overflow-visible!
asm
PUSH R12
=>DAT_1e819782 是 Ghidra 对 R12 当前值的推测或数据引用注释,不是 PUSH 的另一个参数。
真正改变或选择控制流的通常是:
overflow-visible!
条件路口
├─ JZ / JE
├─ JNZ / JNE
├─ JA / JB
├─ JG / JL
└─ 其他 Jcc
强制转向
└─ JMP
进入子函数
└─ CALL
返回调用者
└─ RET
异常或间接控制流
├─ JMP RAX
├─ CALL [RAX+...]
└─ 异常分发
AI 可以主动问 Ghidra:
overflow-visible!
"0x180781A0C 属于哪个函数?"
"给我这个地址前后 40 条指令。"
"反编译 FUN_18077bf2a。"
"谁调用了这个函数?"
"这个函数调用了谁?"
"R12 在后续第一次在哪里被读取?"
"列出所有对 0x180781A0C 的引用。"
"把这段函数的 P-code 给我。"
0x180027EA6 改成 JNZ 后,FairGuard 把 Endfield_TBeta_OS.exe 误判为命中项,而不是让 RenderDoc 检测通过:
先前給的教程節點圖裡,那一排黑白圖片基本就是多個角度的陰影引導圖。SDFBetween2Images 再負責把相鄰兩張引導圖之間的邊界平滑插值。
假設你準備 8 張引導圖:
overflow-visible!
M0:正面光
M1:偏轉約 13°
M2:偏轉約 26°
M3:偏轉約 39°
M4:偏轉約 51°
M5:偏轉約 64°
M6:偏轉約 77°
M7:側面光 90°
某個臉頰像素:
overflow-visible!
M0、M1、M2:亮
M3 開始:暗
最終灰度可能存成:
overflow-visible!
T = 3 / 7 ≈ 0.43
它的意思不是:
overflow-visible!
這個像素亮度是 43%
而是:
overflow-visible!
當水平入光角度越過約 43% 的範圍時,
這個像素切換到陰影。
運行時大致是:
overflow-visible!
hlsl
float threshold = FaceShadowMap.Sample(Sampler, UV).r;
float shadow = step(lightAngle01, threshold);
黑白方向如果相反,就交換 step 參數或接 OneMinus。
8 張是可用下限。若鼻影和臉頰邊界變化複雜,使用 12 或 16 張。
Microsoft 对 CMP 的描述就是:执行减法并据此设置标志,但丢弃减法结果。
overflow-visible!
mov eax, 8
cmp eax, 8
执行后:
overflow-visible!
EAX = 8 // 没被修改
ZF = 1 // 因为 8 - 8 = 0
原始逻辑:
180027EA8 ADD RBX,20h
180027EAC CMP RBX,RSI
180027EAF JNZ 180027E85
相当于:
RBX += 0x20;
if (RBX != RSI)
回到循环头;
其中:
RBX:当前元素指针;RSI:数组结束指针;- 每个元素大小为
0x20。
改成 JZ 后会变成:
RBX += 0x20;
if (RBX == RSI)
回到循环头;
TEST 会执行按位 AND,根据结果设置标志,但不保存 AND 的结果。编辑Microsoft Learn+1
因为任何值和自己按位 AND 都等于自己:
overflow-visible!
C++
eax & eax == eax
所以:
overflow-visible!
asm
test eax, eax
最常见用途就是检查:
overflow-visible!
C++
eax == 0?
SHA-256是什么
SHA-256 不是 WinDbg,也不是调试信息。
它是对整个 DLL 文件计算出的内容指纹:
输入:FairGuardProtect.dll 的全部字节
输出:固定长度的哈希值
本轮使用 PowerShell计算:
Get-FileHash -Algorithm SHA256 FairGuardProtect.dll
用途是确认实验所使用的确实是你刚修改的 DLL。哪怕只修改一个机器码字节,SHA-256 通常也会完全变化。
进入循环第一轮
→ 执行EA6
→ EA6的JNZ成立
→ 直接跳到EB7
→ 没有执行EA8、EAC、EAF
做成可复用的原生节点组
严格来说,Driver 直接挂在 Value 节点上已经完成原生化。
如果希望以后复制到别的材质:
-
选中新的 Value 节点。
-
按
Ctrl+G创建节点组。 -
将节点组命名为:
帧时间_按比例输出
但要注意:复制节点组时 Driver 的行为需要再次验证。实际工程中更推荐:
- 在每个材质中保留一个带 Driver 的 Value
- 或使用一个公共 Empty 对象的自定义属性统一驱动所有材质
renderdoc.dll 在 Ghidra里对应的静态地址是:
0x1800AF6F0
但这里存放的不是明文,而是13字节加密表项:
7F 18 0D 0D 05 1C 1C 12 14 56 54 13 0C
我是怎么定位的
首先从目标集合构造函数:
FUN_180027F60
看到它连续读取一组表项:
0x1800AF690
0x1800AF6A8
0x1800AF6C0
0x1800AF6D8
0x1800AF6F0
0x1800AF700
...
Ghidra对 0x1800AF6F0 的 XREF 显示:
引用位置:0x1800284B3
精确汇编是:
1800284B3 LEA RDX,[0x1800AF6F0]
1800284BA LEA RCX,[RBP-0x29]
1800284BE CALL FUN_18001C3D0
其中:
RDX:加密表项地址;RCX:接收解密结果的临时字符串对象;FUN_18001C3D0:把13字节表项解成宽字符串。
解密函数循环13次:
for (i = 0; i != 13; ++i) {
output[i] = decrypt(input[i], i);
}
将 0x1800AF6F0 的字节代入后得到:
renderdoc.dll
所以闭环是:
0x1800AF6F0
→ 0x1800284B3取得地址
→ FUN_18001C3D0解密
→ FUN_180027F60加入目标名称vector
→ FUN_180027E30枚举并比较
→ 0x180027E9F调用_wcsicmp
在 Ghidra里按 G,输入:
1800AF6F0
可以看到密文表项;输入:
1800284B3
可以看到它被读取和解密的位置。
Windows x64 使用 RCX、RDX、R8、R9 传递前四个整数或指针参数。
LEA 是:
overflow-visible!
Load Effective Address
加载有效地址
但"Load"这个名字容易误导。它不是读取目标地址里的内容,而是计算括号中的地址表达式。
overflow-visible!
LEA RDX,[0x1800AF6F0]
结果:
overflow-visible!
RDX = 0x1800AF6F0
不读取 0x1800AF6F0 处的13字节。
所以:
overflow-visible!
LEA RDX,[地址]
└─ 获得地址本身
MOV RDX,[地址]
└─ 读取地址里的内容
LEA 的正式含义就是计算并加载有效地址。编辑Oracle Docs+1
为什么 LEA 也使用方括号
在 Intel 汇编语法中,方括号通常表示一个"内存地址表达式":
overflow-visible!
asm
[RBP-0x29]
[RAX+RCX*4+0x20]
[RIP+0x1234]
一般指令会:
overflow-visible!
计算括号里的地址
└─ 再访问这个地址里的内存
例如:
overflow-visible!
asm
MOV RAX,[RBP-0x29]
内部概念是:
overflow-visible!
address = RBP - 0x29
RAX = memory[address]
但 LEA 会在第一步就停止:
overflow-visible!
asm
LEA RAX,[RBP-0x29]
概念上是:
overflow-visible!
address = RBP - 0x29
RAX = address
1800284B3处的指令字节 └─ 命令CPU计算出0x1800AF6F0这个地址数值 └─ 把地址数值放入RDX └─ CALL把RDX作为第二参数交给解密函数 └─ 解密函数才真正读取0x1800AF6F0处的13字节
1. Graph Editor:编辑"随时间变化"
典型链路:
overflow-visible!
Timeline Frame
└─ Action
└─ F-Curve
└─ Keyframes
└─ 当前属性值
例如:
overflow-visible!
Frame 1 → Cube Location Z = 0
Frame 40 → Cube Location Z = 3
Graph Editor 中:
overflow-visible!
X轴 = Frame
Y轴 = Property Value
它主要用来处理:
-
Keyframe 时间位置
-
数值变化
-
Bezier Handles
-
Linear / Constant / Bezier Interpolation
-
Ease In / Ease Out
-
Overshoot
-
F-Curve Modifiers,例如 Noise、Cycles、Generator
所以它回答的是:
"这个属性在第几帧应该是多少?"
Blender 官方说明,Graph Editor 显示和编辑决定属性如何随时间变化的 F-Curve;关键帧可以被移动、旋转、缩放,Modifier 可以非破坏性地叠加程序化效果。编辑Blender Documentation+2编辑Blender Documentation+2
2. Driver:建立"属性之间的依赖关系"
典型链路:
overflow-visible!
Source Property
└─ Driver Variable
└─ Driver Type / Expression
└─ 中间输入值 X
└─ Driver F-Curve
└─ Target Property Y
例如让骨骼旋转控制 Shape Key:
overflow-visible!
Bone Rotation Z = 0°
└─ Face Shape Key = 0
Bone Rotation Z = 90°
└─ Face Shape Key = 1
Driver Editor 中:
overflow-visible!
X轴 = Driver表达式计算出的输入值
Y轴 = 最终写入目标属性的值
因此这里的横轴通常不再是帧,而可能是:
-
骨骼旋转角度
-
物体位置
-
两个物体之间的距离
-
Custom Property
-
另一个 Shape Key 的值
-
多个变量经过表达式计算后的结果
官方文档明确说明:Driver 先通过变量和函数或表达式产生一个中间值,再由 Driver F-Curve 把这个中间值映射为最终属性值。编辑Blender Documentation+1
例如:
overflow-visible!
变量:
rot = Bone Rotation Z
表达式:
rot * 2
Driver F-Curve:
输入 0.0 → 输出 0.0
输入 1.0 → 输出 0.8
输入 2.0 → 输出 1.0
完整计算相当于:
overflow-visible!
Bone Rotation
└─ rot
└─ rot * 2
└─ Driver Curve Remapping
└─ Shape Key Value
更简单的做法
也可以删除下面的 current_frame 输入变量,直接把 Expression 写成:
frame * 0.5
Driver 表达式中的 frame 是 Blender 提供的当前帧变量。不过为了明确展示数据来源,我更建议你先使用 Scene → frame_current 的方法。
为什么要乘 0.5
这不代表"这样更好"。只是我们在复刻原 Scene Time 的语义:
Scaled Frame = Frame × Scale
原节点的 Scale 是 0.5,所以先原样保留,目的是减少修复时的变量。

在 UE 5.8 Substrate 中
Trim Sheet 不是 Substrate 專屬節點,也不會改變 Substrate 的 BSDF 結構。
關係是:
overflow-visible!
Trim Sheet
└─ Texture + UV 的資產製作方法
└─ Texture Sample
└─ Base Color / Roughness / Normal 等參數
└─ Substrate Slab BSDF
所以在 UE 中它仍然只是普通 Texture Sample 的資料來源。Substrate 決定材質如何散射光線;Trim Sheet 決定模型從貼圖的哪一條區域取得表面資料。UE 官方把 Material 定義為控制物體表面外觀的資產,而 Trim Sheet 本身並不是一種額外的 Material 類型。编辑Epic Games Developers+1
最簡單的判斷方法是:
overflow-visible!
很多模型是否反覆出現相同的長條形細節?
├─ 是 → 適合 Trim Sheet
└─ 否 → 考慮 Tiling、Atlas 或 Unique Texture
- 先正常启动 Blender,不要双击
.blend。 - 选择"文件 → 打开"。
- 在文件浏览器右侧选项中关闭"加载界面 / Load UI"。
- 打开
E:\BaiduNetdiskDownload\心月狐打包.blend。
我进一步检查后,真正的问题不是 CMC 转向设置,而是相机系统。
当前角色会根据控制台变量选择两套相机:
DDCVar.NewGameplayCameraSystem.Enable = true
→ 使用 GameplayCamera
false
→ 使用旧的 SpringArm + Camera
默认实际使用的是 GameplayCamera,而你移动蓝图取的是:
Get Control Rotation
在 Gameplay Camera System 下,最终画面相机旋转不一定等于 Control Rotation。所以要实现"相对当前画面相机移动",应该只替换移动方向来源:
Get Player Camera Manager
→ Get Camera Rotation
→ Break Rotator
→ Yaw
→ 现有 Get Right Vector 和 Get Forward Vector
也就是删除这一段连接:
Get Control Rotation → Break Rotator
换成:
Get Player Controller
→ Get Player Camera Manager
→ Get Camera Rotation
→ Break Rotator
其余节点全部保留:
Action Value.X → Right Vector → Add Movement Input
Action Value.Y → Forward Vector → Add Movement Input
CMC 的这两项先不要改:
Orient Rotation to Movement = false
Use Controller Desired Rotation = true
关于Unity的(Unity实现多Pass貌似比较容易),虚幻增加Pass可没有那么容易(同样需要对引擎做出大量修改)
,GPU顶点动画通过将动画数据预烘焙到纹理,避免了CPU逐帧更新骨骼动画的开销,适合大规模实例化的动态物体(如毛发、植物),但需要平衡显存占用与动画精度的关系,
https://zhuanlan.zhihu.com/p/669728014

這個叫 AccuRIG 2,是 Reallusion/ActorCore 的自動骨骼綁定工具。它的核心用途就是:
overflow-visible!
靜態人形 Mesh
└ 定位人體關節
├ 身體關節
└ 手指關節
└ 生成 Skeleton 骨架
└ 將 Mesh 綁到骨架
└ 自動計算 Skin Weights
└ 用動作測試變形
└ 匯出到 Blender/Unity/Unreal
官方將它描述為把 FBX 或 OBJ 的靜態雙足模型轉成可動畫角色,支援 A Pose、T Pose、Relaxed A Pose,以及由多個 Mesh 組成的角色。编辑Reallusion Manual+2编辑Reallusion Manual+2
overflow-visible!
綠色圓點
└ 不是最終看見的骨骼
└ 是「關節位置標記」
├ 指根
├ 第一指節
├ 第二指節
├ 指尖
└ 拇指彎曲方向
你要把這些點放到模型真正的手指關節位置。AccuRIG 再根據這些點建立每根手指的骨骼鏈。官方特別要求關節點位於 Mesh 內部,並正確設定拇指方向,否則動畫時拇指可能產生扭曲。编辑Reallusion Manual+1
open visor(頭盔面罩打開)
visor /ˈvaɪzər/,意思是「面罩、遮罩」------特指安全帽、太空頭盔、頭盔上那塊可以掀開/放下的透明或不透明護罩:
- 太空頭盔的 visor 平時放下時會遮住整張臉(保護、密封用)
- open visor = 面罩處於「掀起、打開」的狀態,讓臉露出來
3/4 view(四分之三視角)
這是角色設計、人物插畫、3D建模領域裡非常標準的「視角」術語,指相機/觀看角度介於「正面」和「側面」之間:
- front view(正視圖)= 完全正對著角色,0度
- 3/4 view (四分之三視角)= 角色轉了大約 45度左右 ,你可以同時看到臉的正面輪廓和側面輪廓,兩隻眼睛都看得到,但鼻子、臉頰的立體感也能呈現出來
- side view / profile(側視圖)= 完全側面,90度,只看到一邊臉的輪廓
來自中文「荔枝」(粵語或官話發音音譯),經由早期貿易/植物學文獻進入英文,是少數直接從中文音譯 進入英文的水果名稱之一(類似的還有 ginseng 人參、kumquat 金橘)。
"spunik" 這個拼法我查不到對應的標準英文單字,你要問的應該是 Sputnik,能確認一下嗎?
Sputnik /ˈspʊtnɪk/ 或 /ˈspʌtnɪk/,重音在第一音節
意思:
專有名詞,指史普尼克1號 ------1957年10月4日蘇聯發射的人類史上第一顆人造衛星,這個事件被視為「太空競賽(Space Race)」的起點,也是冷戰時期美蘇科技競爭的重要標誌性事件(引發美國的「史普尼克危機/Sputnik crisis」,促成美國後來成立 NASA、加大科研教育投資)。
詞源:
來自俄文 спутник (sputnik),字面意思是「同伴、旅伴、伴隨運行的東西」,由:
- s-(俄文前綴,意為「與...一起」)
- put'(路、道路)
- -nik(表示「做...的人/物」的名詞字尾)
字面意思接近「一同上路的旅伴」,引申為「(繞行地球的)伴隨物、衛星」------這也是為什麼後來英文用 satellite 當通用詞,但 Sputnik 專指這第一顆衛星的專有名詞。
slop /slɒp/(英)或 /slɑːp/(美),一個音節。
意思:
1. 字面意思(名詞/動詞):
-
名詞:(尤指餵豬的)餿水、廚餘;不成形的濕黏液體
- pig slop(豬食、餿水)
-
動詞:(液體)濺出、灑出來(通常是不小心、邋遢地灑出)
-
The coffee slopped over the edge of the cup.(咖啡從杯緣濺了出來)
2. 引申義:劣質、隨便做的東西(帶貶義) -
slop = 粗製濫造的東西,尤其指低劣的食物或作品
- "This food is slop."(這食物根本是餿水等級的爛東西)
shovelware /ˈʃʌvəlwɛər/,重音在第一音節。
發音重點:
- shovel 讀 /ˈʃʌvəl/(shovel 的 o 讀短音 /ʌ/,跟你之前問過的 dub, study 同一個音)
- -ware 讀 /wɛər/(跟 software, hardware 的 -ware 發音一致)
意思:
名詞,帶貶義色彩,指:
1. (較早期用法)大量塞入光碟/合輯裡、品質參差不齊的軟體集合
早期(1990年代)遊戲雜誌常附贈試玩光碟,裡面塞滿一堆隨便湊數的試用版、低品質小遊戲軟體,像用鏟子(shovel)隨便鏟一堆東西塞進去一樣,故稱 shovelware
2. (現代常見用法)未經妥善調整、直接照搬到新平台的低品質移植版
尤其常見於遊戲產業 ------把一款遊戲不加修改或只做最低限度調整,就直接「鏟」到另一個平台上發售(例如把PC遊戲隨便移植到手機,操作介面、畫質完全沒優化,只是求快、求省成本),品質往往很差:
- "That mobile port is just shovelware."(那個手機移植版根本是隨便做做的爛貨)
shovel (鏟子;動詞:鏟、剷)+ -ware(軟體,如 software, hardware, malware)
字面意思就是「用鏟子隨便鏟出來的軟體」------形象地描繪出「大量、隨便、不用心製作,只是為了填量、賺快錢」的產品特性。
- pitching an idea(推銷一個點子)、pitching to investors(向投資人做提案簡報)
- "kind of pitching" = 「有點像在推銷/提案」的意思------例如形容某段話聽起來像業務話術、像在賣東西
Painter 的项目文件通常是:
overflow-visible!
.spp
而 FBX 是创建项目时使用的 3D Mesh 输入文件 。Adobe 官方流程也是:新建项目后,在 File 项目设置中选择一个 3D Mesh,而不是直接用 Open 打开 FBX。编辑Adobe Help Center
正确操作:
overflow-visible!
File
└ New...
├ Template:PBR - Metallic Roughness
├ File
│ └ Select
│ └ 选择 Androlina.fbx
├ Document Resolution:2048 或 4096
├ Normal Map Format:DirectX
└ OK
因为你主要用于 Unreal Engine,所以这里选:
overflow-visible!
Normal Map Format
└ DirectX
项目创建后,再使用:
overflow-visible!
File
└ Save As...
└ Androlina.spp
以后才能通过:
overflow-visible!
File > Open
打开这个 .spp 项目。
即使模型有很多材质,也不代表它使用 UDIM。Painter 会依据 FBX 中的 Material Slot,为每个材质建立一个独立的 Texture Set;不同材质的 UV 可以分别占据各自的 0--1 空间。
D View 的 Material 模式会显示带光照的最终材质结果,而不是单独的颜色通道。

overflow-visible!
<Project>/Config/DefaultEngine.ini
加入:
overflow-visible!
INI
[ConsoleVariables]
r.SomeVariable=1
例如:
overflow-visible!
INI
[ConsoleVariables]
r.Lumen.Reflections.Allow=1
这种也是永久保存在项目里的,但它的来源会是:
overflow-visible!
LastSetBy: SystemSettingsIni
而不是 ProjectSetting。它的优先级还比 ProjectSetting 高一级。
Lumen Reflection └─ ① Screen Trace └─ ② Mesh Distance Field Trace └─ ③ Global Distance Field Trace └─ ④ Hardware Ray Tracing
Global Distance Field Trace
overflow-visible!
大量 Mesh Distance Fields
└─ 合并
└─ Camera-centered Global Distance Field clipmaps
它把周围大量物体的距离场合并成较低分辨率的全局三维场。
优点:
- 追踪非常快
Hardware Ray Tracing
overflow-visible!
Ray
└─ TLAS
└─ BLAS
└─ 实际三角形
这是真正通过 GPU Ray Tracing 硬件,对 Ray Tracing Scene 中的三角形求交。
优点:
-
几何命中最准确
-
支持 Skeletal Mesh
-
更适合镜面反射
-
可以使用 Hit Lighting
-
可以支持多次镜面反射反弹
缺点:
-
每帧维护 TLAS
-
动态 Skeletal Mesh 需要更新加速结构
-
大量重叠实例会显著增加成本
-
Nanite 通常追踪其 Ray Tracing representation 或 fallback representation,而不是简单等同于屏幕上的完整 Nanite 三角形
Epic 将 Hardware Ray Tracing 描述为获得高质量镜面反射的必要路径,但也指出它有更高的场景更新和追踪成本。编辑Epic Games Developers+1
硬件光追本身通常已经决定了几何命中方式:
overflow-visible!
Reflection Ray
└─ Ray Tracing acceleration structure
└─ Triangle intersection
Surface Cache 和 Hit Lighting 并不是两种不同的三角形求交方法。它们主要决定:
射线撞到三角形后,如何取得这个位置的材质与光照。
Surface Cache
overflow-visible!
Hardware Ray
└─ 命中三角形
└─ 查询 Lumen Surface Cache
└─ 返回缓存的材质与光照
它仍然使用硬件光追寻找交点,但命中后不在那里完整重新计算材质和灯光,而是读取 Lumen 已经缓存的表面光照。
优势:
-
性能较好
-
可以复用 Lumen GI 已经维护的缓存
-
适合非严格镜面的反射
Hit Lighting
overflow-visible!
Hardware Ray
└─ 命中三角形
└─ 在命中位置评估材质
└─ 计算命中位置的直接光、阴影等
不再主要依赖简化的 Surface Cache,而是在射线的真实命中点计算材质和光照。

Lumen 默认先做 Screen Trace;找不到命中,或者射线进入屏幕无法表达的位置,才使用软件或硬件光追。
vacant eyes / vacant stare(空洞的眼神,強調「裡面空空的、沒有情感」)
dull eyes / lifeless eyes(死氣沉沉的雙眼,強調缺乏生氣、光彩)
"His eyes were glazed over, empty of any emotion."(他的雙眼變得呆滯,毫無情感)

非nanite的几何体,距离远会去掉阴影 r.shadow.virtual.usefarShadowCulling 0,多远都 照样显示(nonono)
还有第二重机制,阴影面积剔除机制,r.shadow.virtual.RadiusThreshold 0,现在面积大小也不会限制显示,才对了

光追阴影和nanite的适配存在这样的问题,,如果是特别光滑的小球,就是有这样的黑斑,can't avoid

https://www.bilibili.com/video/BV1Rw4m1e7ka?



纹理采样造成的显存带宽消耗,也叫 Texture Bandwidth。
贴图加载进显存后,不需要每帧从硬盘或系统内存上传;但每一帧渲染时,像素着色器都要从显存读取贴图数据,所以仍然持续消耗 GPU 显存带宽。


UE 5.7/5.8 默认就是按线性渲染流程工作的 。只是 UE 不像 Unity 那样提供一个显眼的 Color Space: Gamma / Linear 总开关,而是把颜色管理拆成输入、计算、输出三层。
overflow-visible!
UE 颜色流程
├─ 纹理输入编码
│ ├─ sRGB = On:颜色贴图
│ │ └─ 采样时 sRGB → Linear
│ └─ sRGB = Off:数据贴图
│ └─ 按原始线性数值读取
├─ 渲染工作空间
│ └─ 在线性空间中进行光照、混合和材质计算
└─ 屏幕输出
└─ Linear HDR → Tonemapper → sRGB/HDR 显示输出
1. UE 中的 Linear
UE 的场景光照和大部分渲染计算使用线性数值。代码层面的 FLinearColor 就是每通道 32 位浮点的线性色彩;Tonemapper 前的 Scene Color 也是 HDR 场景颜色。编辑Epic Games Developers+1
因此你截图中的两种情况,UE 正常采用的是上面的逻辑:
overflow-visible!
光照强度 × 0.5
光照强度 × 1.0
光照强度 × 1.5
这些乘法先在线性空间完成,而不是直接拿屏幕上的 sRGB 数字做乘法。
颜色贴图开启 sRGB 后,GPU 采样时会将存储的 sRGB 值解码为线性值,再交给 Material 和光照系统。Mask 等数据贴图不应进行 Gamma 校正,所以需要关闭 sRGB。编辑Epic Games Developers+1
8位图因为储存空间较少,因此使用的就是Gamma校正过的颜色。在只有8bit的情况下,没有必要在亮部浪费过多,这样就可以表现更多暗部的细节变化,所以实际亮度只有0.2经过gamma校正后被编码成了0.5的像素值。
sRGB编码是为了增加8位颜色的精度,
8 位代码 128 └─ 编码数值约 0.502 └─ 实际代表的线性光约 0.216
显示器收到的数据不一直是 8 位
传统 SDR PC 管线中,最常见的确是:
overflow-visible!
R:8 bit
G:8 bit
B:8 bit
总计:24 bit
这也是 sRGB 与普通 PNG/JPEG、桌面 SDR 显示长期紧密关联的原因。
但今天并不总是 8 位:
overflow-visible!
显示输出
├─ 8-bit SDR
├─ 8-bit + FRC / Dithering
├─ 10-bit SDR
├─ 10-bit HDR10
└─ 某些专业管线中的 12-bit
Windows 11 的 Advanced Color 管线可以根据显示器能力支持每通道 10 位及以上;Direct3D 的 HDR 示例也明确使用 10 位 Swap Chain 输出 HDR10。
最终 Swap Chain ├─ SDR:常见 8-bit └─ HDR10:通常 10-bit
UE 游戏内部截图
普通游戏截图通常接近这条链:
overflow-visible!
UE 场景渲染
└ SceneColor
└ FP16 线性 HDR
└ UE Post Process
├ Exposure
├ Bloom
├ Color Grading
└ Tone Mapper / Output Device
↓
最终显示用 Render Target
或 Swap Chain Back Buffer
↓
GPU Copy 到 Readback Resource
↓
GPU → CPU Readback
↓
FColor / FFloat16Color 数组
↓
PNG / JPEG / EXR 编码
↓
硬盘文件
这里的Direct3D职责是:
overflow-visible!
Direct3D / RHI
├ 执行渲染
├ 执行Texture Copy
├ 将GPU资源复制到可读回资源
└ 让CPU取得像素
如果截图直接读的是中间Render Target,那么甚至不需要先Present,也不需要经过DXGI显示链。
Texture中存的是sRGB编码值 └ GPU以sRGB SRV读取 └ 自动执行sRGB → Linear └ Shader得到线性颜色
截圖 └ 取決於它抓的是線性Render Target,還是已編碼的最終畫面
关掉
sRGB后,UE 不再把贴图里的 8-bit 数值当成"已经做过 sRGB 编码的颜色",而是直接把这些数值当成线性数据使用。
因此暗部会变亮、黑暗区域看起来减少。


Gamma 全部放到后面,只回答一件事:
"线性光里的 0、0.1、0.5、1,到底是谁定义的?"
答案是:
人类定义了坐标单位和参考点,但"线性"本身来自光能的物理可加性,不是根据人眼感觉任意画出来的。
1. "线性"首先是什么意思
假设有一盏灯照到某个位置,测得光的物理强度为 LLL。
再放一盏完全相同的灯,在理想条件下:
overflow-visible!
一盏灯
└ 光量 = L
两盏相同的灯
└ 光量 = L + L = 2L
把灯功率降到原来的十分之一:
overflow-visible!
原来的光
└ L
十分之一功率
└ 0.1L
这就是这里的"线性":
overflow-visible!
数值乘以2
└ 对应物理光贡献乘以2
两个光相加
└ 对应数值直接相加
不是人类先观察"看起来亮了多少",再规定它叫线性;而是光的辐射功率、光谱功率等物理量本身可以测量、相加和按比例缩放。
"相对线性光"的相对是什么意思
计算机里通常不愿意一直保存:
overflow-visible!
8 cd/m²
37 cd/m²
486 cd/m²
于是选择一个参考值作为 1.0:
Lrelative=LphysicalLreferenceL_{\mathrm{relative}}=\frac{L_{\mathrm{physical}}}{L_{\mathrm{reference}}}Lrelative=LreferenceLphysical
假设参考白是100 nit:
overflow-visible!
物理亮度 0 nit
└ 相对值 0.0
物理亮度 10 nit
└ 相对值 0.1
物理亮度 50 nit
└ 相对值 0.5
物理亮度 100 nit
└ 相对值 1.0
所以:
线性值0.1不是"人感觉到10%的亮度",而是"这个物理光量是参考光量的十分之一"。
Buffer里的数字,与眼睛实际收到多少物理光,并不存在天然、永远固定的一一对应关系。
调节显示器亮度时,Swap Chain里的像素可能完全没有变化,但显示器发出的光已经变化了。
在Tone Mapping之前,数值描述的是场景内部的相对光贡献,并不意味着"显示器应当发出多少nit"。
场景数据大部分独立计算;到了最终输出阶段,UE才可能根据系统报告的显示能力,选择如何编码最终画面。DXGI正是UE获得显示能力、建立Swap Chain并把结果交给Windows显示系统的接口层。
不会因为换了一台显示器,就重新改变:
overflow-visible!
模型顶点
材质BRDF
阴影计算
Lumen追踪
GBuffer
大部分场景光照
这些仍然按照UE内部的线性HDR渲染流程计算。
DXGI建立Swap Chain └ Swap Chain提供Back Buffer └ D3D12把它当Render Target └ GPU直接写入这个Buffer └ DXGI Present同一个Buffer
开启HDR
此时UE可能先检查:
overflow-visible!
当前系统是否支持HDR
显示器是否处于HDR模式
目标亮度是多少
Swap Chain支持什么Color Space
然后改变最终输出路径:
overflow-visible!
同一个线性SceneColor
├ SDR模式
│ └ Tone Map到SDR
│ └ 8-bit sRGB Back Buffer
│
└ HDR模式
└ Tone Map到目标HDR范围
└ PQ 10-bit或scRGB FP16 Back Buffer
DXGI的CheckColorSpaceSupport用于检查某个Swap Chain是否支持指定颜色空间;SetColorSpace1则声明该Swap Chain正在使用什么颜色空间。编辑Microsoft Learn+1
关键的是区分"查询"和"传递"
overflow-visible!
能力查询方向
显示器 → Windows/驱动 → DXGI → UE
overflow-visible!
画面数据方向
UE → D3D12/GPU → DXGI Present → Windows/驱动 → 显示器
DXGI ├ 向上:告诉应用有哪些Adapter、Output和Color Space能力 └ 向下:管理Swap Chain,把完成的Back Buffer提交给显示系统



UE 这里为什么是一个全局 SM6
截图里的路径是:
overflow-visible!
Project Settings
└ Platforms
└ Windows
└ D3D12 Targeted Shader Formats
└ SM6
它表达的是:
Windows 的 D3D12 构建,要不要生成并使用
PCD3D_SM6这套平台 Shader Format。
勾选以后,UE会按SM6目标编译这套项目中需要的引擎Shader、材质Shader和各种Permutation。Nanite、Virtual Shadow Maps,以及当前版本中的一些Lumen硬件光追路径也依赖D3D12 + SM6。UE从5.1开始,新建PC项目默认启用D3D12 SM6。编辑Epic Games Developers+1
Unity 2022.3默认目标大约是2.5,开发者可以用#pragma target提高某个Shader所需的功能级别,也可以用#pragma require单独要求某项GPU能力。编辑Unity Documentation+1
例如:
overflow-visible!
hlsl
#pragma target 4.5
大致表示这段Shader可以使用:
overflow-visible!
整数与位运算
├ Compute Shader相关能力
├ Random Write / UAV
├ 更多纹理与Buffer功能
└ 较高阶GPU能力
overflow-visible!
ShaderLab
└ #pragma target
└ #pragma require
这是单个Shader的最低能力要求。
Unity为什么没有相同的"全项目只勾SM6"
Unity的#pragma target 4.5、5.0是Unity自己的跨平台能力集合名称 。它们参考DirectX Shader Model分级,但不与DirectX的SM版本完全一一对应。Unity会再把同一份HLSL编译给D3D、Vulkan、Metal等不同后端。Unity官方也特别说明,Unity Shader Target与DirectX Shader Model、OpenGL版本相似,但不是完全对应关系。编辑Unity Documentation
所以在Unity 2022.3里,你常见的最高写法通常是:
overflow-visible!
hlsl
#pragma target 5.0
而不会简单地写:
overflow-visible!
hlsl
#pragma target 6.0
这不等于Unity的D3D12后端"只能使用DX11"或者"完全没有SM6相关能力";只是Unity 2022.3暴露给ShaderLab作者的跨平台target分级体系停留在这一套名称上。

What is an SM in an NVIDIA GPU?
The basic building block of a GPU is called a streaming multiprocessor (SM) on NVIDIA GPUs or a compute unit (CU) on AMD GPUs (they're the same idea and we'll refer to them both as SM).
SM6 不是 Streaming Multiprocessor 6,而是:
SM6
└ Shader Model 6
DirectX / UE 语境
└ SM6 = Shader Model 6
NVIDIA CUDA / GPU 架构语境
└ SM = Streaming Multiprocessor
├ 一块实际的硬件执行单元
└ 例如"这张GPU有多少个SM"
Epic 在这个设置中明确使用的是"Shader Model 6 Targeted Shader Formats"。编辑Epic Games Developers NVIDIA 文档里的 SM 才是执行线程和计算指令的 Streaming Multiprocessor。编辑NVIDIA Docs+1
DXIL到厂商GPU机器指令的转换
如果新系统使用Vulkan,厂商需要提供适应该系统的Vulkan驱动和内核接口。Vulkan本来就是跨平台API,其Loader可以在Windows、Linux及其他平台上连接到厂商实现;AMD、Intel、NVIDIA等厂商也共同参与规范并提供通过一致性测试的驱动。编辑The Khronos Group+2编辑Khronos Registry+2
于是可能变成:
overflow-visible!
UE / 游戏
└ Vulkan
└ Vulkan Loader
└ NVIDIA / AMD / Intel Vulkan Driver
└ 新操作系统Kernel GPU接口
└ 同一块GPU
Vulkan与DirectX的组织差别
DirectX大致是:
overflow-visible!
Microsoft拥有并主导规范
└ 与NVIDIA、AMD、Intel、游戏和引擎厂商协作
Vulkan则是:
overflow-visible!
Khronos工作组共同制定
├ AMD
├ NVIDIA
├ Intel
├ Arm
├ Qualcomm
├ Samsung
├ 引擎厂商
└ 其他成员
Khronos对Vulkan早期开发的描述就是多个产业成员和游戏引擎厂商共同提交方案、共同建立规范;当前Vulkan一致性测试也由多家硬件实现共同遵循。编辑The Khronos Group+1
因此,谁承担主要工作可以最终定为:
overflow-visible!
DirectX公共标准与Windows整合
└ Microsoft主力
特定GPU的Driver、Compiler和硬件落地
└ NVIDIA / AMD / Intel主力
新Feature从需求到标准化
└ Microsoft、GPU厂商、引擎与开发者合力
Apple 的戰略中心明確是 Metal。Apple把 Metal稱為 Apple 平台硬體加速圖形與計算的基礎,並已用它取代 OpenGL、OpenGL ES 和 OpenCL;最新 Metal 4 甚至直接強調,讓開發者更容易從 DirectX 和 Vulkan 遷移到 Apple 平台。编辑Apple Developer+2编辑Apple Developer+2
MoltenVK 是 Khronos 託管的 Vulkan Portability 實作,它不是 Apple 平台上的原生 Vulkan 驅動,而是把 Vulkan 功能疊在 Metal 之上。現在它已覆蓋到 macOS、iOS、tvOS、visionOS 等 Apple 平台。
Vulkan └ Khronos多方共同制定
但每个平台是否原生支持,取决于系统厂商
Grande 是義大利姓氏(Ariana Grande 本人是義大利裔美國人),義大利文裡字尾的 e 通常都要發音,不像英文/法文那樣常常字尾 e 不發音。所以她的姓氏保留了義大利文的發音習慣,讀作 /ˈɡrændi/(GRAN-dee),而不是英文母語者直覺會唸的 /ɡrænd/(跟 grand 同音)。
用你之前學過的規律拆解:
- visual 的 -sual 讀 /ʒuəl/(濁音),這跟你之前問過的 usual, casual 是同一套規則(s + u + al → /ʒuəl/)
- virtual 的 -tual 讀 /tʃuəl/(這跟你問過的 spiritual 是同一套規則,t + u + al → /tʃuəl/)
VT是「看見以後才知道要載入」
它的基本流程是:
overflow-visible!
第N幀
└ GPU發現畫面需要某些Tile
└ 寫入Feedback
CPU讀取Feedback
└ 從磁碟載入Tile
後續幀
└ 高解析度Tile才出現
因此快速轉鏡頭或突然靠近角色時,可能先看到低解析度Tile,再補成高清。


1963年的Sketchpad已经是交互式计算机图形系统;20世纪60年代也已经出现实时图形飞行模拟器。因为用户在移动光笔、操纵杆或视角以后必须马上看到响应,"多久生成下一张图"从一开始就是系统定义的一部分。
1983年的MipMap就是非常早的典型:预先保存多级低分辨率图像,渲染时根据屏幕覆盖范围选择合适级别。它同时减少走样,并避免为一个屏幕像素读取大量无意义的高频细节。也就是说,图形行业很早就在做"为了最终屏幕结果,只计算当前真正需要的信息"。编辑cs.cmu.edu
一条很清晰的技术祖先链:
1983:MipMap
└ 一张纹理保存多个分辨率
1990年代:大型图像与地形数据库
└ 整张图开始大到无法全部放入纹理内存
1998:Clipmap
└ 论文标题直接叫"A Virtual Mipmap"
└ 只维护观察位置附近需要的Mip区域
2000年代:可编程Shader、硬盘流送、GPU反馈
└ 软件Virtual Texture/MegaTexture
之后:API Sparse Resource与硬件分块支持
└ 页映射逐渐下沉到驱动和GPU能力
现代引擎
└ UE把Page Table、反馈、物理池和Cook流程封装起来
SGI研究人员在1998年的Clipmap论文中,已经明确提出"虚拟MipMap":完整图像可以远大于硬件纹理内存,但每帧只保留所需子集。编辑graphicon.ru
后来的软件Virtual Texture则把它推进到游戏世界:纹理以稀疏形式存在,绝大多数数据留在二级存储中,运行时只让所需区域以相应分辨率进入显存。编辑researchgate.net+1
后来资产规模和显存矛盾变大 └ 可编程GPU和存储系统成熟 └ 技术开始值得使用
早期 └ 算术能力不足 └ 少画线、少画面、少做光照 后来 └ 三角形吞吐成为核心 └ 剔除、LOD、批处理 再后来 └ Shader能力增强 └ 更多逐像素材质和后处理 纹理规模继续扩大 └ 显存容量与带宽成为瓶颈 └ Mip Streaming、Texture Atlas、Virtual Texture 今天 └ 几何和材质规模都继续扩大 └ Nanite、Virtual Shadow Maps、VT、GPU Driven Rendering