找回密码
 立即注册
查看: 172|回复: 0

Unreal Engine中使用GPU加速计算

[复制链接]
发表于 2022-11-24 09:14 | 显示全部楼层 |阅读模式
对于大量数值并行计算,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("CreateInfoPositions"));
        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("start call thread"));
        ENQUEUE_RENDER_COMMAND(FComputeShaderRunner)
        (
                [&](FRHICommandListImmediate& RHICommands)
                {
                        TShaderMapRef<FTestShader> CS(GetGlobalShaderMap(ERHIFeatureLevel::SM5));
                        FRHIComputeShader* RHIComputeShader = CS.GetComputeShader();
                        RHICommands.SetShaderParameter(RHIComputeShader, CS->ParameterMapInfo.LooseParameterBuffers[0].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("Time: %s"), *OutStr);
                        UE_LOG(LogTemp, Log, TEXT("finish"));
                });
        UE_LOG(LogTemp, Log, TEXT("finish call thread"));
usf文件中的shader为了验证计算效率添加了一些正玄余玄计算:
int Count;
RWStructuredBuffer<float3> Positions;

[numthreads(8, 1, 1)]
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[index].x = sin(index) * index * 2;
        Positions[index].y = cos(index) * index * 2;
        Positions[index].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("start cpu"));
        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("Time: %s"), *OutStr);
        UE_LOG(LogTemp, Log, TEXT("finish"));
在同一环境下分别运行CPU,GPU逻辑,执行耗时对比:




将处理结果使用UInstancedStaticMeshComponent显示在场景中,使用instance的方式处理大量静态相同物体节省性能
ATestActorGPU::ATestActorGPU()
{
        PrimaryActorTick.bCanEverTick = true;
        PrimaryActorTick.bStartWithTickEnabled = true;
        RootComponent = CreateDefaultSubobject<USceneComponent>("Root");
        InstanceMesh = CreateDefaultSubobject<UInstancedStaticMeshComponent>("InstanceMesh");
}

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;
[numthreads(8, 8, 1)]
void MainComputeShader(uint3 id : SV_DispatchThreadID)
{
        Texture[id.xy] = float4(0,0,0,0);
}
定义[numthreads(8, 8, 1)],每个线程组线程数量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引擎启动失败
一些测试截图:









本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-6-4 17:44 , Processed in 0.099170 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表