做这个文档的起因是,网上搜索到的教程的版本都比较旧,基本上都是4.24以前的,然后UE4似乎从4.25之后,对底层代码做了不少改动,导致网上的做法都不可行了,所以做这个文档记录一下。
还有我不敢保证更新的版本支持这个写法,比如UE5。
该方法是独立于渲染管线之外的,继承于FGlobalShader,如果想集成到Mesh renderpass 里,想根据渲染顺序读取模型数据之类的的话就不行了(不过一般ComputeShader也不会走管线计算)
首先第一步,新建一个模块(Module)
为什么要新建Module?
因为我们ComputeShader需要继承FGlobalShader,而继承了FGlobalShader类,必须要在引擎之前加载,不然会导致错误,加载失败,所以我们不能把Compute shader写在引擎的逻辑代码上,必须通过Module,使其提前加载。
创建了Module之后,在项目的.uproject文件上修改即可

如果这样还是报错的话,就把模块的位置放到上面试试(亲测可行)
如果实在不行的话,那就只能和网上说一样新建一个Plugins,然后再在里面生成一个module了。
新建模块可以在商城里面找到一个免费的插件

这个免费插件可以帮助我们一键生产模块,自动添加cs和uproject配置,很方便,非常推荐。
这里我新建了一个模块叫ComputeShaderTest
第二步,编写Compute Shader(TestCS.usf)
编写测试用的ComputeShader,这里我们直接输出一个绿色。
TestCS.usf
#include "/Engine/Private/Common.ush"
RWTexture2D<float4> _texture_out;
uint Index;
<span style="" ></span>
void CSMain(uint3 id :SV_DispatchThreadID)
{
_texture_out[id.xy] = float4(0.0,1.0, 0.0, 1.0);
}
文件放在了项目的根目录的Shaders文件夹内
引擎可不会这么聪明找到我们的shader在哪里,所以我们需要添加shader的搜索目录,在模块加载的时候添加,也就是shader编译之前。
ComputeShaderTest.cpp
void FComputeShaderTest::StartupModule()
{
UE_LOG(
ComputeShaderTest, Warning,
TEXT("ComputeShaderTest module has been loaded")
);
AddShaderSourceDirectoryMapping(
TEXT("/Shaders"),
FPaths::GetPath(FPaths::GetProjectFilePath())+"/Shaders"
);
}
第三步,编写我们的ComputeShader类
理论上来说,我们的ComputeShader类,写在哪里都无所谓,所以这里我为了方便,直接和新建的蓝图Actor类写到了一起了。
在UE4里,在我新建的模块里,新建一个继承自Actor的类,这里我起名叫ComputeShaderTestActor。

在写之前我们还需要在 ComputeShaderTest.build.cs() 里引入两个渲染模块:
RenderCore 和 RHI

首先是.h文件
ComputeShaderTestActor.h
#include "GlobalShader.h"
class COMPUTESHADERTEST_API FTestComputeShader :public FGlobalShader
{
private:
//声明全局Shader类型,和 DECLARE_GLOBAL_SHADER 是同一个内容,二选一
DECLARE_SHADER_TYPE(FTestComputeShader,Global);
//------------------------------------ 下面这段可有可无,去掉好像也没影响...
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_UAV(RWTexture2D<float4>, _texture_out)
END_SHADER_PARAMETER_STRUCT()
//------------------------------------
LAYOUT_FIELD(FRWShaderParameter, OutputSurface); //
public:
FTestComputeShader() {}
FTestComputeShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
OutputSurface.Bind(Initializer.ParameterMap, TEXT("_texture_out"));
}
}
至于class后面的这个DLL 导出宏(COMPUTESHADERTEST_API),这个感觉最好加一下,不过不加也没什么问题,就是不知道会不会影响到打包?然后我们需要在public里加入几个方法:
ComputeShaderTestActor.h
//Compute shader 执行方法
static void ExecComputeShader_RenderThread(UTextureRenderTarget2D* RenderTarget, FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel );
static bool ShouldCache(EShaderPlatform Platform)
{
//return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5);
return true;
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment )
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
}
void BindBuffers(FRHICommandList& RHICmdList, FUnorderedAccessViewRHIRef OutputSurfaceUAV, FRHITexture* Texture)
{
FRHIComputeShader* pShaderRHI = RHICmdList.GetBoundComputeShader();
check(OutputSurface.IsBound());
RHICmdList.SetUAVParameter(pShaderRHI,OutputSurface.GetUAVIndex(),OutputSurfaceUAV);
}
void UnbindBuffers(FRHICommandList& RHICmdList)
{
FRHIComputeShader* pShaderRHI = RHICmdList.GetBoundComputeShader();
RHICmdList.SetUAVParameter(pShaderRHI,OutputSurface.GetUAVIndex(),FUnorderedAccessViewRHIRef());
}
稍微解释下:UseComputeShader_RenderThread
是执行我们ComputeShader的方法,它将会在渲染序列进行。
ShouldCache
告诉引擎我们这个shader是否需要生产shader cache
ModifyCompilationEnvironment
根据某些判断可以修改shader的环境,一般指的是宏的声明和修改(变体),因此每一次shader编译都会跑到这个函数。具体可以上网搜一下,这里我们并不需要用到。
BindBuffers
这个方法是我们自定义的,用于在渲染序列中进行参数的绑定。
UnbindBuffers
执行完之后需要解除对资源的绑定,才能在CPU对资源进行操作。
接下来是.cpp文件
首先要加入实例化shader的宏,没有它shader编译就不会执行
ComputeShaderTestActor.cpp
IMPLEMENT_GLOBAL_SHADER(FTestComputeShader, "/Shaders/TestCS.usf", "CSMain", SF_Compute);
之后就是渲染函数的实现,不讲太多了,上面有非常详细的注释。ComputeShaderTestActor.cpp
#include "Engine/TextureRenderTarget2D.h"
//#include "../Shaders/TestCS.usf"
IMPLEMENT_GLOBAL_SHADER(FTestComputeShader, "/Shaders/TestCS.usf", "CSMain", SF_Compute);
void FTestComputeShader::ExecComputeShader_RenderThread( UTextureRenderTarget2D* RenderTarget,
FRHICommandListImmediate& RHICmdList,
ERHIFeatureLevel::Type FeatureLevel
)
{
//检查是否在渲染线程
check(IsInRenderingThread());
check(RenderTarget!= nullptr);
//从global shader获取我们的compute shader
TShaderMapRef<FTestComputeShader> ComputeShader(GetGlobalShaderMap(FeatureLevel));
//使用之前,需要绑定Compute Shader
FRHIComputeShader* cs = ComputeShader.GetComputeShader();
RHICmdList.SetComputeShader(cs);
//创建贴图
int32 SizeX = RenderTarget->SizeX;
int32 SizeY = RenderTarget->SizeY;
FRHIResourceCreateInfo CreateInfo;
//创建UAV Texture2D,一定要注意第三个贴图格式!必须要和renderTarget一致,不然就无法copy到renderTarget上
FTexture2DRHIRef Texture = RHICreateTexture2D(SizeX, SizeY, RenderTarget->GetFormat(), 1, 1, TexCreate_ShaderResource | TexCreate_UAV, CreateInfo);
//创建UAV
FUnorderedAccessViewRHIRef TextureUAV = RHICreateUnorderedAccessView(Texture);
//绑定UAV(Set UAV Parameter)
ComputeShader->BindBuffers(RHICmdList, TextureUAV , Texture);
//执行compute shader
DispatchComputeShader(RHICmdList, ComputeShader, SizeX/32, SizeY/32, 1);
//计算完成之后,解除UAV的绑定,释放GPU权限,这样才能在CPU读取到数据
ComputeShader->UnbindBuffers(RHICmdList);
//把UAV texture 复制到我们的RenderTarget上
RHICmdList.CopyTexture(Texture, RenderTarget->GetRenderTargetResource()->TextureRHI, FRHICopyTextureInfo());
//
/*------------------开始读取数据--------------*/
uint32 LolStride = 0;
//读出贴图数据
unsigned char* TextureData = (unsigned char*)RHICmdList.LockTexture2D(Texture, 0, EResourceLockMode::RLM_ReadOnly, LolStride, false);
TArray<FColor> Bitmap;
for (int32 y = 0; y < SizeY; y++)
{
for (int32 x = 0; x < SizeX; x++)
{
FColor convert;
convert.R = TextureData[(y*SizeX + x) * 4 + 2];//R
convert.G = TextureData[(y*SizeX + x) * 4 + 1];//G
convert.B = TextureData[(y*SizeX + x) * 4 + 0];//B
convert.A = TextureData[(y*SizeX + x) * 4 + 3];//A 0:全透明;255:全不透明
Bitmap.Add(convert);
}
}
//停止贴图读取
RHICmdList.UnlockTexture2D(Texture, 0, false);
/*---------------------------------------------*/
//确保大小正确
if (Bitmap.Num() == SizeX*SizeY)
{
//把贴图存在引擎的截图路径
IFileManager::Get().MakeDirectory(*FPaths::ScreenShotDir(), true);
const FString ScreenFileName(FPaths::ScreenShotDir() / TEXT("VisualizeTexture"));
uint32 width = Bitmap.Num() / Texture->GetSizeY();
//保存成BMP
FFileHelper::CreateBitmap(*ScreenFileName, Texture->GetSizeX(), Texture->GetSizeY(), Bitmap.GetData());
}
}
第四步,把功能实现到引擎蓝图里
.h头文件
回到头文件,找到我们新建的Actor类
ComputeShaderTestActor.h
UENUM()
enum class ERHIFeature :uint8
{
ES2_REMOVED = ERHIFeatureLevel::Type::ES2_REMOVED,
ES3_1 = ERHIFeatureLevel::Type::ES3_1,
SM4_REMOVED = ERHIFeatureLevel::Type::SM4_REMOVED,
SM5 = ERHIFeatureLevel::Type::SM5,
};
UCLASS()
class COMPUTESHADERTEST_API AComputeShaderTestActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AComputeShaderTestActor();
UFUNCTION(BlueprintCallable)
void TestComputeShader(int TextureSize = 256 ,ERHIFeature type = ERHIFeature::SM5);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
};
这里我声明了一个枚举,用来判断不同的平台,大家也可以直接在这个C++里对不同平台直接判断,就不用这么麻烦了.cpp文件
ComputeShaderTestActor.cpp
void AComputeShaderTestActor::TestComputeShader(int TextureSize,ERHIFeature type)
{
if(TextureSize>=1)
{
if(_RT != NULL)
{
_RT->ReleaseResource();
}
//新建一张RenderTarget,也可以从蓝图读取进来,就不需要我们手动释放内存了
_RT = NewObject<UTextureRenderTarget2D>(GetTransientPackage(),NAME_None,RF_Transient);
check(_RT);
_RT->RenderTargetFormat = RTF_RGBA8;
_RT->ClearColor = FLinearColor(0,0,0,1);
_RT->bAutoGenerateMips = false;
_RT->InitAutoFormat(TextureSize, TextureSize);
_RT->UpdateResourceImmediate(true);
ENQUEUE_RENDER_COMMAND(TestCommand)(
[this,type](FRHICommandListImmediate& RHICmdList)
{
FTestComputeShader::ExecComputeShader_RenderThread(_RT,RHICmdList,(ERHIFeatureLevel::Type)type);
});
}
//_RT = rt;
}
使用ENQUEUE_RENDER_COMMAND,通过Lambda表达式,把方法抛到渲染序列中执行即可。最后蓝图里调用

成功输出了一张256的纯绿色贴图了

RenderTarget也能正常输出:
