⛰️

[Unreal Engine 5] 実行時に地形を生成する

2025/01/18に公開

結論

GeoDynamicMeshActor.cpp
#include "GeoDynamicMeshActor.h"
#include <cassert>
#include <random>
#include "Math/UnrealMathUtility.h"
#include "RandomNumberGenerator.h"

namespace {
    const double triangleSideLength = 200.0;
    const double triangleHeight = triangleSideLength * 0.5 * std::sqrt(3.0);
}
void AGeoDynamicMeshActor::BeginPlay()
{
    init(1);

    auto meshCmp = GetDynamicMeshComponent();
    auto mesh = meshCmp->GetMesh();

    for (int y = 0; y < gridSize; ++y) {
        for (int x = 0; x < gridSize; ++x) {
            auto cx = x * triangleSideLength + y * triangleSideLength * 0.5;
            auto cy = y * triangleHeight;
            auto cz = heightMap[x][y];
            vertex[x][y].Set( cx, cy, cz );
            mesh->AppendVertex(vertex[x][y]);
        }
    }

    for (int y = 0; y < gridSize + 1; ++y) {
        for (int x = 0; x < gridSize - 1; ++x) {

            int a = y * gridSize + x;
            int b = a + 1;
            int c = a + gridSize;
            int d = c + 1;
            mesh->AppendTriangle(c, b, a);
            mesh->AppendTriangle(d, b, c);
        }
    }

    meshCmp->NotifyMeshUpdated();
    meshCmp->EnableComplexAsSimpleCollision();
    meshCmp->UpdateCollision();
}

double AGeoDynamicMeshActor::noise(int phase)
{
    auto r = initialMaxHeight * pow( 0.5, static_cast<double>(phase) * 0.9 );
    return RandGen::uni_real(-r, r);
}

void AGeoDynamicMeshActor::init(int phase)
{
    int step = 1 << (maxPhase - phase);
    for (int x = 0; x < gridSize; x += step) {
        for (int y = 0; y < gridSize; y += step) {
            heightMap[x][y] = RandGen::uni_real(0.0, initialMaxHeight);
        }
    }

    while ( ++phase <= maxPhase) {
        divide(phase);
    }
}

void AGeoDynamicMeshActor::divide(int phase)
{
    assert( ( 1 <= phase ) && ( phase <= maxPhase ) );
    int step = 1 << (maxPhase - phase);
    int prevStep = step * 2;

    for (int x = step; x < gridSize; x += prevStep) {
        for (int y = 0; y < gridSize; y += prevStep) {
            heightMap[x][y] = (heightMap[x - step][y] + heightMap[x + step][y]) / 2.0 + noise(phase);
        }
    }

    for (int x = 0; x < gridSize; x += prevStep) {
        for (int y = step; y < gridSize-1; y += prevStep) {
            heightMap[x][y] = (heightMap[x][y - step] + heightMap[x][y + step]) / 2.0 + noise(phase);
        }
    }

    for (int x = step; x < gridSize; x += prevStep) {
        for (int y = step; y < gridSize; y += prevStep) {
            heightMap[x][y] = (heightMap[x + step][y - step] + heightMap[x - step][y + step]) / 2.0 + noise(phase);
        }
    }
}
GeoDynamicMeshActor.h
#pragma once

#include "CoreMinimal.h"
#include "DynamicMeshActor.h"
#include "GeoDynamicMeshActor.generated.h"

UCLASS()
class MYPROJECTCPP_API AGeoDynamicMeshActor : public ADynamicMeshActor
{
    GENERATED_BODY()

protected:
    void BeginPlay() override;

    double noise(int phase);
    void init(int phase = 0);
    void divide(int phase);

    static constexpr int maxPhase = 10;
    static constexpr int gridSize = (1 << maxPhase) + 1;
    static constexpr double initialMaxHeight = 20000.0;

    double heightMap[gridSize][gridSize];
    FVector3d vertex[gridSize][gridSize];
};

補足

  • 中点変位法と呼ばれるフラクタル地形を生成するのアルゴリズムを実装した。

  • mesh 生成では真上から見たときに正三角格子となるようにXY座標を指定した。従って実際にレンダリングされる地形全体は上から見ると菱形になる。

  • 通常の用途であれば、あらかじめ何らかのエディタで生成した地形データを読み込むのが一般的と思われる。実行時に地形を生成する用途は限られる。

    • ローグライクなど、プレイの度にランダムな地形を用意したい場合
    • 一方向に無限に移動できる地形を用意したい場合
    • 地形の生成そのものを目的としたゲームやツールを作成したい場合
  • Dynamic Mesh については 座標から mesh を生成する を参照。

  • 乱数生成時の RandGen::uni_real() については メルセンヌ・ツイスタで乱数を生成する を参照。

参考にしたページ

Discussion