自由学习记录(210)

纯原生 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 == 1GS:[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_INFORMATIONPROCESS_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.dllGhidra 项目数据库中导入的副本/分析对象,不是游戏原目录里正在使用的那份 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+1

只下載公開 GitHub 倉庫,得到的是:

overflow-visible! 复制代码
复制代码
公開部分
├─ 怎樣解析已經拿到的 YAML
├─ 怎樣解碼 Mesh
├─ 怎樣轉座標系
├─ 怎樣建立 Blender/Painter 資產
└─ 怎樣呼叫 Ruri.RipperHook.dll

缺失部分
└─ Ruri.RipperHook.dll 本身及特定遊戲 Hook

這就是他稱為「半開源」的原因。更精確的術語應該是:

開源前端依賴閉源或受限制的後端元件。

名稱雖然叫 Hook,但公開資訊顯示它更像是對 AssetRipper 資產解析流程的橋接、修補或遊戲特化模組。是否另有進程注入行為,要看到未公開 DLL 才能確認。

GitHub

  • Patch Instruction 后,Listing 左侧通常有紫色补丁标记;可以通过菜单查找已修改字节。
  • 项目可能创建恢复快照,但它主要用于崩溃恢复,不等同于可长期浏览的版本历史。
  • 导出的 DLL 也不会携带一份"Ghidra 修改记录"。

查看当前有哪些字节被改过,可以尝试:

复制代码
Search → For Matching Instructions

不过更可靠的是检查 Ghidra 的补丁表。在不同版本中通常位于:

复制代码
Window → Bytes

或搜索 Memory Map 中的 byte modifications。你截图中 0x180027EA6 左侧的紫色图标就表示该位置有用户修改。

原来这里是 74 0FJZ),现在游戏目录里的 DLL 已经是 75 0FJNZ)。因此不是只在 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

180781A0CF 后,Ghidra 可能创建:

overflow-visible! 复制代码

C++

复制代码
undefined8 FUN_180781a0c(...)
{
    ...
}

这并不是运行程序,也不是修改原始 DLL 指令。它主要修改的是 Ghidra 项目中的分析标记

overflow-visible! 复制代码
复制代码
F 创建 Function
├─ 定义函数入口
├─ 定义函数范围
├─ 生成默认函数名
├─ 让 Decompiler 尝试生成伪代码
├─ 建立 callers / callees
└─ 改善参数、局部变量和栈分析

F 是上下文相关的。在数据区域,F 也可能用于在 floatdouble 数据类型之间循环;而工具栏中的字母 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 节点上已经完成原生化。

如果希望以后复制到别的材质:

  1. 选中新的 Value 节点。

  2. Ctrl+G 创建节点组。

  3. 将节点组命名为:

    帧时间_按比例输出

但要注意:复制节点组时 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

原节点的 Scale0.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​=Lreference​Lphysical​​

假设参考白是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.55.0Unity自己的跨平台能力集合名称 。它们参考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

相关推荐
MartinYeung51 小时前
[论文学习]PACT:溯源感知能力合约——面向智能体安全的参数级溯源
人工智能·学习·安全
小O的算法实验室3 小时前
AAAI-26,全局相关性学习:通过谱图神经网络增强进化算法
神经网络·学习·算法
宁风NF3 小时前
JavaScript:网络请求与前端通信
开发语言·前端·javascript·网络·学习·ecmascript
神神道呵he4 小时前
[深度学习] 大模型学习-RAG技术全景解析
人工智能·深度学习·学习
MartinYeung54 小时前
[论文学习]AgentSafetyBench:大语言模型智能体安全评估基准深度分析
学习·安全·语言模型
星夜夏空995 小时前
C++学习(13)——数据库的使用(MySQL)
数据库·c++·学习
艾莉丝努力练剑6 小时前
【Linux:动静态库】Linux 动静态库与可执行文件
linux·运维·服务器·学习·面试·文件系统·动静态库
宁风NF6 小时前
JavaScript:内存、垃圾回收、性能优化
开发语言·前端·javascript·学习·性能优化·es6
凉、介7 小时前
Armv8 原子性:体系结构保证与编程语义
linux·学习·嵌入式·arm·原子操作·atomic