
1. 項目概述為什么DynamicMesh是程序化地形的“快車道”如果你正在用UE5做開放世界、沙盒游戲或者任何需要大范圍、可變化地形的項目肯定對傳統的地形系統又愛又恨。Landscape系統功能強大但動輒幾個G的導入數據、漫長的構建時間以及修改后漫長的等待都讓快速迭代變成了奢望。尤其是在項目前期當你只是想快速驗證一個地形輪廓、一個關卡布局時這種等待尤為煎熬。這就是為什么我們需要尋找一條“快車道”。最近在幾個原型項目中我徹底轉向了使用UE5的DynamicMesh組件來搞程序化地形生成。結果就是標題里說的“5分鐘”一點不夸張從一片空白到擁有帶基礎細節(jié)、可實時編輯的地形真的只需要喝口咖啡的時間。這背后的核心就是DynamicMesh組件帶來的動態(tài)幾何體創(chuàng)建與修改能力它讓我們可以完全繞開Landscape那套重型流程直接在運行時用代碼“捏”出我們想要的地形。簡單來說傳統Landscape像是用黏土雕塑——你得先準備好一大塊黏土高度圖然后慢慢雕刻、烘焙過程不可逆且耗時。而DynamicMesh程序化地形更像是用樂高積木搭建——你可以用代碼定義規(guī)則瞬間生成一個基礎結構并且可以隨時拆掉某一塊換成別的形狀整個過程是即時、動態(tài)、可編程的。這對于需要動態(tài)地形如可破壞環(huán)境、隨機生成關卡Roguelike或者僅僅是快速原型驗證的場景價值巨大。2. 核心思路拆解從高度圖到DynamicMesh的魔法整個流程的核心邏輯鏈條非常清晰理解了這個你就掌握了八成。我們的目標是把一張灰度圖高度圖變成一個在UE5世界里可以行走、碰撞的3D網格Mesh。2.1 流程總覽與核心組件選擇整個工作流可以概括為以下幾步準備數據源一張代表地形的灰度圖高度圖。這是我們的“藍圖”。數據解析與網格構建在UE5中通常是在一個Actor的構造函數或BeginPlay時讀取這張高度圖的像素數據根據像素的亮度0-255計算出每個頂點的高度Z值。創(chuàng)建DynamicMesh使用UE5的UDynamicMesh組件或相關的FDynamicMesh3數據結構將計算出的頂點X, Y, Z和連接它們的三角形面信息填充進去。材質與碰撞為生成的DynamicMesh賦予一個地形材質并為其生成簡單的碰撞體如復雜碰撞或自動生成的凸包碰撞讓角色可以站在上面。實時編輯與優(yōu)化可選利用DynamicMesh的動態(tài)特性實現運行時修改地形高度、平滑、雕刻等功能。在這個流程中有幾個關鍵組件需要你做出選擇UDynamicMeshComponentvsUProceduralMeshComponent雖然UProceduralMeshComponent程序化網格體組件更早被熟知但UDynamicMeshComponent是UE5中更現代、功能更強大的替代品。它背后是FDynamicMesh3數據結構提供了更豐富的網格操作API如網格編輯、布爾運算、細分等并且與Geometry Script幾何體腳本插件集成得更好。因此我們首選UDynamicMeshComponent。Geometry Script插件這是UE5.0以后引入的一個強大工具集它封裝了大量復雜的網格操作算法。對于地形生成中的常見操作如從高度圖生成網格、網格細分、簡化等使用Geometry Script可以極大減少我們自己寫底層算法的工作量。強烈建議在項目設置中啟用此插件。2.2 為什么是高度圖其優(yōu)勢與局限高度圖是地形生成的經典數據源它用一張二維圖像的灰度值來記錄三維地形的高度信息。黑色0通常代表最低點如海平面白色255代表最高點如山峰。選擇高度圖的理由通用性強幾乎所有3D軟件和游戲引擎都支持高度圖導入資源獲取和制作方便。你可以用World Machine、Gaea、甚至Photoshop來生成。數據輕量一張1024x1024的灰度圖不過1MB左右遠比一個同等細節(jié)的靜態(tài)網格體輕量。易于程序化處理像素數組就是二維數組對應網格的頂點高度算法處理起來直觀簡單。無縫銜接傳統流程生成的地形如果需要最終轉為Landscape高度圖可以直接使用。需要注意的局限無法表現懸空結構高度圖只能描述一個“高度場”即每個X,Y點只有一個Z值。因此像洞穴、拱橋、懸崖下方的凹陷這類結構單純靠高度圖無法生成。這需要更復雜的體素Voxel或距離場SDF技術。分辨率與性能權衡網格的頂點數直接等于高度圖的分辨率如1024x1024就是一百多萬個頂點。頂點數越多細節(jié)越豐富但性能開銷也越大。必須根據項目需求謹慎選擇分辨率。3. 實戰(zhàn)步驟詳解5分鐘生成你的第一個程序化地形下面我將帶你一步步用C和藍圖混合的方式實現這個流程。我們假設你已經創(chuàng)建了一個新的C類AProceduralTerrainActor。3.1 環(huán)境準備與項目設置首先確保你的UE5項目設置正確。打開項目設置 - 插件搜索并啟用Geometry Scripting插件。在你的ProceduralTerrainActor.Build.cs文件中添加必要的模塊依賴。通常需要GeometryScriptingCore和DynamicMesh。PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, InputCore, GeometryScriptingCore, DynamicMesh });準備一張高度圖。你可以從網上下載或者用任何圖像編輯軟件畫一個簡單的。為了測試我常用一個中間白、四周黑的圓形圖來模擬一座山。將其導入UE5紋理設置中記得將sRGB選項關閉因為高度數據不是顏色信息并將壓縮設置改為TC_Masks或TC_Default以避免不必要的顏色壓縮。3.2 核心C代碼解析從像素到網格在AProceduralTerrainActor的頭文件中我們需要聲明一些關鍵組件和函數。// ProceduralTerrainActor.h #pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include DynamicMesh/DynamicMesh3.h // 引入DynamicMesh核心數據結構 #include ProceduralTerrainActor.generated.h class UDynamicMeshComponent; class UTexture2D; // 用于引用高度圖紋理 UCLASS() class YOURPROJECT_API AProceduralTerrainActor : public AActor { GENERATED_BODY() public: AProceduralTerrainActor(); protected: virtual void BeginPlay() override; // 聲明一個動態(tài)網格組件 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Terrain) UDynamicMeshComponent* DynamicMeshComp; // 在編輯器或藍圖中可分配的高度圖紋理 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Terrain Generation) UTexture2D* HeightMapTexture; // 地形尺寸參數單位厘米 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Terrain Generation, meta (ClampMin 100.0)) float TerrainWidth; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Terrain Generation, meta (ClampMin 100.0)) float TerrainLength; // 地形高度縮放像素值0-255會乘以這個系數得到最終高度 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Terrain Generation, meta (ClampMin 0.0)) float HeightScale; // 網格細分程度頂點數 細分1。值越大網格越精細性能開銷越大。 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Terrain Generation, meta (ClampMin 1, ClampMax 1024)) int32 MeshSubdivision; public: // 一個可以在藍圖中調用的生成地形的函數 UFUNCTION(BlueprintCallable, Category Terrain) void GenerateTerrain(); private: // 內部函數從紋理讀取數據并構建DynamicMesh bool BuildMeshFromHeightMap(UDynamicMesh* TargetMesh); };接下來是.cpp文件中的關鍵實現。BuildMeshFromHeightMap是核心。// ProceduralTerrainActor.cpp #include ProceduralTerrainActor.h #include Components/DynamicMeshComponent.h #include Engine/Texture2D.h #include DynamicMesh/MeshNormals.h // 用于計算法線 #include GeometryScript/MeshBasicEditFunctions.h // 使用Geometry Script簡化操作 #include GeometryScript/MeshNormalsFunctions.h AProceduralTerrainActor::AProceduralTerrainActor() { PrimaryActorTick.bCanEverTick false; // 創(chuàng)建并設置根組件 DynamicMeshComp CreateDefaultSubobjectUDynamicMeshComponent(TEXT(DynamicMeshComp)); RootComponent DynamicMeshComp; // 設置默認參數 TerrainWidth 2000.0f; // 20米寬 TerrainLength 2000.0f; // 20米長 HeightScale 500.0f; // 高度縮放5米 MeshSubdivision 128; // 129x129的頂點網格 } void AProceduralTerrainActor::BeginPlay() { Super::BeginPlay(); // 游戲開始時自動生成地形你也可以在編輯器中通過按鈕調用 if (HeightMapTexture) { GenerateTerrain(); } } void AProceduralTerrainActor::GenerateTerrain() { if (!HeightMapTexture) { UE_LOG(LogTemp, Warning, TEXT(No HeightMap Texture assigned!)); return; } UDynamicMesh* TargetMesh DynamicMeshComp-GetDynamicMesh(); if (TargetMesh BuildMeshFromHeightMap(TargetMesh)) { // 成功生成網格后需要通知組件更新渲染和碰撞 DynamicMeshComp-NotifyMeshUpdated(); // 為網格生成碰撞這里使用簡單碰撞查詢復雜項目可能需要自定義 DynamicMeshComp-SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); DynamicMeshComp-SetCollisionObjectType(ECC_WorldStatic); // 可以在這里調用Geometry Script的函數生成更復雜的碰撞如GenerateComplexCollisionMesh } } bool AProceduralTerrainActor::BuildMeshFromHeightMap(UDynamicMesh* TargetMesh) { // 1. 清空現有網格 TargetMesh-Reset(); // 2. 鎖定紋理數據以讀取像素 FTexture2DMipMap* MipMap HeightMapTexture-GetPlatformData()-Mips[0]; FByteBulkData* ImageData MipMap-BulkData; const uint8* RawData (const uint8*)ImageData-LockReadOnly(); const int32 TexWidth HeightMapTexture-GetSizeX(); const int32 TexHeight HeightMapTexture-GetSizeY(); // 3. 計算頂點和面的數量 const int32 NumVertsX MeshSubdivision 1; const int32 NumVertsY MeshSubdivision 1; TArrayFVector3f Vertices; TArrayFVector3f Normals; TArrayFVector2f UVs; TArrayint32 Triangles; Vertices.Reserve(NumVertsX * NumVertsY); UVs.Reserve(NumVertsX * NumVertsY); Triangles.Reserve((NumVertsX - 1) * (NumVertsY - 1) * 6); // 4. 生成頂點和UV for (int32 y 0; y NumVertsY; y) { float V (float)y / (NumVertsY - 1); // 0.0 到 1.0 int32 TexY FMath::RoundToInt(V * (TexHeight - 1)); for (int32 x 0; x NumVertsX; x) { float U (float)x / (NumVertsX - 1); // 0.0 到 1.0 int32 TexX FMath::RoundToInt(U * (TexWidth - 1)); // 讀取高度圖灰度值 (假設是單通道或取R通道) int32 PixelIndex (TexY * TexWidth TexX) * 4; // RGBA格式每像素4字節(jié) float GrayValue RawData[PixelIndex] / 255.0f; // 歸一化到0-1 // 計算頂點位置 float PosX (U - 0.5f) * TerrainWidth; float PosY (V - 0.5f) * TerrainLength; float PosZ GrayValue * HeightScale; Vertices.Add(FVector3f(PosX, PosY, PosZ)); UVs.Add(FVector2f(U, V)); // 基礎的UV用于貼圖采樣 } } // 5. 生成三角形索引兩個三角形組成一個網格面 for (int32 y 0; y NumVertsY - 1; y) { for (int32 x 0; x NumVertsX - 1; x) { int32 TopLeft y * NumVertsX x; int32 TopRight TopLeft 1; int32 BottomLeft (y 1) * NumVertsX x; int32 BottomRight BottomLeft 1; // 第一個三角形 (TopLeft - BottomLeft - TopRight) Triangles.Add(TopLeft); Triangles.Add(BottomLeft); Triangles.Add(TopRight); // 第二個三角形 (TopRight - BottomLeft - BottomRight) Triangles.Add(TopRight); Triangles.Add(BottomLeft); Triangles.Add(BottomRight); } } // 6. 解鎖紋理數據 ImageData-Unlock(); // 7. 將數據寫入DynamicMesh // 使用Geometry Script提供的函數可以更簡潔這里演示基礎方法 TargetMesh-EditMesh([Vertices, Triangles](FDynamicMesh3 EditMesh) { // 添加頂點 TArrayint32 VertIDs; VertIDs.SetNumUninitialized(Vertices.Num()); for (int32 i 0; i Vertices.Num(); i) { VertIDs[i] EditMesh.AppendVertex((FVector)Vertices[i]); } // 添加三角形面 for (int32 i 0; i Triangles.Num(); i 3) { FIndex3i TriVertIDs(VertIDs[Triangles[i]], VertIDs[Triangles[i1]], VertIDs[Triangles[i2]]); EditMesh.AppendTriangle(TriVertIDs); } }, EDynamicMeshChangeType::GeneralEdit, EDynamicMeshAttributeChangeFlags::Unknown); // 8. 計算頂點法線用于光照 UE::Geometry::FMeshNormals::QuickComputeVertexNormals(*TargetMesh-GetMeshRef()); return true; }3.3 藍圖封裝與參數實時調整為了讓美術和策劃同學也能方便使用我們將C函數暴露給藍圖并制作一個簡單的編輯器工具。在GenerateTerrain函數上我們已經添加了UFUNCTION(BlueprintCallable)這樣在藍圖中就能直接調用。在AProceduralTerrainActor的藍圖中你可以將HeightMapTexture、TerrainWidth等參數設為可在細節(jié)面板中編輯。添加一個按鈕或通過Construction Script需謹慎可能影響編輯器性能在編輯器中實時預覽地形變化。創(chuàng)建一個簡單的材質使用世界位置或基于UV來混合不同的紋理如草地、巖石、雪地并將其賦給DynamicMeshComp。實時調整技巧你可以將GenerateTerrain函數綁定到參數屬性的OnPropertyChanged事件上。這樣在編輯器中滑動HeightScale或MeshSubdivision的滑塊時地形會實時重新生成并更新顯示。這是傳統Landscape系統無法比擬的迭代速度。注意在編輯器中進行實時更新尤其是在Construction Script中時如果網格分辨率很高或更新非常頻繁可能會導致編輯器卡頓。建議為實時更新添加一個閾值或防抖機制比如在參數停止變化0.5秒后再觸發(fā)生成。4. 高度圖配置的進階技巧與避坑指南一張好的高度圖是生成漂亮地形的關鍵。這里分享一些從實踐中總結的配置技巧和常見問題。4.1 高度圖制作與預處理要點來源選擇專業(yè)軟件生成World Machine、Gaea是行業(yè)標準能生成極其自然且?guī)Х謱硬馁|信息的地形。圖像軟件繪制Photoshop、Krita等用筆刷和濾鏡手動繪制適合風格化或特定形狀的地形。程序化噪聲在UE5內部使用Perlin噪聲、Simplex噪聲等算法實時生成。這提供了無限的隨機性和動態(tài)可能。你可以將噪聲生成的灰度圖作為UTextureRenderTarget2D再傳遞給我們的生成函數。關鍵預處理步驟尺寸與比例高度圖的長寬比最好與你在UE中設定的TerrainWidth和TerrainLength比例一致否則地形會被拉伸。模糊處理來自軟件導出的高度圖或噪聲圖邊緣可能過于尖銳“像素感”。在導入UE前用圖像軟件做一個輕微的高斯模糊1-2像素可以讓生成的網格斜坡更平滑避免出現“階梯”狀瑕疵。歸一化確保你的高度圖使用了完整的0-255灰度范圍。如果圖像整體偏灰例如值集中在100-150生成的地形起伏會非常平緩。在Photoshop中使用“色階”或“曲線”工具將黑場和白場拉到直方圖兩端可以顯著增強地形對比度。4.2 UE5紋理導入設置“黃金法則”這是最容易出錯的一步錯誤的設置會導致高度數據被錯誤解讀生成的地形面目全非。設置項推薦值原因解析sRGB關閉 (False)sRGB是一種顏色校正曲線用于顯示顏色貼圖。高度數據是純粹的數值信息不是顏色。開啟sRGB會導致引擎對數據進行非線性轉換嚴重扭曲高度值。壓縮設置Masks (TC_Masks)或Default (TC_Default)TC_Masks是無損壓縮格式完美保留所有灰度信息是首選。TC_Default在某些情況下也可用。絕對避免使用DXT1/BC1等有損壓縮格式它們會嚴重破壞數據的連續(xù)性。Mip Gen SettingsNoMipmaps地形的細節(jié)在遠距離觀看時如果使用低級別的Mipmap高度信息會被平均化導致遠處地形輪廓變形。對于用于程序化讀取的高度圖通常不需要Mipmap。紋理組ColorMap或Heightmap如果項目中有自定義的Heightmap紋理組選擇它。否則選擇ColorMap即可。這主要影響內存和流送預算的分類。一個快速檢查方法在UE5內容瀏覽器中將高度圖紋理的預覽類型切換到“灰度”你應該看到一個清晰的黑白漸變圖沒有奇怪的色帶或斑塊。如果有說明壓縮或sRGB設置很可能錯了。4.3 性能優(yōu)化與LOD考量用DynamicMesh生成地形雖然快但一個高細分度的網格如1024x1024頂點數超過百萬對渲染和碰撞都是壓力。對于大型地形不能簡單用一個高模網格覆蓋全場。分塊Chunking這是最有效的優(yōu)化手段。不要用一張巨大的高度圖生成一個巨大的網格。而是將地形劃分為多個小塊例如32x32個網格單元為一塊每個塊是一個獨立的DynamicMeshComponent。只生成和渲染玩家周圍的塊遠處的塊可以卸載或使用更低精度的表示。這需要更復雜的管理邏輯但它是開放世界地形的標準做法。動態(tài)LOD為每個地形塊實現多級細節(jié)LOD。當一塊地形距離攝像機很遠時使用簡化后的高度圖如降采樣到128x128來生成低模網格。UDynamicMeshComponent本身不自動處理LOD但你可以通過替換不同精度的Mesh來實現。Geometry Script中的SimplifyMesh函數可以用于網格簡化。碰撞優(yōu)化DynamicMeshComp-SetCollisionEnabled()生成的可能是復雜的三角面碰撞開銷大。對于游戲玩法通常不需要如此精確的碰撞??梢钥紤]為地形生成一個簡化的碰撞體網格。使用UCapsuleComponent或UBoxComponent陣列來近似復雜地形的行走表面適用于平臺跳躍游戲。使用UE5的Chaos物理系統提供的簡單形狀碰撞。5. 常見問題排查與實戰(zhàn)心得在實際操作中你肯定會遇到各種奇怪的問題。這里記錄了幾個最典型的案例和解決方法。5.1 地形生成結果異常排查表現象可能原因解決方案地形一片平坦沒有高度變化1. 高度圖紋理sRGB未關閉。2. 紋理壓縮格式有損數據被破壞。3.HeightScale參數設置為0或過小。4. 代碼中讀取像素的通道錯誤如高度圖是單通道但代碼按RGBA讀取了Alpha通道。1. 檢查并關閉紋理的sRGB。2. 將壓縮格式改為TC_Masks。3. 增大HeightScale值。4. 確認高度圖通道并調整像素索引計算PixelIndex。地形出現“階梯”狀或塊狀瑕疵1. 高度圖分辨率過低而MeshSubdivision過高導致采樣不足。2. 高度圖本身有銳利的邊緣或未做抗鋸齒處理。1. 使用更高分辨率的高度圖或降低MeshSubdivision。2. 對高度圖進行輕微的模糊預處理。地形UV拉伸或紋理顯示不正確1. 生成UV時計算錯誤U/V坐標計算。2. 地形材質中紋理平鋪Tiling設置不當。1. 檢查代碼中UVs.Add(FVector2f(U, V))部分確保U,V計算正確。2. 在材質中將UV坐標乘以一個平鋪系數如TexCoord * 50。編輯器運行時卡頓或崩潰1. 在Construction Script或Tick中頻繁調用GenerateTerrain()。2.MeshSubdivision設置過高如512生成網格過大。3. 紋理鎖定時未正確解鎖導致內存泄漏。1. 避免在Construction Script中生成復雜網格改用按鈕觸發(fā)或事件驅動。2. 在編輯器中預覽時使用較低的細分值打包時再提高。3. 確保ImageData-Unlock()在所有路徑下都被執(zhí)行使用FINALLY塊或ScopeLock模式。角色無法站立或穿過地形1. 未為DynamicMeshComponent啟用或生成碰撞。2. 生成的碰撞體過于復雜被引擎簡化或忽略。1. 調用SetCollisionEnabled并設置正確的碰撞類型和響應。2. 考慮使用GenerateComplexCollisionMeshGeometry Script或生成一個簡化的碰撞代理網格。5.2 從原型到生產的進階思考當你用5分鐘搞定原型后若想將其用于實際項目還需要考慮更多材質與紋理混合真實的地形需要根據高度、坡度、朝向法線來混合多種材質如草地、泥土、巖石、雪。這需要在材質圖表中編寫復雜的邏輯通常使用World Position、Pixel Normal和高度圖采樣結果作為混合蒙版。與植被系統Foliage集成UE5的植被系統通常與Landscape綁定。要讓植被生長在DynamicMesh地形上你需要手動計算每個實例的生成位置和法線對齊或者編寫接口將你的地形數據“模擬”成Landscape數據供植被系統使用。序列化與保存DynamicMesh是運行時動態(tài)生成的其數據默認不會保存到關卡中。如果你需要保存編輯后的地形狀態(tài)需要將FDynamicMesh3的數據序列化如轉換為頂點/索引數組并保存到USaveGame或自定義的資產中在加載時再還原。與Nanite的結合UE5的Nanite虛擬幾何體技術主要針對靜態(tài)網格。目前UDynamicMeshComponent不完全支持Nanite。如果你的地形在生成后是靜態(tài)的可以考慮將其轉換為UStaticMesh然后啟用Nanite以獲得極致的渲染性能。但這會失去動態(tài)編輯的能力。我個人最深刻的體會是DynamicMesh地形生成的核心優(yōu)勢在于“速度”和“靈活性”。它不適合替代所有情況下的Landscape但在需要快速驗證、動態(tài)修改或風格化表達的場合它是無可替代的利器。不要試圖用它一次性生成一個16km x 16km的超大世界而是思考如何將它作為你地形工具鏈中的一把“瑞士軍刀”在正確的環(huán)節(jié)發(fā)揮其快、準、靈的特點。例如用它來快速生成關卡中的動態(tài)障礙物、可變形的地面或是程序化地下洞穴的頂部結構這些才是它大放異彩的地方。