ProceduralMeshComponentを使ってみる

今回はUE4.8から追加されたProceduralMeshComponentを使ってみます。

ただしこの機能は実験的に追加されているものですので、今後廃止になるということはないとは思いますが使い方が変更される可能性があります。

使用の際にはその点に十分注意して使用してください。

ProceduralMeshComponentはその名の通り、手続き的(Procedural)に生成されたメッシュを取り扱うコンポーネントです。

手続き的、というのはつまり、プログラムによる計算を用いてメッシュを生成できるということです。

今までは予めDCCツールで作成しておいたメッシュをボーンやモーフターゲットを用いて変形するのが関の山でしたが、この機能によってC++、もしくはBPからメッシュを生成することが可能になるというわけです。

それで何が出来るのか、というと、例えばアクターを使わずに大量の弾を描画して弾幕を作ってみたり、キャラクタの移動した軌跡を描画してみたりすることが可能です。

ただし、毎フレーム、メッシュの変形を行うのはそれなりに重くなるものと思われますので注意してください。

では、まずはBPでの使い方から見て行きましょう。といっても、BPとC++で特に変化することはほとんどありませんが。

最初に適当にBPをActorを継承する形で作成します。

ここに [コンポーネントを追加] → [Procedural Mesh] を選択してProceduralMeshComponentを追加します。

ue374.jpg

追加をしてもメッシュは表示されません。何もメッシュが生成されていないからです。

次に Construction Script をいじりましょう。今回はここでメッシュを生成します。

メッシュの生成には ProceduralMeshComponent が持っている [Create Mesh Section] という命令を使用します。

ue375.jpg

この関数への入力としては、最低でも [Vertices] と [Triangles] の2種類を入れなければなりません。

[Vertices] には頂点座標の配列を指定し、[Triangles] にはどの頂点で三角ポリゴンを形成するかの情報を頂点インデックスとして渡してやる必要があります。

[Vertices] と [Triangles] の関係は以下の図のようになっています。

ue376.jpg

図のような四角形は三角ポリゴン2枚で構成されています。頂点数は4で、図のV0~V3が頂点座標です。

この座標V0~V3を [Vertices] に順番に追加したとします。すると、V0~の頂点インデックスはそれぞれ 0~の番号が割り当てられます。

[Triangles] にはこのインデックス番号を用いて三角ポリゴン2枚を描く順番を指定することになります。

三角ポリゴン1枚には3頂点が必要なため、2枚の三角ポリゴンは6頂点で描画されることになります。

そのため、インデックス番号が6つ設定されているというわけです。

また、三角ポリゴンを形成するインデックス番号の指定順番も重要になります。

ここで考えなければならないのは三角ポリゴンの表面(面法線が正の方向)はどちらかということですが、ここには右ねじの法則が適用されます。

いわゆるサムズアップの用に親指を立てて他の指を軽く握った場合、親指以外の指の進行方向に沿って頂点座標が選ばれるとその親指の向きが法線方向になります。

上の図で言うなら、このポリゴンの法線は画面を見ているあなたの方向を向いています。

さて、とりあえずは簡単な三角形ポリゴンを表示してみましょう。

以下のように Construction Script を作成してみてください。

ue377.jpg

組み上げたらビューポートに戻ってみましょう。何もなかった空中に三角ポリゴンが描画されているのがわかるはずです。

その他の引数の意味を見て行きましょう。

[Normals] は法線、[UV0] はテクスチャ座標、[Vertex Colors] が頂点カラーで [Tangents] が接線方向になります。

これらの配列は [Vertices] と同じ要素数でなければなりません。

[Create Collision] はONにすると生成したモデルからコリジョンも生成します。

[Section Index] はメッシュのセクション番号で、セクションが違うと別のマテリアルを割り当てることが出来るようになります。

ProceduralMeshComponentにマテリアルを適用するにはコンポーネントを選択し、[Rendering] → [Override Materials] にマテリアルを追加すればOKです。

複数のセクション番号を利用する場合はその分だけマテリアルを追加しておきましょう。

ue378.jpg 

このようにProceduralMeshComponentを利用すると比較的簡単にポリゴンを生成することが出来ます。

しかし、複雑な計算による手続き的なメッシュ生成はBPでやるよりC++でやるほうが簡単でしょう。

というわけで、続きからC++によるProceduralMeshのあ使い方を調べていきましょう。

今回は簡単ですがそれなりに見栄えがするということで、スプライン曲線にそってパイプを作成するアクターをC++で作ってみます。

基本的にはBPで使ったのと同じで、CreateMeshSection命令を利用します。

使い方も同じですが、BPと違って一応全ての引数に何らかの値を与える必要があります。

とはいえ、今回は [Vertices], [Triangles], [Normals] のみで、他の配列は空の配列を渡します。

アクターの作成にはまずエディタを立ち上げ、メニューから [ファイル] → [新規C++クラス] を選択します。

ue379.jpg

親クラスの選択ウィンドウが開いたらアクターを選択して [次へ]

ue380.jpg

アクターの名前には [ProceduralSplineMesh] という名前をつけます。

この図ではすでに作成済みのためエラーが出ていますが、初回時には [クラスを作成] ボタンが有効になっているので押してください。

ue381.jpg

これで.hファイルと.cppファイルが追加されます。

あとは.hファイルと.cppファイルを編集するだけですが、説明するほどのことはやってないのでぺたっと貼り付けちゃいます。

ProceduralSplineMesh.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/Actor.h"

#include "ProceduralSplineMesh.generated.h"

class UProceduralMeshComponent;

class USplineComponent;

UCLASS()

class UE48CPPTEST_API AProceduralSplineMesh : public AActor

{

    GENERATED_BODY()

public:

    // Sets default values for this actor's properties

    AProceduralSplineMesh();

    // Called when the game starts or when spawned

    virtual void BeginPlay() override;

    // Called every frame

    virtual void Tick( float DeltaSeconds ) override;

    virtual void OnConstruction(const FTransform& Transform) override;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Procedural)

    UProceduralMeshComponent* ProceduralMesh;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Procedural)

    USplineComponent* Spline;

    UPROPERTY(EditAnywhere, Category=Procedural)

    float SplitLength;

    UPROPERTY(EditAnywhere, Category=Procedural)

    float Radius;

    UPROPERTY(EditAnywhere, Category=Procedural)

    int32 DivideNum;

    UFUNCTION(BlueprintCallable, Category=Procedural)

    void CreatePipe();

};

ProceduralSplineMesh.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "UE48CppTest.h"

#include "ProceduralSplineMesh.h"

#include "ProceduralMeshComponent.h"

#include "Components/SplineComponent.h"

// Sets default values

AProceduralSplineMesh::AProceduralSplineMesh()

{

    // 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;

    ProceduralMesh = CreateDefaultSubobject<UProceduralMeshComponent>("ProceduralMesh");

    Spline = CreateDefaultSubobject<USplineComponent>("Splline");

    SplitLength = 10.0f;

    Radius = 5.0f;

    DivideNum = 8;

}

// Called when the game starts or when spawned

void AProceduralSplineMesh::BeginPlay()

{

    Super::BeginPlay();

}

// Called every frame

void AProceduralSplineMesh::Tick( float DeltaTime )

{

    Super::Tick( DeltaTime );

}

// パイプの生成

void AProceduralSplineMesh::CreatePipe()

{

    if *1

    {

        return;

    }

    ProceduralMesh->ClearMeshSection(0);

    TArray<FVector> vertices;

    TArray<int32> indices;

    TArray<FVector> normals;

    TArray<FVector2D> uvs;

    TArray<FColor> colors;

    TArray<FProcMeshTangent> tangents;

    FMatrix invMat = GetTransform().Inverse().ToMatrixWithScale();

    float length = Spline->GetSplineLength();

    FVector initDir = invMat.TransformVector(Spline->GetWorldDirectionAtDistanceAlongSpline(0.0f));

    FVector initUp(0.0f, 0.0f, 1.0f);

    if (FMath::Abs(initDir.Z) > (1.0f - 1e-4f))

    {

        initUp = FVector(1.0f, 0.0f, 0.0f);

    }

    FVector initPos = invMat.TransformPosition(Spline->GetWorldLocationAtDistanceAlongSpline(0.0f));

    // 初期ポイントを計算する

    float rotAngle = FMath::DegreesToRadians(360.0f / static_cast<float>(DivideNum));

    {

        FVector right = FVector::CrossProduct(initUp, initDir);

        initUp = FVector::CrossProduct(initDir, right);

        initUp.Normalize();

        FQuat rot(initDir, rotAngle);

        FVector up = initUp;

        for (int i = 0; i < DivideNum; i++)

        {

            vertices.Add(initPos + up * Radius);

            normals.Add(up);

            up = rot.RotateVector(up);

        }

        // 初期ポイントでフタをする

        for (int i = 0; i < DivideNum - 2; i++)

        {

            indices.Add(0);

            indices.Add(i + 1);

            indices.Add(i + 2);

        }

    }

    float dist = 0.0f;

    int32 initIndex = 0;

    while (dist < length)

    {

        dist = FMath::Min(dist + SplitLength, length);

        FVector dir = invMat.TransformVector(Spline->GetWorldDirectionAtDistanceAlongSpline(dist));

        FVector pos = invMat.TransformPosition(Spline->GetWorldLocationAtDistanceAlongSpline(dist));

       

        FVector right = FVector::CrossProduct(initUp, dir);

        initUp = FVector::CrossProduct(dir, right);

        initUp.Normalize();

        FQuat rot(dir, rotAngle);

        // 頂点の追加

        FVector up = initUp;

        for (int i = 0; i < DivideNum; i++)

        {

            vertices.Add(pos + up * Radius);

            normals.Add(up);

            up = rot.RotateVector(up);

        }

        // インデックスの追加

        for (int i = 0; i < DivideNum; i++)

        {

            int32 idx = initIndex + i;

            int32 nidx = initIndex + ((i + 1) % DivideNum);

            indices.Add(idx);

            indices.Add(idx + DivideNum);

            indices.Add(nidx);

            indices.Add(idx + DivideNum);

            indices.Add(nidx + DivideNum);

            indices.Add(nidx);

        }

        initIndex += DivideNum;

    }

    // 最後のフタをする

    for (int i = 0; i < DivideNum - 2; i++)

    {

        indices.Add(initIndex);

        indices.Add(initIndex + i + 2);

        indices.Add(initIndex + i + 1);

    }

    ProceduralMesh->CreateMeshSection(0, vertices, indices, normals, uvs, colors, tangents, false);

}

// Construction ScriptのC++

void AProceduralSplineMesh::OnConstruction(const FTransform& Transform)

{

    CreatePipe();

}

赤太字の部分が追加されている部分です。

面倒ならコピペしてください。

若干解説をすると、昔はコンポーネントを作成する場合は TSubobjectPtr というテンプレートを使用していたのですが、いつの間にか非推奨になりました。

現在は普通にポインタを持っていればOKなようです。ガベージコレクション周りに修正が入ったのでしょう。

コンポーネントは UProceduralMeshComponent と USplineComponent を持っています。

その他のパラメータとしては、SplitLength は頂点を生成する長さの単位です。スプラインの長さがここで指定した長さになる部分に頂点が生成されます。

Radius はパイプの幅、DivideNum はパイプの円の分割数で、増やせば増やすほど滑らかになります。

CreatePipe()命令は現在の設定でパイプを作成します。

この命令は単体でも呼び出し可能ですが、Construction Script の要領で OnConstruction() イベントでも発行されます。

これにより、エディタ上でスプラインを変更するとそれに応じてメッシュも生成されるようになります。

ProceduralMeshComponent に登録する頂点座標や法線は World → Local 変換を行っています。

ProceduralMeshComponent のローカル空間にメッシュを生成する必要があるためです。

さて、これでビルドすると実はエラーが発生します。

なぜ発生するのかというと、ProceduralMeshComponent がプラグインとして提供されており、そのモジュールが見つからないからです。

モジュールについてはヒストリアさんのブログにありますので参照してください。

[UE4] モジュールについて

http://historia.co.jp/archives/3097

ここに書いてあるとおり、参照するモジュールを {プロジェクト名}.Build.cs ファイルに追加する必要があります。

今回はこの2行を追加しました。

PrivateDependencyModuleNames.AddRange(new string { "ProceduralMeshComponent" });

PrivateIncludePathModuleNames.AddRange(new string { "ProceduralMeshComponent" });

PrivateでなくてもPublicでも問題ないとは思いますが、Privateで困ることもないので今回はPrivateに追加しました。

PrivateとPublicの違いは参照範囲と思われるのですが、どうなんでしょう?

教えて、詳しい人!

これを追加するとビルドが通ります。

あとは作成した ProceduralSplineMesh アクターを継承したBPでも作成し、これを配置して色々いじってみてください。

いじるとこんな感じですね。割と楽しい。

さすがに複雑な計算をBPでやるのは骨が折れるので、ProceduralMeshComponent を使う場合はC++の方がいいと思います。

*1:SplitLength <= 1e-4f) || (Radius <= 1e-4f) || (DivideNum <= 0