站点链接:技术博客页 | 公众号:溯光的图形笔记
切口:一帧画完之后,还有谁在动它
前面几期我们讲的一直是"画面怎么算出来"。这一次换个方向问:一帧已经算完、正准备送给显示器的时候,还有谁能在上面动手脚?
好了,直接说结论:答案是有——而且动手的不是引擎,是一个从进程外面挤进来的第三方运行时。它不改引擎源码、不进游戏的构建流程,只把自己伪装成系统图形库放在目标程序旁边,等程序自己伸手来拿。
打个比方:这不像"换掉厨房的配方",倒像"在菜端上桌前,后厨门口站着一个人,给每一盘菜撒一把自己的调料"。菜还是原来那盘菜,几何、光照、阴影一个没动;被改的只是端上桌那一步的摆盘与色泽。
本篇是这个系列的第一篇,只做一件事:把"注入式后处理"这个题域的边界画清楚——它是什么、它插在哪一环、它和引擎内部的后处理有什么本质区别、这条产品线是怎么长出来的。
0.1 全系列目录总览(十一篇)
先把全系列的地图摊开。本系列沿着"一个外部注入的运行时"从入口一直拆到效果生态,十一篇的落点如下(本篇即第一行):
| # | 篇目 | 这一篇要回答的那一问 |
|---|---|---|
| 一 | 总论:注入式后处理与管线内后处理的边界 | 它到底动的是哪一段?和引擎自带的后处理差在哪? |
| 二 | 注入机制:代理 DLL、加载顺序与导出表挂钩 | 它凭什么能进到别人进程里,进去之后又怎么把函数接过来? |
| 三 | ReShade FX:语言血统、独立编译器与纹理语义 | 效果是拿什么写的?编译器为什么能单独拿出去用? |
| 四 | effect runtime:technique / pass 图、render state 与运行时数据源 | 一个 effect 从源码到画面,中间过了几道? |
| 五 | 跨 API 抽象:D3D9/10/11/12、OpenGL、Vulkan 的差异与统一 | 同一套东西怎么盖住六种图形 API? |
| 六 | Add-on API:事件模型、跨 API 资源交换与两种构建的分野 | 想在运行时插手更深的事,接口长什么样? |
| 七 | 深度缓冲的工程学:自动关闭、MSAA、UI 缺失与子矩形错位 | 为什么深度类效果时灵时不灵? |
| 八 | 效果生态与许可地图:同一份清单里的 MIT 与"禁止再分发" | 那些效果到底能不能用、能不能转? |
| 九 | 自跑一:注入本地 D3D11 demo——逐效果前后对比 | 真跑一遍是什么样子? |
| 十 | 自跑二:效果栈与帧耗账,外加自写一个小 .fx | 代价是多少?自己写一个要几步? |
| 十一 | 合规与工程取舍的合集:把十一篇的账并到一处 | 一条条摆开:哪些是技术必然,哪些是可以商量的取舍? |
阅读路径:只想知道"这东西能不能用、会不会出事"的朋友,读本篇的第 0.3 节、第三章与第七章就够;想搞清楚它怎么实现的,从第二章一路读下去;打算自己写 add-on 的,第三、四、六篇是主线。
0.2 本篇要讲清的一件事
先来看看结论。一句话:注入式后处理不是"后处理的一种实现",而是"后处理的一种位置"——它把一个后处理运行时放在了引擎与显示器之间那个原本没有人站的位置上。位置一变,能拿到的东西、能改的东西、要付的代价,全都跟着变。
上面这张图里,最要紧的是中间那两个刻度之间短短的一段:引擎自己的后处理链已经跑完了,交换链(swap chain)的呈现(present)还没发生。这一段在引擎看来是"什么都不用做"的空档,而在注入器看来正好是"这一帧还是我一个人的"——背缓冲(back buffer)里装着一张完整的成品图,且还没有人把它交出去。
0.3 本篇不涉及什么(边界声明)
按本站惯例,先把边界摆在前面,免得大家读到一半发现"想知道的没写":
| 不涉及 | 为什么 |
|---|---|
| 后处理算法的实现——泛光(bloom)怎么做降采样链、色调映射(tone mapping)用什么曲线、抗锯齿(anti-aliasing)有哪些家族 | 这块已经由《实时渲染管线探析》第三篇《后处理链:Bloom、色调映射与抗锯齿》占位,本系列只引用、不复述 |
| 挂钩机制的通用原理——导出函数、虚拟表(vtable)、PLT/GOT、内联跳转(inline hook)各自的写法 | 那是另一张票(图形 API Hook 机制)的题域,本系列只在第二篇讲"注入器自己那一层挂钩在做什么" |
| 预设安装与"整合包"——哪个包好看、怎么装、怎么调 | 使用向内容,不进这个系列;且官方明确要求不要分发二进制与着色器文件(第八篇细说) |
| 任何绕过做法——绕过深度限制、绕过网络检测、绕过反作弊 | 红线。本系列全部实验都在自有开源 demo 上做,商业游戏一次都不碰 |
参数口径也说一句:系列里所有"本机读数"都来自同一台机器——Windows 11(build 26200)、RTX 3060 Laptop(6144 MiB 显存、驱动 560.70)、.NET 运行时 4.8 以上;帧耗类读数一律关垂直同步、不锁帧。凡是不满足这些条件的结论,文中都会另标。
0.4 四条读法约定(沿用本系列体例)
写这个系列的过程中,有四条约定帮我们避开了四类常见误读,先摊在这里:
- 官方文字优先,二手转述只作线索。 凡涉及版本、行为与立场的说法,一律追到官网、发布说明或代码仓库原文;二手内容明标为二手。本系列也确实抓到过一处被广泛转述的过期结论——它在中文技术文里流传了近十年,第四章会点名。
- "拿不到"与"改不了"必须分开说。 注入式后处理的限制有两类:一类是信息缺失(拿不到几何缓冲),一类是能力受限(改不了管线结构)。两者常被混成一句"不如引擎内后处理",但成因、后果与补救方式完全不同。
- "没复现"就是"没复现"。 本机做不到的,一律写"测不了",不拿别人的结论替自己作证,也不用"应该会""大概率"这类词蒙过去(本篇第八章与第十篇各会用到一次)。
- 对比图必须是单变量。 开与关之间只允许差一个开关,机器、机位、帧位都不能动——第八章会按这条给出一组可直接验看的样本。
这四条在后面十篇里都会再用到,所以放在第一篇交代清楚。
一、官方定义:它是「注入器」,不是「滤镜包」
1.1 官方自己怎么说的
言归正传,先看官方自己怎么说。ReShade 官网与代码仓库的自我介绍用的是同一个短语,一字不差:
a generic post-processing injector for games and video software ——面向游戏与视频软件的通用后处理注入器
请注意这个词序:injector(注入器)是名词,post-processing(后处理)是定语。官方对自己的第一定位是"注入器"——一个能进入别人进程、并在那里长期驻留的运行时;至于它在这个运行时里做什么,才是"后处理"。中文圈里常把它叫"画质增强工具""滤镜包",那是从用途出发的说法,会把它的实现难度和工程约束全遮掉。
站点标语说得更直白:Advanced post-processing everywhere.(到处都能做高级后处理)——"everywhere"这个词,正是靠注入实现的。
它的公开能力边界官方也写得很清楚:暴露对帧颜色(frame color)与深度信息(depth information)的自动化、通用访问;而深度那一项,在多人游戏中会被自动禁用,官方给的理由是防止被利用。这句话看着像免责声明,其实是整个系列的工程主线之一——第七章与第八章都会回到它。
覆盖面上,官方支持的图形 API 是 Direct3D 9 / 10 / 11 / 12、OpenGL 与 Vulkan 六个;运行要求 Windows 7 SP1 及以上加 .NET Framework 4.8。许可方面,它自 2017 年 1 月 1 日起按 BSD 三条款许可(BSD 3-Clause) 开源,部分源文件在文件头另行声明可选 MIT 双许可。
1.2 三条术语线:注入器、代理库、翻译层
说白了,这是本篇最该切开的一节。中文内容里,"注入""包装""翻译"经常被当成同一件事讲,而它们的目标其实互相正交:
看出来了吗:ReShade 的"进入方式"属于第二条线,它的"目的"属于第一条线;而 DXVK 是第三条线的典型,它翻译调用是为了让程序能跑,不是为了在帧上加效果。
一个反例最能说明问题:把 DXVK 当成"画质工具"讲,就像把"翻译官"当成"化妆师"——两者都动嘴,但一个负责换语言,一个负责换样子。
| 术语 | 它要解决什么问题 | 代表 | 后会处理吗 |
|---|---|---|---|
| 注入器(injector) | 在成品帧上叠加画面处理 | ReShade、ENBSeries、GShade | 会——这正是它的目的 |
| 代理动态链接库(proxy DLL)/包装层(wrapper) | 让旧图形 API 的调用能被转发/替换 | dgVoodoo2、代理式部署本身 | 不一定——它只是一条进入通道 |
| 翻译层(translation layer) | 把一套 API 的调用翻译成另一套 | DXVK(Direct3D 8/9/10/11 → Vulkan) | 不是为了后处理 |
再把三者的关系收成一句话:注入器是目的,代理库是手段,翻译层是邻居——三者正交,所以它们能并存、也能叠加(第二章那条代理链就是叠加的产物)。
两张图对照着看:第一张是"图形管线本身长什么样"的中立底图,第二张是"抽象层/包装层"这个概念的位置示意。第二张的尺寸只有 380×286,在本站正文里只能当原理小图——拿不到大图就如实标明用途,不硬撑成大幅图,这本身也是一条选材纪律。
1.3 三个词的双语对照
依然是惯例,关键术语在这一节一次给全,后文不再重复标注:
- 注入器(injector):运行时把代码或动态链接库送进目标进程的工具。
- 后处理注入器(post-processing injector):特指在渲染管线末端对成品帧做处理的注入器——官方原词。
- 代理动态链接库(proxy DLL)/包装动态链接库(wrapper DLL):用与系统库同名的文件替换原库,在转发调用的同时挂钩。社区常直呼 proxy dll hook。
- 翻译层(translation layer):把一套图形 API 的调用翻译成另一套,DXVK 官方用词。
- 背缓冲(back buffer):正在被绘制、尚未交给显示器的那一张画面。
- 深度缓冲(depth buffer):记录每个像素远近的那张辅助图,后处理里最关键的"额外信息"。
- 交换链(swap chain):一组轮流充当"正在画"与"正在显示"的缓冲及其切换机制。
- 叠加层(overlay):运行时挂在目标进程里的操作界面,用来开关效果与调参数。
- 加载顺序(load order):操作系统决定先加载同名库中哪一个的顺序——代理库能生效的根因。
1.4 「自动禁用深度」这句话的工程含义
回头再看官方那句"深度信息在多人游戏中会被自动禁用"。它表面上是一句免责声明,实际交代了三件工程事实:
- 深度访问是一项特权,不是默认能力。 官方把它做成了可关闭的开关,说明这条通道在实现上本来就有条件——这是第七篇"深度为什么时有时无"的伏笔。
- 关闭是自动的,不询问用户。 触发条件写在工具内部(检测到网络活动),用户无感。于是同一款游戏、同一个 effect,联机与单机的表现可能不同,而画面上的差异看起来像"效果坏了"。
- 它承认了风险存在。 工具主动退让,是因为这类能力"有可能被拿去做别的事"——这句话是整条产品线信任问题的起点,第七章会把它讲完。
顺带把"everywhere"也收一下:它一方面说明覆盖面广(官方列出的 API 是六个),另一方面说明它的实现必须建立在"不依赖具体引擎"这个前提上——这个前提决定了后面几章的全部技术选择:跨 API 抽象、独立的着色语言、可被外部加载的扩展接口,全都是为它服务的。
二、它插在哪一环:帧呈现路径上的落点
2.1 一帧的三个时刻
先来看看一帧的三个时刻。要把"插在哪一环"说清楚,先给一帧分三个时刻:
- 画完(rendered):引擎的渲染线程把这一帧的所有绘制命令提交完,后处理链也跑完了,背缓冲里是一张成品图。
- 交出去(presented):交换链把背缓冲交给窗口系统,从这一刻起这幅图理论上不再归应用程序管。
- 上屏(displayed):显示器真正把它刷出来。
注入器要的是第 1 与第 2 时刻之间。这里的工程含义是:它有完整的一张图,却没有这场绘制的"上下文"——没有几何、没有材质、没有光源列表,也没有那张记录着介质的几何缓冲(G-buffer)。它能拿到的只有两样:成品帧本身,以及(在条件满足时)深度缓冲。
先把这两面一起说了。先来看看这两面。一句话概括这个位置的全部优势与全部限制:位置在末端,所以不挑引擎;也正因为它在末端,所以信息最少。
2.2 进入方式:同名替换与加载顺序
下面我们看具体做法。下面我们进入具体做法。那么它怎么进到那个位置?答案是不做注入动作,反而让程序主动把它加载进来——这就是上文"代理动态链接库"的含义。
操作系统加载一个可执行程序时,会按既定顺序去若干目录里找它依赖的动态链接库。如果目标程序自己所在目录里有一个与系统库同名的文件,那这一个会先被找到。于是注入器只需要把自己命名成目标程序要用的那个图形库的名字,放进同一个目录,就等于是"被请进去"的。
官方给出的命名规则,本身就说明了它能覆盖多少种程序:
| 被替换的库名 | 覆盖的图形 API |
|---|---|
d3d9.dll |
Direct3D 9 |
d3d10.dll |
Direct3D 10 |
d3d11.dll |
Direct3D 11 |
dxgi.dll |
Direct3D 11 / 12 的公共呈现层 |
opengl32.dll |
OpenGL |
dinput8.dll |
5.0 起新增的备用名(原本是输入设备的库) |
dxgi.dll)与可执行文件同处一地,清单与字节数逐行列出——程序启动时会先命中这一个@ 本机读数(实验目录实时清单)
上面第二张图里那个数字(系统目录下 3601 个动态链接库)值得停一秒:系统库如此之多,同名替换在技术上完全可行;也正因为可行,它对反作弊与安全工具来说是一个天然敏感动作——这是第七章要展开的信任问题。
2.3 挂钩点:进去之后改哪里
OK,进了进程之后要做什么?进去只是拿到"我在这个进程里"的身份,真正要改的是呈现这一步。图示意的这一段路径是:应用程序 → 交换链的呈现调用 → 图形驱动 → 显示子系统。注入器在"呈现调用"这一处把控制权接过来,先按用户配置画完自己的一串效果,再调用原始实现把最终结果交出去。
这一进一出,就是整个方案的核心节奏:拦下、加料、放行。而"拦下"这一步具体的机制(导出表挂钩、以及它内部用到的那个开源挂钩库)属于本篇的边界之外,留到第二篇与那张 Hook 票里讲。
2.4 一条配置片段:代理链
新版还多了一层玩法:注入器之间可以串联。配置文件里出现 [PROXY] 段,允许指定"先加载我、再以另一个名字加载它",于是翻译层与注入器可以叠在同一个进程里:
; ReShade.ini —— 代理链(Proxy chain)示例
[PROXY]
EnableProxyLibrary=1 ; 打开代理链
ProxyLibrary=d3d11.dll ; 交给链上的下一环(此处以翻译层为例)
下面对这段特别说明两点:
EnableProxyLibrary打开后,注入器不再独占这个图形库名,而是把调用继续往下一环转发——链上的顺序就是生效顺序;- 这一段是"两个改调用的东西共处一个进程"的工程解,和 1.2 节那条"包装层与翻译层正交"的结论正好互为印证:正因为目的不同,它们才能叠在一起而不冲突。
2.5 一次观察记录:从文件摆放看进入方式
先来看看一条能自己动手验的现象。上面讲的是设计意图,这里补一条本机可直接观察的现象,便于大家自己验证:
在目标程序目录里,代理库与真正的系统库是同一个名字、不同位置。程序启动时,操作系统按加载顺序搜索目录,先在程序自己的目录里命中那个同名文件,于是被加载的是注入器;而注入器自己会按记录下来的路径再去加载真正的系统库,把调用转交过去。
这条观察有三点值得记住:
- 全程没有"注入动作"。 没有远程线程,没有往别人内存里写数据,只有一次文件名与位置的安排——这也是它能在各种环境下长期存活的原因。
- 删除即卸载。 想撤掉,把那个同名文件删掉就行,系统库还躺在原处。
- 代价是"绑定在目录上"。 换一个程序、换一种 API,就得换一个文件名重新摆一次。这解释了第五篇为什么必须搞跨 API 抽象——它把这件重复劳动收进了工具内部。
顺带说清一个常被问到的细节:它并不要求目标程序"配合"。整个过程依赖的全是操作系统的既定加载规则,这也是为什么同一种部署方式能同时适用于单机游戏、视频软件乃至任何用到图形 API 的程序。
三、与引擎内后处理的边界
3.1 引擎内后处理手里有什么
先来看看引擎那一侧手里有什么。《实时渲染管线探析》第三篇讲过的那条链,是在引擎内部跑的:场景先渲进若干张中间缓冲,后处理链再把它们一张张读出来加工。它能拿到的东西很奢侈——
- 几何缓冲(G-buffer):每个像素的法线、材质参数、粗糙度……想做屏幕空间环境光遮蔽(screen space ambient occlusion, SSAO)与屏幕空间反射(screen space reflection, SSR)时,法线是最关键的那一条;
- 运动矢量(motion vector):上一帧到这一帧每个像素移动了多少,时域抗锯齿(temporal anti-aliasing, TAA)与运动模糊都靠它;
- 多张渲染目标(multiple render targets, MRT):一次绘制同时写好几张图,互不干扰;
- "我知道自己在画什么":引擎知道这个像素属于哪盏灯、哪块材质、哪个透明物体,因此可以按需选择性地处理。
它不需要猜,也不需要绕。
3.2 注入式手里有什么
OK,翻到注入式这一侧。同一件事换个位置,手里的东西立刻缩水:
- 成品帧(back buffer):一张已经合成好的图,颜色是最终颜色,几何信息已经不可逆地丢了;
- 深度缓冲(深度的可读性视条件而定):能拿到时,它把"远近"这一维补回来一部分;拿不到时,所有依赖深度的效果一起失效;
- 没有源码、没有构建流程、没有引擎的钩子:一切都得从外部完成。
3.3 四行逐项对照
| 维度 | 引擎内后处理 | 注入式后处理 |
|---|---|---|
| 可读资源 | 颜色、深度、G-buffer、运动矢量、MRT | 颜色 +(条件满足时的)深度;无 G-buffer |
| 可写时机 | 管线的任意一段,可与绘制交错 | 只在帧末:后处理链之后、呈现之前的那一个缺口 |
| 代价结构 | 改引擎代码或改管线配置,成本落在项目里 | 零侵入:不碰引擎、不改构建;成本落在运行时开销与兼容性上 |
| 需要源码吗 | 需要(接入引擎的渲染管线) | 不需要——对任何程序一视同仁,这正是它通用性的来源 |
这张表里的第二行与第四行是一组:"不需要源码"既是它最大的优点,也是它所有限制的源头。因为没有源码,它就无从知道这次绘制的语义;因为不知道语义,它只能从图像本身推测,而"从图像推测"必然不如"从管线里直接拿"准确。
3.4 三个能直接推出的结论
结论一:屏幕空间近似是位置决定的,不是效果本身的缺陷。 注入式做环境光遮蔽,事实上更适合叫"屏幕空间环境光遮蔽"——它拿不到法线,只能用深度图与相邻像素的明暗去估计遮蔽。所以这类效果在画面边缘、在屏幕外几何、在自身遮蔽处会出现典型失真。这不是实现不好,是位置使然。
结论二:深度决定了一条清晰的功能分界线。 只动颜色、只看像素邻域的效果(色彩分级、色调曲线、去色带、显示器件模拟)完全不依赖深度;而依赖深度的效果(景深、环境光遮蔽、屏幕空间反射)在深度不可用时直接失效或闪烁。第七篇会解释深度为什么时有时无,这一节先记住这条分界。
结论三:通用性是用"每帧多跑一遍全屏像素"换来的。 引擎内后处理可以只对需要的部分处理,注入器大多是全屏逐像素——这一点直接体现在帧耗上,第十篇会拿本机读数来算这笔账。
不过这句话在本机是要打问号的。 我们按第 0.3 节那套口径做实验时,四种深度设置下都没能拿到深度(第七篇会把读数摆出来)。"深度可读"从来不是自动成立的——它取决于工具能否识别出那张缓冲,而这一步在成品程序上往往不由使用者决定。
3.5 屏幕空间近似的三类典型失真
既然拿了"屏幕空间"这条路,就有三处失真必须认下来——它们不是实现的瑕疵,而是位置决定的:
- 屏幕外无信息。 屏幕空间方法只看得到屏幕里画出来的东西。画面外、被遮挡、以及背面的几何,它一概不知道。所以环境光遮蔽类效果在物体边缘常出现一圈本不该有的光晕,因为"边缘之外"在它的世界里是空的。
- 缺一维就靠猜。 引擎内的同类算法直接读法线,屏幕空间方法只能从深度图的相邻差值去反推朝向。深度变化剧烈的斜面上,这个反推会失准,表现为遮蔽过重或过轻。
- 深度不连续处会渗。 景深需要判断"这个像素的远近",而前景与背景交界的那几个像素,邻域里混着两个截然不同的距离,取样一平均就会在前景边上渗出一圈背景色。这类渗色在细发丝、枝叶、网栏等处最明显。
把这三条与第三章开头那张对照表连起来看,会发现它们全是同一句话的推论:位置在末端,所以信息最少。 换个角度说,这些失真也就是"不挑引擎、零侵入"要付的那部分账。
顺带做一次算术,让"代价"有个量级:全屏逐像素的处理,代价与像素数成正比——1920×1080 是约 207 万像素,3840×2160 是约 829 万像素,恰好 4 倍。所以同一个效果在 4K 屏上的开销是 1080p 的四倍,这是它最容易被低估的一处(第十篇会用帧耗曲线把这条坐实)。
3.6 像素账:这件事的成本主轴
言归正传,把这笔账单独摊开。上面那条算术值得单独展开,因为它是整个题域的成本主轴,后面好几章都要回来引用。
| 分辨率 | 像素数(约) | 相对 1080p 的倍数 |
|---|---|---|
| 1280×720 | 92 万 | 0.44 倍 |
| 1920×1080 | 207 万 | 1 倍(基准) |
| 2560×1440 | 369 万 | 1.78 倍 |
| 3840×2160 | 829 万 | 4 倍 |
这张表里的数字全是乘出来的,不含任何实测成分——正因为如此,它可以先于实验就成立:像素数翻一倍,全屏处理的像素处理量就翻一倍。
由此可以得到三条推论:
- 分辨率是这台账上最大的变量。 从 1080p 到 4K 是 4 倍,比"多开一两个效果"(通常每项 1 个全屏 pass)的影响大得多。所以"同一个效果,有人说不卡、有人说很卡"很常见——他们大概率不在同一块屏上。
- 效果条数是线性叠加的。 一个 effect 若由若干全屏 pass 构成,那么每多一个 pass,就多一份全屏像素处理量。这条规律解释了为什么"叠了七八个效果"的预设会明显吃满帧耗(第十篇会给出本机的对应曲线)。
- 它换来的是"不需要源码"。 引擎内后处理可以只对需要的地方处理(比如只处理受光物体),而注入器无从知道哪些像素"需要"——它只能对整张图做。上面那 4 倍,就是通用性付的钱。
顺便把"效果越多越贵"这句话收严一点:真正决定开销的是全屏 pass 的个数,而不是效果条数——一个效果可以只跑一个 pass,也可以跑五六个。这一点在第三、四篇讲 effect 语言与 pass 图时会有更清楚的解释。
四、家族谱系:从 ENB 到 ReShade
4.1 全量清单
依然是惯例,先把清单列全。先把这条线上的东西列全(宁全勿缺),再归类。下表按时间大致顺序排列,许可与开源状态逐项可核:
| 工具 | 官方自我定位 | 许可 | 开源 | 属于后处理吗 |
|---|---|---|---|---|
| ENBSeries | 面向具体游戏的图形改造,通过修改渲染函数调用叠加效果;分通用版与独占版 | 专有 EULA(免费但禁止逆向) | 否 | 是(更早的注入式) |
| SweetFX | 一套着色器套件(本身不含注入能力) | 并入 ReShade 后随其后 | 是(早期组件) | 是(作为效果集) |
| ReShade | 通用后处理注入器 | BSD 三条款(部分文件双许可 MIT) | 是 | 是 |
| GShade | 无盈利的 ReShade 分支,内置大量预设与游戏识别 | 二进制非开源;所含组件按各自许可 | 否 | 是 |
| Special K | PC 游戏的修复与增强框架(画质、延迟、HDR、帧节奏),可挂载官方 ReShade | GPL v3 | 是 | 部分(有独立的后处理与显示链改造) |
| dgVoodoo2 | 旧图形 API 的包装层:把 Glide、DirectX 1–7、D3D8.1、D3D9 输出到 D3D11/12 | 免费软件 | 否 | 否(目的是兼容,不是画面处理) |
| DXVK | 翻译层:把 Direct3D 8/9/10/11 翻译成 Vulkan | zlib/libpng | 是 | 否 |
4.2 二次归类:清单是"面",归类是"骨"
下面我们进入归类这一步。把上表按两条轴一归,结构立刻清楚:
轴一 · 它要干什么
- 在成品帧上加处理:ENBSeries、ReShade、GShade、Special K(部分)
- 让旧程序的调用能继续跑:dgVoodoo2(包装)、DXVK(翻译)
轴二 · 它是不是开放的
- 开源且许可宽松:ReShade(BSD)、DXVK(zlib)、Special K(GPL)
- 开源但强约束:GShade(二进制不开放,工程内组件各自许何)
- 专有/免费软件:ENBSeries、dgVoodoo2
两条轴交叉后有个特别值得注意的格子:"专有 × 后处理"这一格里蹲着比 ReShade 更早的 ENBSeries。这个题域最早的主力其实不是开源的;ReShade 之所以成为事实标准,很大程度上是因为它在 2017 年选择了开源,并接住了 SweetFX 那一批效果。
按上表数一下开源比例:7 件里 3 件开源(约 43%)——ReShade、Special K、DXVK。这个比例不算高,但它解释了本系列为什么必须逐件核许可:默认"这东西是开源工具"很容易出错。
4.3 一处必须澄清的长期误读
好了,说一处必须澄清的事。中文与英文社区里都流传一句"SweetFX 在 ReShade 出来之后就没了"。这句话是错的,而且错得挺关键——它把这段历史讲反了。
真实的顺序是:ReShade 一开始就是为了支持 SweetFX 那套着色器而被造出来的(PCGamingWiki 的原话是"the original shader suite that ReShade was created to support")。SweetFX 2.0 的预览版直接把 ReShade 当底层注入器,文件头署的是 "SweetFX: v2.0 Preview 8 by CeeJay.dk / API hook: Reshade by Crosire"。后来官方把"ReShade + SweetFX 打包版"标记为弃用,改推独立版 ReShade——是打包形态退役了,不是 SweetFX 消失了。
把这条说清有实际用处:它解释了为什么今天的效果集里还能看到 SweetFX 时代的名字(锐化、色彩、抗锯齿),也解释了 ReShade 为什么从第一天起就把"效果"与"注入"分成两层——这两层从一开始就是两拨人在维护。
4.4 谱系里的两次交接
把上表按时间轴看,这条线上发生过两次意味深长的"交接":
第一次是「效果集」的交接。 从 SweetFX 到 ReShade,效果不再由一个团队打包提供,而是由各作者各自维护、各自授权。这次交接的直接后果是效果数量爆发式增长,间接后果则是许可状况从此不再统一——官方那份效果包清单里,同一个安装器拉下来的东西,一边是宽松的 MIT,另一边是"保留所有权利、禁止再分发"。第八篇会专门给这张许可地图。
第二次是「信任」的交接。 2023 年那次分支事件之后,社区里大量讨论转向官方版本。这次交接没有改变技术路线,却改变了一条默认预期:用户开始把"是否透明"当成选择工具的一等标准。对一个必须把代码放进别人进程、还要长期驻留的工具来说,这条默认预期的分量不比性能轻。
两次交接合起来说明一件事:这条产品线的天花板,一半由技术决定,一半由治理决定。 本系列把这个判断贯穿到底——第七、八、九三章正是它的三个侧面。
五、六个时间节点
先来看看这条线被什么改过方向。这一节给一条带年份的编年脉络。年份口径:一律取官方发布说明或官网公告的日期,不取二手转述。
| 时间 | 事件 | 一句话后果 |
|---|---|---|
| 2008–2009 | ENBSeries 活跃于 GTA IV 时代(作者 Boris Vorontsov) | 证明"改渲染函数调用"这条路在商业游戏上可行 |
| 2015-03-29 | ReShade Framework 首个版本发布(版本号自带日期:5102.03.29.4151) |
注入器、效果集与配置器第一次装进同一个包,这条线从"逐游戏定制"变成有通用底座 |
| 约 2015 | SweetFX 2.0 预览版改用 ReShade 作底层注入器 | 效果集与注入器正式分层,两个社区合流 |
| 2017-01-01 | ReShade 由闭源转为 BSD 三条款开源 | 生态开始指数级生长;此后效果集由各作者自行维护 |
| 2018-02-07 | 某在线射击游戏官方永久屏蔽 ReShade | 注入式工具与反作弊的第一次公开硬碰;此后"白名单"成为常态 |
| 2023-02-06/07 | 一次分支事件:某分支在更新中注入"检测到某第三方更新器就强制重启用户电脑"的代码,作者公开称是"给个教训";社区哗然,仓库被移除,随后道歉并移除该代码 | 信任成为这个生态的工程变量——工具能不能留在玩家机器上,取决于它可不可信 |
关于这条线上第一个版本日期的口径(本篇按"年份一律取官方发布说明或官网公告"的规矩核过一遍):
中文技术文里流传较广的说法是"2015 年 8 月首发",但官网发布区与发布说明里查不到这个日期的依据——
论坛帖子只给相对时间,不给绝对日期。可逐字核到的最早一条是 ReShade Framework 的首个版本,
它把日期写进了版本号本身:5102.03.29.4151,即 2015-03-29;首发帖由框架作者发布并写明
"We are proud to have released the first version of the ReShade Framework"。
另一处要分清的是"首个 Windows 版本"这个说法:1.0 的发布帖里作者自己写的是
"Since things are working mostly alright as of version 0.19 it's time to get out of the public beta and
publish a first release of ReShade, version 1.0."——可见 1.0 之前已经有 0.18/0.19 等公开测试版,
所以那次不是"首个 Windows 版本",而是"走出公开测试的第一个正式版"。本篇据此改口径,
并把这一行锚在日期可逐字核到的那件事上。
说白了,这六个节点可以分两类。把六个节点按性质分一下,会发现一个比例:技术性质的节点四个(ENB 时期、Framework 首个版本、改用注入器、转开源),治理性质的节点两个(被永久屏蔽、分支事件)——治理占三分之一(约 33%)。对一个"从外面进别人进程"的工具来说,这个比例并不意外,它也解释了本系列为什么把合规与许可单独拉出两章来讲。
上面第二张图是"它真的进来了"的第一手证据:注入日志的第一行会打印自己的版本号,紧接着是被成功接管的两个模块名(本例是呈现层与 Direct3D 11 两个库)。这行日志也是本系列自查的第一步——第九篇与第十篇的所有对比图,都从这一行开始。
六、两种构建:签名版与 Addon 版的分野
6.1 官方给了两个安装包
先看看官方给了什么。打开官网下载区,会看到两个并列的安装程序:一个带数字签名(连签名指纹都公布在官网上),另一个标注"完整 add-on 支持"。官方的表述很直接:
主发布版基本禁用了 add-on 支持,内部只挂钩极少量图形 API 函数——刚好够让内置的深度缓冲检测在单机游戏里工作。 完整 add-on 支持只在这个未签名的构建里启用。
功能与"是否被信任"在这款工具里被做成了两个版本:一个能力受限但可被反作弊方接受,一个能力完整但官方明说只用于单机。
6.2 逐项对照
| 维度 | 有签名的主发布版 | 完整 add-on 支持版 |
|---|---|---|
| 数字签名 | 有(指纹公开) | 无 |
| add-on 加载 | 基本禁用 | 完整 |
| 挂钩的函数数量 | 极少(够深度检测用) | 更多 |
| 官方适用场景 | 单机,且在线风险较低 | 仅单机;官方明示联机可能招致封禁 |
| 多人游戏检测 | 有(并据此关深度) | 不做检测(也因此风险自负) |
6.3 为什么这么切
先来看看它为什么这么切。这一刀切得很聪明,也很无奈:能力越大,被误认为作弊工具的概率越高。签名是对平台与反作弊方的姿态,受限功能是对普通玩家的保护;想深入折腾的开发者拿到的是未签名版,代价是"你自己承担联机风险"。
对写作者的启示是:凡涉及"开启 add-on"的说明,都必须同时给出这两个版本的区别——只讲"打开某个开关就能用 add-on",等于把读者往风险那边推。第八篇的许可地图里还会看到同一逻辑的另一种形态。
6.4 三条可操作的建议
说白了,这一章可以落成三条建议。把这一章落到可用的话上,就是三条:
- 只想看画面:用有签名的主发布版。它的功能被砍到"刚好够深度检测",但也就意味着它的行为边界最可预期。
- 要写 add-on 或做深调试:用完整支持版,并且只在单机环境用——包括"看上去是单机、但内部带服务器架构"的那类程序(第七章会讲为什么这种也会触发风险)。
- 不管用哪一版,都别把它留在联机环境的程序目录里。撤除成本极低(删掉那个同名文件即可),风险却是账号级的——这笔交换在任何情况下都不划算。
七、在线的边界:反作弊怎么说,工具自己怎么退
7.1 反作弊方的公开表态
先看看最权威的一条。先把最权威的一条摆出来。反作弊服务 BattlEye 的官方问答里写着:他们一般只封禁真正的作弊程序,或者绕过其保护机制的组件;而非作弊性质的叠加层与画质增强工具——原文点名了 ReShade 与 SweetFX——一般是受支持的,除非游戏开发者另有要求。括号里同时注明,这几样在某几款在线游戏里已被屏蔽。
这句话里有两个关键词:「一般」和「除非」。前者说明默认态度是放行,后者说明最终决定权在游戏开发者手上,不在工具与反作弊方。
7.2 白名单是有滞后的
OK,名单是怎么起作用的?默认放行是靠白名单落地的,而白名单有两个天然的滞后源:
- 工具每次更新,版本号一变,就需要反作弊方把新版本重新加白;
- 有些反作弊方案把白名单放在游戏目录里,必须随游戏补丁一起更新,节奏更慢。
于是玩家侧的常见体验是:「昨天还能进,今天更新完就进不去」。这不是封装备,而是新版本还没被加白——技术上和"被禁"完全是两件事,但体感上一样。
7.3 工具自己怎么退
下面我们进入工具自己这一侧。另一侧的退让是工具自己的设计:检测到网络活动就自动关闭深度访问。官方给出的理由是防止在多人游戏里被利用。
这条设计的因果链值得拆开看——它的触发条件是网络活动,而不只是"进了联机游戏":
- 工具检测到网络活动 → 关闭深度 → 依赖深度的效果闪烁或彻底不生效;
- 若某款单机游戏内部带服务器架构,也可能被误判,症状一样;
- 与"绕过"相关的做法(例如把这项检测注释掉)本系列一律不写——它既是违约行为,也会把读者带进封号风险。
上面这条时间线把三态排在一起:反作弊方的默认表态在前,个别游戏开发者的屏蔽要求居中,白名单的滞后在后——三者的顺序本身就是因果:默认放行是靠名单落地的,而名单更新永远慢一拍。
那么老 API 时代呢?Direct3D 9 的呈现路径比今天短得多,注入器要接住的环节更少,这也是这类工具最早在那条线上成熟的原因。
还有最后一种状态要交代:深度也可能被工具自己关掉。这一状态的读数本篇没有取得——它的触发条件是"检测到网络活动",属于特定运行环境下的行为;我们只用官方口径陈述,不拿推测充当实测。
上面两张图对应本章的两个事实:三态时间线,以及老 API 时代注入器的立足点。
7.4 本系列的红线与口径
- 全部实验在自有开源 demo 上做,不碰商业游戏;
- 不写任何绕过做法,包含绕过深度限制、绕过网络检测、绕过反作弊;
- 读到"某人装了没被封"这类个案时,本系列一律标注为个案,不作为安全性结论。
7.5 三个常见误解
言归正传,把高频误解一次说清。这一章涉及的信息最容易被传歪,索性把三个高频误解一次说清:
误解一:"装了就会被封号。" 官方 FAQ 的口径是:默认放行这类工具,除非游戏开发者另有要求。个别游戏的处置是禁止启动(装了进不去)而非封禁账号。把"被屏蔽"直接读成"封号",是两种不同强度的措施被混为一谈。
误解二:"效果不好用是没配好。" 相当一部分"效果不生效"其实与配置无关——深度被自动关闭、MSAA 破坏深度、界面元素不在深度里,这几种情况下无论怎么调参数都不会有结果。第七篇会把这条讲成一张排查表。
误解三:"这是技术问题,与规则无关。" 从 2018 年被在线游戏永久屏蔽,到白名单必须随版本更新,这条线上每一个"突然不能用"的时点,成因都在规则侧而不在代码侧。理解这一点,能省下大量无意义的排障时间。
八、真图一:在自有 demo 上做一次干净的注入
8.1 方法学:同一 demo、同一机位、只改一个开关
先来看看这四组图是怎么拍的。这一节的四组图全部遵守题图里那条约定:同一台机器、同一个演示程序、同一个机位、同一帧位,只改一个开关。开关就是效果列表里的那一项——开与关之间,除了那一项,别的什么都没变。
之所以把方法学放在图前面说,是因为对比图最容易糊弄人:换一台机器、换一帧、换个视角,任何一点差异都可能被当成"效果"。把变量压到一个,对比才有意义。
至于为什么用最小示例而不是更华丽的场景,也有三条现实理由:可控(画面元素少,一眼能看出改了什么)、可复现(任何人拿到同一份源码与同一套配置就能重跑)、无附带风险(开源 demo 不涉及商业游戏的许可与规则问题)。代价是画面单调——这一点在 8.4 节会如实登记。
自己验看时建议固定看三个位置:物体边缘(看屏幕空间方法的典型失真)、远近交界(看深度类效果是否成立)、细密纹理处(看像素邻域类效果的实际作用范围)。三处都看一遍,四组图的差别就很直观了。
8.2 六组对照 + 一个对照组
下面我们进入逐组对照。第一组是色彩分级(color grading)类效果:按参数逐像素重映射颜色(业界常见的做法是查一张颜色映射表)。实测平均绝对差 43.4(满量程 255),全帧每个像素都被动过,但不依赖深度。
第二组是显示器件模拟:扫描线与荫罩是逐像素的空间图案,所以它必须读像素的邻域。实测 18.6,同样不依赖深度。
第三组是像素邻域类效果:这类效果只看相邻像素的差异,用来锐化或抑制噪声。实测 19.2,仍然不依赖深度。
第四组是泛光(bloom):把亮部提取出来、模糊之后叠加回去,所以它会同时改到亮部与它周围的一片区域。实测 34.7。
第五组是暗角(vignette):按到画面中心的距离做亮度衰减。它是六组里幅度最大的,实测 63.2——"效果明显"与"实现廉价"是两件事,它的算法其实最省。
第六组是效果栈(effect stack):三个效果叠加在一起。顺序即执行顺序,前一个的输出就是后一个的输入。这也是"后处理"三个字最容易被低估的地方——它不是一串互不相干的滤镜,而是一条真的链。
最后这一张是对照组:把效果全部关掉,前后两帧逐字节相同(平均绝对差 0.000)。它自己没有任何画面信息,却比上面任何一张都重要——它证明上面那些差异只可能来自效果开关,而不是机器、机位或帧位的漂移。
OK,六组加一个对照看完,回到整体:三组只碰像素本身,两组改的是空间分布,一组改的是链路顺序,而幅度从 15.9 到 63.2 不等。每一组只用一项变化,就把所属档位的性质交代清楚了。
验看时还有一个小提醒:不要跨组比较。几组用的开关各不相同,组与组之间的差异里混着"换了哪一项"这个变量。要比,就在同一组里比左右两帧。
8.3 从这六组里读出的三条
- 幅度是可量化的:从效果栈的 15.9 到暗角的 63.2(平均绝对差,满量程 255)——"效果明显"这句话在这里有了数字。
- 代价和像素数成正比:全屏逐像素处理,效果越多、分辨率越高,代价越大。第十篇会把这些读数与帧耗曲线摆在一起。
- 顺序是可预期的:第四个效果栈的画面,正是前三个按顺序叠加的结果——注入式后处理是可复现的,这一点对读者判断它的可靠性很重要。
把这三条量化一下,便于后面几篇对照:本系列 11 篇里,真正需要自跑取数的是 2 篇(第十、十一),其余 9 篇是机制梳理与生态梳理;而本篇这 7 张本机图(六组对照 + 一个对照组)用的是单一开关变量,覆盖了"改像素/改空间分布/改链路顺序"三类形态。也就是说,本篇的图组已经能代表全系列对比图的方法学。
8.4 一处如实登记
上面六组效果有一个共同点值得单独交代:它们全都不依赖深度。这不是选材偏好,而是本机的实测结果——我们试了四种深度设置口径,ReShade 始终没能识别出这个 demo 的深度缓冲:
| 深度设置口径 | DisplayDepth 的输出 | 判读 |
|---|---|---|
清屏前复制(DepthCopyBeforeClears=1) |
4 种颜色、通篇 212 | 非真深度 |
清屏后复制(=0) |
与上完全相同 | 非真深度 |
| 指定清屏索引 | 与上完全相同 | 非真深度 |
反转深度输入(RESHADE_DEPTH_INPUT_IS_REVERSED=1) |
与上完全相同 | 非真深度 |
四种口径的输出逐字节一致,说明拿到的是同一张恒定的默认图,而不是我们场景里的深度。相应地,依赖深度的两类效果也无从验证:景深(FocalDOF)的前后差异只有 0.32(不到 1 个色阶),深度切片(Depth_Slicer)是 0.000。
所以,依赖深度的对照这一组,本篇不做:不修图、不换场景凑效果、也不拿自绘顶替。它反而成了这一章命题的一手证据——"深度能不能读到"不是必然的,它取决于工具能否识别出那张缓冲;而官方把深度访问做成"检测到网络活动就自动关闭"的开关,本身就说明这条通道天生带着条件。
九、全系列路线图与阅读路径
9.1 从这一篇往后怎么走
先来看看后十篇怎么排。本篇把边界画完了,接下来按"从入口到生态"的顺序拆下去:
- 入口层(第二篇):它怎么进进程,进去之后怎么把函数接管过来;
- 语言与运行时层(第三、四篇):效果用什么语言写,一个 effect 从源码到画面过几道;
- 抽象与接口层(第五、六篇):同一套东西怎么同时盖住六种图形 API,想在运行时插手更深要什么接口;
- 约束层(第七篇):深度为什么时有时无——这一篇是很多"效果不生效"的答案;
- 生态与实测层(第八、九、十篇):效果能不能用、能不能转,以及真跑一遍的账;
- 收束篇(第十一篇):把十一篇的账并到一处,哪些是技术必然、哪些是可以商量的取舍。
9.2 三类读者的最短路径
- 只想知道"能不能用、会不会出事":本篇 0.3、第三章、第七章,加第十篇的帧耗账;
- 想搞清楚它怎么实现:第二篇一路到第七篇;
- 打算自己写 add-on:第三、四、六篇是主线,第八篇的许可地图是前置必读(有些效果不能转)。
9.3 本篇三条结论
- 它是"位置"而不是"算法":注入式与引擎内后处理的分野不在效果名单上,而在它插在哪一环——帧末端、成品帧、信息最少的位置。
- 位置决定了全部能力与限制:能拿到成品帧和"可能拿得到的深度",拿不到几何缓冲;不挑引擎、零侵入,代价是只能做屏幕空间近似。
- 这是一条有信任成本的工程线:从 2018 年被某在线游戏永久屏蔽,到 2023 年那次分支事件,"能不能留在用户机器上"从来不只是技术问题——这也是本系列全部合规口径的来源。
9.4 本篇与后面十篇的接口
最后交代一下这一篇给后续留了什么,免得读第二篇时觉得"前面讲过了怎么又讲":
- 本篇讲位置,第二篇讲入口:本篇只说清"它站在帧末端的那个缺口上",至于"它怎么站稳"(同名库的加载顺序、导出表挂钩)属于入口层,是第二篇的题域。
- 本篇讲边界,第五、七篇讲代价:位置决定的限制,本篇只推到"信息最少"这一层;跨 API 抽象的工程代价与深度的可用性,各有一篇专门拆。
- 本篇讲生态的两次交接,第八篇给许可地图,第九、十篇给实测账:生态是一件事,许可是另一件事,能不能跑得动是第三件事——分三处讲,是因为它们的判据完全不同。
一句话:本篇把题域的边画完,从第二篇开始往里填。
资源清单
官方一手来源
- ReShade 官网(版本、签名指纹、分发政策原文)
- ReShade API 文档(add-on 接口参考)
- ReShade 代码仓库(许可、源码文件清单)
- ReShade FX 语言参考(效果语言与预处理器宏)
- 效果包清单(各效果包的官方条目)
- ReShade 发布说明(历代版本变更)
反作弊与合规
- BattlEye 官方问答(对叠加层/画质工具的表态)
- ReShade 3.4 发布说明(白名单机制的官方说明)
- ReShade 排障帖:常见问题(深度缓冲行为)
- ReShade 5.0 发布说明(两种构建的分野)
谱系与其他工具
- ENBSeries 官网 与 其许可条款
- Special K 官网 与 其 ReShade 集成页
- dgVoodoo2 官网
- DXVK 官网
- PCGamingWiki:ReShade 条目(与 SweetFX 的渊源)
图片素材(Wikimedia Commons,逐张核过许可)
- Category:Graphics pipeline(
3D-Pipeline.svg,CC0,5273×1189) - Category:DirectX
- Category:OpenGL
- Category:Application programming interfaces
下篇预告
下一篇是《注入机制:代理 DLL、加载顺序与导出表挂钩》。本篇只讲到"它被程序自己加载进来"这一层,下一篇要往下追三件事:同名的库被加载之后,调用是怎么转手的;导出表在这中间扮演什么角色;以及那套"自动安装挂钩"的机制与某个开源挂钩库的关系。
落款
本文由@溯光 出品,转载请注明出处。本文同步发布于公众号「溯光的图形笔记」与个人博客 yyy3d.online。
这一篇是系列开篇,把边界画完就收;希望能对大家有所帮助——尤其是那句"它站在帧末端的缺口上",后面十篇都会回到这同一个位置上来。