Unreal Engine中使用GPU加速计算
对于大量数值并行计算,GPU处理更有优势效率更高,比如对大地形Mask的处理,植被撒点点云的计算等等UE中的shader有多种类型:
[*]MaterialShader
[*]GlobalShader
[*]NiagaraShader
UE中也提供了在材质中添加Custom Node,在节点输入自定义shader处理逻辑.并且材质编辑器中内置了大量节点可以组合使用处理渲染效果.如果只是使用GPU处理计算可以使用GlobalShader的方式.
详细介绍可以看官方文档: https://docs.unrealengine.com/5.1/en-US/shader-development-in-unreal-engine/
下面使用的是GlobalShader处理计算逻辑,将GPU计算后的结果填充到数组中供UE使用.
新增一个插件,并修改插件LoadingPhase
"Modules": [
{
"Name": "ComputeShaderRuntime",
"Type": "Runtime",
"LoadingPhase": "PostConfigInit",
"WhitelistPlatforms": [ "Win64", "Mac", "Android", "IOS", "Linux" ]
}
]在插件中新增shader目录存放自定义shader代码
处理shader文件虚拟目录,在module.cpp中
void FComputeShaderRuntimeModule::StartupModule()
{
FString PluginShaderDir =
FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("ComputeShader"))->GetBaseDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping("/CustomShaders", PluginShaderDir);
}
在与shader绑定的cpp文件中:
IMPLEMENT_GLOBAL_SHADER(FTestShader, "/CustomShaders/MyShader.usf", "MainComputeShader", SF_Compute);
新增FGlobalShader子类声明变量供Shader中使用
class FTestShader : public FGlobalShader
{
DECLARE_SHADER_TYPE(FTestShader, Global);
FTestShader();
explicit FTestShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer);
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return GetMaxSupportedFeatureLevel(Parameters.Platform) >= ERHIFeatureLevel::SM5;
};
static void ModifyCompilationEnvironment(
const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment);
LAYOUT_FIELD(FShaderResourceParameter, Count);
LAYOUT_FIELD(FShaderResourceParameter, Positions);
};
FTestShader::FTestShader()
{
}
FTestShader::FTestShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
Count.Bind(Initializer.ParameterMap, TEXT("Count"));
Positions.Bind(Initializer.ParameterMap, TEXT("Positions"));
}
void FTestShader::ModifyCompilationEnvironment(
const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
OutEnvironment.CompilerFlags.Add(CFLAG_StandardOptimization);
}
IMPLEMENT_GLOBAL_SHADER(FTestShader, "/CustomShaders/MyShader.usf", "MainComputeShader", SF_Compute);
其中SF_Compute声明是Compute Shader,这是DirectX 11 API新加入的特性,可直接将GPU作为并行处理器加以利用,GPU将不仅具有3D渲染能力,也具有其他的运算能力,多线程处理技术使游戏更好地利用系统的多个核心,官方说明: https://learn.microsoft.com/en-us/windows/win32/direct3d11/direct3d-11-advanced-stages-compute-shader
添加逻辑调用GPU执行计算:
UE_LOG(LogTemp, Log, TEXT("start gpu"));
OutPoints.Init(FVector3f::ZeroVector, Count);
TResourceArray<FVector3f> PositionArray;
PositionArray.Init(FVector3f::ZeroVector, Count);
FRHICommandListImmediate& RHICommands = GRHICommandList.GetImmediateCommandList();
FRHIResourceCreateInfo CreateInfoPositions(TEXT(&#34;CreateInfoPositions&#34;));
CreateInfoPositions.ResourceArray = &PositionArray;
PositionsBuffer = RHICreateStructuredBuffer(
sizeof(FVector3f), sizeof(FVector3f) * Count, BUF_UnorderedAccess | BUF_ShaderResource, CreateInfoPositions);
PositionsBufferUAV = RHICreateUnorderedAccessView(PositionsBuffer, false, false);
UE_LOG(LogTemp, Log, TEXT(&#34;start call thread&#34;));
ENQUEUE_RENDER_COMMAND(FComputeShaderRunner)
(
[&](FRHICommandListImmediate& RHICommands)
{
TShaderMapRef<FTestShader> CS(GetGlobalShaderMap(ERHIFeatureLevel::SM5));
FRHIComputeShader* RHIComputeShader = CS.GetComputeShader();
RHICommands.SetShaderParameter(RHIComputeShader, CS->ParameterMapInfo.LooseParameterBuffers.BaseIndex,
CS->Count.GetBaseIndex(), sizeof(int), &Count);
RHICommands.SetUAVParameter(RHIComputeShader, CS->Positions.GetBaseIndex(), PositionsBufferUAV);
RHICommands.SetComputeShader(RHIComputeShader);
double StartTime = FPlatformTime::Seconds();
DispatchComputeShader(RHICommands, CS, Count/8, 1, 1);
uint8* data = static_cast<uint8*>(RHILockBuffer(PositionsBuffer, 0, Count * sizeof(FVector3f), RLM_ReadOnly));
FMemory::Memcpy(OutPoints.GetData(), data, Count * sizeof(FVector3f));
RHIUnlockBuffer(PositionsBuffer);
double EndTime = FPlatformTime::Seconds();
double OffsetTime = EndTime - StartTime;
std::string Str = std::to_string(OffsetTime);
FString OutStr = UTF8_TO_TCHAR(Str.c_str());
UE_LOG(LogTemp, Error, TEXT(&#34;Time: %s&#34;), *OutStr);
UE_LOG(LogTemp, Log, TEXT(&#34;finish&#34;));
});
UE_LOG(LogTemp, Log, TEXT(&#34;finish call thread&#34;));
usf文件中的shader为了验证计算效率添加了一些正玄余玄计算:
int Count;
RWStructuredBuffer<float3> Positions;
void MainComputeShader(uint3 groupId : SV_GroupID,
uint3 groupThreadId : SV_GroupThreadID,
uint3 dispatchThreadId : SV_DispatchThreadID,
uint groupIndex : SV_GroupIndex)
{
int index = dispatchThreadId.x +
dispatchThreadId.y * groupId.x * 8 +
dispatchThreadId.z * groupId.x * 8 * groupId.y * 8;
Positions.x = sin(index) * index * 2;
Positions.y = cos(index) * index * 2;
Positions.z = sin(index) + cos(index) + Count;
}
对于Dispatch和numthreads的含义,微软官方有一篇介绍https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/sv-groupindex
按照上图的示例,Dispatch(5,3,2),numthreads(10,8,3).5x3x2=30 使用30个线程组进行处理.每个线程组里的线程数量由numthreads决定10x8x3=240,一共30x240=7200个线程进行处理.
上图中自定义的逻辑中,每个线程组中线程数量是8x1x1=8,线程组数量是处理的总数据量/每个线程组线程数量决定,如果需要处理64数组长度的数据,64/8=8个线程组,每个线程组8个线程处理所有计算,index可以通过声明一个buffer存储,每个线程组偏移得到,也可以计算index值:
int index = DispatchThreadID.x + DispatchThreadID.y *( DispatchSize.x * size_x) + DispatchThreadID.z * (DispatchSize.x * size_x ) * (DispatchSize.y * size_y);
将shader中的逻辑在c++再实现一遍,对比CPU和GPU计算效率
UE_LOG(LogTemp, Log, TEXT(&#34;start cpu&#34;));
OutPoints.Init(FVector3f::ZeroVector, Count);
double StartTime = FPlatformTime::Seconds();
for (int i = 0; i < Count; i++)
{
OutPoints.X = FMath::Sin(float(i)) * i * 2;
OutPoints.Y = FMath::Cos(float(i)) * i * 2;
OutPoints.Z = FMath::Sin(float(i)) + FMath::Cos(float(i))+Count;
}
double EndTime = FPlatformTime::Seconds();
double OffsetTime = EndTime - StartTime;
std::string Str = std::to_string(OffsetTime);
FString OutStr = UTF8_TO_TCHAR(Str.c_str());
UE_LOG(LogTemp, Error, TEXT(&#34;Time: %s&#34;), *OutStr);
UE_LOG(LogTemp, Log, TEXT(&#34;finish&#34;));
在同一环境下分别运行CPU,GPU逻辑,执行耗时对比:
将处理结果使用UInstancedStaticMeshComponent显示在场景中,使用instance的方式处理大量静态相同物体节省性能
ATestActorGPU::ATestActorGPU()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
RootComponent = CreateDefaultSubobject<USceneComponent>(&#34;Root&#34;);
InstanceMesh = CreateDefaultSubobject<UInstancedStaticMeshComponent>(&#34;InstanceMesh&#34;);
}
void ATestActorGPU::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (bUpdateDisplay)
{
bUpdateDisplay = false;
InstanceMesh->ClearInstances();
for (int i = 0; i < Count; i++)
{
FTransform Trans;
Trans.SetLocation(Points);
Trans.SetScale3D(FVector::OneVector);
InstanceMesh->AddInstance(Trans);
}
}
}
Compute Shader中还可以通过RWTexture2D将纹理传入到shader中进行处理,比如一张256x256大小的纹理
RWTexture2D<float4> Texture;
void MainComputeShader(uint3 id : SV_DispatchThreadID)
{
Texture = float4(0,0,0,0);
}
定义,每个线程组线程数量8x8x1=64
Dispatch(256/8,256/8,1)=Dispatch(32,32,1)
总线程数量64x32x32=65536(纹理大小也是256x256=65536)
SV_DispatchThreadID官方的解释是 SV_GroupID x numthreads + GroupThreadID
按照这个公式在第一个维度的处理范围是(0-7,0-7,1),其他维度同理(参照截图中的红框)
这样每个线程组有64个线程处理,每个线程组获取的数组下标正确,他们都在并行处理逻辑,一共有32*32=1024个线程组
注意事项
[*]UE5 DX12有问题,CPU向GPU传递参数时出错,修改成DX11后正常
[*]方法内部定义的数组变量会被释放,定义成类成员变量
[*]使用FVector3f传递,不是FVector
[*]usf文件中变量错误,使用错误,虚拟目录错误都会导致UE引擎启动失败
一些测试截图:
页:
[1]