4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

独自のレンダリングパスでVoxelを描画してみる

4
Last updated at Posted at 2025-12-20

前置き

本内容はUnrealEngine環境での独自のVoxel描画環境を構築するものであり、
既存のプラグインやツールを利用したものではありませんので、予めご了承ください。
また、細かい解説を省いている部分が多々ありますので、必要な方は都度公式ドキュメント等を参照いただくことを推奨いたします。
何かの実装に引用される場合には、あくまで参考程度にしていただけますと幸いです。

概要

内容

独自のRenderPassを実装し、任意のActorが持つVoxel情報を可視化できるようにし
独自のVoxel描画環境構築が目的です。

DX12やVulkan等のレンダリング処理フローが大枠理解できている方だったり、
UEのエンジン構造に詳しい方にとっては退屈な内容に思うかもしれません。
よく知らない方もこれを機に少しでも何か興味を持っていただけるものがあればとてもうれしいです。

フロー

1. 独自のレンダリングパスの実装
2. 独自の描画オブジェクトの実装
3. Voxelの描画実装
おまけ. Voxelデータを用いた変わった描画

実装

環境

IDE: Rider or VisualStudio2022
UE ver : 5.6.1
C++ ver C++20

利用モジュールの追加

[ProjectName].Build.cs内のPublicDependencyModuleNamesに
以下3種類をの追加をしてください
RHI: GPU 抽象化レイヤでDX12/Vulkan等のグラフィックスレイヤーとの仲介を担う機能のモジュール
RenderCore: RHI の上に乗るUEのエンジン共通のレンダリング基盤機能のモジュール
Renderer: BasePass/Shadow/PostProcess等の実際のレンダラー機能モジュール

"RenderCore",
"RHI",
"Renderer"

独自のレンダリングパスの実装

シェーダー構造体の用意

新しいレンダリングパスの追加で、独自のシェーダーを利用するので、
以下二つVertex/PixelShaderの定数データ(Uniform)と
RenderDependencyGraph(RDG)のレンダーターゲット設定を用意する。

とりあえずの用意だけなので最低の記載だけにしてます。
SHADER_PARAMETER: 定数(第一引数: メンバ型 第二引数: メンバ名前)
RENDER_TARGET_BINDING_SLOTS: のレンダーターゲット設定

BEGIN_SHADER_PARAMETER_STRUCT / END_SHADER_PARAMETER_STRUCTマクロを
利用することでSHADER用構造体の定義が楽にできます。
※どういう構造体なの気になる方はマクロの中追ってみてもいいですが、
マクロ内マクロあって、追うのめんどいんでAI使って形成の例とかを出力させてみるのがいいかなと

IMPLEMENT_GLOBAL_SHADERは、定義したCS(FGlobalShader)とush側の関数の紐づけです。
変更検知・再コンパイル・キャッシュ管理もこの型情報を通じて行われます。
※ushで記述のメンバとFGlobalShaderのパラメータは一致させてください、
していない場合エラーログ出力またはクラッシュします。

// Global shaders
class FVoxelMeshVS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FVoxelMeshVS);
    SHADER_USE_PARAMETER_STRUCT(FVoxelMeshVS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FMatrix44f, LocalToWorld)
        SHADER_PARAMETER(FMatrix44f, WorldToClip)
    END_SHADER_PARAMETER_STRUCT()
};

class FVoxelMeshPS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FVoxelMeshPS);
    SHADER_USE_PARAMETER_STRUCT(FVoxelMeshPS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FLinearColor, VoxelColor)
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FVoxelMeshVS, "/Voxel/VoxelMesh.usf", "VoxelMeshVS", SF_Vertex);
IMPLEMENT_GLOBAL_SHADER(FVoxelMeshPS, "/Voxel/VoxelMesh.usf", "VoxelMeshPS", SF_Pixel);

BEGIN_SHADER_PARAMETER_STRUCT(FVoxelMeshPassParameters, )
    RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()

VoxelMesh.usfの中身は以下で、特に変わったことは何もしていないです。

struct FVSInput
{
    float3 Position : ATTRIBUTE0;
};

struct FVSOutput
{
    float4 Position : SV_POSITION;
};

float4x4 LocalToWorld;
float4x4 WorldToClip;

FVSOutput VoxelMeshVS(FVSInput In)
{
    FVSOutput Out;
    float4 LocalPos = float4(In.Position, 1.0);
    float3 WorldPos = mul(LocalPos, LocalToWorld).xyz;
    Out.Position = mul(float4(WorldPos, 1.0), WorldToClip);
    return Out;
}

float4 VoxelColor;

float4 VoxelMeshPS(FVSOutput In) : SV_Target0
{
    return VoxelColor;
}

RenderPassの追加の関数の用意

FRDGBuilder#AddPassを利用することで、RenderPassの追加ができます。
一旦テスト描画をするので設定は仮で行います。

描画確認のために設定で、
確実に描画をするために、RasterizerStateのカリング設定を「CM_None」
DepthStencilStateの深度テストによる描画条件を「CF_Always」に指定。

上記二つのEnumについての基本的な種類の一覧を掲載しておきます。
DX12の値をセットにすると個人的に見やすかったのでDX12のEnumも記載しますが、
なじみがない人は無視で問題ないです。

ERasterizerCullMode

種類 意味/挙動
CM_None (D3D12_CULL_MODE_NONE) カリングなしの両面描画
CM_CW (D3D12_CULL_MODE_BACK) 表を描画、裏をカリング
CM_CCW (D3D12_CULL_MODE_FRONT) 裏を描画、表をカリング

ECompareFunction

種類 描画条件
CF_Less (D3D12_COMPARISON_FUNC_LESS) 新しいピクセルの深度 < 既存の深度バッファの値
CF_LessEqual (D3D12_COMPARISON_FUNC_LESS_EQUAL) 新しいピクセルの深度 ≤ 既存の深度バッファの値
CF_Greater (D3D12_COMPARISON_FUNC_GREATER) 新しいピクセルの深度 > 既存の深度バッファの値
CF_GreaterEqual (D3D12_COMPARISON_FUNC_GREATER_EQUAL) 新しいピクセルの深度 ≥ 既存の深度バッファの値
CF_Equal (D3D12_COMPARISON_FUNC_EQUAL) 新しいピクセルの深度 == 既存の深度バッファの値
CF_NotEqual (D3D12_COMPARISON_FUNC_NOT_EQUAL) 新しいピクセルの深度 ≠ 既存の深度バッファの値
CF_Never (D3D12_COMPARISON_FUNC_NEVER) 常に失敗 (どんな深度値でも描画されない)
CF_Always (D3D12_COMPARISON_FUNC_ALWAYS ) 常に成功 (深度バッファの値に関係なく描画を許可)

※他コード内の細かな値の解説は省きます

void AddVoxelDebugRenderPass(
    FRDGBuilder& GraphBuilder,
    FRDGTextureRef SceneColor,
    FRDGTextureRef SceneDepth)
{
    auto* PassParameters = GraphBuilder.AllocParameters<FVoxelMeshPassParameters>();
    PassParameters->RenderTargets[0] = FRenderTargetBinding(SceneColor, ERenderTargetLoadAction::ELoad);
    PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(SceneDepth, ERenderTargetLoadAction::ELoad, ERenderTargetLoadAction::ELoad, FExclusiveDepthStencil::DepthWrite_StencilNop);

    const FIntPoint SceneExtent = SceneColor->Desc.Extent;
    GraphBuilder.AddPass(
        RDG_EVENT_NAME("VoxelDebugRenderPass"),
        PassParameters,
        ERDGPassFlags::Raster,
        [PassParameters, SceneExtent](FRHICommandListImmediate& RHICmdList)
        {
            check(IsInRenderingThread());

            FGraphicsPipelineStateInitializer GraphicsPSO;
            RHICmdList.ApplyCachedRenderTargets(GraphicsPSO);

            GraphicsPSO.BlendState        = TStaticBlendState<>::GetRHI();
            GraphicsPSO.RasterizerState   = TStaticRasterizerState<FM_Solid, CM_None>::GetRHI();
            GraphicsPSO.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
            GraphicsPSO.PrimitiveType     = PT_TriangleList;

            ERHIFeatureLevel::Type FeatureLevel = GMaxRHIFeatureLevel;
            TShaderMapRef<FVoxelMeshVS> VertexShader(GetGlobalShaderMap(FeatureLevel));
            TShaderMapRef<FVoxelMeshPS> PixelShader(GetGlobalShaderMap(FeatureLevel));

            static FVertexDeclarationRHIRef VertexDecl;
            if (!VertexDecl.IsValid())
            {
                FVertexDeclarationElementList Elements;
                Elements.Add(FVertexElement(
                    0,                      // StreamIndex
                    0,                      // Offset
                    VET_Float3,             // Type
                    0,                      // AttributeIndex (POSITION0)
                    sizeof(FVector3f)       // Stride
                ));
                VertexDecl = RHICreateVertexDeclaration(Elements);
            }
            GraphicsPSO.BoundShaderState.VertexDeclarationRHI = VertexDecl;
            GraphicsPSO.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
            GraphicsPSO.BoundShaderState.PixelShaderRHI  = PixelShader.GetPixelShader();

            SetGraphicsPipelineState(RHICmdList, GraphicsPSO, 0);

            RHICmdList.SetViewport(0, 0, 0.0f, SceneExtent.X, SceneExtent.Y, 1.0f);

            FVoxelMeshVS::FParameters VSParams;
            VSParams.LocalToWorld = FMatrix44f::Identity;
            SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), VSParams);

            FVoxelMeshPS::FParameters PSParams;
            PSParams.VoxelColor = FLinearColor::Red;
            SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), PSParams);

            RHICmdList.DrawIndexedPrimitive(
                /*IndexBuffer=*/nullptr,
                /*BaseVertexIndex=*/0,
                /*FirstInstance=*/0,
                /*NumVertices=*/0,
                /*StartIndex=*/0,
                /*NumPrimitives=*/0,
                /*NumInstances=*/1);

            // RDG handles render pass lifetime via RENDER_TARGET_BINDING_SLOTS
        });
}

RenderPassの追加

自身のプロジェクト/ゲームモジュールに、FDelegateHandleのメンバを定義する。
FDelegateHandleは自前のRenderDelegateを差し込みデリゲートハンドルの保持に利用。
Register/RemovePostOpaqueRenderDelegateは「PostOpaque」不透明描画後に該当関数の発火をします。
※標準のライティング処理も完了しているため、ここの描画にはライティングの影響反映はできません
登録する関数は自作したPass追加の関数を実行します。
これで、レンダリング処理の追加が可能になりました。

※Register/RemoveOverlayRenderDelegateもありますが、Overlayはポストプロセス後なので、今回はRegisterPostOpaqueRenderDelegateを利用します。

public:
    virtual void StartupModule() override
    {
        IRendererModule& RendererModule = FModuleManager::LoadModuleChecked<IRendererModule>(TEXT("Renderer"));
        PostOpaqueHandle = RendererModule.RegisterPostOpaqueRenderDelegate(
            FPostOpaqueRenderDelegate::CreateRaw(this, &FGameModule::OnPostOpaqueRender));
    }

    virtual void ShutdownModule() override
    {
        if (PostOpaqueHandle.IsValid())
        {
            if (IRendererModule* RendererModule = FModuleManager::GetModulePtr<IRendererModule>(TEXT("Renderer")))
            {
                RendererModule->RemovePostOpaqueRenderDelegate(PostOpaqueHandle);
            }
            PostOpaqueHandle.Reset();
        }
    }

private:
    void OnPostOpaqueRender(FPostOpaqueRenderParameters& Parameters)
    {
        if (!Parameters.GraphBuilder)
        {
            return;
        }
        AddVoxelMeshPass(*Parameters.GraphBuilder, Parameters.ColorTexture, Parameters.DepthTexture);
    }

    FDelegateHandle PostOpaqueHandle;
};

テスト描画

※これはテスト用設定の話なので、無視して作業をしても問題なく、
テストする場合でもテスト終了後は該当コードを削除しても問題ないです。

独自のRenderPassでの描画が成功するのかを確認するため、
固定値でVertex/IndexBufferを用意して描画をする

const FIntPoint SceneExtent = SceneColor->Desc.Extent;
    GraphBuilder.AddPass(
        RDG_EVENT_NAME("VoxelRenderPass"),
        PassParameters,
        ERDGPassFlags::Raster,
        [PassParameters, SceneExtent](FRHICommandListImmediate& RHICmdList)
        {
            check(IsInRenderingThread());
            
            // Fixed cube mesh (8 vertices, 12 triangles)
            static const FVector3f PositionsRT[] = {
                FVector3f(-0.5f,-0.5f,-0.5f), FVector3f(+0.5f,-0.5f,-0.5f), FVector3f(+0.5f,+0.5f,-0.5f), FVector3f(-0.5f,+0.5f,-0.5f),
                FVector3f(-0.5f,-0.5f,+0.5f), FVector3f(+0.5f,-0.5f,+0.5f), FVector3f(+0.5f,+0.5f,+0.5f), FVector3f(-0.5f,+0.5f,+0.5f)
            };
            static const uint32 IndicesRT[] = {
                0,1,2,  0,2,3,
                4,6,5,  4,7,6,
                4,5,1,  4,1,0,
                7,3,2,  7,2,6,
                4,0,3,  4,3,7,
                5,6,2,  5,2,1
            };
            constexpr uint32 NumPositionsRT = sizeof(PositionsRT) / sizeof(PositionsRT[0]);
            constexpr uint32 NumIndicesRT   = sizeof(IndicesRT)   / sizeof(IndicesRT[0]);

            FGraphicsPipelineStateInitializer GraphicsPSO;
            RHICmdList.ApplyCachedRenderTargets(GraphicsPSO);

            GraphicsPSO.BlendState        = TStaticBlendState<>::GetRHI();
            GraphicsPSO.RasterizerState   = TStaticRasterizerState<FM_Solid, CM_None>::GetRHI();
            GraphicsPSO.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
            GraphicsPSO.PrimitiveType     = PT_TriangleList;

            ERHIFeatureLevel::Type FeatureLevel = GMaxRHIFeatureLevel;
            TShaderMapRef<FVoxelMeshVS> VertexShader(GetGlobalShaderMap(FeatureLevel));
            TShaderMapRef<FVoxelMeshPS> PixelShader(GetGlobalShaderMap(FeatureLevel));

            static FVertexDeclarationRHIRef VertexDecl;
            if (!VertexDecl.IsValid())
            {
                FVertexDeclarationElementList Elements;
                Elements.Add(FVertexElement(
                    0,                      // StreamIndex
                    0,                      // Offset
                    VET_Float3,             // Type
                    0,                      // AttributeIndex (POSITION0)
                    sizeof(FVector3f)       // Stride
                ));
                VertexDecl = RHICreateVertexDeclaration(Elements);
            }
            GraphicsPSO.BoundShaderState.VertexDeclarationRHI = VertexDecl;
            GraphicsPSO.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
            GraphicsPSO.BoundShaderState.PixelShaderRHI  = PixelShader.GetPixelShader();

            SetGraphicsPipelineState(RHICmdList, GraphicsPSO, 0);

            RHICmdList.SetViewport(0, 0, 0.0f, SceneExtent.X, SceneExtent.Y, 1.0f);

            TRefCountPtr<FRHIBuffer> LocalVertexBuffer;
            TRefCountPtr<FRHIBuffer> LocalIndexBuffer;
            {
                const uint32 VertexStride   = sizeof(FVector3f);
                const uint32 VertexDataSize = NumPositionsRT * VertexStride;
                FRHIBufferCreateDesc VBDesc = FRHIBufferCreateDesc::CreateVertex(TEXT("VoxelFixedVB"), VertexDataSize)
                    .DetermineInitialState();
                LocalVertexBuffer = RHICmdList.CreateBuffer(VBDesc);
                void* VBData = RHICmdList.LockBuffer(LocalVertexBuffer, 0, VertexDataSize, RLM_WriteOnly);
                FMemory::Memcpy(VBData, PositionsRT, VertexDataSize);
                RHICmdList.UnlockBuffer(LocalVertexBuffer);
            }
            {
                const uint32 IndexStride   = sizeof(uint32);
                const uint32 IndexDataSize = NumIndicesRT * IndexStride;
                FRHIBufferCreateDesc IBDesc = FRHIBufferCreateDesc::CreateIndex(TEXT("VoxelFixedIB"), IndexDataSize, IndexStride)
                    .DetermineInitialState();
                LocalIndexBuffer = RHICmdList.CreateBuffer(IBDesc);
                void* IBData = RHICmdList.LockBuffer(LocalIndexBuffer, 0, IndexDataSize, RLM_WriteOnly);
                FMemory::Memcpy(IBData, IndicesRT, IndexDataSize);
                RHICmdList.UnlockBuffer(LocalIndexBuffer);
            }

            RHICmdList.SetStreamSource(0, LocalVertexBuffer.GetReference(), 0);

            FVoxelMeshVS::FParameters VSParams;
            VSParams.LocalToWorld = FMatrix44f::Identity;
            SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), VSParams);

            FVoxelMeshPS::FParameters PSParams;
            PSParams.VoxelColor = FLinearColor::Red;
            SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), PSParams);

            RHICmdList.DrawIndexedPrimitive(
                LocalIndexBuffer.GetReference(),
                /*BaseVertexIndex=*/0,
                /*FirstInstance=*/0,
                /*NumVertices=*/NumPositionsRT,
                /*StartIndex=*/0,
                /*NumPrimitives=*/NumIndicesRT / 3,
                /*NumInstances=*/1);

        });

以下は実行結果の例です。
設定した固定値のメッシュ内容で描画できているはずです。
Videotogif.gif

独自の描画オブジェクトの実装

リソースバッファの用意

CPU側で用意する最低限のVoxelのリソース構造体を用意します。
FRenderResourceがエンジン全体で管理されるGPUリソース定義のベース構造体で
こちらを継承することで定義が可能になります。
IsValidとReleaseRHIは最低必要なデータチェックと解放で定義しています。

情報として、以下パラメータを用意します。
Centers:各Voxelの中心座標(Local)
Scales:各Voxelの密度
VolumeMinLS/VolumeMaxLS:Voxelを形成する空間のAABB
VoxelSizeLS:Voxelのデフォルトの一辺の長さ

struct FVoxelRenderResource : public FRenderResource
{
    TArray<FVector3f>  Centers;
    TArray<float>      Scales;

    FVector3f VolumeMinLS = FVector3f::ZeroVector;
    FVector3f VolumeMaxLS = FVector3f::ZeroVector;
    float     VoxelSizeLS = 0.0f;

    bool IsValid() const
    {
        return Centers.Num() == Scales.Num() && Centers.Num() > 0;
    }

    void InitializeInstances(const TArray<FVector3f>& InCenters, const TArray<float>& InScales)
    {
        Centers    = InCenters;
        Scales     = InScales;
    }

    void ReleaseAll()
    {
        Centers.Reset();
        Scales.Reset();
    }
};

VoxelResource管理オブジェクトの実装

FRenderResourceのデータキャッシュとそのVoxelデータ形成を担う
VoxelVolumeを定義。

UCLASS(BlueprintType)
class UVoxelVolume : public UObject
{
    GENERATED_BODY()
public:
    TSharedPtr<struct FVoxelRenderResource> RenderResources;

    UFUNCTION(BlueprintCallable, Category="Voxel")
    void BuildVoxelGrid(const FVector& RegionSize, float BlockSize);

    virtual void BeginDestroy() override;

};

処理は以下の様になっている。
FVoxelRenderResourceはレンダリングで利用するデータのため
ゲームスレッドではなく、レンダリングスレッドで更新処理を行っています。

static void AddVoxelInstance(const FVector3f& Center, float EdgeLength,
                             TArray<FVector3f>& OutCenters,
                             TArray<float>& OutScales)
{
    OutCenters.Add(Center);
    OutScales.Add(EdgeLength);
}

static void InitInstances_RenderThread(
    FVoxelRenderResource& Out,
    const TArray<FVector3f>& Centers,
    const TArray<float>& Scales,
    FRHICommandListImmediate& RHICmdList)
{
    Out.ReleaseAll();
    Out.InitializeInstances(Centers, Scales);
}

void UVoxelVolume::BuildVoxelGrid(const FVector& RegionSize, float BlockSize)
{
    if (BlockSize <= 0.f)
    {
        return;
    }
    if (!RenderResources.IsValid())
    {
        RenderResources = MakeShared<FVoxelRenderResource>();
    }

    TArray<FVector3f>  CentersArr;
    TArray<float>      ScalesArr;

    const FVector3f Region = (FVector3f)RegionSize;

    const float Half = BlockSize * 0.5f;
    const int32 NX = FMath::Max(1, FMath::FloorToInt(Region.X / BlockSize));
    const int32 NY = FMath::Max(1, FMath::FloorToInt(Region.Y / BlockSize));
    const int32 NZ = FMath::Max(1, FMath::FloorToInt(Region.Z / BlockSize));
    const float PackedLenX = NX * BlockSize;
    const float PackedLenY = NY * BlockSize;
    const float PackedLenZ = NZ * BlockSize;
    const FVector3f PackedHalf(PackedLenX * 0.5f, PackedLenY * 0.5f, PackedLenZ * 0.5f);

    const FVector3f VolumeMinLS = -PackedHalf;
    const FVector3f VolumeMaxLS =  PackedHalf;

    const FVector3f Start(
        -PackedLenX * 0.5f + Half,
        -PackedLenY * 0.5f + Half,
        -PackedLenZ * 0.5f + Half);

    CentersArr.Reserve(NX * NY * NZ);
    ScalesArr.Reserve(NX * NY * NZ);

    for (int32 ix = 0; ix < NX; ++ix){
        for (int32 iy = 0; iy < NY; ++iy){
            for (int32 iz = 0; iz < NZ; ++iz){
                const FVector3f Center = Start + FVector3f(ix * BlockSize, iy * BlockSize, iz * BlockSize);
                AddVoxelInstance(Center, BlockSize, CentersArr, ScalesArr);
            }
        }
    }

    RenderResources->VolumeMinLS = VolumeMinLS;
    RenderResources->VolumeMaxLS = VolumeMaxLS;
    RenderResources->VoxelSizeLS = BlockSize;

    ENQUEUE_RENDER_COMMAND(InitVoxelVolumeGridBuffersCmd)(
        [Shared = RenderResources, CentersCopy = MoveTemp(CentersArr), ScalesCopy = MoveTemp(ScalesArr)](FRHICommandListImmediate& RHICmdList)
        {
            if (!Shared.IsValid()) return;
            InitInstances_RenderThread(*Shared.Get(), CentersCopy, ScalesCopy, RHICmdList);
        });
}

void UVoxelVolume::BeginDestroy()
{
    Super::BeginDestroy();
    if (RenderResources.IsValid())
    {
        TSharedPtr<FVoxelRenderResource> Local = RenderResources;
        ENQUEUE_RENDER_COMMAND(ReleaseVoxelVolumeBuffersCmd)(
            [Local](FRHICommandListImmediate&)
            {
                if (Local.IsValid())
                {
                    Local->ReleaseAll();
                }
            });
        RenderResources.Reset();
    }
}

Voxelデータを描画専用の処理のオブジェクト実装

UEにの機能の既存の描画機能を参考に作っています。
この後出てくる独自のVoxelコンポーネントはUPrimitiveComponentを継承しています。
UPrimitiveComponentのゲームスレッド状態を描画向けにスナップショットし、
レンダラーに必要なリソースを渡す役割のクラスとして
FPrimitiveSceneProxyが存在します。
Voxel用の独自PrimitiveSceneProxyクラスがこちらです。

今回独自のRenderPassを使うため以下二つの機能は利用しません。
GetViewRelevance、GetDynamicMeshElements
※デバッグ表示で利用するメッシュ情報はあります。

struct FDebugMeshRHI
{
    FBufferRHIRef VertexBuffer;
    FBufferRHIRef IndexBuffer;
    uint32 NumVertices = 0;
    uint32 NumIndices  = 0;
    bool IsValid() const { return VertexBuffer.IsValid() && IndexBuffer.IsValid() && NumVertices > 0 && NumIndices > 0; }
    void Reset() { VertexBuffer.SafeRelease(); IndexBuffer.SafeRelease(); NumVertices = NumIndices = 0; }
};

class FVoxelSceneProxy : public FPrimitiveSceneProxy
{
public:
    explicit FVoxelSceneProxy(const UVoxelRenderComponent* InComponent);
    virtual ~FVoxelSceneProxy() override;

    // 今回は使わない
    virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override;
    virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views,
                                        const FSceneViewFamily& ViewFamily,
                                        uint32 VisibilityMap,
                                        FMeshElementCollector& Collector) const override;

    const TSharedPtr<FVoxelRenderResource>& GetRenderResources() const { return VolumeRenderResources; }

    const FDebugMeshRHI& GetDebugMesh() const { return DebugMesh; }
    FMatrix GetInstanceTransform() const { return GetLocalToWorld(); }

    static const TArray<FVoxelSceneProxy*, TInlineAllocator<64>>& GetProxies_RenderThread();
    static void ClearProxies_RenderThread();
    static void RegisterProxy_RenderThread(FVoxelSceneProxy* Proxy);
    static void UnregisterProxy_RenderThread(FVoxelSceneProxy* Proxy);
    
    static void BeginNewEpoch_RenderThread();
    static void ClearEpochProxies_RenderThread(uint64 Epoch);
    static void ClearCurrentEpochProxies_RenderThread();
    static void SetActiveEpochToCurrent_RenderThread();
    static void SetActiveEpoch_RenderThread(uint64 Epoch);
    static uint64 GetActiveEpoch_RenderThread();

    uint64 GetRegistrationEpoch_RenderThread() const { return RegistrationEpoch_RT; }
    virtual uint32 GetMemoryFootprint() const override { return sizeof(*this); }
    virtual SIZE_T GetTypeHash() const override { return 0; }

private:
    TSharedPtr<FVoxelRenderResource> VolumeRenderResources;
    
    FDebugMeshRHI DebugMesh;
    void BuildDebugMesh_RenderThread(
        FRHICommandListImmediate& RHICmdList,
        const TArray<FVector3f>& Centers,
        const TArray<float>& Scales);
    static TArray<FVoxelSceneProxy*, TInlineAllocator<64>> GProxies_RT;
    static uint64 GCurrentEpoch_RT;
    static uint64 GActiveEpoch_RT;
    uint64 RegistrationEpoch_RT = 0;
};

static_assert(TIsTriviallyCopyConstructible<FVoxelSceneProxy*>::Value, "Pointer storage is trivially copyable");

const TArray<FVoxelSceneProxy*, TInlineAllocator<64>>& GetVoxelProxies_RenderThread();
void ClearVoxelProxies_RenderThread();
bool IsVoxelProxyActive_RenderThread(const FVoxelSceneProxy* Proxy);

今回は独自のRenderPassであり、既存の他のProxyも含めた管理を利用するのは
ちょっと面倒なため、独自でProxyを管理するためにGProxies_RTを用意しています。
GCurrentEpoch_RT, GActiveEpoch_RTは
Editor内での未実行(編集)中と実行中でプロキシを使い分けるために用意しています。

TArray<FVoxelSceneProxy*, TInlineAllocator<64>> FVoxelSceneProxy::GProxies_RT;
uint64 FVoxelSceneProxy::GCurrentEpoch_RT = 0;
uint64 FVoxelSceneProxy::GActiveEpoch_RT = 0;

void FVoxelSceneProxy::RegisterProxy_RenderThread(FVoxelSceneProxy* Proxy)
{
    check(IsInRenderingThread());
    Proxy->RegistrationEpoch_RT = GCurrentEpoch_RT;
    GProxies_RT.Add(Proxy);
}

void FVoxelSceneProxy::UnregisterProxy_RenderThread(FVoxelSceneProxy* Proxy)
{
    check(IsInRenderingThread());
    GProxies_RT.RemoveSwap(Proxy);
}

const TArray<FVoxelSceneProxy*, TInlineAllocator<64>>& FVoxelSceneProxy::GetProxies_RenderThread()
{
    check(IsInRenderingThread());
    return GProxies_RT;
}

void FVoxelSceneProxy::ClearProxies_RenderThread()
{
    check(IsInRenderingThread());
    GProxies_RT.Reset();
}

void FVoxelSceneProxy::BeginNewEpoch_RenderThread()
{
    check(IsInRenderingThread());
    ++GCurrentEpoch_RT;
}

void FVoxelSceneProxy::ClearEpochProxies_RenderThread(uint64 Epoch)
{
    check(IsInRenderingThread());
    for (int32 i = GProxies_RT.Num() - 1; i >= 0; --i)
    {
        FVoxelSceneProxy* Proxy = GProxies_RT[i];
        if (Proxy && Proxy->RegistrationEpoch_RT == Epoch)
        {
            if (Proxy->VolumeRenderResources.IsValid())
            {
                Proxy->VolumeRenderResources->ReleaseAll();
            }
            GProxies_RT.RemoveAtSwap(i);
        }
    }
}

void FVoxelSceneProxy::ClearCurrentEpochProxies_RenderThread()
{
    check(IsInRenderingThread());
    ClearEpochProxies_RenderThread(GCurrentEpoch_RT);
}

void FVoxelSceneProxy::SetActiveEpochToCurrent_RenderThread()
{
    check(IsInRenderingThread());
    GActiveEpoch_RT = GCurrentEpoch_RT;
}

void FVoxelSceneProxy::SetActiveEpoch_RenderThread(uint64 Epoch)
{
    check(IsInRenderingThread());
    GActiveEpoch_RT = Epoch;
}

uint64 FVoxelSceneProxy::GetActiveEpoch_RenderThread()
{
    check(IsInRenderingThread());
    return GActiveEpoch_RT;
}

const TArray<FVoxelSceneProxy*, TInlineAllocator<64>>& GetVoxelProxies_RenderThread()
{
    return FVoxelSceneProxy::GetProxies_RenderThread();
}

void ClearVoxelProxies_RenderThread()
{
    check(IsInRenderingThread());
    const auto& Proxies = FVoxelSceneProxy::GetProxies_RenderThread();
    for (const FVoxelSceneProxy* Proxy : Proxies)
    {
        if (!Proxy) continue;
        const TSharedPtr<FVoxelRenderResource>& Data = Proxy->GetRenderResources();
        if (Data.IsValid())
        {
            Data->ReleaseAll();
        }
    }
    FVoxelSceneProxy::ClearProxies_RenderThread();
}

bool IsVoxelProxyActive_RenderThread(const FVoxelSceneProxy* Proxy)
{
    check(IsInRenderingThread());
    if (!Proxy) return false;
    return Proxy->GetRegistrationEpoch_RenderThread() == FVoxelSceneProxy::GetActiveEpoch_RenderThread();
}

FVoxelSceneProxy::FVoxelSceneProxy(const UVoxelRenderComponent* InComponent)
    : FPrimitiveSceneProxy(InComponent)
{
    VolumeRenderResources = InComponent->GetSharedRenderResources();

    TArray<FVector3f> CentersCopy;
    TArray<float>     ScalesCopy;
    if (VolumeRenderResources.IsValid())
    {
        CentersCopy = VolumeRenderResources->Centers;
        ScalesCopy  = VolumeRenderResources->Scales;
    }

    ENQUEUE_RENDER_COMMAND(RegisterVoxelProxyCmd)(
        [This = this, CentersCopy = MoveTemp(CentersCopy), ScalesCopy = MoveTemp(ScalesCopy)](FRHICommandListImmediate& RHICmdList)
        {
            RegisterProxy_RenderThread(This);
            if (CentersCopy.Num() > 0)
            {
                This->BuildDebugMesh_RenderThread(RHICmdList, CentersCopy, ScalesCopy);
            }
        });
}

FVoxelSceneProxy::~FVoxelSceneProxy()
{
    if (IsInRenderingThread())
    {
        UnregisterProxy_RenderThread(this);
        DebugMesh.Reset();
        if (VolumeRenderResources.IsValid())
        {
            VolumeRenderResources->ReleaseAll();
        }
        VolumeRenderResources.Reset();
    }
    else
    {
        FDebugMeshRHI DebugMeshCopy = DebugMesh; // shallow copy of RHI refs
        DebugMesh.Reset();
        TSharedPtr<FVoxelRenderResource> ResourcesCopy = MoveTemp(VolumeRenderResources);
        VolumeRenderResources.Reset();

        FVoxelSceneProxy* ProxyPtr = this;
        ENQUEUE_RENDER_COMMAND(UnregisterVoxelProxyCmd)(
            [ProxyPtr, DebugMeshCopy = MoveTemp(DebugMeshCopy), ResourcesCopy](FRHICommandListImmediate&) mutable
            {
                if (ProxyPtr)
                {
                    UnregisterProxy_RenderThread(ProxyPtr);
                }
                DebugMeshCopy.Reset();
                if (ResourcesCopy.IsValid())
                {
                    ResourcesCopy->ReleaseAll();
                }
            });
    }
}

FPrimitiveViewRelevance FVoxelSceneProxy::GetViewRelevance(const FSceneView* View) const
{
    FPrimitiveViewRelevance Result;
    Result.bDrawRelevance    = IsShown(View);
    Result.bDynamicRelevance = false;
    Result.bRenderInMainPass = false;
    Result.bOpaque           = true;
    return Result;
}

void FVoxelSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views,
                                              const FSceneViewFamily& ViewFamily,
                                              uint32 VisibilityMap,
                                              FMeshElementCollector& Collector) const
{
}

void FVoxelSceneProxy::BuildDebugMesh_RenderThread(
    FRHICommandListImmediate& RHICmdList,
    const TArray<FVector3f>& Centers,
    const TArray<float>& Scales)
{
    TArray<FVector3f> Positions;
    TArray<uint32>    Indices;
    Positions.Reserve(Centers.Num() * 8);
    Indices.Reserve(Centers.Num() * 36);

    auto AppendCube = [](float Half, const FVector3f& C, TArray<FVector3f>& P, TArray<uint32>& I)
    {
        const FVector3f V[] = {
            C + FVector3f(-Half,-Half,-Half), C + FVector3f(+Half,-Half,-Half), C + FVector3f(+Half,+Half,-Half), C + FVector3f(-Half,+Half,-Half),
            C + FVector3f(-Half,-Half,+Half), C + FVector3f(+Half,-Half,+Half), C + FVector3f(+Half,+Half,+Half), C + FVector3f(-Half,+Half,+Half)
        };
        const uint32 Base = static_cast<uint32>(P.Num());
        P.Append(V, UE_ARRAY_COUNT(V));
        const uint32 Idx[] = {
            0,1,2,  0,2,3,
            4,6,5,  4,7,6,
            4,5,1,  4,1,0,
            7,3,2,  7,2,6,
            4,0,3,  4,3,7,
            5,6,2,  5,2,1
        };
        for (uint32 k = 0; k < UE_ARRAY_COUNT(Idx); ++k) { I.Add(Base + Idx[k]); }
    };

    for (int32 i = 0; i < Centers.Num(); ++i)
    {
        const float Edge = Scales.IsValidIndex(i) ? Scales[i] : 100.f;
        const float Half = Edge * 0.5f;
        AppendCube(Half, Centers[i], Positions, Indices);
    }

    DebugMesh.Reset();
    const uint32 VBSize = Positions.Num() * sizeof(FVector3f);
    const uint32 IBSize = Indices.Num() * sizeof(uint32);
    if (VBSize == 0 || IBSize == 0)
    {
        return;
    }

    FRHIBufferCreateDesc VDesc = FRHIBufferCreateDesc::CreateVertex(TEXT("VoxelProxyPosVB"), VBSize).DetermineInitialState();
    DebugMesh.VertexBuffer = RHICmdList.CreateBuffer(VDesc);
    if (DebugMesh.VertexBuffer.IsValid())
    {
        void* VData = RHICmdList.LockBuffer(DebugMesh.VertexBuffer, 0, VBSize, RLM_WriteOnly);
        FMemory::Memcpy(VData, Positions.GetData(), VBSize);
        RHICmdList.UnlockBuffer(DebugMesh.VertexBuffer);
    }

    FRHIBufferCreateDesc IDesc = FRHIBufferCreateDesc::CreateIndex(TEXT("VoxelProxyIB"), IBSize, sizeof(uint32)).DetermineInitialState();
    DebugMesh.IndexBuffer = RHICmdList.CreateBuffer(IDesc);
    if (DebugMesh.IndexBuffer.IsValid())
    {
        void* IData = RHICmdList.LockBuffer(DebugMesh.IndexBuffer, 0, IBSize, RLM_WriteOnly);
        FMemory::Memcpy(IData, Indices.GetData(), IBSize);
        RHICmdList.UnlockBuffer(DebugMesh.IndexBuffer);
    }

    DebugMesh.NumVertices = Positions.Num();
    DebugMesh.NumIndices  = Indices.Num();
}

Voxel描画用コンポーネントの実装

ExtentはVoxelを生成する空間サイズで、BlockSizeはVoxelの一つ当たりのサイズです。

UCLASS(ClassGroup=(Rendering), meta=(BlueprintSpawnableComponent))
class UVoxelRenderComponent : public UPrimitiveComponent
{
    GENERATED_BODY()

public:
    UVoxelRenderComponent();
    
    UPROPERTY(EditAnywhere, Category="Voxel")
    TObjectPtr<UVoxelVolume> VolumeAsset = nullptr;
    
    UPROPERTY(EditAnywhere, Category="Voxel", meta=(ClampMin="0.0", UIMin="0.0"))
    FVector Extent = FVector(100.0f);
    
    UPROPERTY(EditAnywhere, Category="Voxel", meta=(ClampMin="1.0", UIMin="1.0"))
    float BlockSize = 20.0f;
    

protected:
    virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
    virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override;
    virtual void OnRegister() override;
    virtual void OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) override;

#if WITH_EDITOR
    virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
#endif

public:
    UFUNCTION(BlueprintCallable, Category="Voxel")
    void RebuildFromExtent();


public:
    TSharedPtr<FVoxelRenderResource> GetSharedRenderResources() const
    {
        return VolumeAsset ? VolumeAsset->RenderResources : nullptr;
    }
};

中身は以下ですが、具体的な処理系はすべて前述したほかクラスが担っていますので、
こちらのクラスはコンポーネントとしての振る舞いと
各イベントでの処理発火を行っているだけです。

UVoxelRenderComponent::UVoxelRenderComponent()
{
    PrimaryComponentTick.bCanEverTick = false;
}

FPrimitiveSceneProxy* UVoxelRenderComponent::CreateSceneProxy()
{
    return new FVoxelSceneProxy(this);
}

FBoxSphereBounds UVoxelRenderComponent::CalcBounds(const FTransform& LocalToWorld) const
{
    const FBox LocalBox(-Extent, Extent);
    return FBoxSphereBounds(LocalBox).TransformBy(LocalToWorld);
}

void UVoxelRenderComponent::OnRegister()
{
    Super::OnRegister();
    if (VolumeAsset)
    {
        const FVector RegionSize = Extent;
        VolumeAsset->BuildVoxelGrid(RegionSize, FMath::Max(1.0f, BlockSize));
    }
}

void UVoxelRenderComponent::OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport)
{
    Super::OnUpdateTransform(UpdateTransformFlags, Teleport);
    MarkRenderTransformDirty();
}

void UVoxelRenderComponent::RebuildFromExtent()
{
    if (!VolumeAsset)
    {
        return;
    }
    const FVector RegionSize = Extent;
    VolumeAsset->BuildVoxelGrid(RegionSize, FMath::Max(1.0f, BlockSize));
    MarkRenderStateDirty();
}

#if WITH_EDITOR
void UVoxelRenderComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
    Super::PostEditChangeProperty(PropertyChangedEvent);
    const FName Name = PropertyChangedEvent.Property ? PropertyChangedEvent.Property->GetFName() : NAME_None;
    if (Name == GET_MEMBER_NAME_CHECKED(UVoxelRenderComponent, Extent)
        || Name == GET_MEMBER_NAME_CHECKED(UVoxelRenderComponent, BlockSize)
        || Name == GET_MEMBER_NAME_CHECKED(UVoxelRenderComponent, VolumeAsset))
    {
        RebuildFromExtent();
    }
}
#endif

Voxelの描画実装

Voxel描画用のRenderPass追加

Proxyを取得して、Proxyのデータを用いてメッシュ描画するようにします。

void AddVoxelRenderPass(
    FRDGBuilder& GraphBuilder,
    FRDGTextureRef SceneColor,
    FRDGTextureRef SceneDepth,
    const void* OpaqueView)
{
    auto* PassParameters = GraphBuilder.AllocParameters<FVoxelMeshPassParameters>();
    PassParameters->RenderTargets[0] = FRenderTargetBinding(SceneColor, ERenderTargetLoadAction::ELoad);
    PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(
        SceneDepth,
        ERenderTargetLoadAction::ELoad,
        ERenderTargetLoadAction::ELoad,
        FExclusiveDepthStencil::DepthRead_StencilNop);

    const FIntPoint SceneExtent = SceneColor->Desc.Extent;
    GraphBuilder.AddPass(
        RDG_EVENT_NAME("VoxelRenderPass"),
        PassParameters,
        ERDGPassFlags::Raster,
        [PassParameters, SceneExtent, OpaqueView](FRHICommandListImmediate& RHICmdList)
        {
            FGraphicsPipelineStateInitializer GraphicsPSO;
            RHICmdList.ApplyCachedRenderTargets(GraphicsPSO);

            GraphicsPSO.BlendState        = TStaticBlendState<>::GetRHI();
            GraphicsPSO.RasterizerState   = TStaticRasterizerState<FM_Wireframe, CM_None>::GetRHI();
            GraphicsPSO.DepthStencilState = TStaticDepthStencilState<false, CF_GreaterEqual>::GetRHI();
            GraphicsPSO.PrimitiveType     = PT_TriangleList;

            ERHIFeatureLevel::Type FeatureLevel = GMaxRHIFeatureLevel;
            TShaderMapRef<FVoxelMeshVS> VertexShader(GetGlobalShaderMap(FeatureLevel));
            TShaderMapRef<FVoxelMeshPS> PixelShader(GetGlobalShaderMap(FeatureLevel));

            GraphicsPSO.BoundShaderState.VertexDeclarationRHI = GetVoxelPositionOnlyVertexDecl();
            GraphicsPSO.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
            GraphicsPSO.BoundShaderState.PixelShaderRHI  = PixelShader.GetPixelShader();

            SetGraphicsPipelineState(RHICmdList, GraphicsPSO, 0);

            RHICmdList.SetViewport(0, 0, 0.0f, SceneExtent.X, SceneExtent.Y, 1.0f);

            const FSceneView* View = reinterpret_cast<const FSceneView*>(OpaqueView);
            if (!View)
            {
                return;
            }
            FVoxelMeshPS::FParameters PSParams;
            PSParams.VoxelColor = FLinearColor::Red;
            SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), PSParams);

            const auto& Proxies = GetVoxelProxies_RenderThread();
            for (const FVoxelSceneProxy* Proxy : Proxies)
            {
                if (!IsVoxelProxyActive_RenderThread(Proxy))
                {
                    continue;
                }
                if (!Proxy->IsShown(View))
                {
                    continue;
                }
                const FBoxSphereBounds Bounds = Proxy->GetBounds();
                if (!View->ViewFrustum.IntersectBox(Bounds.Origin, Bounds.BoxExtent))
                {
                    continue;
                }
                const FDebugMeshRHI& Mesh = Proxy->GetDebugMesh();
                if (!Mesh.IsValid())
                {
                    continue;
                }

                RHICmdList.SetStreamSource(0, Mesh.VertexBuffer.GetReference(), 0);

                FVoxelMeshVS::FParameters VSParams;
                VSParams.LocalToWorld = FMatrix44f(Proxy->GetInstanceTransform());
                VSParams.WorldToClip  = FMatrix44f(View->ViewMatrices.GetViewProjectionMatrix());
                SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), VSParams);

                RHICmdList.DrawIndexedPrimitive(
                    Mesh.IndexBuffer,
                    /*BaseVertexIndex=*/0,
                    /*FirstInstance=*/0,
                    Mesh.NumVertices,
                    /*StartIndex=*/0,
                    Mesh.NumIndices / 3,
                    /*NumInstances=*/1);
            }
        });
}

Voxelのコンポーネントを持つActorを追加

独自に作ったコンポーネントつけたActor作るだけなのでBPでも問題ないです。

UCLASS()
class AVoxelActor : public AActor
{
    GENERATED_BODY()
public:
    AVoxelActor();

protected:
    UPROPERTY(VisibleAnywhere, Category="Voxel")
    UVoxelRenderComponent* VoxelComponent;
};

↓すいません、UVoxelVolumeをCreateDefaultSubobjectで作ってますが、NewObjectが推奨です。

AVoxelActor::AVoxelActor()
{
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.bStartWithTickEnabled = true;
    VoxelComponent = CreateDefaultSubobject<UVoxelRenderComponent>(TEXT("VoxelRenderComponent"));
    SetRootComponent(CreateDefaultSubobject<USceneComponent>(TEXT("Root")));
    VoxelComponent->SetupAttachment(RootComponent);

    UVoxelVolume* DefaultVolume = CreateDefaultSubobject<UVoxelVolume>(TEXT("VolumeAsset"));
    if (DefaultVolume)
    {
        VoxelComponent->VolumeAsset = DefaultVolume;
        DefaultVolume->BuildVoxelGrid(FVector(100), 20.f);
    }
}

レベルに該当アクターを配置すると以下の様に描画されると思います。
VoxelTest DebugGame Unreal Editor GIF

Voxelのスケールによっての変化とかつけず、ただの箱でつまんなく申し訳ないです・・・

おまけ Voxelデータを用いた変わった描画

Voxelの情報からVoxelの立方体的見た目の描画ではなく、メタボール的に融合した有機的形状の描画を再現していく。

実装するフローとしては以下で行っていく。

Voxel情報(FVoxelRenderResource)
↓
密度テクスチャを生成(メタボール関数で各インスタンスの寄与を累積)
↓
Seedテクスチャを生成(等値面=0.5を横切るセルでサブボクセル精度の表面位置を記録)
↓
JFAテクスチャを生成(26近傍JFAで最近傍シードを全セルに伝播)
↓
SDFテクスチャを生成(最近傍シードまでの符号付き距離を計算)
↓
レイマーチ描画(密度ベースSDFで有機的表面を検出、法線・ライティング適用)

Voxelデータを3Dテクスチャに変換

上記フローで記載しましたが、Voxelのデータを3Dテクスチャに変換して描画をする際に利用します。

テクスチャの生成場所はAddRenderPassで行います。
以下の関数を用意して追記していく前提で以降話が続きます。

Voxelの描画実装のタイミングで実装したものとは別で新設していますが、
以前のものは削除したり中身をこちらの内容に書き換えての実装でも問題ないです。

void AddVoxelRaymarchPass(
      FRDGBuilder& GraphBuilder,
      FRDGTextureRef SceneColor,
      FRDGTextureRef SceneDepth,
      const void* OpaqueView)
  {
      const FSceneView* View = static_cast<const FSceneView*>(OpaqueView);
      if (!View) return;

      const auto& Proxies = GetVoxelProxies_RenderThread();
      for (const FVoxelSceneProxy* Proxy : Proxies)
      {
          if (!IsVoxelProxyActive_RenderThread(Proxy)) continue;
          if (!Proxy->IsShown(View)) continue;

          const FBoxSphereBounds Bounds = Proxy->GetBounds();
          if (!View->ViewFrustum.IntersectBox(Bounds.Origin, Bounds.BoxExtent)) continue;

          const TSharedPtr<FVoxelRenderResource>& Resource = Proxy->GetRenderResources();
          if (!Resource.IsValid()) continue;

          const FVoxelSdfTextures SdfTextures = BuildVoxelSdfTexture(GraphBuilder, *Resource.Get());
          if (!SdfTextures.SdfTex) continue;

          // todo: 描画
      }
  }

全ての必要テクスチャ生成をまとめて実行し、
描画に利用するテクスチャをまとめた構造体の出力をする関数を用意します。
出力データは密度と距離場テクスチャです

具体的なテクスチャ生成処理に移る前処理として、テクスチャの解像度を求めます。

struct FVoxelRenderTextureResult
{
    FRDGTextureRef SdfTex = nullptr;
    FRDGTextureRef DensityTex = nullptr;
    FIntVector VolumeDimensions = FIntVector::ZeroValue;
};

static FVoxelRenderTextureResult BuildVoxelRenderTextureResult(FRDGBuilder& GraphBuilder, const FVoxelRenderResource& Resource)
{
    if (!Resource.IsValid()) return FVoxelRenderTextureResult{};

    const FVector3f VolumeMinLS = Resource.VolumeMinLS;
    const FVector3f VolumeMaxLS = Resource.VolumeMaxLS;
    const float VoxelSizeLS = Resource.VoxelSizeLS;
    const FVector3f ExtentLS = (VolumeMaxLS - VolumeMinLS);
    const int32 NX = FMath::Max(1, FMath::RoundToInt(ExtentLS.X / VoxelSizeLS));
    const int32 NY = FMath::Max(1, FMath::RoundToInt(ExtentLS.Y / VoxelSizeLS));
    const int32 NZ = FMath::Max(1, FMath::RoundToInt(ExtentLS.Z / VoxelSizeLS));
    const FIntVector VolumeDimensions(NX, NY, NZ);

    FRDGTextureDesc DensityDesc = FRDGTextureDesc::Create3D(VolumeDimensions, PF_R32_UINT, FClearValueBinding::None, TexCreate_ShaderResource | TexCreate_UAV);
    FRDGTextureDesc SeedDesc    = FRDGTextureDesc::Create3D(VolumeDimensions, PF_A32B32G32R32F, FClearValueBinding::None, TexCreate_ShaderResource | TexCreate_UAV);
    FRDGTextureDesc SdfDesc     = FRDGTextureDesc::Create3D(VolumeDimensions, PF_R32_FLOAT, FClearValueBinding::None, TexCreate_ShaderResource | TexCreate_UAV);

    FRDGTextureRef DensityTex = GraphBuilder.CreateTexture(DensityDesc, TEXT("Voxel.Density"));
    FRDGTextureRef SeedPing   = GraphBuilder.CreateTexture(SeedDesc,    TEXT("Voxel.SeedPing"));
    FRDGTextureRef SeedPong   = GraphBuilder.CreateTexture(SeedDesc,    TEXT("Voxel.SeedPong"));
    FRDGTextureRef SdfTex     = GraphBuilder.CreateTexture(SdfDesc,     TEXT("Voxel.SDF"));

    FRDGTextureUAVRef DensityUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(DensityTex, 0));
    AddClearUAVPass(GraphBuilder, DensityUAV, 0u);
    FRDGTextureUAVRef SdfUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(SdfTex, 0));
    AddClearUAVPass(GraphBuilder, SdfUAV, 0.0f);
    AddSplatInstancesPass(GraphBuilder, Resource, DensityTex, VolumeDimensions, VolumeMinLS, VoxelSizeLS);

    AddSeedPass(GraphBuilder, DensityTex, SeedPing, VolumeDimensions, VoxelSizeLS);
    FRDGTextureRef SeedAll = AddJFAPasses(GraphBuilder, SeedPing, SeedPong, VolumeDimensions);
    AddDistanceToSdfPass(GraphBuilder, SeedAll, SdfTex, VolumeDimensions, VolumeMinLS, VoxelSizeLS, DensityTex);

    FVoxelRenderTextureResult Outputs;
    Outputs.SdfTex = SdfTex;
    Outputs.DensityTex = DensityTex;
    Outputs.VolumeDimensions = VolumeDimensions;
    return Outputs;
}

  static FVoxelSdfTextures BuildVoxelSdfTexture(FRDGBuilder& GraphBuilder, const FVoxelRenderResource& Resource)
  {
      if (!Resource.IsValid()) return FVoxelSdfTextures{};

      const FVector3f VolumeMinLS = Resource.VolumeMinLS;
      const FVector3f VolumeMaxLS = Resource.VolumeMaxLS;
      const float VoxelSizeLS = Resource.VoxelSizeLS;
      const FVector3f ExtentLS = (VolumeMaxLS - VolumeMinLS);
      const int32 NX = FMath::Max(1, FMath::RoundToInt(ExtentLS.X / VoxelSizeLS));
      const int32 NY = FMath::Max(1, FMath::RoundToInt(ExtentLS.Y / VoxelSizeLS));
      const int32 NZ = FMath::Max(1, FMath::RoundToInt(ExtentLS.Z / VoxelSizeLS));
      const FIntVector VolumeDimensions(NX, NY, NZ);

      FRDGTextureDesc DensityDesc = FRDGTextureDesc::Create3D(VolumeDimensions, PF_R32_UINT, FClearValueBinding::None, TexCreate_ShaderResource | TexCreate_UAV);
      FRDGTextureDesc SeedDesc    = FRDGTextureDesc::Create3D(VolumeDimensions, PF_A32B32G32R32F, FClearValueBinding::None, TexCreate_ShaderResource | TexCreate_UAV);
      FRDGTextureDesc SdfDesc     = FRDGTextureDesc::Create3D(VolumeDimensions, PF_R32_FLOAT, FClearValueBinding::None, TexCreate_ShaderResource | TexCreate_UAV);

      FRDGTextureRef DensityTex = GraphBuilder.CreateTexture(DensityDesc, TEXT("Voxel.Density"));
      FRDGTextureRef SeedPing   = GraphBuilder.CreateTexture(SeedDesc,    TEXT("Voxel.SeedPing"));
      FRDGTextureRef SeedPong   = GraphBuilder.CreateTexture(SeedDesc,    TEXT("Voxel.SeedPong"));
      FRDGTextureRef SdfTex     = GraphBuilder.CreateTexture(SdfDesc,     TEXT("Voxel.SDF"));

      // todo: テクスチャの初期化
      // todo: 密度(占有)テクスチャの占有セルの設定
      // todo: Seedテクスチャの生成
      // todo: JFA(Jump Flooding Algorithm)テクスチャの生成
      // todo: SDF(Signed Distance Field)テクスチャの生成

      FVoxelSdfTextures Outputs;
      Outputs.SdfTex = SdfTex;
      Outputs.DensityTex = DensityTex;
      return Outputs;
  }

AddRenderPass内には以下の様に描画処理前にテクスチャ生成を加えます。

const FVoxelRenderTextures RenderTextures = BuildVoxelRenderTextures(GraphBuilder, *Resource.Get());
if (!RenderTextures.SdfTex)
{
     continue;
}

// todo: 描画

テクスチャの初期化

BuildVoxelRenderTextureResult内に以下
空の密度と距離場テクスチャの中身の初期化を追記します。

AddClearUAVPassはUE標準のRDG Utility関数で、読み書き用の
UAV(FRDGTextureUAVRef/FRDGBufferUAVRef)を任意値で初期化するものです。
※UAV(UnorderedAccessView)は、バッファをランダムに読み書きする設定ファイルです。

FRDGTextureUAVRef DensityUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(DensityTex, 0));
AddClearUAVPass(GraphBuilder, DensityUAV, 0u);
FRDGTextureUAVRef SdfUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(SdfTex, 0));
AddClearUAVPass(GraphBuilder, SdfUAV, 0.0f);

// todo: 密度(占有)テクスチャの占有セルの設定
// todo: Seedテクスチャの生成
// todo: JFA(Jump Flooding Algorithm)テクスチャの生成
// todo: SDF(Signed Distance Field)テクスチャの生成

密度(メタボール)テクスチャの生成

従来の単純な占有フラグではなく、メタボール方式で密度を累積します。
これにより複数の球が自然に融合した有機的な形状が得られます。

以下の内容のusfファイルを作成します。

メタボール関数を用意します。
メタボール関数は Wyvill/Blinn型の滑らかな減衰計算方法をとります。
これにより表面が非常に滑らかにします。
※この滑らかさをC2連続性と呼ぶようで、C0(線形),C1(2次多項式),C2(Wyvill/Blinn型)って感じで、数字が高いほどなめらがであるという意味っぽいです。学びですねぇ~

float MetaballFalloff(float distSq, float radiusSq)
{
    if (distSq >= radiusSq) return 0.0;
    float t = distSq / radiusSq; // 0 to 1
    float oneMinusT = 1.0 - t;
    return oneMinusT * oneMinusT * oneMinusT; // (1-t)^3
}

各ボクセルの座標とスケールをもとに該当テクセルの占有密度をDensityUAV に書き込んでいます。

以下詳細です。

  • 中心座標を正規化 = セル座標(rel)

  • Voxel直径 * スケール * 0.5 = 半径(halfEdgeLS)

  • 半径 / Voxel直径 * 影響範囲拡大率(OverlapMultiplier) = 影響範囲半径(radiusCell)

  • 影響範囲半径 * 確認領域倍率(falloffExtend) = 確認範囲半径(searchRadius)

  • 確認範囲半径 + 0.5 = 少数点以下繰り上げ丸め込み = 確認セル数(r)

  • 影響範囲半径^2 = extendedRadiusSq
    => sqrtを避けるため、数式的には A < sqrt(B)のような比較を A^2 < Bで比較する目的

  • すべてのCellに移動しながら、影響範囲を考慮して密度テクスチャに加算書き込み

64スレッド数になっているのは、GPUの並列実行の1つの束(Wave)はハードの種類によって
変化するのですが、基本的に8の倍数で最大64程度のため64に設定しています。
※余談ですが、Waveと同じ意味で扱われる用語がAMDでは「Wavefront」、NVIDIAでは「Warp」、Vulkanでは「Subgroup」、Metalでは「SIMD-group」とややこしいです。統一してほしいですねぇ~

RWTexture3D<uint>  DensityUAV;
int3 VolumeDimensions;
uint NumInstances;
float3 VolumeMinLS;
float VoxelSizeLS;
float BaseEdgeLengthLS;
float OverlapMultiplier;
StructuredBuffer<float4> InstanceCenters;
StructuredBuffer<float>  InstanceScales;

[numthreads(64,1,1)]
void SplatInstancesCS(uint3 Gid : SV_GroupID, uint GIndex : SV_GroupIndex, uint3 DTid : SV_DispatchThreadID)
{
    uint idx = DTid.x;
    if (idx >= NumInstances) return;

    const float3 C = InstanceCenters[idx].xyz;
    const float  S = InstanceScales[idx];
    const float3 rel = (C - VolumeMinLS) / max(VoxelSizeLS, 1e-4);
    const int3   baseCell = int3(floor(rel));

    const float halfEdgeLS = max(BaseEdgeLengthLS * S * 0.5, 0.0);
    const float baseRadiusCell = halfEdgeLS / max(VoxelSizeLS, 1e-4);
    const float radiusCell = baseRadiusCell * OverlapMultiplier;

    const float falloffExtend = 1.5;
    const float searchRadius = radiusCell * falloffExtend;
    int r = (int)ceil(searchRadius + 0.5);
    r = clamp(r, 0, 64);

    const float extendedRadiusSq = searchRadius * searchRadius;

    for (int dz = -r; dz <= r; ++dz){
        int z = baseCell.z + dz;
        if (z < 0 || z >= VolumeDimensions.z) continue;

        for (int dy = -r; dy <= r; ++dy){
            int y = baseCell.y + dy;
            if (y < 0 || y >= VolumeDimensions.y) continue;

            for (int dx = -r; dx <= r; ++dx){
                int x = baseCell.x + dx;
                if (x < 0 || x >= VolumeDimensions.x) continue;

                float3 cellCenter = float3(x, y, z) + 0.5;
                float3 toCell = cellCenter - rel;
                float distSq = dot(toCell, toCell);
                float contribution = MetaballFalloff(distSq, extendedRadiusSq);

                if (contribution > 0.0){
                    uint densityBits = uint(contribution * DENSITY_SCALE);
                    InterlockedAdd(DensityUAV[int3(x,y,z)], densityBits);
                }
            }
        }
    }
}

密度テクスチャの生成処理はコンピュートシェーダーを利用します。
上記ushで定義した処理を実行するには、計算パスの追加をする必要があります

FComputeShaderUtils::AddPassはそのフレームで実行する計算パスの追加です。
※手動的にGraphBuilder::AddPassとFComputeShaderUtils::Dispatchを利用することもできますが、本件はFComputeShaderUtils::AddPassで十分になります。

class FSplatInstancesCS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FSplatInstancesCS);
    SHADER_USE_PARAMETER_STRUCT(FSplatInstancesCS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(uint32, NumInstances)
        SHADER_PARAMETER(FVector3f, VolumeMinLS)
        SHADER_PARAMETER(float, VoxelSizeLS)
        SHADER_PARAMETER(FIntVector, VolumeDimensions)
        SHADER_PARAMETER_RDG_BUFFER_SRV(StructuredBuffer<float4>, InstanceCenters)
        SHADER_PARAMETER_RDG_BUFFER_SRV(StructuredBuffer<float>,  InstanceScales)
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture3D<uint>, DensityUAV)
        SHADER_PARAMETER(float, BaseEdgeLengthLS)
        SHADER_PARAMETER(float, OverlapMultiplier)
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FSplatInstancesCS, "/Voxel/VoxelDensity.usf", "SplatInstancesCS", SF_Compute);

static void AddSplatInstancesPass(
    FRDGBuilder& GraphBuilder,
    const FVoxelRenderResource& Resource,
    FRDGTextureRef DensityTex,
    const FIntVector& VolumeDimensions,
    const FVector3f& VolumeMinLS,
    float VoxelSizeLS)
{
    const uint32 NumInstances = Resource.Centers.Num();
    TArray<FVector4f> PackedCenters; PackedCenters.Reserve(NumInstances);
    for (uint32 i = 0; i < NumInstances; ++i)
    {
        const FVector3f C = Resource.Centers.IsValidIndex(i) ? Resource.Centers[i] : FVector3f::ZeroVector;
        PackedCenters.Add(FVector4f(C, 1.0f));
    }
    FRDGBufferRef InstanceBuffer = CreateStructuredBuffer(GraphBuilder, TEXT("Voxel.InstanceCenters"), sizeof(FVector4f), PackedCenters.Num(), PackedCenters.GetData(), PackedCenters.Num() * sizeof(FVector4f));
    FRDGBufferSRVRef InstanceSRV = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(InstanceBuffer));

    TArray<float> PackedScales; PackedScales.Reserve(NumInstances);
    for (uint32 i = 0; i < NumInstances; ++i)
    {
        const float S = Resource.Scales.IsValidIndex(i) ? Resource.Scales[i] : 1.0f;
        PackedScales.Add(S);
    }
    
    FRDGBufferRef ScalesBuffer = CreateStructuredBuffer(GraphBuilder, TEXT("Voxel.InstanceScales"), sizeof(float), PackedScales.Num(), PackedScales.GetData(), PackedScales.Num() * sizeof(float));
    FRDGBufferSRVRef ScalesSRV = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(ScalesBuffer));

    TShaderMapRef<FSplatInstancesCS> CS(GetGlobalShaderMap(GMaxRHIFeatureLevel));
    auto* Params = GraphBuilder.AllocParameters<FSplatInstancesCS::FParameters>();
    Params->NumInstances     = NumInstances;
    Params->VolumeMinLS      = VolumeMinLS;
    Params->VoxelSizeLS      = VoxelSizeLS;
    Params->VolumeDimensions = VolumeDimensions;
    Params->InstanceCenters  = InstanceSRV;
    Params->InstanceScales   = ScalesSRV;
    Params->DensityUAV       = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(DensityTex, 0));
    Params->BaseEdgeLengthLS = VoxelSizeLS;
    Params->OverlapMultiplier = 2.0f;

    const uint32 GroupSize = 64u;
    const uint32 GroupsX   = (NumInstances + GroupSize - 1u) / GroupSize;
    FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("Voxel.SplatInstances"), ERDGPassFlags::Compute, CS, Params, FIntVector(GroupsX, 1, 1));
}

BuildVoxelVolumeTextures内に以下
密度テクスチャの生成パス関数の利用を追記します。

AddSplatInstancesPass(GraphBuilder, Resource, DensityTex, VolumeDimensions, VolumeMinLS, VoxelSizeLS);

// todo: Seedテクスチャの生成
// todo: JFA(Jump Flooding Algorithm)テクスチャの生成
// todo: SDF(Signed Distance Field)テクスチャの生成

Seedテクスチャの生成

密度テクスチャをもとに、等値面/閾値(ISO_THRESHOLD=0.5)を横切るセルを検出し、
サブボクセル精度で表面のセルを推定します。
※近似であり精度は粗いです

以下の内容のusfファイルを作成します。

密度テクスチャよりデータ抽出する関数を用意します。

float SampleDensity(int3 coord)
{
  if (any(coord < int3(0,0,0)) || any(coord >= VolumeDimensions))
      return 0.0;
  return float(DensityTex[coord]) / DENSITY_SCALE;
}

密度テクスチャをもとに、表面のセルと判断した場合SeedUAVに、その座標と表面フラグを書き込んでいます。

以下詳細です。

  • 密度テクスチャから指定セルの密度値の取得

  • 指定セルに書き込むシード値の定義/デフォルト設定(0,0,0,-1.0)
    => 表面セルではない場合デフォルトが書き込まれる

  • density >= ISO_THRESHOLD = 内部(占有)セルであるかの有無

  • 隣接する6方向のセルを見て自分が表面かチェック(isSurface)

  • シード値をテクスチャに書き込み

表面(isSurface==true)の場合

  • 周囲の占有セルを踏まえて座標にoffset加算
  • シード値を(座標:xyz,0)に更新
Texture3D<uint>   DensityTex;
RWTexture3D<float4> SeedUAV;
int3 VolumeDimensions;
float VoxelSizeLS;

static const float DENSITY_SCALE = 10000.0;
static const float ISO_THRESHOLD = 0.5;

[numthreads(8,8,8)]
void SeedCS(uint3 DTid : SV_DispatchThreadID)
{
    if (any(DTid >= (uint3)VolumeDimensions)) return;

    const float density = SampleDensity(int3(DTid));

    float4 seed = float4(0,0,0,-1.0);
    bool isSurface = false;

    float densityPX = SampleDensity(int3(DTid) + int3(1,0,0));
    float densityNX = SampleDensity(int3(DTid) + int3(-1,0,0));
    float densityPY = SampleDensity(int3(DTid) + int3(0,1,0));
    float densityNY = SampleDensity(int3(DTid) + int3(0,-1,0));
    float densityPZ = SampleDensity(int3(DTid) + int3(0,0,1));
    float densityNZ = SampleDensity(int3(DTid) + int3(0,0,-1));

    if (density >= ISO_THRESHOLD)
    {
        if (densityPX < ISO_THRESHOLD || densityNX < ISO_THRESHOLD ||
            densityPY < ISO_THRESHOLD || densityNY < ISO_THRESHOLD ||
            densityPZ < ISO_THRESHOLD || densityNZ < ISO_THRESHOLD)
        {
            isSurface = true;
        }
    }

    if (isSurface)
    {
        float3 grad;
        grad.x = densityPX - densityNX;
        grad.y = densityPY - densityNY;
        grad.z = densityPZ - densityNZ;

        float gradLen = length(grad);
        float3 seedPos = float3(DTid);

        if (gradLen > 1e-4)
        {
            float offset = (ISO_THRESHOLD - density) / gradLen;
            offset = clamp(offset, -1.0, 1.0);
            seedPos = float3(DTid) + normalize(grad) * offset;
        }

        seed.xyz = seedPos;
        seed.w = 0.0;
    }

    SeedUAV[DTid] = seed;
}

処理はコンピュートシェーダーを利用します。
密度テクスチャの時と同様に計算パスの追加をします。

class FSeedCS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FSeedCS);
    SHADER_USE_PARAMETER_STRUCT(FSeedCS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FIntVector, VolumeDimensions)
        SHADER_PARAMETER(float, VoxelSizeLS)
        SHADER_PARAMETER_RDG_TEXTURE(Texture3D<uint>, DensityTex)
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture3D<float4>, SeedUAV)
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FSeedCS, "/Voxel/VoxelDistanceField.usf", "SeedCS", SF_Compute);

static void AddSeedPass(FRDGBuilder& GraphBuilder, FRDGTextureRef DensityTex, FRDGTextureRef OutSeedTex, const FIntVector& VolumeDimensions, float VoxelSizeLS)
{
    TShaderMapRef<FSeedCS> CS(GetGlobalShaderMap(GMaxRHIFeatureLevel));
    auto* Params = GraphBuilder.AllocParameters<FSeedCS::FParameters>();
    Params->VolumeDimensions       = VolumeDimensions;
    Params->VoxelSizeLS            = VoxelSizeLS;
    Params->DensityTex = DensityTex;
    Params->SeedUAV    = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(OutSeedTex, 0));
    const FIntVector Groups = DivideCeil3D(VolumeDimensions, 8);
    FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("Voxel.Seed"), ERDGPassFlags::Compute, CS, Params, Groups);
}

BuildVoxelVolumeTextures内に以下
シードテクスチャの生成パス関数の利用を追記します。

AddSeedPass(GraphBuilder, DensityTex, SeedPing, VolumeDimensions, VoxelSizeLS);

// todo: JFA(Jump Flooding Algorithm)テクスチャの生成
// todo: SDF(Signed Distance Field)テクスチャの生成

JFA(Jump Flooding Algorithm)のテクスチャ空埋め

シードから全セルへ最近傍シード情報を伝播します。
26近傍JFAを使用し、より正確な距離場を生成します。

以下の内容のusfファイルを作成します。

シードテクスチャをもとに、表面ではないセル(w=-1)のセルを対象に
26方向探索をして最も距離の近い占有セルを書き込みます。

以下詳細です。

  • 全セルの26方向を指定距離のJFAで表面セル探索、既存書き込みの比較をして書き込み
Texture3D<float4> InSeed; // RGBA32F
RWTexture3D<float4> OutSeed;
int Step;

[numthreads(8,8,8)]
void JfaCS(uint3 DTid : SV_DispatchThreadID)
{
    if (any(DTid >= (uint3)VolumeDimensions)) return;

    float4 best = InSeed[DTid];
    float3 cellPos = float3(DTid);
    float  bestDist2 = best.w >= 0.0 ? dot(best.xyz - cellPos, best.xyz - cellPos) : 1e20;

    [unroll] for (int dz = -1; dz <= 1; ++dz){
        [unroll] for (int dy = -1; dy <= 1; ++dy){
            [unroll] for (int dx = -1; dx <= 1; ++dx){
                if (dx == 0 && dy == 0 && dz == 0) continue; // Skip self

                int3 offset = int3(dx, dy, dz) * Step;
                int3 p = int3(DTid) + offset;

                if (any(p < int3(0,0,0)) || any(p >= VolumeDimensions)) continue;

                float4 s = InSeed[p];
                if (s.w >= 0.0){
                    float d2 = dot(s.xyz - cellPos, s.xyz - cellPos);
                    if (d2 < bestDist2){
                        best = s;
                        bestDist2 = d2;
                    }
                }
            }
        }
    }

    OutSeed[DTid] = best;
}

処理はコンピュートシェーダーを利用します。
解像度を最大にして、ステップ毎に1/2を行いJFA距離(MaxDim)を縮小し、
シェーダに渡します。
JFA距離1(隣)になるまで繰り返し計算パスを追加します。

SeedPing/SeedPongの二種類があるのは、ステップを進めるなかで、
出力したテクスチャが次ステップでの入力たテクスチャになるため、
交互に入れ変え扱うことでテクスチャを形成します。

例)
ステップ1: Ping(読む) → Pong(書く)
ステップ2: Pong(読む) → Ping(書く) // 1の出力を今回の入力に
ステップ3: Ping(読む) → Pong(書く) // 2の出力を今回の入力に

class FJFACS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FJFACS);
    SHADER_USE_PARAMETER_STRUCT(FJFACS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FIntVector, VolumeDimensions)
        SHADER_PARAMETER(int32, Step)
        SHADER_PARAMETER_RDG_TEXTURE(Texture3D<float4>, InSeed)
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture3D<float4>, OutSeed)
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FJFACS, "/Voxel/VoxelDistanceField.usf", "JfaCS", SF_Compute);

static FRDGTextureRef AddJFAPasses(FRDGBuilder& GraphBuilder, FRDGTextureRef SeedPing, FRDGTextureRef SeedPong, const FIntVector& VolumeDimensions)
{
    int32 MaxDim = FMath::Max3(VolumeDimensions.X, VolumeDimensions.Y, VolumeDimensions.Z);
    int32 Step = 1 << (31 - FMath::CountLeadingZeros(MaxDim));
    bool bPingToPong = true;
    while (Step >= 1)
    {
        TShaderMapRef<FJFACS> CS(GetGlobalShaderMap(GMaxRHIFeatureLevel));
        auto* Params = GraphBuilder.AllocParameters<FJFACS::FParameters>();
        Params->VolumeDimensions  = VolumeDimensions;
        Params->Step = Step;
        Params->InSeed  = bPingToPong ? SeedPing : SeedPong;
        Params->OutSeed = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(bPingToPong ? SeedPong : SeedPing, 0));
        const FIntVector Groups = DivideCeil3D(VolumeDimensions, 8);
        FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("Voxel.JFA step=%d", Step), ERDGPassFlags::Compute, CS, Params, Groups);
        bPingToPong = !bPingToPong;
        Step >>= 1;
    }
    return bPingToPong ? SeedPing : SeedPong;
}

BuildVoxelVolumeTextures内に以下
シードテクスチャのJFAによる空埋め処理関数の利用を追記します。

FRDGTextureRef SeedAll = AddJFAPasses(GraphBuilder, SeedPing, SeedPong, VolumeDimensions);

// todo: SDF(Signed Distance Field)テクスチャの生成

SDFテクスチャの生成

シードテクスチャよりセル情報の中心座標から表面までの距離を計算します。
符号は表面より内側はマイナスに外側はプラスの意味とします。

以下の内容のusfを用意します。

密度テクスチャよりデータ抽出する関数を用意します。

float SampleDensity(int3 coord)
{
  if (any(coord < int3(0,0,0)) || any(coord >= VolumeDimensions))
      return 0.0;
  return float(DensityTex[coord]) / DENSITY_SCALE;
}

密度テクスチャをもとに内/外を判断し、
シードテクスチャをもとに表面までの距離を書き込んでいきます。

以下詳細です。

  • セルのシードと密度の取得
  • 密度の閾値比較(density >= ISO_THRESHOLD) による内外の取得

表面セルではない
内側の場合: -Voxel直径を書き込み
外側の場合: 1e6を書き込み

表面セルである

  • 現在セル中心の位置 - 最近傍表面シードの位置 = 表面までの距離
  • 内外の符号づけ
  • SdfUAVに距離を書き込み
Texture3D<uint>   DensityTex;
Texture3D<float4> SeedTex;
RWTexture3D<float> SdfUAV;
float3 VolumeMinLS;
[numthreads(8,8,8)]
void DistanceToSdfCS(uint3 DTid : SV_DispatchThreadID)
{
    if (any(DTid >= (uint3)VolumeDimensions)) return;

    const float4 s = SeedTex[DTid];
    const float density = SampleDensity(int3(DTid));
    const bool isInside = (density >= ISO_THRESHOLD);

    if (s.w < 0.0)
    {
        SdfUAV[DTid] = isInside ? -VoxelSizeLS : 1e6;
        return;
    }

    const float3 p = VolumeMinLS + (float3(DTid) + 0.5) * VoxelSizeLS;
    const float3 q = VolumeMinLS + (s.xyz + 0.5) * VoxelSizeLS;
    float dist = length(p - q);
    
    if (isInside)
    {
        dist = -dist;
    }

    SdfUAV[DTid] = dist;
}

処理はコンピュートシェーダーを利用します。
計算パスを追加します。

class FDistanceToSdfCS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FDistanceToSdfCS);
    SHADER_USE_PARAMETER_STRUCT(FDistanceToSdfCS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(FIntVector, VolumeDimensions)
        SHADER_PARAMETER(FVector3f, VolumeMinLS)
        SHADER_PARAMETER(float, VoxelSizeLS)
        SHADER_PARAMETER_RDG_TEXTURE(Texture3D<float4>, SeedTex)
        SHADER_PARAMETER_RDG_TEXTURE(Texture3D<uint>, DensityTex)
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture3D<float>, SdfUAV)
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FDistanceToSdfCS, "/Voxel/VoxelDistanceField.usf", "DistanceToSdfCS",  SF_Compute);

static void AddDistanceToSdfPass(
    FRDGBuilder& GraphBuilder,
    FRDGTextureRef InSeed,
    FRDGTextureRef OutSdf,
    const FIntVector& VolumeDimensions,
    const FVector3f& VolumeMinLS,
    float VoxelSizeLS,
    FRDGTextureRef DensityTex)
{
    TShaderMapRef<FDistanceToSdfCS> CS(GetGlobalShaderMap(GMaxRHIFeatureLevel));
    auto* Params = GraphBuilder.AllocParameters<FDistanceToSdfCS::FParameters>();
    Params->VolumeDimensions         = VolumeDimensions;
    Params->VolumeMinLS             = VolumeMinLS;
    Params->VoxelSizeLS             = VoxelSizeLS;
    Params->SeedTex                 = InSeed;
    Params->DensityTex              = DensityTex;
    Params->SdfUAV                  = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(OutSdf, 0));
    const FIntVector Groups = DivideCeil3D(VolumeDimensions, 8);
    FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("Voxel.DistanceToSDF"), ERDGPassFlags::Compute, CS, Params, Groups);
}

BuildVoxelVolumeTextures内に以下
記述を追加します。

AddDistanceToSdfPass(GraphBuilder, SeedAll, SdfTex, VolumeDimensions, VolumeMinLS, VoxelSizeLS, DensityTex);

描画の実装

SDF表現をフルスクリーン三角形でレイマーチし、SceneColor と SceneDepthに直接書き込むパスを追加します。
対象ボリュームの可視判定(視錐台とAABB)を通過したものだけ描画されます。

以下の内容のusfファイルを作成します。

頂点シェーダはとてもシンプルで、画面を完全に覆う大きな三角形にしてます。

struct FVSOut { float4 PositionCS : SV_POSITION; };

FVSOut FullscreenVS(uint VertexID : SV_VertexID)
{
    const float2 Pos[3] = { float2(-1,-1), float2(-1,3), float2(3,-1) };
    FVSOut o; o.PositionCS = float4(Pos[VertexID], 0, 1); return o;
}

ピクセルシェーダが描画のメイン処理です。

各VoxelVolumeの持つAABBの領域とレイの接触判定関数を用意します。

bool RayAABB(float3 ro, float3 rd, float3 bmin, float3 bmax, out float t0, out float t1)
{
    const float3 dirSign = sign(rd);
    const float3 invAbs  = rcp(max(abs(rd), 1e-6));
    const float3 invDir  = dirSign * invAbs;

    const float3 tbot = (bmin - ro) * invDir;
    const float3 ttop = (bmax - ro) * invDir;

    const float3 tmin3 = min(tbot, ttop);
    const float3 tmax3 = max(tbot, ttop);

    t0 = max(max(tmin3.x, tmin3.y), max(tmin3.z, 0.0));
    t1 = min(tmax3.x, min(tmax3.y, tmax3.z));
    return t1 >= t0;
}

スムーズステップ補間用の関数を用意します。
=> 3次エルミート補間の計算式です。

float3 SmoothFrac(float3 f)
{
    return f * f * (3.0 - 2.0 * f);
}

ローカル空間座標を正規化UVW座標に変換を用意します。
=>2DUV座標のような、3Dテクスチャをサンプリングするための正規化座標のことです。

float3 ComputeUVW(float3 pLS, float3 extent)
{
    return (pLS - VolumeMinLS) / extent;
}

密度と距離のサンプリングをする関数も用意します。

密度サンプリングはより滑らかな値にするため、以下処理を加えています。

  • edgeDistで境界までの距離を測り、端に近いほど密度を減らすedgeFalloffを求めます。
  • ボリューム境界で不自然さを防ぐために範囲外は0にし、無効領域に密度が出ないようにする。
  • 隣接8セルの影響で自分の密度に混ぜる密度をSmoothFrac滑らかな値(f)にします。
float SampleDensity(float3 uvw)
{
    float3 edgeDist = min(uvw, 1.0 - uvw);
    float edgeFalloff = saturate(min(min(edgeDist.x, edgeDist.y), edgeDist.z) * 10.0);

    if (any(uvw < -0.05) || any(uvw > 1.05))
        return 0.0;

    float3 uvwClamped = clamp(uvw, 0.0, 1.0);

    float3 cellCoord = uvwClamped * float3(VolumeDims) - 0.5;
    int3 base = int3(floor(cellCoord));
    float3 f = frac(cellCoord);
    f = SmoothFrac(f);

    // Clamp to valid range
    int3 c000 = clamp(base, int3(0,0,0), VolumeDims - int3(1,1,1));
    int3 c100 = clamp(base + int3(1,0,0), int3(0,0,0), VolumeDims - int3(1,1,1));
    int3 c010 = clamp(base + int3(0,1,0), int3(0,0,0), VolumeDims - int3(1,1,1));
    int3 c110 = clamp(base + int3(1,1,0), int3(0,0,0), VolumeDims - int3(1,1,1));
    int3 c001 = clamp(base + int3(0,0,1), int3(0,0,0), VolumeDims - int3(1,1,1));
    int3 c101 = clamp(base + int3(1,0,1), int3(0,0,0), VolumeDims - int3(1,1,1));
    int3 c011 = clamp(base + int3(0,1,1), int3(0,0,0), VolumeDims - int3(1,1,1));
    int3 c111 = clamp(base + int3(1,1,1), int3(0,0,0), VolumeDims - int3(1,1,1));

    // Sample 8 corners
    float d000 = float(DensityTex.Load(int4(c000, 0))) / DENSITY_SCALE;
    float d100 = float(DensityTex.Load(int4(c100, 0))) / DENSITY_SCALE;
    float d010 = float(DensityTex.Load(int4(c010, 0))) / DENSITY_SCALE;
    float d110 = float(DensityTex.Load(int4(c110, 0))) / DENSITY_SCALE;
    float d001 = float(DensityTex.Load(int4(c001, 0))) / DENSITY_SCALE;
    float d101 = float(DensityTex.Load(int4(c101, 0))) / DENSITY_SCALE;
    float d011 = float(DensityTex.Load(int4(c011, 0))) / DENSITY_SCALE;
    float d111 = float(DensityTex.Load(int4(c111, 0))) / DENSITY_SCALE;

    // Smooth trilinear interpolation
    float d00 = lerp(d000, d100, f.x);
    float d10 = lerp(d010, d110, f.x);
    float d01 = lerp(d001, d101, f.x);
    float d11 = lerp(d011, d111, f.x);
    float d0 = lerp(d00, d10, f.y);
    float d1 = lerp(d01, d11, f.y);
    float density = lerp(d0, d1, f.z);

    return density * edgeFalloff;
}

float SampleSDF(float3 pLS)
{
    float3 extent = max(VolumeMaxLS - VolumeMinLS, 1e-4);
    float3 uvw = ComputeUVW(pLS, extent);

    if (any(uvw < 0.0) || any(uvw > 1.0))
    {
        float3 clamped = clamp(pLS, VolumeMinLS, VolumeMaxLS);
        return length(pLS - clamped) + VoxelSizeLS;
    }

    return SDFTex.SampleLevel(SDFSampler, uvw, 0);
}

レイ上のヒット位置を二分探索で詰める処理も用意します。
tLo と tHi はレイ上の区間(ヒットがありそうな範囲)でその中点(tMid)
を取ってSDFで距離を評価して、閾値未満なら
「もっと手前にあるはず」なので tHi = tMid。
そうでなければ tLo = tMid。これを n回繰り返して、ヒット位置を高精度に絞り込んでいます。

float BinarySearchHit(float3 ro, float3 rd, float tLo, float tHi, int iterations, float threshold)
{
    for (int j = 0; j < iterations; ++j)
    {
        float tMid = (tLo + tHi) * 0.5;
        float3 pMid = ro + rd * tMid;
        float dMid = SampleSDF(pMid);
        if (dMid < threshold)
            tHi = tMid;
        else
            tLo = tMid;
    }
    return (tLo + tHi) * 0.5;
}

RenderPassの追加部分で説明しましたが、こちらは不透明描画後のタイミングで、
UE内蔵のライティング処理も完了した後のため、オブジェクトに影や色の反射はなく立体感がなく
描画物の変化がわかりづらいため、
独自で簡易ライティング + フレネルによる色の変化をあたえ立体感を出します。

具体的には以下処理をしています。
・pLSの前後にeps(offset)を加算して密度その密度を抽出し、
その差を取って 密度が増える方向 = 表面の法線近似 と定義しています。
=> (d(x+eps)-d(x-eps), …) = 法線ベクトル
※勾配が小さすぎる場合は上向きの固定法線にしています。

・法線をワールドに変換。
・NdotLでライト方向との内積をして、ambientを足して暗部を持ち上げます。
・視線方向との角度で縁が明るくなる効果を足しています。
・密度(density)を使って baseColor を少し色変化させます
・求めた各色を合成します。(baseColor * lighting + fresnel)

float3 ComputeSimpleLighting(float3 pLS, float3 hitPosW)
{
    float eps = VoxelSizeLS * 0.5;
    float3 normal;
    normal.x = SampleDensity(pLS + float3(eps,0,0)) - SampleDensity(pLS - float3(eps,0,0));
    normal.y = SampleDensity(pLS + float3(0,eps,0)) - SampleDensity(pLS - float3(0,eps,0));
    normal.z = SampleDensity(pLS + float3(0,0,eps)) - SampleDensity(pLS - float3(0,0,eps));

    float gradLen = length(normal);
    if (gradLen > 1e-4)
        normal = normal / gradLen;
    else
        normal = float3(0, 1, 0);

    float3 normalWS = normalize(mul(float4(-normal, 0), LocalToWorld).xyz);
    float3 lightDir = normalize(float3(0.5, 0.8, 0.3));
    float NdotL = max(dot(normalWS, lightDir), 0.0);
    float ambient = 0.2;
    float lighting = ambient + (1.0 - ambient) * NdotL;

    float3 viewDir = normalize(CameraWorldPos - hitPosW);
    float fresnel = pow(1.0 - max(dot(normalWS, viewDir), 0.0), 3.0);

    float density = SampleDensity(pLS);
    float3 extent = max(VolumeMaxLS - VolumeMinLS, 1e-4);
    float3 uvw = saturate((pLS - VolumeMinLS) / extent);

    float3 baseColor = float3(0.9, 0.5, 0.4);
    baseColor = lerp(baseColor, float3(0.7, 0.3, 0.3), saturate(density - 0.5));

    float3 finalColor = baseColor * lighting + float3(1.0, 0.8, 0.7) * fresnel * 0.4;

    return finalColor;
}

上記でそろえた関数を使ってレイを飛ばし、SDFをたどって接触した箇所の色と深度を書き込みます。

具体的には以下処理をしています。

  • 画面座標(In.PositionCS)から-1〜1の正規化座標(NDC)を作り、ワールド座標(InvViewProj) を求めます。
  • AABBボリュームとの交差判定をして、交差(ヒット)しなければ書き込みはしません。
  • レイマーチを行い、到達場所の距離を取得し、その値に応じて次の前進量を変化させすすみます。
    => ヒット判定(2種類)
  1. d < adaptiveEps:表面に十分近い → 二分探索で精密化
  2. d < 0 かつ前回は外側:符号反転を検出 → 二分探索で精密化

ヒットしなければ書き込みはしません。
ヒットした場合はの深度と、簡易ライティングによって求めた色を書き込みます。

Texture3D<float> SDFTex; SamplerState SDFSampler;
Texture3D<uint>  DensityTex;
static const float DENSITY_SCALE = 10000.0;
static const float ISO_THRESHOLD = 0.5;

float3 VolumeMinLS;
float3 VolumeMaxLS;
float  VoxelSizeLS;
float  RoundRadiusLS;
int    DebugMode; // 0=normal, 1=show density, 2=show SDF
float4x4 LocalToWorld;
float4x4 WorldToLocal;
float4x4 InvViewProj;
int3    VolumeDims;
float3   CameraWorldPos;
float2   ViewportInvSize;
float2   ViewportMin;
float4x4 ViewProj;

struct RaymarchOut { float4 Color : SV_Target0; float Depth : SV_Depth; };

RaymarchOut RaymarchPS(FVSOut In)
{
    float2 pix = In.PositionCS.xy;
    float2 uv = (pix - ViewportMin) * ViewportInvSize;
    float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
    float4 p4 = mul(float4(ndc, 1, 1), InvViewProj);
    float3 pW = p4.xyz / max(p4.w, 1e-4);
    float3 roW = CameraWorldPos;
    float3 rdW = normalize(pW - roW);

    float3 ro = mul(float4(roW,1), WorldToLocal).xyz;
    float3 rd = normalize(mul(float4(rdW,0), WorldToLocal).xyz);
    float tEnter, tExit;
    if (!RayAABB(ro, rd, VolumeMinLS, VolumeMaxLS, tEnter, tExit))
    {
        clip(-1); RaymarchOut o; o.Color = 0; o.Depth = 0; return o;
    }

    float t = tEnter;
    const int   MaxSteps   = 192;
    const float Safety     = 0.5;
    const float MinStep    = max(0.003 * VoxelSizeLS, 0.0005);

    const float BaseHitEps = VoxelSizeLS * 0.08;

    bool hit = false;
    float3 pLS = ro + rd * t;
    float prevD = 1e6;
    float prevT = t;

    [loop]
    for (int i = 0; i < MaxSteps; ++i)
    {
        if (t > tExit) break;

        pLS = ro + rd * t;
        float d = SampleSDF(pLS);

        float adaptiveEps = BaseHitEps * (1.0 + 0.001 * t);
        adaptiveEps = clamp(adaptiveEps, BaseHitEps * 0.5, BaseHitEps * 3.0);

        if (d < adaptiveEps)
        {
            t = BinarySearchHit(ro, rd, max(prevT, tEnter), t, 4, adaptiveEps * 0.5);
            pLS = ro + rd * t;
            hit = true;
            break;
        }

        if (d < 0.0 && prevD > 0.0)
        {
            t = BinarySearchHit(ro, rd, prevT, t, 8, 0.0);
            pLS = ro + rd * t;
            hit = true;
            break;
        }

        if (t > tExit + VoxelSizeLS && d > VoxelSizeLS) break;

        prevD = d;
        prevT = t;

        float adaptiveSafety = lerp(0.3, Safety, saturate(d / VoxelSizeLS));
        float stepLen = max(d * adaptiveSafety, MinStep);
        t += stepLen;
    }

    if (!hit) { clip(-1); RaymarchOut o; o.Color = 0; o.Depth = 0; return o; }

    float3 hitPosW = mul(float4(pLS,1), LocalToWorld).xyz;
    float4 clipPos = mul(float4(hitPosW,1), ViewProj);
    float deviceZ = clipPos.z / max(clipPos.w, 1e-4);
    deviceZ = saturate(deviceZ);
    
    float3 finalColor = ComputeSimpleLighting(pLS, hitPosW);

    RaymarchOut o;
    o.Color = float4(finalColor, 1.0);
    o.Depth = deviceZ;
    return o;
}

AddRenderPass内には以下の様に描画設定でPassの追加します。

auto* PassParameters = GraphBuilder.AllocParameters<FVoxelRaymarchPassParameters>();
PassParameters->SDFTex = RenderResult.SdfTex;
PassParameters->DensityTex = RenderResult.DensityTex;
PassParameters->RenderTargets[0] = FRenderTargetBinding(SceneColor, ERenderTargetLoadAction::ELoad);
PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(
    SceneDepth,
    ERenderTargetLoadAction::ELoad,
    ERenderTargetLoadAction::ELoad,
    FExclusiveDepthStencil::DepthWrite_StencilNop);

const FIntPoint SceneExtent = SceneColor->Desc.Extent;
GraphBuilder.AddPass(
    RDG_EVENT_NAME("Voxel.RaymarchRendering"),
    PassParameters,
    ERDGPassFlags::Raster,
    [PassParameters, SceneExtent, View, Proxy, RenderResult, Resource, SceneDepth](FRHICommandListImmediate& RHICmdList)
    {
        FGraphicsPipelineStateInitializer GraphicsPSO;
        RHICmdList.ApplyCachedRenderTargets(GraphicsPSO);

        GraphicsPSO.BlendState        = TStaticBlendState<CW_RGBA, BO_Add, BF_SourceAlpha, BF_InverseSourceAlpha, BO_Add, BF_One, BF_InverseSourceAlpha>::GetRHI();
        GraphicsPSO.RasterizerState   = TStaticRasterizerState<FM_Solid, CM_None>::GetRHI();
        GraphicsPSO.DepthStencilState = TStaticDepthStencilState<true, CF_GreaterEqual>::GetRHI();
        GraphicsPSO.PrimitiveType     = PT_TriangleList;

        ERHIFeatureLevel::Type FeatureLevel = GMaxRHIFeatureLevel;
        TShaderMapRef<FRaymarchFullscreenVS>  VS(GetGlobalShaderMap(FeatureLevel));
        TShaderMapRef<FRaymarchPS>  PS(GetGlobalShaderMap(FeatureLevel));

        GraphicsPSO.BoundShaderState.VertexDeclarationRHI = GEmptyVertexDeclaration.VertexDeclarationRHI;
        GraphicsPSO.BoundShaderState.VertexShaderRHI = VS.GetVertexShader();
        GraphicsPSO.BoundShaderState.PixelShaderRHI  = PS.GetPixelShader();
        SetGraphicsPipelineState(RHICmdList, GraphicsPSO, 0);

        RHICmdList.SetViewport(0, 0, 0.0f, SceneExtent.X, SceneExtent.Y, 1.0f);
        FRaymarchFullscreenVS::FParameters VSParams;
        SetShaderParameters(RHICmdList, VS, VS.GetVertexShader(), VSParams);

        FRaymarchPS::FParameters PSParams;
        PSParams.VolumeMinLS = Resource->VolumeMinLS;
        PSParams.VolumeMaxLS = Resource->VolumeMaxLS;
        PSParams.VoxelSizeLS = Resource->VoxelSizeLS;
        const FMatrix LocalToWorld = Proxy->GetLocalToWorld();
        const FMatrix WorldToLocal = LocalToWorld.InverseFast();
        PSParams.LocalToWorld = FMatrix44f(LocalToWorld);
        PSParams.WorldToLocal = FMatrix44f(WorldToLocal);
        PSParams.InvViewProj = FMatrix44f(View->ViewMatrices.GetInvViewProjectionMatrix());
        PSParams.ViewProj    = FMatrix44f(View->ViewMatrices.GetViewProjectionMatrix());
        PSParams.CameraWorldPos = static_cast<FVector3f>(View->ViewMatrices.GetViewOrigin());
        PSParams.ViewportInvSize = FVector2f(1.0f / static_cast<float>(SceneExtent.X), 1.0f / static_cast<float>(SceneExtent.Y));
        PSParams.ViewportMin = FVector2f(0.0f, 0.0f);
        PSParams.VolumeDims = RenderResult.VolumeDimensions;
        PSParams.SDFTex = RenderResult.SdfTex;
        PSParams.DensityTex = RenderResult.DensityTex;
        PSParams.SDFSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
        SetShaderParameters(RHICmdList, PS, PS.GetPixelShader(), PSParams);
        
        RHICmdList.DrawPrimitive(0, 1, 1);
    });

上記AddRenderPassをRegisterPostOpaqueRenderDelegateで紐づけできていない場合は
以前Voxelのワイヤ表示のPassを紐づけた時と設定してください。

Voxelの密度変化アニメーションの実装

上記の描画はVoxelのサイズ、数もそうですが、密度(Scale)と中心位置が動的に変化するとより面白い結果になるため、毎フレーム変化させるアニメーションを実装します。
値が変化すればいいのです。以下実装はサンプル程度で置いておきます。解説は省きます。

void UVoxelVolume::AnimateScales(float TimeSeconds, float Amplitude, float Frequency)
{
    if (!RenderResources.IsValid()) return;
    if (BaseScales_GT.Num() == 0) return;
    const int32 N = BaseScales_GT.Num();
    TArray<float> NewScales; NewScales.SetNumUninitialized(N);
    const float TwoPiF = 6.28318530718f * Frequency;
    for (int32 i = 0; i < N; ++i)
    {
        const float s0 = BaseScales_GT[i];
        const float phase = (float)i * 0.13f; // simple per-index phase
        // 0..1 の正規化スケール(中心0.5、振幅0.5)
        const float t01 = 0.5f + 0.5f * FMath::Sin(TwoPiF * TimeSeconds + phase);
        NewScales[i] = s0 * t01;
    }
    // Render threadに安全に反映
    TSharedPtr<FVoxelRenderResource> Shared = RenderResources;
    ENQUEUE_RENDER_COMMAND(UpdateVoxelScalesCmd)(
        [Shared, NewScales = MoveTemp(NewScales)](FRHICommandListImmediate&)
        {
            if (Shared.IsValid())
            {
                Shared->Scales = NewScales;
            }
        });
}

void UVoxelVolume::AnimateCenters(float TimeSeconds, float Amplitude, float Frequency)
{
    if (!RenderResources.IsValid()) return;
    if (BaseCenters_GT.Num() == 0) return;
    const int32 N = BaseCenters_GT.Num();
    TArray<FVector3f> NewCenters; NewCenters.SetNumUninitialized(N);
    const float TwoPiF = 6.28318530718f * Frequency;
    for (int32 i = 0; i < N; ++i)
    {
        const FVector3f c0 = BaseCenters_GT[i];
        const float phase = (float)i * 0.19f;
        const float w = TwoPiF * TimeSeconds + phase;
        // small Lissajous offset per cell (kept modest to avoid exiting volume)
        const FVector3f offset(
            Amplitude * FMath::Sin(w),
            Amplitude * FMath::Sin(1.37f * w + 0.5f),
            Amplitude * FMath::Sin(1.91f * w + 1.0f));
        NewCenters[i] = c0 + offset;
    }
    // Render threadに安全に反映
    TSharedPtr<FVoxelRenderResource> Shared = RenderResources;
    ENQUEUE_RENDER_COMMAND(UpdateVoxelCentersCmd)(
        [Shared, NewCenters = MoveTemp(NewCenters)](FRHICommandListImmediate&)
        {
            if (Shared.IsValid())
            {
                Shared->Centers = NewCenters;
            }
        });
}
void UVoxelRenderComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
    if (!VolumeAsset) return;
    const UWorld* World = GetWorld();
    const float T = (World && World->IsGameWorld()) ? World->GetTimeSeconds() : static_cast<float>(FApp::GetCurrentTime());
    VolumeAsset->AnimateScales(T, ScaleAmplitude, ScaleFrequency);
    VolumeAsset->AnimateCenters(T, CenterAmplitude, CenterFrequency);
}

Gifで画質粗くて申し訳ないですが、以下の様にできます。
VoxelTest DebugGame Unreal Editor GIF

参考資料/成果物

こういったVoxelを利用して描画する手法の記事はとても多くありますが、
個人的にはUE環境においては以下記事がとても丁寧でおすすめです。
https://jarllarsson.github.io/gen/gunkraymarcher.html#Intro

今回私が実装したリポジトリは以下です。
https://github.com/ShiromiSasami/VoxelTest

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?