运行模型:代理 DLL、Hook 与 HackerDevice
3Dmigoto 不是传统意义上的“资源包加载器”。它的核心工作方式是把自己放进游戏的 DirectX 调用链里,让游戏仍然以为自己在调用系统的 `d3d11.dll` / `dxgi.dll`,但关键对象已经被 3Dmigoto 包装过。换句话说,MOD 能生效不是因为游戏主动支持 MOD,而是因为 3Dmigoto 截获了渲染 API 的对象创建、资源绑定、着色器绑定和 draw call。
运行模型:代理 DLL、Hook 与 HackerDevice
3Dmigoto 不是传统意义上的“资源包加载器”。它的核心工作方式是把自己放进游戏的 DirectX 调用链里,让游戏仍然以为自己在调用系统的 d3d11.dll / dxgi.dll,但关键对象已经被 3Dmigoto 包装过。换句话说,MOD 能生效不是因为游戏主动支持 MOD,而是因为 3Dmigoto 截获了渲染 API 的对象创建、资源绑定、着色器绑定和 draw call。
这也是为什么基于 XXMI / GIMI / WWMI / EFMI / APMI 的角色 MOD,本质上都要回答同一个底层问题:某个游戏角色在 GPU 管线中被拆成哪些 DrawIndexed、绑定了哪些 VB/IB/SRV/CB/UAV、当前 VS/PS/CS hash 是什么,以及我们能在哪个时机把原资源换成自定义资源。
DLL 入口不是终点,而是接管入口
在 D3D11 路径中,DirectX11/D3D11Wrapper.cpp 定义了全局 Globals StaticG 与 Globals *G,并在 InitializeDLL() 中完成配置读取、目标进程检查、NVAPI 初始化、日志初始化等步骤。这里有一个容易被忽略的点:InitializeDLL() 并不等于“MOD 已经生效”,它只是让 3Dmigoto 具备接管后续 D3D11 对象的条件。
关键逻辑包括:
| 层级 | 本地源码位置 | 作用 |
|---|---|---|
| DLL 包装层 | DirectX11/D3D11Wrapper.cpp | 伪装成 DirectX DLL,加载配置,转发真实系统 DLL 调用 |
| 设备包装层 | DirectX11/HookedDevice.cpp | hook ID3D11Device1 vtable,把游戏创建资源/着色器的调用导向 3Dmigoto |
| 上下文包装层 | DirectX11/HookedContext.cpp | hook ID3D11DeviceContext1 vtable,把 draw、bind、clear、dispatch 等上下文调用导向 3Dmigoto |
| 高层设备逻辑 | DirectX11/HackerDevice.* | 创建/替换资源、创建 shader、管理 swap chain 与资源池 |
| 高层上下文逻辑 | DirectX11/HackerContext.* | 在 draw 前后运行 override、记录当前管线状态、执行命令列表 |
HookedDevice.cpp 与 HookedContext.cpp 里都能看到同一种设计:文件开头明确说自己只负责 hook 和转发,真正功能交给 HackerDevice / HackerContext。这说明 3Dmigoto 的架构有两层:底层负责“把调用截住”,上层负责“解释 ini 并改变渲染状态”。
vtable hook:为什么游戏没有感知到 MOD 逻辑
D3D11 的对象是 COM 接口。游戏拿到的是 ID3D11Device、ID3D11DeviceContext、IDXGISwapChain 这样的接口指针。3Dmigoto 的做法是替换或者包装这些接口的 vtable,使游戏调用:
context->DrawIndexed(...)
context->PSSetShaderResources(...)
device->CreateTexture2D(...)时,实际先进入 3Dmigoto 的包装函数。包装函数可以选择:
- 直接转发给原始 D3D11 对象。
- 在转发前记录当前状态。
- 在转发前替换资源或 shader。
- 跳过原 draw,改为执行自定义 draw。
- 在转发后恢复状态或执行 post command。
这就是“无源码 MOD”的根基。游戏没有开放角色换模接口,但 GPU 渲染调用必须经过 D3D11;3Dmigoto 在这个边界层工作。
HackerContext 是 draw call 的闸门
角色 MOD 最关心的是 draw call。DirectX11/HackerContext.cpp 中可以看到多个 draw 包装函数,例如:
HackerContext::DrawIndexed(...)
HackerContext::Draw(...)
HackerContext::DrawIndexedInstanced(...)
HackerContext::Dispatch(...)这些函数的基本结构是:
DrawContext c = DrawContext(...);
BeforeDraw(c);
if (!c.call_info.skip)
mOrigContext1->DrawIndexed(...);
AfterDraw(c);这段结构非常重要。它解释了 3Dmigoto 的 MOD 不是“替换一个文件就结束”,而是围绕一次 draw call 建立了前后两个执行窗口:
| 时机 | 可以做什么 |
|---|---|
BeforeDraw | 查找当前 shader hash、运行 [ShaderOverride] pre command、执行 handling = skip、绑定自定义 VB/IB/Texture |
| 原始 draw | 如果没有被 skip,则游戏原 draw 正常执行 |
AfterDraw | 运行 post command,恢复被临时修改的 shader / stereo / resource 状态 |
很多“模型消失”“F6 关闭后异常”“只隐藏不重绘”等问题,都是因为这个三段结构没有被正确处理:pre 阶段 skip 了原 draw,但没有在同一状态下补 draw;或者 post 阶段没有把被替换的资源恢复;或者开关变量只控制了部分 command list。
ShaderOverride 为什么能驱动角色 MOD
BeforeDraw() 会查找当前绑定的 VS/HS/DS/GS/PS hash,并在 G->mShaderOverrideMap 中寻找匹配项。命中后会调用:
ProcessShaderOverride(&i->second, isPixelShader, &data);而 ProcessShaderOverride() 的核心是:
RunCommandList(mHackerDevice, this, &shaderOverride->command_list, &data->call_info, false);所以 [ShaderOverrideXXX] 的意义不是“替换 shader”这么简单。它实际上是“当某个 shader hash 出现在当前 draw 管线中时,运行一段命令列表”。这段命令列表可以检查纹理、替换资源、跳过 draw、执行自定义 shader、重新 draw。角色 MOD 通常把 VS hash 当作角色部件的入口,是因为角色的 VS 更稳定地描述了该 mesh 的几何处理路径。
Device hook 负责资源生命周期
如果只截 draw,不截资源创建,就无法知道纹理 hash、VB/IB hash,也无法安全创建自定义资源。HookedDevice.cpp 包装了 CreateBuffer、CreateTexture1D/2D/3D、CreateShaderResourceView 等函数。3Dmigoto 借此在资源创建时记录 descriptor、初始数据 hash、资源句柄关系。
这解释了一个常见现象:同样是 TextureOverride hash = xxxxxxxx,有时能匹配,有时不能匹配。因为 hash 不只是文件名,它与资源创建时的描述、初始数据、后续污染追踪都有关。若游戏在运行中更新纹理、复制资源、动态生成 RenderTarget,3Dmigoto 需要额外追踪才能保持 hash 可解释。
从蓝原/鸣潮/终末地工作流看底层共性
我们当前围绕蓝原构建 APMI,本质上是在给一个 Unity 游戏建立 3Dmigoto 语义层:
| 高层工具概念 | 3Dmigoto 底层概念 |
|---|---|
| Component | 一组能被识别的 draw / IA 状态 / IB 范围 |
| FrameAnalysis | FrameAnalysisContext 对 draw、shader、resource 的 dump |
| 导出 MOD | 生成 [Resource]、[TextureOverride]、[ShaderOverride]、[CommandList] |
| 热重载 | 重新读取 ini,刷新 command list 与 override map |
| F6 开关 | 全局变量 / key binding / condition 控制命令列表路径 |
| 模型消失 | 原 draw 被 skip,但替代 draw 没有正确执行或资源绑定不完整 |
如果要真正理解 MOD 为什么有效,不能只看生成的 .ini。必须把 .ini 看成 3Dmigoto 在 BeforeDraw -> Draw -> AfterDraw 中执行的一套脚本。
参考
- 3Dmigoto 开源仓库:https://github.com/bo3b/3Dmigoto
- 3Dmigoto 官方 Wiki:https://github.com/bo3b/3Dmigoto/wiki
- 本地源码:
F:\Code\3Dmigoto-master\DirectX11\D3D11Wrapper.cpp - 本地源码:
F:\Code\3Dmigoto-master\DirectX11\HookedDevice.cpp - 本地源码:
F:\Code\3Dmigoto-master\DirectX11\HookedContext.cpp - 本地源码:
F:\Code\3Dmigoto-master\DirectX11\HackerContext.cpp