随便搜了一下没发现这类型的文章,刚好也要用到这个功能,于是就做了。
本文介绍了Reshade这个大家常用工具的一个不怎么有人用,但又特别强大的功能特性:add-ons(在这里我翻译为插件)。含个人见解,如和您的理解不同,请积极提出,我一定乐于接受并改正。阅读本文你需要:
一定的C++基础
一定的图形API基础
熟悉Reshade本体的各种操作
本文主要以辐射4、Reshade 5.4.2、DX11为例子,但Reshade自然是支持各种图形API和游戏的,因此其他游戏也可以参照此介绍。但要注意的是:Reshade插件不能用在任何网络游戏或者会检测作弊的游戏上面,其独特的实现方式会直接触发反作弊机制!!!

可能有有经验的小伙伴立马就跳出来回答了:我知道!就是平时在游戏中用的那个DLL,能够开启很多特效来增强画质。这里我就要对此进行一点点的区分。首先要知道的是,我们平时用的Reshade可以分为三个部分:
Reshade.dll:其提供了一个基本框架以加载并管理特效(effect)的使用,以及可视化的界面以供用户操作。
特效(Effects):以HLSL, GLSL or SPIR-V等语言编写的特效,他们规定了屏幕上面的像素将要怎么变化。
插件(Add-ons):利用高级语言编写CPU指令的方式,调用Reshade抽象并包装好的函数。能够对所使用的资源以及特效进行管理。用人话来说,就是能让这些特效不再是无条件出现,可以让它在合适的时间出现在合适的地方。同时也能去操作图形API的底层逻辑,实现很多骚操作。
至此,相信各位也能看出来这三者的区别,也对本次要介绍的主要对象有个大致的了解。

除了刚才说到的:“方便管理和调度以外”。使用Reshade插件的另一好处就是高效,在观察了一些作者制作的特效之后,我发现作者们为了兼容Reshade的各种开关,写出来的特效文件或多或少都带了一些条件分支(也就是if...else...语言)。但由于CPU和GPU的不同,(且特效文件一般由GPU去执行),这些条件分支对于GPU性能的影响特别大,而通过插件的方式能够避免这种情况的发生。
同时,dll这种动态链接库的方式,也能更好地和游戏插件的代码配合,比如辐射4的F4SE(已经测试过完美兼容)。
对于我这种苦苦挣扎于整明白图形API,并使用他来做些好玩意儿的人,插件特性的出现无疑是个惊喜。最直观的表现就是我不必再需要和庞大的图形库漫无目的地自由摔跤,一些常用的功能都有人给我包装起来了。

实际上,官方主页上面有Reshade插件的食用方法,因此我这里就在其基础上,翻译过来并添加了一些个人理解。
以防自己翻译的不准确,这里把自己不太确定的词用图片的方式说明一下:
覆盖层(OverLays):

就是平时用的用户交互界面
2.图形管线:
https://learn.microsoft.com/zh-cn/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline

英文原文:
https://reshade.me/docs
a.前言
ReShade API可以让你与ReShade载入的应用程序的资源和渲染命令进行一个狠狠的操作。 它抽象了ReShade支持的各种图形API(Direct3D 9/10/11/12、OpenGL和Vulkan)之间的差异。总之你只要包含了他的头文件,写出来的插件就可以随便用在不同的图形API里面。
ReShade插件本质来说就是个DLL,它使用仅有头文件的ReShade API来注册回调事件,然后在这些回调被ReShade调用后搞大事情。没有其他更多花里胡哨的东西,不需要导出任何函数,也不需要针对任何库进行链接。只需将ReShade资源库中的include目录添加到你的DLL项目中,并包含reshade.hpp头文件即可愉快食用。
b.基本例子
下面是一个非常基本的代码例子,这个插件注册了一个回调,每次有帧被Present之后就会执行。
//只包含这个就够了,不需要把什么d3d11.h或者reshade_api_device.hpp也包含进来
#include <reshade.hpp>
static void on_present(reshade::api::command_queue *queue, reshade::api::swapchain *swapchain, const reshade::api::rect *source_rect, const reshade::api::rect *dest_rect, uint32_t dirty_rect_count, const reshade::api::rect *dirty_rects)
{
//这里写你喜欢的东西,每次Present时就会执行
}
//Dll的Main函数
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// 在调用ReShade API的其他函数之前,要先调用'reshade::register_addon()'。
// 它将在当前进程中寻找ReShade的实例,然后初始化API。
if (!reshade::register_addon(hinstDLL))
return FALSE;
// 这行代码为"present"这个事件注册了一个回调,该事件在每次有新帧被present时发生。
// 函数签名必须符合'reshade::addon_event_traits<reshade::addon_event::present>::decl'所定义的类型。
// 加我领队了解更多,对于事件的相关内容,可以去看'reshade_events.hpp'的注释.
reshade::register_event<reshade::addon_event::present>(&on_present);
break;
case DLL_PROCESS_DETACH:
//取消先前在进程附加期间再次注册的事件回调。
reshade::unregister_event<reshade::addon_event::present>(&on_present);
// 最后从ReShade中取消对该插件的注册(这也会自动取消该插件注册的任何事件和覆盖层界面的注册)。
reshade::unregister_addon(hinstDLL);
break;
}
return TRUE;
} 欢呼吧,到这里你已经写好了一个基本的没啥卵用的Reshade插件
建立一个插件DLL之后,将其文件扩展名从.dll改为.addon,并将其放入与ReShade相同的目录。下次启动游戏的时候他就会被自动读取。
其他更复杂的例子可以去看Github上面的example

c.覆盖层
Reshade也支持添加覆盖层,比如说,它可以用来显示调试信息,或者用来让用户在应用中进行互动。覆盖层是通过使用Dear ImGui 1.86版本来创建的。
用法自然是先包含imgui.h,然后再包含reshade.hpp,之后ReShade就会自动覆盖Dear ImGui的函数。搞完之后正常使用Dear ImGui(无需构建其源代码文件)即可。
#define IMGUI_DISABLE_INCLUDE_IMCONFIG_H
#define ImTextureID unsigned long long // 将ImGui的纹理ID类型改为'reshade::api::resource_view'句柄的类型。(固定套路)
#include <imgui.h>
#include <reshade.hpp>
bool g_popup_window_visible = false;
static void draw_debug_overlay(reshade::api::effect_runtime *runtime)
{
ImGui::TextUnformatted("Some text");
if (ImGui::Button("Press me to open an additional popup window"))
g_popup_window_visible = true;
if (g_popup_window_visible)
{
ImGui::Begin("Popup", &g_popup_window_visible);
ImGui::TextUnformatted("Some other text");
ImGui::End();
}
}
static void draw_settings_overlay(reshade::api::effect_runtime *runtime)
{
ImGui::Checkbox("Popup window is visible", &g_popup_window_visible);
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// 这行代码也将同时填充Dear ImGui函数。
if (!reshade::register_addon(hinstDLL))
return FALSE;
// 在ReShade中用指定的名称注册新的覆盖层。
// 当ReShade覆盖层被打开时,会作为一个额外的窗口显示。
// 其内容是由Dear ImGui在指定的回调函数中发出的命令来定义的。
reshade::register_overlay("Test", &draw_debug_overlay);
// 也可以通过传递'nullptr'而不是特定的文字来注册一个特殊的设置覆盖层。
// 这东西会显示在ReShade界面(就是选效果的那个界面)的add-on列表中附加组件信息的下方,可用于向用户展示设置。
reshade::register_overlay(nullptr, &draw_settings_overlay);
break;
case DLL_PROCESS_DETACH:
reshade::unregister_addon(hinstDLL);
break;
}
return TRUE;
} 要注意的是:不要在回调中调用ImGui::Begin和ImGui::End来创建覆盖窗口本身(这两兄弟是正常玩ImGui要用到的),ReShade已经在调用回调函数前后帮你把事办好了。
然而你是可以用不同的标题调用ImGui::Begin和ImGui::End来打开额外的弹出窗口(不过不建议这样做,因为这些窗口在VR中会整得很复杂)。
覆盖层的名字在ReShade和所有插件之间是共享的,这意味着你可以用一个已经被ReShade或其他插件使用的名称来注册,从而将小部件附加到他们的覆盖层上。
例如,reshade::register_overlay("Settings", ...)允许你在ReShade的设置页面上添加部件,而reshade::register_overlay("OSD", ...)允许你为ReShade提供的始终可见的屏幕显示(时钟、FPS、帧时间)添加额外信息。

d. 抽象
图形API的抽象是以Direct3D 12和Vulkan API为基底的,因此,对于以前使用过这些API的开发者来说,许多术语理应比较熟悉。
所有类和方法的详细文档可以在相关的头文件中找到(关于抽象对象类,请看reshade_api_device.hpp,关于可用事件,请看reshade_events.hpp)。
其他所有的基础对象都是由reshade::api::device创建的。它代表了一个逻辑上的渲染设备,通常被映射到一个物理GPU(但也可能被映射到多个GPU)。ReShade会在应用程序创建设备后调用reshade::addon_event::init_device事件,它可以用来做一些只需要用到一次的初始化工作。而reshade::addon_event::destroy_device事件会在这个设备再次被销毁之前被调用,这可以用来执行清理工作。
// 下面演示的这个回调函数可以通过'reshade::register_event<reshade::addon_event::init_device>(&on_init_device)'来注册。
static void on_init_device(reshade::api::device *device)
{
// 以防万一有人只想用本地图形API来处理一部分事情,而不是把活全揽了
// 可以用如下的方式。
if (device->get_api() == reshade::api::device_api::d3d11)
{
ID3D11Device *const d3d11_device = (ID3D11Device *)device->get_native();
// ...
}
// 当然最好还是通过图形API的抽象来。
// E.g. 想要在GPU内存中创建一个新的800x600大小的纹理,可以像这样来调用'reshade::api::device::create_resource()'。
reshade::api::resource texture = {};
const reshade::api::resource_desc desc(
800, 600, 1, 1,
reshade::api::format::r8g8b8a8_unorm,
1,
reshade::api::memory_heap::gpu_only,
reshade::api::resource_usage::shader_resource | reshade::api::resource_usage::render_target);
if (!device->create_resource(desc, nullptr, reshade::api::resource_usage::undefined, &texture))
{
// 错误处理 ...
}
// ...
} 为了执行渲染指令,应用程序必须将这些指令记录到reshade::api::command_list中,然后提交给reshade::api::command_queue。在一些图形API中,只有一个隐含的指令列表和队列,但现代的图形API如Direct3D 12和Vulkan其实都允许创建多个这样的指令队列,以实现更高效的多线程渲染。
ReShade将在任何此类对象被程序创建后调用reshade::addon_event::init_command_list和reshade::addon_event::init_command_queue事件(包括旧图形API的隐式事件)。类似地,reshade::addon_event::destroy_command_list和reshade::addon_event::destroy_command_queue在其销毁时被调用。
ReShade还会将把当前的指令列表的对象传递给每一个指令事件,比如reshade::addon_event::draw, reshade::addon_event::dispatch等等,这些指令可以用来向该指令列表添加额外的命令或替换原生程序中的命令。(实际上就是Hook了)
// 下面演示的这个回调函数可以通过'reshade::register_event<reshade::addon_event::draw>(&on_draw)'注册。
static bool on_draw(reshade::api::command_list *cmd_list, uint32_t vertices, uint32_t instances, uint32_t first_vertex, uint32_t first_instance)
{
// 在每次绘制三角形之前,将渲染目标清除为红色
if (vertices == 3 && instances == 1)
{
reshade::api::resource_view rtv = ...;
const float clear_color[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
cmd_list->clear_render_target_view(rtv, clear_color);
}
// 返回'true'可以防止原生的指令被实际执行(也就是只执行我们自己写的这个,不再去管原本的Draw指令)。
// 返回'false'自然就是都执行了(相当于在原本的Draw前面加上这一段代码)。
return false;
}
在屏幕上显示结果是通过reshade::api::swapchain这个交换链对象来实现的。这是一个应用程序用以渲染的缓存的集合,这些缓存最终会被呈现在屏幕上。可能会有多个交换链,例如,如果程序正在向多个窗口渲染,或者同时向一个屏幕和一个VR头显渲染。
ReShade会在应用程序创建这样的对象后再次调用reshade::addon_event::init_swapchain事件(而reshade::addon_event::destroy_swapchain则是在销毁时)。
此外,ReShade会在交换链创建之前调用reshade::addon_event::create_swapchain事件,所以插件可以在实际创建交换链之前去改它的描述。例如,如果要将分辨率强制为一个特定的值,可以做如下的操作:
// 下面演示的这个回调函数可以通过'reshade::register_event<reshade::addon_event::create_swapchain>(&on_create_swapchain)'注册。
static bool on_create_swapchain(reshade::api::swapchain_desc &desc, void *hwnd)
{
// 如果应用程序试图在800x600下创建交换链,则将分辨率改为1920x1080。
if (desc.texture.width == 800 &&
desc.texture.height == 600)
{
desc.texture.width = 1920;
desc.texture.height = 1080;
}
// 返回'true'可以防止原生的指令被实际执行。
// 返回'false'自然就是都执行了。
return true;
}

e.其他一些注意事项
ReShade会将一个独立的后处理效果的运行阶段与大多数交换链联系起来。这是一个通常由ReShade 覆盖层 控制的运行阶段,但它也可以通过编程方式来控制:使用ReShade API来调用reshade::api::effect_runtime对象。
与所描述的基本API抽象对象不同,任何缓冲区、纹理、渲染管线等都是通过句柄来引用的。这些都是由应用程序创建并传递给事件(比如reshade::addon_event::init_resource, reshade::addon_event::init_pipeline, ...),或者可以通过ReShade::api::device API的对象创建(通过reshade::api::device::create_resource(), reshade::api::device::create_pipeline(), ...)。
缓存和纹理是通过reshade::api::resource 句柄来引用的。深度模板、渲染目标、着色器资源或这些资源的无序访问视图通过 reshade::api::resource_view 句柄引用。而采样器状态对象通过reshade::api::sampler 句柄被引用,(部分)管道状态对象通过reshade::api::pipeline 被引用,以此类推。