基於Perlin噪聲的隨機地形生成【UE4】

在前面的教程中,我們在虛幻引擎中添加了Perlin噪聲,以便輕鬆地在代碼/藍圖中複用。現在可以利用Perlin噪聲來生成網格了。

在這裏插入圖片描述

1、RuntimeMeshComponent

經過一番研究,我發現了這個RuntimeMeshComponent,它可以在運行時生成網格(mesh),封裝了一些底層的操作。

問題是,它僅適用於虛幻4.10-4.16,而我使用的是4.18。因此我決定爲較新版本的虛幻引擎分叉並升級項目。

這個組件允許我們從一組頂點、三角形、法線等數據來生成網格。

2、什麼是網格

用RuntimeMeshComponent生成網格需要以下信息:

  • 頂點:構成網格的所有單個點
  • 三角形:將頂點連接在一起以形成網格表面的三角形
  • 法線:每個頂點的法向量。它們垂直於由其頂點形成的三角形,用於照明目的
  • 切線:定義頂點紋理方向的 2D 矢量。
  • UV:每個頂點的紋理座標,介於 0 和 1 之間。
  • 頂點顏色:每個頂點的顏色

讓我們看一種非常簡單的網格 — 由兩個三角形組成的正方形: 在這裏插入圖片描述

頂點次序是從左到右,從下到上,所以第一個頂點是左下角,然後是右下角,然後是左上角和右上角。

三角形由逆時針排列的三個頂點組成,因此我們可以使兩個三角形組成這個方形網格:

  • 三角形 0 :0 -> 2 ->3->0
  • 三角形 1: 0 -> 3->1-> 0

3、在代碼中生成頂點和三角形

在代碼中,頂點和三角形被定義爲數組:

  • 頂點數組是向量數組。數組中的每個值都是一個 3D 矢量,表示頂點的位置
  • 三角形數組是整數數組。數組中的每個值都是頂點數組的索引,該索引對應於三角形的點

例如,在我們的例子中(使用僞代碼):

Array<Vector3> Vertices = (
	{0, 0, 0}, // Bottom left
	{1, 0, 0}, // Bottom right
	{0, 1, 0}, // Top left
	{1, 1, 0}  // Top right
)

基於這些頂點的Triangles數組如下所示:

Array<int> Triangles = (
	0, 2, 3,
	0, 3, 1
);

Triangles數組中的每個值都是Vertices數組中的一個索引。每組 3 個值形成一個三角形,所有三角形都是通過逆時針列出其頂點來定義的。

我們稍後將看到法線和其他參數,因爲它們與網格生成沒有直接關係。

4、用Perlin噪聲生成頂點

爲了生成我們的地形,需要大量的Perlin噪聲值來製作一個像樣的網格。

爲簡單起見,我們可以沿着柵格生成這些值。假設我們沿x和y方向每100個虛幻單位爲單位採樣一個Perlin噪聲值。可以在二維循環中生成這些值:

UPerlinNoiseComponent* Noise; // A reference to our noise component
Noise = Cast<UPerlinNoiseComponent>(GetOwner()->GetComponentByClass(UPerlinNoiseComponent::StaticClass()));

TArray<FVector> Vertices;
int NoiseResolution = 300;
int TotalSizeToGenerate = 12000;
int NoiseSamplesPerLine = TotalSizeToGenerate / NoiseResolution;

// The number of vertices we'll have is the number of points in our [x,y] grid.
Vertices.Init(FVector(0, 0, 0), NoiseSamplesPerLine * NoiseSamplesPerLine);

for (int y = 0; y < NoiseSamplesPerLine; y ++) {
	for (int x = 0; x < NoiseSamplesPerLine; x ++) {
		float NoiseResult = Noise->GetValue(x + 0.1, y + 0.1, 1.0); // We have to add 0.1 because the noise function doesn't work with integers
		int index = x + y * NoiseSamplesPerLine;
		Vertices[index] = FVector(x * NoiseResolution, y * NoiseResolution, NoiseResult);
	}
}

此循環執行以下幾項操作:

  • 根據兩個選項計算我們需要生成的點數,NoiseResolution是兩點之間的距離,TotalSizeToGenerate是希望網格的大小。
  • 使用我們需要的點數初始化頂點數組
  • 在 x 和 y 上循環以獲取噪聲值,並將它們添加到Vertices數組中

現在這很好,但是這存在一些問題:

  • 噪聲輸出值介於 -1 和 1 之間,這在我們的遊戲中並不真正可見
  • 我們無法控制噪聲樣本的距離

讓我們爲此引入一些設置,並稍微清理一下代碼:

TArray<FVector> Vertices;
int NoiseResolution = 300;
int TotalSizeToGenerate = 12000;
int NoiseSamplesPerLine = TotalSizeToGenerate / NoiseResolution;

float NoiseInputScale = 0.01; // Making this smaller will "stretch" the perlin noise terrain
float NoiseOutputScale = 2000; // Making this bigger will scale the terrain's height

void GenerateVertices() {
	Vertices.Init(FVector(0, 0, 0), NoiseSamplesPerLine * NoiseSamplesPerLine);
	for (int y = 0; y < NoiseSamplesPerLine; y ++) {
		for (int x = 0; x < NoiseSamplesPerLine; x ++) {
			float NoiseResult = GetNoiseValueForGridCoordinates(x, y);
			int index = GetIndexForGridCoordinates(x, y);
			FVector2D Position = GetPositionForGridCoordinates(x, y);
			Vertices[index] = FVector(Position.X, Position.Y, NoiseResult);
			UV[index] = FVector2D(x, y);
		}
	}
}

// Returns the scaled noise value for grid coordinates [x,y]
float GetNoiseValueForGridCoordinates(int x, int y) {
	return Noise->GetValue(
		(x * NoiseInputScale) + 0.1,
		(y * NoiseInputScale) + 0.1
	) * NoiseOutputScale;
}

int GetIndexForGridCoordinates(int x, int y) {
	return x + y * NoiseSamplesPerLine;
}

FVector2D GetPositionForGridCoordinates(int x, int y) {
	return FVector2D(
		x * NoiseResolution,
		y * NoiseResolution
	);
}

這與以前的代碼相同,但使用兩個新的 scale 參數,並且重構爲更清晰。

我們現在也分配UV只是爲了有一些基本的紋理座標,這將使我們的材質拼貼的紋理適用於每個四邊形。

現在的噪聲生成輸出值都在[-1000,1000]範圍內,這在虛幻引擎中應該更加明顯。我們還可以縮放給定的值作爲噪聲的輸入,這使我們能夠拉伸或縮放地形(如果比例非常低,我們將獲取非常接近的點,而如果比例很高,我們將獲取相距很遠且差異很大的點)。

5、生成三角形

現在,我們可以使用剛剛創建的頂點索引來生成三角形,進而生成四邊形,每個四邊形包含兩個三角形(如上一個繪圖所示)。

TArray<int> Triangles;

void GenerateTriangles() {
	int QuadSize = 6; // This is the number of triangle indexes making up a quad (square section of the grid)
	int NumberOfQuadsPerLine = NoiseSamplesPerLine - 1; // We have one less quad per line than the amount of vertices, since each vertex is the start of a quad except the last ones
	// In our triangles array, we need 6 values per quad
	int TrianglesArraySize = NumberOfQuadsPerLine * NumberOfQuadsPerLine * QuadSize;
	Triangles.Init(0, TrianglesArraySize);

	for (int y = 0; y < NumberOfQuadsPerLine; y++) {
		for (int x = 0; x < NumberOfQuadsPerLine; x++) {
			int QuadIndex = x + y * NumberOfQuadsPerLine;
			int TriangleIndex = QuadIndex * QuadSize;

			// Getting the indexes of the four vertices making up this quad
			int bottomLeftIndex = GetIndexForGridCoordinates(x, y);
			int topLeftIndex = GetIndexForCoordinates(x, y + 1);
			int topRightIndex = GetIndexForCoordinates(x + 1, y + 1);
			int bottomRightIndex = GetIndexForCoordinates(x + 1, y);

			// Assigning the 6 triangle points to the corresponding vertex indexes, by going counter-clockwise.
			Triangles[TriangleIndex] = bottomLeftIndex;
			Triangles[TriangleIndex + 1] = topLeftIndex;
			Triangles[TriangleIndex + 2] = topRightIndex;
			Triangles[TriangleIndex + 3] = bottomLeftIndex;
			Triangles[TriangleIndex + 4] = topRightIndex;
			Triangles[TriangleIndex + 5] = bottomRightIndex;
		}
	}
}

現在有了可用的三角形就可以使用了。要生成實際的網格,我們只需要調用RuntimeMeshComponent的CreateMeshSection函數。

要在你的項目中安裝RuntimeMeshComponent,請首先在Github上下載我的升級版本,然後按照這個教程進行安裝,並參考這個教程將其暴露給C++代碼:

// We need a reference to the runtime mesh
URuntimeMeshComponent* RuntimeMesh = Cast<URuntimeMeshComponent>(GetOwner()->GetComponentByClass(URuntimeMeshComponent::StaticClass()));
int VerticesArraySize = NoiseSamplesPerLine * NoiseSamplesPerLine;

// These other values will be seen in a later part, for now their default value will do
TArray<FVector> Normals;
TArray<FRuntimeMeshTangent> Tangents;
TArray<FVector2D> UV;
TArray<FColor> VertexColors;

Normals.Init(FVector(0, 0, 1), VerticesArraySize);
Tangents.Init(FRuntimeMeshTangent(0, -1, 0), VerticesArraySize);
UV.Init(FVector2D(0, 0), VerticesArraySize);
VertexColors.Init(FColor::White, VerticesArraySize);

void GenerateMesh() {
	RuntimeMesh->CreateMeshSection(0,
		Vertices,
		Triangles,
		Normals,
		UV,
		VertexColors,
		Tangents,
		true, EUpdateFrequency::Infrequent
	);
}

void GenerateMap() {
	GenerateTriangles();
	GenerateVertices();
	GenerateMesh();
}

GenerateMap();

將所有這些代碼放在一個 actor 組件中,就可以通過將該組件提供給也具有PerlinNoiseComponent 和RuntimeMeshComponent 的組件來生成 地形。

本教程的完整TerrainComponent代碼可以從Github下載。

例如,如果將GenerateMap函數公開給藍圖,則可以通過以下方式創建地形: 在這裏插入圖片描述

結果如下:

在這裏插入圖片描述


原文鏈接:UE4程序化生成地形 — BimAnt

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章