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.
![]()
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.
![]()
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
- Generate terrain mesh and collision first
- Sample surface normals at scatter points to align props to slope
- Use `MultiMeshInstance3D` for repeating props (rocks, grass tufts)
- Use individual `MeshInstance3D` for unique hero assets
- Bake lightmap UVs with `uv2_` channels if using baked lighting
- Target under 50k tris per terrain chunk on mid-range hardware
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.