这篇教程的理论基础仍然建立在第一篇教程上。如果你想自己造轮子,那我相信,只要你把这三篇帖子全部搞明白,就完全有能力实现自己想要的任何效果(3DMigoto 的功能真是太强大了)。
如果你只想直接使用 INI,不想过多了解底层原理,那我建议你直接跳转到具体操作_ini一节。
需要注意的是,笔者水平有限,且这套架构未经极其海量的测试,如遇到各种奇怪的错误,笔者由于时间有限,也许不能一一解决。不过不必担心,如果读者能全部吃透这三篇帖子(主要为本篇与第一篇),完全有能力自行排错。
本篇帖子的原理虽然基于第一篇,但是思路刚好相反。它们是两种完全不同的方法,相互独立,读者也可自行将其与第一篇的思路进行组合。本篇帖子不强制要求基于 SMT 还是 EFMI 这种 Mod 制作工具,而是旨在使用 3DMigoto 的语法,手搓 INI与 HLSL 来实现跨网格的骨骼矩阵替换。
第一篇帖子的思路是:在渲染衣服的部分,使用衣服的骨骼,但强行替换上皮肤的着色器(PS),以实现衣服的材质与皮肤一致。
而本篇帖子的思路是:在渲染皮肤(手部)的部分,强行注入并使用衣服的骨骼矩阵,继续使用皮肤自身的着色器,从而实现完美无色差的渲染。
可以看到这两种方法殊途同归,各有优劣:
替换 PS 法(第一篇):更灵活,但是难度极高(不仅要换 PS,还要手动对应 PS 绑定的所有动态、静态资源区和常量缓冲区),下限较低,极其容易黑屏。
替换 VS 骨骼法(本篇):效果下限极高(完美保留了原生材质的描边、高光等一切渲染特性,绝对没有色差),但是门槛较高,需要对底层机制都有所涉猎。
使用本方法后,能极大程度地还原正确的渲染效果(包括描边),基本上看不出任何色差。
接下来讲的是比较硬核的内容,我会对《终末地》的渲染流水线进行补充说明,并详细解释骨骼替换的原理。建议你在阅读之前至少满足以下条件,否则你会感觉在看天书:
1.阅读过我第一篇的帖子,至少了解《终末地》渲染管线的基础流程。
2.有最基础的代码能力,至少能看得懂 AI 写的代码。
3.对 3DMigoto 的语法有一定了解,至少需要会手写 TextureOverride 跟 Resource。
4.能够制作一个完整的人物 Mod(已解决接缝权重问题,并在 Blender 中处理完毕)。
5.有基础的图形学知识(例如看过 GAMES101 的前两节课)。
使用该方法后,能极大程度地拥有正确的渲染效果(包括描边),基本上看不出来色差,具体效果如下:


本篇文章是基于你已经做好了人物的mod,已经解决接缝权重问题,但是却无法解决接缝色差问题的基础上,解决色差问题的,所有在blender的操作一律不讲。这里需要你的手的模型在手的ib能正常生成模型,衣服的模型能在衣服的ib生成模型,这种状态即可。
- 依然先解释一些术语
b (Constant Buffer):常量缓冲(CBV),存放摄像机、偏移量等全局变量。
t (Shader Resource View):着色器资源视图(SRV),只读的纹理或大数组。
s (Sampler):采样器。
u (Unordered Access View):无序访问视图(UAV),支持 GPU 并发读写的缓冲区。
cs (Compute Shader):计算着色器,与渲染流水线并行执行,我们用来搬运数据的核心。
Ring Buffer(环形缓冲区):一种线性数据结构,把普通数组的首尾“连起来”形成一个环。里面的数据每帧都会向前追加,写到尾部悬崖时再重新从头部覆写。
TAA (Temporal Anti-Aliasing):时间抗锯齿。TAA 极度依赖正确的“历史帧”与“当前帧”的相对运动速度(Velocity)。如果速度出错,模型会产生极其严重的拉伸或闪烁
- 终末地的渲染管线补充

以上是网上的图,是正常的一次draw中所经过的流程,其中顶点着色器就是vs,在第一篇中我们替换的ps就是图中的片元着色器。
另外也有一些应用阶段任务让GPU运行,例如Compute Shader,把GPU当作高度并行的通用处理器,即cs,也是本篇文章的主要使用的着色器。
经过对终末地的渲染流程的详细分析,这里给出几个重要的结论:
- 物体提供vs-cb1找偏移量,到vs-t0中读取骨骼矩阵,在cb1中读取摄像机矩阵与视图矩阵等常量,最终计算出骨骼的正确变换投影。此外,一次draw中cup只会把当前draw所用到的骨骼写入vs-t0,多余的骨骼不会写进去,比如衣服draw的时候只传入衣服有的骨骼,手部draw的时候只传入手部的骨骼
因此我们的核心思路就是在衣服部位draw的时候通过cb1读取vs-t0的骨骼矩阵数据,然后存起来,到皮肤draw的时候,把衣服的骨骼矩阵替换掉皮肤的骨骼矩阵,以实现骨骼信息转移。
- cb区域以及vs-t0都是ring buffer,每帧都在循环覆写,vs-t0存储了骨骼矩阵,cb-0到cb5区域存储了其他信息,一个画面内所有需要被读取的数据都存储在其中,所有骨骼矩阵都存储在vs-t0。
仔细观察不同draw绑定槽位的哈希可以发现,即使draw的id不同,他们绑定的cb,vs-t0的哈希是相同的,且同一个区域还被绑定到不同槽位,再加上这个cb的哈希是足足有4MB大小的缓冲区,vs-t0有8MB+的大小;且我做过测试,将一个全0缓冲区提取骨骼矩阵,让其每帧运行,它会逐渐覆写,最终填满部分缓冲区。由此推断cbx与vs-t0是ringbuffer。


- 不同draw的cb1有隐藏偏移量pfirstconstant,这是cpu阶段直接操作这个draw的指针的,3dmigoto无法获取这个偏移量也就是说不能直接写ini复制cb1内的值找到正确的偏移量去vs-t0读取数据。
也就是说使用copyxxx指令是无效的,3dmigoto会从0开始复制cb1,不是从指针起始索引开始复制,但是填入的时候,却是从指针的偏移量开始填入,就会导致错误数据;ref xxx也是无效的,因为cb区域是ringbuffer,当前的draw结束后,数据就里面往前推移或者被覆写了。因此需要编写hlsl将偏移量存储起来。
- 同一帧的vs,ps有pfirstconstant的偏移量,可以直接通过cb[x]读取到正确的数据,且更换vs,ps后,由于pfirstconstant的作用是直接改变GPU指针的位置,更换的vs,ps依旧可以直接通过cb[x]访问到正确的数据,而并发的cs就没有绑定这个指针,因此想存储cb1中保存的偏移量信息,就得使用vs跟ps
简单来说:
物体在渲染时,会通过 cb1 常量缓冲区提供一个偏移量,然后跑到 vs-t0(一个高达 8MB 的大仓库)中,根据这个偏移量读取对应的骨骼矩阵。此外,一次 Draw Call 中,CPU 只会把当前物体用到的骨骼写入 vs-t0,多余的骨骼不会写进去(比如画衣服时只传衣服的骨骼,画手时只传手的几十根骨骼)。
因此我们的核心思路就是:在画衣服时,通过 cb1 读取 vs-t0 中的骨骼矩阵并存入金库;到画手部(皮肤)时,把刚才存好的衣服骨骼矩阵,强行注入并替换掉手部的骨骼矩阵,实现“借尸还魂”。
然而,这里有两个巨大的底层机制陷阱:
Ring Buffer 覆写:cb1 和 vs-t0 都是环形缓冲区,每帧都在高速循环覆写。
隐藏指针 pfirstconstant:不同 Draw 的 cb1 有一个隐藏偏移量,CPU 直接操作这个指针跳转。3DMigoto 无法直接获取这个隐藏偏移量,导致 copy 指令复制下来的 cb1 数据是错位的。
同一帧的 VS(顶点着色器)和 PS(像素着色器)绑定了这个隐藏指针,可以直接读到正确的 cb1 数据;但并发运行的 CS(计算着色器)却没有绑定这个指针!因此,必须先通过 VS 和 PS 将真实的偏移量信息“偷”出来存好,然后再交给 CS 去搬运骨骼。
由于操作实在过于复杂,这里只讲核心逻辑。
- 提取真实的 CB1 偏移量
既然 VS 能读到正确的 cb1,但 VS 无法直接写入 UAV(无序访问视图),我们退而求其次:在 VS 中读取 cb1 的值,将其作为 TEXCOORD(纹理坐标)输出给 PS,然后在 PS 中使用 ps-u7 写入我们的自定义缓冲区保存起来
struct V2P {
float4 pos : SV_Position;
// id 是整数,也必须禁止插值!
nointerpolation uint id : TEXCOORD0;
nointerpolation uint4 raw_bits : TEXCOORD1;
};
cbuffer CB1 : register(b1) {
float4 cb1_data[64];
};
V2P main(uint id : SV_VertexID) {
V2P o;
o.pos = float4(-0.9 + (id % 8) * 0.2, -0.9 + (id / 8) * 0.2, 0.5, 1.0);
o.id = id;
// 完美二进制拷贝
o.raw_bits = asuint(cb1_data[id]);
return o;
}由于这是顶点着色器,后续数据会进行模糊处理,所以这里要声明nointerpolation 禁止插值,后续ps代码如下
struct V2P {
float4 pos : SV_Position;
// 接收端也必须完全对应!
nointerpolation uint id : TEXCOORD0;
nointerpolation uint4 raw_bits : TEXCOORD1;
};
RWStructuredBuffer<uint4> u7 : register(u7);
float4 main(V2P i) : SV_Target {
// 按位写入金库,毫发无损!
u7[i.id] = i.raw_bits;
return float4(0, 0, 0, 0);
}
后续只需要使用resourcexxx=ps-u7,即可将cb1的值存储下来
- 拿到cb1的buf后就可以在里面读取偏移量,到vs-t0中找对应的骨骼矩阵了,读取矩阵的代码如下
StructuredBuffer<uint4> OriginalT0 : register(t0);
StructuredBuffer<uint4> SafeBodyCB1 : register(t1);
RWStructuredBuffer<uint4> BodyBonesOut : register(u0);
[numthreads(64, 1, 1)]
void main(uint3 tid : SV_DispatchThreadID) {
uint id = tid.x;
if (id >= 768) return;
uint offset_current = SafeBodyCB1[5].x;
uint offset_prev = SafeBodyCB1[5].y;
// 正常提取当前帧
if (offset_current <= 1000000) {
BodyBonesOut[id] = OriginalT0[offset_current + id];
}
// 正常提取历史帧 (存放到 768 的偏移位置)
if (offset_prev <= 1000000) {
BodyBonesOut[768 + id] = OriginalT0[offset_prev + id];
}
}这里通过SafeBodyCB1[5].x,SafeBodyCB1[5].y分别读取当前帧偏移,上一帧偏移,然后到对应矩阵中找模型骨骼矩阵并存入vs-u0中
然后是注入模型骨骼矩阵代码如下
StructuredBuffer<uint4> BodyBonesIn : register(t0);
StructuredBuffer<uint4> SafeHandCB1 : register(t1);
RWStructuredBuffer<uint4> FakeT0 : register(u0);
[numthreads(64, 1, 1)]
void main(uint3 tid : SV_DispatchThreadID) {
uint id = tid.x;
if (id > 768) return;
uint offset_current = SafeHandCB1[5].x;
uint offset_prev = SafeHandCB1[5].y;
uint gap = (offset_current > offset_prev) ? (offset_current - offset_prev) : (offset_prev - offset_current);
// 【1. 写入当前帧骨骼】
if (offset_current > 1535 && offset_current <= 1000000) {
// 绝对安全的真实骨骼区:0 ~ 500
if (id < 768) {
FakeT0[offset_current + id] = BodyBonesIn[id];
}
// 埋入防伪令牌 (在安全的 768 槽位)
if (id == 768) {
FakeT0[offset_current + 768] = uint4(offset_current, offset_prev, gap, 88888888);
}
}
// 【2. 写入历史帧骨骼(恢复 TAA!)】
if (offset_prev > 1535 && offset_prev <= 1000000) {
// 防碰撞雷达保险
if (id < gap) {
// 读取上一帧在 offset_prev 留下的防伪令牌
uint4 history_token = FakeT0[offset_prev + 768];
bool is_valid_history = (history_token.x == offset_prev && history_token.w == 88888888);
if (offset_prev > offset_current || !is_valid_history) {
// 【异常帧补天】:发生绕回或遇到了旧的垃圾数据
// 强制用当前帧骨骼填平,消除这一帧的闪烁!
if (id < 768) {
FakeT0[offset_prev + id] = BodyBonesIn[id];
}
} else {
// 【正常帧完美呈现】:防伪验证通过!
// 核心修改:读取身体的“历史骨骼”(偏移 768),完美恢复 TAA 动态模糊!
if (id < 768) {
FakeT0[offset_prev + id] = BodyBonesIn[768 + id];
}
}
// 给 prev 贴上黑匣子标记
if (id == 768) {
FakeT0[offset_prev + 768] = uint4(offset_current, offset_prev, gap, 99999999);
}
}
}
}
这是最核心的一步。手部的骨骼空间很少,而衣服的骨骼很多。如果在注入时没有做足够的空间判定和防伪过滤,历史帧(Prev)的矩阵就会和当前帧(Current)的矩阵在显存里发生重叠与相互覆写(多线程踩踏),导致模型满屏乱飞。
此外,为了保证抗锯齿(TAA)不失效、边缘不产生锯齿,我们必须把上一步提取的“身体历史帧骨骼”正确地塞给“手部的历史帧偏移量”。
至此hlsl代码部分就结束了,回顾一下我们的流程是什么,是首先在衣服部分使用提取cb1的两个代码,再在身体部分使用提取vs-t0的两个代码,并把结果保存起来,然后再在手部使用提取cb1的两个代码,再使用注入vs-t0的代码,流程就结束了,十分简单。
剩下的就是编写 INI 了。代码量虽然看起来大,但实际上是因为模型有 4 个渲染通道(Shadow 阴影、Rasterization 光栅化、Main 主渲染、Outline 描边),每个通道都需要独立提取和注入,防止显存冲突。你只需要写好 Main 通道,其余三个通道复制并修改 filter_index 即可。
;; =========================================================================
;; 【3】自定义显存缓冲区定义 (Custom Memory Buffers)
;; 核心架构:为 4 个渲染通道(Main/Shadow/Rasterization/Outline)独立开辟金库
;; =========================================================================
; --- CB1 偏移量缓存(用于存储当前帧和历史帧的偏移地址)---
[ResourceSafeCB1_UAV]
type = RWStructuredBuffer
stride = 16
array = 64
[ResourceSafeCB1_SRV]
type = Buffer
stride = 16
array = 64
; --- 身体骨骼提取库(存储从原生 vs-t0 中提纯的 Body 骨骼)---
; 包含 Main, Shadow, Rasterization, Outline 四个通道的读写/只读视图
[ResourceBodyBones_UAV_Main]
type = RWStructuredBuffer
stride = 16
array = 1536
[ResourceBodyBones_SRV_Main]
type = Buffer
stride = 16
array = 1536
[ResourceBodyBones_UAV_Shadow]
type = RWStructuredBuffer
stride = 16
array = 1536
[ResourceBodyBones_SRV_Shadow]
type = Buffer
stride = 16
array = 1536
[ResourceBodyBones_UAV_Rasterization]
type = RWStructuredBuffer
stride = 16
array = 1536
[ResourceBodyBones_SRV_Rasterization]
type = Buffer
stride = 16
array = 1536
[ResourceBodyBones_UAV_Outline]
type = RWStructuredBuffer
stride = 16
array = 1536
[ResourceBodyBones_SRV_Outline]
type = Buffer
stride = 16
array = 1536
; --- 伪造的全局骨骼大仓库 (FakeT0) ---
; 【极其重要】:大小扩容至 600000,填平物理显存悬崖,防止尾部骨骼数据丢失拉伸!
[ResourceFakeT0_UAV_Main]
type = RWStructuredBuffer
stride = 16
array = 600000
[ResourceFakeT0_SRV_Main]
type = Buffer
stride = 16
array = 600000
[ResourceFakeT0_UAV_Shadow]
type = RWStructuredBuffer
stride = 16
array = 600000
[ResourceFakeT0_SRV_Shadow]
type = Buffer
stride = 16
array = 600000
[ResourceFakeT0_UAV_Rasterization]
type = RWStructuredBuffer
stride = 16
array = 600000
[ResourceFakeT0_SRV_Rasterization]
type = Buffer
stride = 16
array = 600000
[ResourceFakeT0_UAV_Outline]
type = RWStructuredBuffer
stride = 16
array = 600000
[ResourceFakeT0_SRV_Outline]
type = Buffer
stride = 16
array = 600000
;; =========================================================================
;; 【4】着色器拦截与标记 (Shader Overrides)
;; 拦截游戏的原生 VS,将其标记为对应的渲染通道 (Shadow=200, Raster=201, Main=202等)
;; =========================================================================
[ShaderOverridevs2]
hash = 847947b4a1ad40cf
filter_index = 200 ; 标记为阴影通道
[ShaderOverridevs10]
hash = ac358c21b925075b
filter_index = 201 ; 标记为光栅化/预渲染深度通道
[ShaderOverridevs1]
hash = b1ca4834786821dd
filter_index = 202 ; 标记为主渲染通道 (Main)
[ShaderOverridevs33]
hash = cdf11b288d812606
filter_index = 203 ; 标记为描边通道 (Outline)身体
[ShaderOverridevs88]
hash = 8e1c0782db9e85d1
filter_index = 303 ; 标记为描边通道 (Outline)手臂
;; =========================================================================
;; 【5】网格拦截与核心逻辑劫持 (Mesh Interception & Logic)
;; 在画 Body 时提取数据;在画 Hand 时替换骨骼并进行联合绘制
;; =========================================================================
; 屏蔽一些不必要的手部组件渲染
[TextureOverride_IB_149f97a2_hand_Component141]
hash = b960f7ad
this = null
[TextureOverride_IB_149f97a2_hand_Component1411]
hash = 8ef1f8f0
this = null
[TextureOverride_IB_149f97a2_hand_Component11]
hash = 23afb707
this = null
; --- 步骤 1:拦截 Body,执行提取逻辑 ---
[TextureOverride_IB_cced9603_body_Component1]
hash = cced9603
handling = skip ; 跳过原生渲染,留到手部一起画
if vs == 200
run = CustomShaderExtractSafeCB1
run = CustomShaderExtractBody_Shadow
elif vs == 201
run = CustomShaderExtractSafeCB1
run = CustomShaderExtractBody_Rasterization
elif vs == 202
run = CustomShaderExtractSafeCB1
run = CustomShaderExtractBody_Main
elif vs == 203
run = CustomShaderExtractSafeCB1
run = CustomShaderExtractBody_Outline
endif
; --- 步骤 2:拦截 Hand,执行注入与合并绘制逻辑 ---
[TextureOverride_IB_149f97a2_hand_Component1]
hash = 149f97a2
handling = skip ; 取消原生渲染
ps-t13 = ResourceBML
ps-t14 = ResourceNM
vs-t1 = copy vs-t0 ; 备份原生大仓库(如有需要)
; 绑定手部的顶点缓冲
vb0 = Resource149f97a2Position
vb1 = Resource149f97a2Texcoord
vb2 = Resource149f97a2Blend
ib = Resource_149f97a2_Component1
drawindexed = 82308,0,0 ; 绘制手部
; 绑定身体的顶点缓冲并重新绑定纹理
ps-t13 = ResourceBML
ps-t14 = ResourceNM
vb0 = Resourcecced9603Position
vb1 = Resourcecced9603Texcoord
vb2 = Resourcecced9603Blend
ib = Resource_cced9603_Component1
; 根据当前 Pass 触发对应的自定义 Draw 流程
if vs == 200
run = CustomShaderShadowDraw
elif vs == 201
run = CustomShaderRasterizationDraw
elif vs == 202
run = CustomShaderMainDraw
elif vs == 303
run = CustomShaderOutlineDraw
endif
;; =========================================================================
;; 【6】自定义绘制宏 (Custom Draw Calls)
;; 执行 Compute Shader 注入骨骼,替换 vs-t0,并绘制模型
;; =========================================================================
[CustomShaderShadowDraw]
run = CustomShaderExtractSafeCB1
run = CustomShaderInjectHand_Shadow
vs-t0 = ResourceFakeT0_SRV_Shadow ; 将伪造的骨骼仓库塞给 VS
drawindexed = 232116,93654,0 ; 绘制身体第一部分
drawindexed = 82308,325770,0 ; 绘制身体第二部分
[CustomShaderRasterizationDraw]
run = CustomShaderExtractSafeCB1
run = CustomShaderInjectHand_Rasterization
vs-t0 = ResourceFakeT0_SRV_Rasterization
drawindexed = 232116,93654,0
drawindexed = 82308,325770,0
[CustomShaderMainDraw]
run = CustomShaderExtractSafeCB1
run = CustomShaderInjectHand_Main
vs-t0 = ResourceFakeT0_SRV_Main
drawindexed = 232116,93654,0
drawindexed = 82308,325770,0
[CustomShaderOutlineDraw]
run = CustomShaderExtractSafeCB1
run = CustomShaderInjectHand_Outline
vs-t0 = ResourceFakeT0_SRV_Outline
drawindexed = 232116,93654,0
drawindexed = 82308,325770,0
;; =========================================================================
;; 【7】Compute Shader 骨骼提取与注入逻辑 (CS Dispatch)
;; 通过运算着色器在后台处理内存读写,独立分拆 4 个通道防止显存冲突
;; =========================================================================
[CustomShaderExtractSafeCB1]
vs = extract_vs.hlsl
ps = extract_ps.hlsl
ps-u7 = ResourceSafeCB1_UAV
depth_enable = false
blend = disable
cull = none
topology = point_list
draw = 64, 0
ResourceSafeCB1_SRV = copy ResourceSafeCB1_UAV
ps-u7 = null
; --- Main 通道 ---
[CustomShaderExtractBody_Main]
cs = extract_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = vs-t0
cs-u0 = ResourceBodyBones_UAV_Main
dispatch = 12, 1, 1 ; 分发 12*64=768 个线程提取身体骨骼
ResourceBodyBones_SRV_Main = copy ResourceBodyBones_UAV_Main
cs-u0 = null
[CustomShaderInjectHand_Main]
cs = inject_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = ResourceBodyBones_SRV_Main
cs-u0 = ResourceFakeT0_UAV_Main
dispatch = 12, 1, 1 ; 分发 32*64 线程注入 (如果只注 768,可优化为 12,1,1)
ResourceFakeT0_SRV_Main = copy ResourceFakeT0_UAV_Main
cs-u0 = null
; --- Shadow 通道 ---
[CustomShaderExtractBody_Shadow]
cs = extract_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = vs-t0
cs-u0 = ResourceBodyBones_UAV_Shadow
dispatch = 12, 1, 1
ResourceBodyBones_SRV_Shadow = copy ResourceBodyBones_UAV_Shadow
cs-u0 = null
[CustomShaderInjectHand_Shadow]
cs = inject_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = ResourceBodyBones_SRV_Shadow
cs-u0 = ResourceFakeT0_UAV_Shadow
dispatch = 12, 1, 1
ResourceFakeT0_SRV_Shadow = copy ResourceFakeT0_UAV_Shadow
cs-u0 = null
; --- Rasterization (Pre-Z/Velocity) 通道 ---
[CustomShaderExtractBody_Rasterization]
cs = extract_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = vs-t0
cs-u0 = ResourceBodyBones_UAV_Rasterization
dispatch = 12, 1, 1
ResourceBodyBones_SRV_Rasterization = copy ResourceBodyBones_UAV_Rasterization
cs-u0 = null
[CustomShaderInjectHand_Rasterization]
cs = inject_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = ResourceBodyBones_SRV_Rasterization
cs-u0 = ResourceFakeT0_UAV_Rasterization
dispatch = 12, 1, 1
ResourceFakeT0_SRV_Rasterization = copy ResourceFakeT0_UAV_Rasterization
cs-u0 = null
; --- Outline 通道 ---
[CustomShaderExtractBody_Outline]
cs = extract_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = vs-t0
cs-u0 = ResourceBodyBones_UAV_Outline
dispatch = 12, 1, 1
ResourceBodyBones_SRV_Outline = copy ResourceBodyBones_UAV_Outline
cs-u0 = null
[CustomShaderInjectHand_Outline]
cs = inject_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = ResourceBodyBones_SRV_Outline
cs-u0 = ResourceFakeT0_UAV_Outline
dispatch = 12, 1, 1
ResourceFakeT0_SRV_Outline = copy ResourceFakeT0_UAV_Outline
cs-u0 = null
虽然代码看起来很长,实际上并非那么回事,代码很长的原因在于同一个操作重复了四次,因为模型有四个vs每个都需要单独提取骨骼矩阵信息,因此我们只拿上主色阶段,即
elif vs == 202
run = CustomShaderExtractSafeCB1
run = CustomShaderExtractBody_Main
作为例子讲解,读者如果看过本系列的第一篇帖子,知道filter_index的用法,复制四份出来自然很容易。
- 首先是资源定义区
[ResourceSafeCB1_UAV]
type = RWStructuredBuffer
stride = 16
array = 64
[ResourceSafeCB1_SRV]
type = Buffer
stride = 16
array = 64
这是准备拷贝cb区域的缓冲区,cb1只读取了十几个字节,因此这里设置64个字节是合理的
[ResourceBodyBones_UAV_Main]
type = RWStructuredBuffer
stride = 16
array = 1536
[ResourceBodyBones_SRV_Main]
type = Buffer
stride = 16
array = 1536
这里是复制衣服的骨骼矩阵的缓冲区,由于我认为最多肯定存在256个骨骼,所以我这里定义了256x3x2=1536个字节,实际上骨骼远没有那么多根,衣服我看最多也就一百多根骨骼,但是多复制点数据是没什么大毛病的。
然后是准备替换的手部的骨骼矩阵的缓冲区
[ResourceFakeT0_UAV_Main]
type = RWStructuredBuffer
stride = 16
array = 600000
[ResourceFakeT0_SRV_Main]
type = Buffer
stride = 16
array = 600000
这里必须详细解释一个极其致命的物理悬崖问题:
原版的 FakeT0 缓冲区大约是 8MB(array = 525824)。因为 vs-t0 是 Ring Buffer,当偏移量指针走到最边缘(比如 525496)时,原版手部只有几十根骨头,刚好能塞下;但我们的自定义全身模型有 156 根骨头(需占用 468 个位置)!
525496 + 468 = 525964。这超出了 525824 的边界!
CPU 遇到越界会自动绕回头部,但 3DMigoto 做不到。越界的数据会直接掉进虚空,全部变成 0! 这会导致模型每隔几秒钟就有一帧缩成一个点、全屏拉伸。
解决办法: 强行扩大缓冲区的容量,在悬崖外面填出一块平地。例如将 array 改为 600000(元素数量),完美容纳所有溢出的骨骼!
- 下一个是着色器标记
[ShaderOverridevs1]
hash = b1ca4834786821dd
filter_index = 202 ; 标记为主渲染通道 (Main)
标记到主上色阶段的vs,以便筛选
- 接下来是customshader
[CustomShaderExtractSafeCB1]
vs = extract_vs.hlsl
ps = extract_ps.hlsl
ps-u7 = ResourceSafeCB1_UAV
depth_enable = false
blend = disable
cull = none
topology = point_list
draw = 64, 0
ResourceSafeCB1_SRV = copy ResourceSafeCB1_UAV
ps-u7 = null
这是提取cb1的数据到ResourceSafeCB1_SRV 的代码,这一步后ResourceSafeCB1_SRV 就存储了cb1的数据
; --- Main 通道 ---
[CustomShaderExtractBody_Main]
cs = extract_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = vs-t0
cs-u0 = ResourceBodyBones_UAV_Main
dispatch = 12, 1, 1 ; 分发 12*64=768 个线程提取身体骨骼
ResourceBodyBones_SRV_Main = copy ResourceBodyBones_UAV_Main
cs-u0 = null
这一步是提取衣服骨骼的值,存储到ResourceBodyBones_SRV_Main
[CustomShaderInjectHand_Main]
cs = inject_bones_cs.hlsl
cs-t1 = ResourceSafeCB1_SRV
cs-t0 = ResourceBodyBones_SRV_Main
cs-u0 = ResourceFakeT0_UAV_Main
dispatch = 12, 1, 1
ResourceFakeT0_SRV_Main = copy ResourceFakeT0_UAV_Main
cs-u0 = null
这一步就是注入衣服骨骼到手部draw的对应骨骼缓冲区了
- 然后在衣服部位内过滤主上色阶段的draw,执行提取cb1以及提取骨骼矩阵的hlsl
[TextureOverride_IB_cced9603_body_Component1]
hash = cced9603
handling = skip ; 跳过原生渲染,留到手部一起画
if vs == 202
run = CustomShaderExtractSafeCB1
run = CustomShaderExtractBody_Main
endif
- 在手部过滤主上色阶段的draw,执行提取cb1以及注入骨骼矩阵的hlsl
[TextureOverride_IB_149f97a2_hand_Component1]
hash = 149f97a2
handling = skip ; 取消原生渲染
ps-t13 = ResourceBML
ps-t14 = ResourceNM
vs-t1 = copy vs-t0 ; 备份原生大仓库(如有需要)
; 绑定手部的顶点缓冲
vb0 = Resource149f97a2Position
vb1 = Resource149f97a2Texcoord
vb2 = Resource149f97a2Blend
ib = Resource_149f97a2_Component1
drawindexed = 82308,0,0 ; 绘制手部
; 绑定身体的顶点缓冲并重新绑定纹理
ps-t13 = ResourceBML
ps-t14 = ResourceNM
vb0 = Resourcecced9603Position
vb1 = Resourcecced9603Texcoord
vb2 = Resourcecced9603Blend
ib = Resource_cced9603_Component1
; 根据当前 Pass 触发对应的自定义 Draw 流程
if vs == 202
run = CustomShaderMainDraw
endif
可以先draw手部原来的部分,此时还没有替换骨骼矩阵,然后再替换矩阵,draw身体的部分,单独写一个customshader会便于管理,里面执行的是customshader及注入骨骼的customshader,然后绑定完vs-t0再draw即可
[CustomShaderMainDraw]
run = CustomShaderExtractSafeCB1
run = CustomShaderInjectHand_Main
vs-t0 = ResourceFakeT0_SRV_Main
drawindexed = 232116,93654,0
drawindexed = 82308,325770,0
至此一次简单的骨骼替换就完成了,你只需要把这一次draw的逻辑再写三份,分别在阴影的draw,光栅化的draw,轮廓线的draw中分别使用filiter_index=xxx if vs==xxx,进行过滤然后再执行一样的逻辑即可,最终代码就是我发的超长的代码。
实际上逻辑很简单,我也把各个功能分块了,这样会便于理解。
整个骨骼“借尸还魂”的流水线可以浓缩为极简的三步:
- 提取 (Extract):在渲染衣服 (Body) 时,截获真实的内存偏移量,将衣服的“当前帧”与“历史帧”骨骼矩阵从游戏大仓库 (vs-t0) 完整拷贝到我们的专属金库中。
- 注入 (Inject):在渲染手部 (Hand) 时,拿到手部本该写入的地址。在我们自己扩建的假仓库 (FakeT0) 中,将这个位置强行塞满第一步存好的衣服骨骼。
- 欺骗渲染 (Render):将手部原生绑定的 vs-t0 替换为我们的 FakeT0 并执行绘制。手部的着色器在不知情的情况下,会拿着全身骨骼去渲染手部网格,从而实现完美无色差!
总体而言,逻辑都不复杂,也不需要自己手搓hlsl,实际上难度并不是很高,跟着我的ini写,理解了其中的逻辑原理,很容易就可以替换骨骼,实现完全无色差的模型了。
ini以及hlsl在附件
- 关于骨骼运动状态不一致的问题
显然,这种方法比替换ps的方法要方便一些,但是需要注意的是,这个方法替换的骨骼会有一帧延迟,即你如果是手部先draw,然后再draw衣服,那么手部draw的时候,这一帧内,身体的骨骼数据还没有被填入缓冲区内,此时的数据拿到的是空的,到了下一帧开始的时候,才拿到衣服的骨骼数据,那么此时又到了手部开始draw,此时拿到的骨骼数据其实是上一帧的骨骼数据,因此这里实际上是有一帧延迟的
想要解决这个问题其实也并非很困难,这里笔者只提供一个思路,只需要把人物的第一帧的所有的ib的draw骨骼缓冲区全部存储到一个缓冲区内(本文中1536大小的buffer,使用的时候需要扩大这个大小),到第二帧开始draw的时候,先把vs-t0拷贝一份到vs-t1中,然后把vs-t0的数据使用本文中的方法,替换骨骼缓冲区,然后再把vs-t1的骨骼矩阵使用文中的方法复制到到自定义的资源区内,这样每一个部件的每一次draw都是延迟一帧的骨骼,总体模型的运动状态是一致的



闽公网安备35010302000678号


















































