← Back to Blog 3d-modeling

Procedural Mesh Generation at Runtime in Godot 4: Build Dynamic 3D Assets with ArrayMesh

By BitSoul Team5/28/2026Updated 8/1/20265 min read111 views
Procedural Mesh Generation at Runtime in Godot 4: Build Dynamic 3D Assets with ArrayMesh

Static imported assets cover 90% of what a game needs — but that last 10% often breaks pipelines. Terrain that deforms under player footsteps, modular dungeons assembled at runtime, water surfaces that ripple with real geometry: none of these work with a pre-authored GLB. Godot 4's `ArrayMesh` and `SurfaceTool` APIs give you everything you need to build, UV-map, and collide dynamic 3D geometry entirely in GDScript or C# — no external DCC tool required.

Understanding ArrayMesh and SurfaceTool

Godot 4 exposes two complementary APIs for procedural geometry. `ArrayMesh` is the low-level approach: you build a dictionary of typed arrays — positions, normals, UVs, indices — and call `add_surface_from_arrays()` to commit them as a renderable surface. `SurfaceTool` wraps that process in a builder pattern, letting you call `add_vertex()`, `add_normal()`, and `add_uv()` per-vertex before calling `commit()` to get a finished `ArrayMesh`.

Choose `SurfaceTool` when you are generating geometry procedurally and want clean code; choose raw `ArrayMesh` when you are manipulating large vertex buffers for performance — for example, streaming terrain chunks or deforming a cloth simulation.

Both paths end at the same place: a `MeshInstance3D` with your mesh assigned, ready to receive any `StandardMaterial3D` or custom shader.

```gdscript
func make_quad() -> ArrayMesh:
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
st.set_uv(Vector2(0, 0))
st.add_vertex(Vector3(-0.5, 0, -0.5))
st.set_uv(Vector2(1, 0))
st.add_vertex(Vector3(0.5, 0, -0.5))
st.set_uv(Vector2(0, 1))
st.add_vertex(Vector3(-0.5, 0, 0.5))
st.set_uv(Vector2(1, 0))
st.add_vertex(Vector3(0.5, 0, -0.5))
st.set_uv(Vector2(1, 1))
st.add_vertex(Vector3(0.5, 0, 0.5))
st.set_uv(Vector2(0, 1))
st.add_vertex(Vector3(-0.5, 0, 0.5))
st.generate_normals()
return st.commit()
```

`generate_normals()` computes smooth face normals from your triangle winding — call it before `commit()` or your mesh will be pitch black in a lit scene.

Understanding ArrayMesh and SurfaceTool — illustrated

Building a Procedural Terrain Patch

A height-mapped terrain patch is the canonical ArrayMesh use case. The algorithm is straightforward: lay out a grid of vertices, sample a noise function for each Y position, then stitch triangles across the grid.

```gdscript
func generate_terrain(size: int, resolution: int, noise: FastNoiseLite) -> ArrayMesh:
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var step := float(size) / resolution

for z in range(resolution):
for x in range(resolution):
var positions := [
Vector3(x * step, noise.get_noise_2d(x, z) * 4.0, z * step),
Vector3((x+1) * step, noise.get_noise_2d(x+1, z) * 4.0, z * step),
Vector3(x * step, noise.get_noise_2d(x, z+1) * 4.0, (z+1) * step),
Vector3((x+1) * step, noise.get_noise_2d(x+1, z+1) * 4.0, (z+1) * step),
]
var uvs := [
Vector2(float(x)/resolution, float(z)/resolution),
Vector2(float(x+1)/resolution, float(z)/resolution),
Vector2(float(x)/resolution, float(z+1)/resolution),
Vector2(float(x+1)/resolution, float(z+1)/resolution),
]
for i in [0, 1, 2]:
st.set_uv(uvs[i]); st.add_vertex(positions[i])
for i in [1, 3, 2]:
st.set_uv(uvs[i]); st.add_vertex(positions[i])

st.generate_normals()
return st.commit()
```

Plug in any `FastNoiseLite` instance configured with `TYPE_SIMPLEX_SMOOTH` and a seed, and you get a uniquely shaped patch every time. For a 32x32 resolution, this runs in well under 1 ms on the main thread; for anything above 64x64, call it from a `Thread` and swap the mesh on completion to avoid frame hitches.

Key performance rule: never regenerate the `ArrayMesh` every frame. Rebuild only when underlying data changes — on chunk load, on player-triggered deformation, or on game-state transitions.

UV Mapping and Normals for Procedural Meshes

UVs on procedural geometry are yours to define, which is both the power and the trap. A common mistake is mapping UV coordinates to world-space X/Z directly — this works for planar surfaces but breaks texture scaling the moment mesh scale changes. The correct approach is to normalise UVs to [0, 1] over the patch, then scale the texture tiling inside the material using `uv1_scale` on `StandardMaterial3D`.

```gdscript
var mat := StandardMaterial3D.new()
mat.albedo_texture = preload("res://textures/ground_albedo.png")
mat.uv1_scale = Vector3(4.0, 4.0, 1.0)
mat.normal_enabled = true
mat.normal_texture = preload("res://textures/ground_normal.png")
mesh_instance.material_override = mat
```

For normals, `SurfaceTool.generate_normals()` produces flat normals per triangle by default. For terrain, call `generate_tangents()` after `generate_normals()` if you are using a normal map — tangent data is required for correct normal map orientation in Godot 4's PBR pipeline.

| Situation | Call order |
|---|---|
| Flat-shaded geometry | `generate_normals()` then `commit()` |
| Normal-mapped surface | `generate_normals()` then `generate_tangents()` then `commit()` |
| Hand-authored normals | Set manually per vertex, skip `generate_normals()` |
| Smoothed mesh | Call `index()` before `generate_normals()` to merge duplicate verts |

Calling `index()` before generating normals is important for smooth shading: it merges coincident vertices, so shared edges receive averaged normals rather than hard edges between every triangle.

UV Mapping and Normals for Procedural Meshes — illustrated

Collision, LODs, and Performance Tips

A visible mesh with no collision is a ghost. For procedural terrain, create a `StaticBody3D` with a `ConcavePolygonShape3D` derived directly from your mesh:

```gdscript
func add_collision(mesh_instance: MeshInstance3D) -> void:
var static_body := StaticBody3D.new()
var collision := CollisionShape3D.new()
collision.shape = mesh_instance.mesh.create_trimesh_shape()
static_body.add_child(collision)
mesh_instance.add_child(static_body)
```

`create_trimesh_shape()` is exact but expensive for large meshes. For gameplay surfaces, prefer generating a simplified collision mesh at half the terrain resolution and using that for physics while the full-res mesh handles visuals.

Godot 4 does not yet have a first-class LOD API for custom `ArrayMesh` the way Unreal's Nanite does, but you can implement manual LODs: generate the same patch at resolution 64, 32, and 16; swap `MeshInstance3D.mesh` based on camera distance in `_process()`; and use `VisibilityNotifier3D` to pause updates on off-screen chunks entirely.

For chunk-based open worlds, maintain a pool of pre-allocated `MeshInstance3D` nodes and reuse them rather than instantiating new ones per chunk — Godot's renderer handles mesh swaps cheaply but node creation has overhead.

Using Marketplace Assets as Procedural Bases

Procedural geometry shines brightest when combined with high-quality authored assets. A common hybrid workflow: generate terrain geometry at runtime, but decorate it with game-ready rocks, vegetation, and props sourced from the BitSoul marketplace. GLB assets drop directly into Godot 4's import pipeline with PBR materials intact — place them as `MeshInstance3D` children of your procedural terrain node and they inherit the transform automatically.

For foliage and scattered props, Godot 4's `MultiMesh` + `MultiMeshInstance3D` pair with procedural generation cleanly: calculate positions on your terrain mesh using raycasts or normal sampling, pack them into a `MultiMesh`, and render thousands of instances in a single draw call. Browse the BitSoul marketplace for optimised low-poly vegetation packs built specifically for this workflow.

Quick procedural placement checklist

Procedural mesh generation feels complex until you understand the three calls that matter — `begin()`, `add_vertex()`, `commit()` — and then it becomes the fastest way to prototype any kind of dynamic geometry in your game. Pair it with strong authored assets from https://bitsoulhosting.com/marketplace and you have a pipeline that scales from a weekend prototype to a shipped title.

Tags: Godot 4 procedural generation ArrayMesh SurfaceTool runtime mesh game development 3D assets

Skip the modelling — download it instead

A free BitSoul account gets you 2 game-ready models every month plus 25 AI Engine credits to generate one of your own, no card required. Clean topology, PBR textures, and GLB downloads that drop straight into Unreal, Unity, Godot or Blender — plus OBJ and 3D-printable STL export.

Create a free account → Browse 846 models