LOADING

UE4.26-UE4.27 Compute shader的实现过程

做这个文档的起因是,网上搜索到的教程的版本都比较旧,基本上都是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也能正常输出: