RabbitFX 是一个通过反编译修改游戏运行时的着色器代码从而魔改其渲染管线
进而实现发光、反虚化、像素剔除等功能的模组插件
由于其定制化正则表达式代码,目前仅适用于鸣潮,但其原理在3Dmigoto上通用
截止本文,其第一作者 caverabbit 已更新至6.2版本,本文将对该版本进行分析解释
!!!阅读本篇前,请确保你已经对3Dmigoto的ini有一定了解或已看完笔者的前置教程!!!
RabbitFX-Ver6.2中包括
[README.txt]、[deltaTime.ini]、[RabbitFX.ini]和[ShaderCacheSettings.ini]共四个文件
其中[ShaderCacheSettings.ini]被放在{DISABLED}文件夹中
3Dmigoto会跳过读取前缀为[DISABLED]的文件和文件夹
[deltaTime.ini]与[RabbitFX]无变量关联
ShaderCacheSettings.ini & README.txt
这两个文件为非必要组件
# ShaderCacheSettings.ini 全文
[Rendering]
cache_shaders = 1
该ini文件没有使用namespace,说明[Rendering]作用于全局
打开加载器根目录的[d3dx.ini],以下是搜索结果
# d3dx.ini 局部
[Rendering]
; GPU program manipulations.
; Type of shader hashes in use:
; 3dmigoto = Traditional hash used by 3DMigoto (unseeded software FNV-1)
; embedded = Use the first half of the MD5-like hash embedded within the
; shaders to skip the hash calculation altogether.
; bytecode = Only hash bytecode and signatures with hardware accelerated
; CRC32C. Used to minimise duplicate shaders in certain games, but
; potentially carries a higher risk of hash collisions between
; unrelated shaders in some games (e.g. that only differ in
; variable names). May occasionally avoid hash changes on game
; updates due to changes in the game developer's build environment
; (shader compiler version, build path embedded in debug info,
; constants renamed, etc). Will not avoid hash changes if the
; shader code, constant values, etc are changed.
shader_hash = 3dmigoto
; Switch to newer texture hashes that are less susceptible to corruption and
; don't have collisions if part of the image matches. May have a slight
; performance penalty since more of the image is hashes. Do not enable if
; upgrading an existing fix!
texture_hash = 1
; Shaders in game will be replaced by these custom shaders.
override_directory = ShaderFixes
; Automatically patched shaders will be written here if caching is enabled.
cache_directory = ShaderCache
; Shaders that are directly compiled by the game, instead of binary, go here.
storage_directory = ShaderFromGame
; cache all compiled .txt shaders into .bin. this removes loading stalls.
cache_shaders = 0
以下是 caverabbit 的解释
# README.txt
Setting cache_shaders to 1 should improve performance between game sessions
by saving processed shaders after loading something for the first time.
将cache_shaders设置为1可以通过在首次加载某些内容后保存已处理的着色器来提高游戏会话之间的性能。
Enabling this will cause the game to lag initially as it generates files
but it will reduce loading times in the future.
启用此功能将导致游戏在生成文件时最初出现延迟,但这将减少未来的加载时间
If you already have shader cache enabled, the following step
does not need to be repeated.
如果您已经启用了着色器缓存,则不需要重复以下步骤。
To enable shader cache, make a copy of ShaderCacheSettings.ini
and put it inside the RabbitFX folder.
要启用着色器缓存,请复制ShaderCacheSettings.ini并将其放入RabbitFX文件夹中。
deltaTime.ini - 帧时间计算模块
namespace = time ; 定义命名空间为"time",用于变量隔离
; ---------------------------DESCRIPTION---
; deltaTime() ; 功能描述:计算帧间隔时间
; Returns time passed since last frame ; 返回上一帧到当前帧的时间差
;
; -------------------------------EXAMPLE---
; x = $\time\deltaTime ; 使用示例:获取时间差
; x will be time passed since last frame in seconds ; 单位:秒
[Constants] ; 常量/全局变量声明区
global $deltaTime = 0 ; 存储帧时间差的全局变量
global $timeAtLastFrame = 0 ; 存储上一帧时间戳的全局变量
[Present] ; 每帧结束时执行的回调
if $timeAtLastFrame != time ; 检测时间戳是否变化(新帧)
if $timeAtLastFrame > time ; 处理时间倒流(如游戏暂停)
$timeAtLastFrame = 0 ; 重置时间戳
endif
$deltaTime = time - $timeAtLastFrame ; 计算当前帧时间差
$timeAtLastFrame = time ; 更新上一帧时间戳
endif
RabbitFX.ini - 高级渲染效果模块
颜色变换流程:
输入颜色 → 加载遮罩 → 审查检测 → RGB转HSV →色相偏移 →
饱和度调整 → 明度调整 → HSV转RGB →亮度倍增 → 输出颜色
原始文件解析
; ========================================================
; RabbitFX 图形后处理配置文件
; 版本: 6.2
; 核心功能: HSV色彩调整/动态光效/敏感内容审查
; ========================================================
namespace = RabbitFX ; 定义命名空间,防止变量冲突
; --------------------------
; [Constants] 全局变量声明区
; --------------------------
[Constants]
; 持久化变量(退出游戏后仍保存)
persist global $censor = 0 ; 审查模式开关 (0=关闭, 1=开启)
; 菜单状态控制
global $menu = 1 ; 当前菜单状态 (0=菜单中, 1=游戏中)
global $menuCheck = 1 ; 菜单状态检测标志
; HSV颜色参数
global $h = 0.0 ; 色相 (0-360)
global $s = 0.0 ; 饱和度 (0-100)
global $v = 0.0 ; 明度 (0-100)
global $brightness = 1.0 ; 亮度乘数
global $interpolate = 1.0 ; 效果插值系数
; 寄存器备份变量
global $temp217x = 0 ; 用于备份x217寄存器
global $temp217y = 0 ; 用于备份y217寄存器
global $temp217z = 0 ; 用于备份z217寄存器
global $temp217w = 0 ; 用于备份w217寄存器
; ----------------------
; [KeyCensor] 按键绑定
; ----------------------
[KeyCensor]
key = \ ; 触发键: 反斜杠(\)
type = cycle ; 循环切换模式
$censor = 0,1 ; 在0和1之间切换审查模式
; ----------------------
; [Present] 帧末处理
; ----------------------
[Present]
; 菜单状态同步
if $menu != $menuCheck ; 检测状态变化
$menu = $menuCheck ; 更新菜单状态
endif
$menuCheck = 1 ; 重置检测标志(默认游戏状态)
x172 = $menu * $censor ; 将状态写入寄存器x172(着色器使用)
; ------------------------------
; [TextureOverrideMenu] 菜单检测
; ------------------------------
[TextureOverrideMenu]
hash = 7017fed2 ; 菜单纹理哈希值
$menuCheck = 0 ; 检测到菜单纹理时设为0
; --------------------------
; 资源定义区
; --------------------------
[ResourceGlowMap] ; 辉光贴图资源声明
[ResourceFXMap] ; 特效贴图资源声明
; --------------------------
; [CommandListRun] 主渲染逻辑
; --------------------------
[CommandListRun]
; 寄存器备份
$temp217x = x217
$temp217y = y217
$temp217z = z217
$temp217w = w217
; HSV参数计算(转换到着色器所需范围)
x217 = ($h / 360.0) * $interpolate ; 色相 → [0,1]范围
y217 = ($s / 100.0) * $interpolate ; 饱和度 → [0,1]
z217 = ($v / 100.0) * $interpolate ; 明度 → [0,1]
w217 = $brightness ; 直接传递亮度
y172 = 1 ; 激活标志位
; 绑定纹理资源到着色器槽
ps-t50 = ResourceGlowMap ; 辉光贴图绑定到t50
ps-t51 = ResourceFXMap ; 特效贴图绑定到t51
; 清空纹理引用(避免残留)
ResourceGlowMap = null
ResourceFXMap = null
; --------------------------
; [CommandListClear] 清理逻辑
; --------------------------
[CommandListClear]
; 重置HSV参数
$h = 0.0
$s = 0.0
$v = 0.0
$brightness = 1.0
$interpolate = 1.0
; 恢复寄存器原始值
x217 = $temp217x
y217 = $temp217y
z217 = $temp217z
w217 = $temp217w
; 解绑纹理
ps-t50 = null
ps-t51 = null
y172 = 0 ; 关闭激活标志
; ========================================================
; [ShaderRegexDiffuse] 漫反射着色器修改
; 功能: 实现动态HSV调整/敏感内容检测
; ========================================================
[ShaderRegexDiffuse]
shader_model = ps_4_0 ps_5_0 ; 目标着色器模型
temps = shift color tmask override ini menu treg0 treg1 treg2 treg3 ; 声明临时寄存器
run = CommandListClear ; 执行前先运行清理命令
; --------------------------
; 正则匹配模式(原始着色器)
; --------------------------
[ShaderRegexDiffuse.Pattern]
; 复杂正则表达式匹配着色器汇编代码,主要捕获:
; - 采样指令头(header)
; - 常量缓冲区访问(func)
; - 输出模式(pattern1/pattern2)
(?<header>\h*sample_\w_indexable\(texture2d\)\(float,float,float,float\)\sr\d+\.\w+,\s(?<texcoord0>v\d+).*\n(?:(?!(?:\h*min\sr\d+\.\w+,\sc)|(?:\h*\w+\ho[03])).*\n)*)(?<func>(?<funcmain>\h*min\s(?<val>r\d\.\w+),\s(?<cb>cb\d+\[\d+\])\.\w+,\scb\d+\[\d+\]\.\w+\n)(?<body>(?:(?!\h*\w+\ho[03]).*\n)*))*(?:(?<pattern1>(?<pattern1o3>\h*(?:(?!x)\w)+\ho3\.\w+.*\n)(?<pattern1split>(?:(?!(?:\h*min\sr\d+\.\w+,\sc)|(?:\h*\w+\ho0)).*\n)*)(?<pattern1func>(?<pattern1funcmain>\h*min\s(?<pattern1val>r\d\.\w+),\s(?<pattern1cb>cb\d+\[\d+\])\.\w+,\scb\d+\[\d+\]\.\w+\n)(?<pattern1body>(?:(?!\h*\w+\ho0).*\n)*))*(?<pattern1o0>\h*\w+\ho0\.\w+.*\n))|(?<pattern2>(?<pattern2o0>\h*\w+\ho0\.\w+.*\n)(?<pattern2split>(?:(?!(?:\h*min\sr\d+\.\w+,\sc)|(?:\h*\w+\ho3]).*\n)*)(?<pattern2func>(?<pattern2funcmain>\h*min\s(?<pattern2val>r\d\.\w+),\s(?<pattern2cb>cb\d+\[\d+\])\.\w+,\scb\d+\[\d+\]\.\w+\n)(?<pattern2body>(?:(?!\h*\w+\ho3).*\n)*))*(?<pattern2o3>\h*\w+\ho3\.\w+.*\n)))
; --------------------------
; 新增资源声明注入
; --------------------------
[ShaderRegexDiffuse.InsertDeclarations]
dcl_resource_texture1d (float,float,float,float) t120 ; 状态纹理
dcl_resource_texture2d (float,float,float,float) t50 ; 辉光贴图
dcl_resource_texture2d (float,float,float,float) t51 ; 特效贴图
dcl_sampler s15, mode_default ; 默认采样器
; --------------------------
; 着色器代码替换逻辑
; --------------------------
[ShaderRegexDiffuse.Pattern.Replace]
; 保留原始采样指令头
${header}
; 注入审查检测系统(当$menu.x=1时激活)
${func:+
ld_indexable(texture1d)(float,float,float,float) ${menu}.xyzw, l(172, 0), t120.xyzw\n
${funcmain}
if_nz ${menu}.x\n
; 多条件敏感内容检测(共4组条件,需满足至少3组)
; ...详细条件判断汇编代码...
if_nz ${treg0}.x\n
mov ${val}, l(1.0) ; 满足条件时覆盖原始值
endif\n
endif\n
${body}
}
; 主渲染逻辑
mov ${override}.x, l(0)\n
ld_indexable(texture1d)(float,float,float,float) ${ini}.xyzw, l(172, 0), t120.xyzw\n
if_nz ${ini}.y\n
; 加载特效贴图(t51)
resinfo_indexable(texture2d)(float,float,float,float) ${treg0}.xyyy, l(0), t51.xyzw\n
; ...纹理采样与像素丢弃逻辑...
; 加载辉光贴图(t50)
resinfo_indexable(texture2d)(float,float,float,float) ${treg0}.xyyy, l(0), t50.xyzw\n
; ...纹理采样逻辑...
; HSV色彩空间转换(约50条指令)
; 1. RGB转HSV
; 2. 应用色相偏移($h)
; 3. 调整饱和度($s)
; 4. 调整明度($v)
; 5. HSV转回RGB
; ...详细色彩处理汇编代码...
; 最终颜色输出
${pattern1:+
mov o3.xyzw, ${color}.xyzw\n
}
endif
; 模式选择输出
if_nz ${override}.x\n
mov o0.xyz, ${treg3}.xyz ; 输出修改后的颜色
else\n
${pattern1o0} ; 原始输出
endif
; ========================================================
; 其他着色器修改规则(清理/审查专用)
; ========================================================
[ShaderRegexDecontamination0]
shader_model = ps_4_0 ps_5_0
run = CommandListClear
; ...匹配简单着色器的正则模式...
[ShaderRegexDecontamination1]
shader_model = ps_4_0 ps_5_0
run = CommandListClear
; ...匹配中等复杂度着色器的正则模式...
[ShaderRegexDecontamination2]
shader_model = ps_4_0 ps_5_0
run = CommandListClear
; ...匹配极简着色器的正则模式...
; ========================================================
; [ShaderRegexSoundTattooCensorFilter] 音频纹身审查
; ========================================================
[ShaderRegexSoundTattooCensorFilter]
shader_model = ps_4_0 ps_5_0
temps = menu
pre x172 = $menu * $censor ; 预处理:计算审查标志
; 匹配原始审查代码
[ShaderRegexSoundTattooCensorFilter.Pattern]
(?<censor>(?<before>\s*and.*\n\s*discard.*\n\s*mad.*l\()(?:.+)(?<after>\)\n\s*lt.*\n\s*discard.*\n\s*and.*\n))
; 注入纹理声明
[ShaderRegexSoundTattooCensorFilter.InsertDeclarations]
dcl_resource_texture1d (float,float,float,float) t120
; 条件替换逻辑
[ShaderRegexSoundTattooCensorFilter.Pattern.Replace]
\n
; 动态审查强度调整(-0.15增强效果)
ld_indexable(texture1d)(float,float,float,float) ${menu}.xyzw, l(172, 0), t120.xyzw\n
if_z ${menu}.x\n
${censor} ; 原始审查代码
else\n
${before}-0.15${after} ; 增强版审查
endif\n
\n
魔改文件分析
以上的逐行解析,虽然十分详实,但也只能知道原理如何,看完不懵的也是神人了
而且,作为已经迭代到Ver6.2的综合性插件,尽管他已经非常专业精简高效了
但对于想要了解学习基础原理的逆向者来说,仍然十分冗长
笔者在研究早期版本时,结合原神的TexFx简化出了一版
能够根据遮罩贴图删除漫反射像素和轮廓线的仿制插件
# ZelbertLine.ini 全文
; 定义命名空间,防止变量冲突
namespace = ZelbertAlpha
; ==============================================
; [ShaderRegexOutlineTransparencyLL] 着色器修改规则
; 功能: 实现轮廓透明度处理
; 目标着色器模型: PS 4.0/5.0
; ==============================================
[ShaderRegexOutlineTransparencyLL]
shader_model = ps_4_0 ps_5_0 ; 目标着色器模型版本
filter_index = 037730.555 ; 过滤器索引(自定义标识)
; 声明临时寄存器变量
temps = ini tex69 tex70 dim texcoord
; 注释掉的命令列表执行(备用)
;run = CommandListClearInstanceValues
; ==============================================
; 正则表达式匹配部分
; ==============================================
[ShaderRegexOutlineTransparencyLL.Pattern]
; 使用(?s)启用单行模式(.匹配换行符)
(?s)
; 捕获组1:匹配轮廓输出声明(7个输出寄存器)
(?P<MatchOutline>
dcl_input_ps_siv linear noperspective v7.xyzw, position\n
dcl_output o0\.xyzw\n
dcl_output o1\.xyzw\n
dcl_output o2\.xyzw\n
dcl_output o3\.xyzw\n
dcl_output o4\.xyzw\n
dcl_output o5\.xyzw\n
dcl_output o6\.xyzw\n
.* ; 匹配后续任意字符(包括换行)
)
; 捕获组2:匹配直到ret指令的所有内容
(?P<TillRet>.*)
ret ; 匹配返回指令
; ==============================================
; 新增资源声明注入
; ==============================================
[ShaderRegexOutlineTransparencyLL.InsertDeclarations]
; 声明1D纹理(用于状态读取)
dcl_resource_texture1d (float,float,float,float) t120
; 声明2D纹理(轮廓遮罩)
dcl_resource_texture2d (float,float,float,float) t18
; 声明采样器
dcl_sampler s14, mode_default ; 默认采样器14
dcl_sampler s15, mode_default ; 默认采样器15
; ==============================================
; 着色器代码替换逻辑
; ==============================================
[ShaderRegexOutlineTransparencyLL.Pattern.Replace]
; 保留原始输出声明部分
${MatchOutline}\n
; 保留原始着色器主体代码
${TillRet}
; ===== 注入的新代码开始 =====
; 采样轮廓遮罩纹理(使用v2.xy坐标)
sample_indexable(texture2d)(float,float,float,float) r9.xyzw, v2.xy, t18.xyzw, s15\n
; 检查遮罩的y分量(透明度通道)
if_nz r9.y\n
; 如果y分量非零,根据x分量(轮廓强度)决定是否丢弃像素
discard_nz r9.x\n
endif\n
; ===== 注入的新代码结束 =====
让3Dmigoto分别识别漫反射和轮廓线,只需要修改正则表达式匹配的着色器代码
而[ShaderRegex]系列的代码,本质上只是一个批处理匹配到的文本的自动化脚本
通过小键盘在Hunt模式转储着色器文件,然后在[ShaderFixes]中直接修改,也能达到相同的效果
区别在于,通过插件实现方便高效,便于维护,通过文件实现简单直接,容易失效
由于文档局限性,仅在此说明原理,具体实战操作将放到视频中进行演示
评论