在前面的教程中,我們在虛幻引擎中添加了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