SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

【UE4】ProceduralMeshについて再確認

はじめに

こんにちは。
長らく厄災ガノンだけ残して放置していたゼルダの伝説BotWをクリアしたのですが神ゲーでした、続編が待ち遠しいです。
ゼルダ無双も買いたいなあ…と思うエンジニアの佐々木です。

UE4の業務でProceduralMeshを取り扱うことがあったので
今回はそちらについて書いていこうと思います。

業務としては、
1、とあるデータに基づいて動的にメッシュ(ProceduralMesh)が生成される ←他の方の担当
2、生成されたメッシュに対して、メッシュから取れる情報だけでアレコレする ←自分の担当

業務内容を聞いた時点では「プロシージャルメッシュってなんだ?」というレベルだったので
ProceduralMeshについてと、メッシュから情報を取ってくる方法の2点について備忘録的に書いていきます。

といっても実は既に素晴らしい記事があり、とても分かりやすいため、参考サイトを挟みながらゆるくいきます。

ProceduralMeshとは

「Procedural」を翻訳してみると「手続き型」とでてきます。
つまり、プログラムによって動的に生成されたメッシュのことです。
メッシュの変形をしたいときや、何かのデータに基づいて生成するなどの理由で事前にアセットを用意できない場合に便利ですね。

参考サイト

monsho.blog63.fc2.com

www.orfeasel.com

正直なところ、ProceduralMeshについては上記のサイトを見れば十分理解できます。

…がせっかくなので上記サイトを参考にProceduralMeshを用いて立方体を生成し、
実際に各メッシュにアクセスするところまでやってみようと思います。
BPだけでもできますが、大変なのでC++で行います。

ProceduralMeshを使ってみよう

立方体を作ってみる

※立方体を作るところまではほとんど上記サイトのコピペです。
流れを超簡単に説明すると
1、モジュール追加
2、アクタ作成
3、頂点座標と頂点インデックスを用いてメッシュ作成
4、立方体ができる!!
です。

モジュールの追加

まず大前提としてProceduralMeshComponentはプラグインなのでモジュールの追加をする必要があります。
[プロジェクト名].Build.csファイルに記述します。

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "ProceduralMeshComponent" });
クラスの作成

今回はシンプルにメッシュを生成するアクタを作成します。
まずはエディタからC++クラスを作成しましょう。
左上のメニューから[ファイル]->[新規C++クラス]を選択します。
f:id:spark-sasaki-jun:20210430184036p:plain
親クラスは[Actor]を選択し
f:id:spark-sasaki-jun:20210430184139p:plain
名前はなんでも良いので「AProceduralMeshTest」というクラス名で作成しました。
(画像では既に作成済みですが)[クラス作成]を押して.hと.cppを追加します。
f:id:spark-sasaki-jun:20210430191114p:plain

ProceduralMesh生成処理

メッシュの生成は一般的なものと同じです。
頂点座標と頂点インデックスを用いてメッシュを生成します。
前述したとおりコピペなのでそのままソースコードを載せます。

ProceduralMeshTest.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ProceduralMeshTest.generated.h"

class UProceduralMeshComponent;

UCLASS()
class FORBLOG_API AProceduralMeshTest : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AProceduralMeshTest();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	UProceduralMeshComponent* CustomMesh;

	TArray<FVector> Vertices;

	TArray<int32> Triangles;

	void AddTriangle(int32 v1, int32 v2, int32 v3);

	void GenerateCubeMesh();

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

ProceduralMeshTest.cpp

#include "ProceduralMeshTest.h"
#include "ProceduralMeshComponent.h"
#include "Runtime/Engine/Classes/Kismet/KismetSystemLibrary.h"

// Sets default values
AProceduralMeshTest::AProceduralMeshTest()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    CustomMesh = CreateDefaultSubobject<UProceduralMeshComponent>("CustomMesh");
    SetRootComponent(CustomMesh);
    CustomMesh->bUseAsyncCooking = true;
}

// Called when the game starts or when spawned
void AProceduralMeshTest::BeginPlay()
{
    Super::BeginPlay();

    GenerateCubeMesh();
}

// Called every frame
void AProceduralMeshTest::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

void AProceduralMeshTest::AddTriangle(int32 v1, int32 v2, int32 v3)
{
    Triangles.Add(v1);
    Triangles.Add(v2);
    Triangles.Add(v3);
}

void AProceduralMeshTest::GenerateCubeMesh()
{
    // These are relative locations to the placed Actor in the world
    Vertices.Add(FVector(0, -100, 0));     // lower left - 0
    Vertices.Add(FVector(0, -100, 100));   // upper left - 1
    Vertices.Add(FVector(0, 100, 0));      // lower right - 2 
    Vertices.Add(FVector(0, 100, 100));    // upper right - 3

    Vertices.Add(FVector(100, -100, 0));   // lower front left - 4
    Vertices.Add(FVector(100, -100, 100)); // upper front left - 5

    Vertices.Add(FVector(100, 100, 100));  // upper front right - 6
    Vertices.Add(FVector(100, 100, 0));    // lower front right - 7

    // Back face of cube
    AddTriangle(0, 2, 3);
    AddTriangle(3, 1, 0);

    // Left face of cube
    AddTriangle(0, 1, 4);
    AddTriangle(4, 1, 5);

    // Front face of cube
    AddTriangle(4, 5, 7);
    AddTriangle(7, 5, 6);

    // Right face of cube
    AddTriangle(7, 6, 3);
    AddTriangle(3, 2, 7);

    // Top face
    AddTriangle(1, 3, 5);
    AddTriangle(6, 5, 3);

    // bottom face
    AddTriangle(2, 0, 4);
    AddTriangle(4, 7, 2);

    TArray<FLinearColor> VertexColors;
    VertexColors.Add(FLinearColor(0.f, 0.f, 1.f));
    VertexColors.Add(FLinearColor(1.f, 0.f, 0.f));
    VertexColors.Add(FLinearColor(1.f, 0.f, 0.f));
    VertexColors.Add(FLinearColor(0.f, 1.f, 0.f));
    VertexColors.Add(FLinearColor(0.5f, 1.f, 0.5f));
    VertexColors.Add(FLinearColor(0.f, 1.f, 0.f));
    VertexColors.Add(FLinearColor(1.f, 1.f, 0.f));
    VertexColors.Add(FLinearColor(0.f, 1.f, 1.f));

    CustomMesh->CreateMeshSection_LinearColor(0, Vertices, Triangles, TArray<FVector>(), TArray<FVector2D>(), VertexColors, TArray<FProcMeshTangent>(), true);
}
アクタの作成

[追加/インポート]もしくは[コンテンツブラウザ上で右クリック]->[ブループリントクラスを作成]を押してを作成します。。
f:id:spark-sasaki-jun:20210430193628p:plain
親クラスを選択する画面が出るので先ほど作成したクラスで[選択]を押します。
今回の場合は「AProceduralMeshTest」です。
f:id:spark-sasaki-jun:20210430193916p:plain

マテリアルの作成

アクタの作成と同様に
[追加/インポート]もしくは[コンテンツブラウザ上で右クリック]->[マテリアル]を押してを作成します。
今回マテリアルのノードは「Vertex Color」だけです。
f:id:spark-sasaki-jun:20210430201844p:plain
先ほど作成したアクタのイベントグラフのBeginPlayでマテリアルをセットします。
f:id:spark-sasaki-jun:20210430201540p:plain

いざ実行

f:id:spark-sasaki-jun:20210329180707g:plain
わーい、立方体ができた~!

各メッシュから頂点情報を取得してみる

ここまではほとんどコピペだったので、ここからは自分で処理を書いていきます。
メッシュから各頂点情報を取得できることが確認できればなんでも良いので
メッシュの輪郭にラインを描画して、三角ポリゴンの重心座標にキューブを生成してみます。
こちらも流れを超簡単に説明すると
1、セクションごとにメッシュの情報を取得
2、インデックスバッファ数からポリゴン数を求める
3、ポリゴンごとに頂点情報を取得
4、ライン描画してキューブ生成!!

先に生成するキューブのアクタを作成しておきます。
といってもコンポーネントにCubeのスタティックメッシュを追加しただけのアクタです。
f:id:spark-sasaki-jun:20210508162957p:plain

C++の場合

C++に処理を追加してBPから呼び出す形でやってみます。
引数にはUProceduralMeshComponentを渡します。
コメントを書いたので処理を見てもらった方が早いです。

void AProceduralMeshTest::TestProceduralMesh(UProceduralMeshComponent* ProcMesh)
{
    // セクション数を取得
    const int32 Sections = ProcMesh->GetNumSections();

    // セクションの数だけループ
    for (int32 i = 0; i < Sections; i++)
    {
        // メッシュの情報を取得
        FProcMeshSection* MeshData = ProcMesh->GetProcMeshSection(i);
        // インデックスバッファ数からポリゴン数を求める
        int32 IndexBufferNum = MeshData->ProcIndexBuffer.Num();
        int32 PolygonNum = IndexBufferNum / 3;

        // ポリゴンの数だけループ
        for (int32 j = 0; j < PolygonNum; j++)
        {
            // 頂点インデックス
            int32 v0Index = MeshData->ProcIndexBuffer[j * 3];
            int32 v1Index = MeshData->ProcIndexBuffer[j * 3 + 1];
            int32 v2Index = MeshData->ProcIndexBuffer[j * 3 + 2];

            // メッシュの頂点情報
            FProcMeshVertex v0 = MeshData->ProcVertexBuffer[v0Index];
            FProcMeshVertex v1 = MeshData->ProcVertexBuffer[v1Index];
            FProcMeshVertex v2 = MeshData->ProcVertexBuffer[v2Index];

            // ローカル座標での頂点座標
            FVector v0PosLocal = v0.Position;
            FVector v1PosLocal = v1.Position;
            FVector v2PosLocal = v2.Position;

            // ワールド座標での頂点座標へ変換
            FVector v0PosWorld = (v0PosLocal * ProcMesh->GetComponentScale()) + ProcMesh->GetComponentLocation();
            FVector v1PosWorld = (v1PosLocal * ProcMesh->GetComponentScale()) + ProcMesh->GetComponentLocation();
            FVector v2PosWorld = (v2PosLocal * ProcMesh->GetComponentScale()) + ProcMesh->GetComponentLocation();

            // メッシュの輪郭にラインを描画
            UKismetSystemLibrary::DrawDebugLine(GetWorld(), v1PosWorld, v2PosWorld, FLinearColor::Red, 60.0f, 5.0f);
            UKismetSystemLibrary::DrawDebugLine(GetWorld(), v2PosWorld, v0PosWorld, FLinearColor::Red, 60.0f, 5.0f);
            UKismetSystemLibrary::DrawDebugLine(GetWorld(), v0PosWorld, v1PosWorld, FLinearColor::Red, 60.0f, 5.0f);

            // キューブのアクタを生成してポリゴンの重心座標に配置する
            // 面倒なので今回はここでパスを指定する形で…
            // /Content 以下のパスが /Game 以下のパスに置き換わり、コンテントブラウザーで名前が test なら test.test_C を指定する
            FString Path = "/Game/02_ProceduralMesh/BP_Cube.BP_Cube_C";
            // 上記で設定したパスに該当するクラスを取得
            TSubclassOf<class AActor> SubClass = TSoftClassPtr<AActor>(FSoftObjectPath(*Path)).LoadSynchronous();
            if (SubClass)
            {
                // アクタをスポーン
                AActor* Actor = GetWorld()->SpawnActor<AActor>(SubClass);
                if (Actor)
                {
                    // ポリゴンの重心座標を求める
                    FVector CenterPos = (v0PosWorld + v1PosWorld + v2PosWorld) / 3;
                    // 求めた重心座標に生成したキューブを配置する
                    Actor->SetActorLocation(CenterPos);
                }
            }
        }
    }
}

あとはこのように呼び出すだけです。
f:id:spark-sasaki-jun:20210508163326p:plain

BPの場合

横に長くなってしまったので画像2枚になってしまいました…
やっていることは上記のC++と同じです。
f:id:spark-sasaki-jun:20210508164001p:plain
f:id:spark-sasaki-jun:20210508164130p:plain

ローカル座標からワールド座標へ変換している箇所は関数を作成しましたが、こちらもC++と同じです。
f:id:spark-sasaki-jun:20210508164344p:plain

いざ実行

実行して、先ほど作成したTestProceduralMesh関数を呼びます。
f:id:spark-sasaki-jun:20210508175047g:plain

やった~
カメラが荒ぶっていますが、メッシュの輪郭にラインが引けて、ポリゴンの重心座標にキューブが生成されていますね。

おわりに

今回は特に扱っていませんが
UProceduralMeshComponentには他にも法線やテクスチャ座標などもあり、色々なことができると思います。

なんだか散らかっている記事になってしまいましたが以上となります。
読んでいただきありがとうございました!
それではまた~~~