Runtime Mesh Generation
Generate and modify 3D meshes dynamically during gameplay.
Introduction
Games like Astroneer, Trail Makers, and Besiege let players build and deform meshes at runtime. This guide explains how professional engines construct vertex buffers, calculate normals, and upload meshes to the GPU in real time — then builds the same in Appley.
Advanced Guide 3 of 25
Screenshot
How Professional Games Do It
Understand mesh structure
A mesh is vertices (positions), normals (lighting direction), UVs (texture coordinates), and triangles (vertex index groups of 3). Professional engines build these arrays and upload them to GPU buffers.
Generate vertices
Create vertex positions based on your algorithm (terrain, voxel, procedural shape). Store positions, UVs, and normals in parallel arrays indexed by vertex number.
Calculate normals
For smooth shading, compute face normals (cross product of triangle edges), then average normals at shared vertices. For flat shading, each triangle gets its own normal.
Build triangle indices
Triangles reference vertices by index. For a grid mesh, triangles follow a predictable winding pattern. Ensure correct winding (counter-clockwise = front face).
Upload to GPU
Create a mesh from the vertex and index arrays. Upload to the GPU as a vertex buffer. For dynamic meshes, update the buffer in place rather than recreating it.
Optimize updates
For frequently changing meshes (terrain deformation), only update the changed region. Use sub-mesh chunks so a small edit does not require rebuilding the entire mesh.
FluxScript Implementation
Here is how to build this system in Appley using FluxScript (Appley Scripting):
// Runtime mesh generation: procedural tube along a spline
on start {
// Define a spline path
path = [vec3(0,0,0), vec3(5,2,3), vec3(10,1,8), vec3(15,3,12)]
segments = 64
radius = 1.0
vertices = []
normals = []
uvs = []
triangles = []
// Generate vertices along the spline
for i = 0 to segments {
t = i / segments
pos = spline.sample(path, t)
tangent = spline.tangent(path, t)
normal = spline.normal(path, t)
binormal = spline.binormal(path, t)
// Ring of vertices around the spline point
ringSegments = 16
for j = 0 to ringSegments {
angle = (j / ringSegments) * 360
dir = normal * math.cos(angle) + binormal * math.sin(angle)
vertex = pos + dir * radius
vertices.push(vertex)
normals.push(dir)
uvs.push(vec2(t * 8, j / ringSegments * 4))
}
}
// Build triangles connecting rings
ringSize = 16
for i = 0 to segments - 1 {
for j = 0 to ringSize - 1 {
v0 = i * ringSize + j
v1 = i * ringSize + (j + 1) % ringSize
v2 = (i + 1) * ringSize + j
v3 = (i + 1) * ringSize + (j + 1) % ringSize
// Two triangles per quad
triangles.push([v0, v2, v1])
triangles.push([v1, v2, v3])
}
}
// Create mesh from arrays
mesh = meshBuilder.create({
vertices: vertices,
normals: normals,
uvs: uvs,
triangles: triangles
})
// Upload to GPU and assign to object
tubeObject = scene.find("ProceduralTube")
tubeObject.setMesh(mesh)
tubeObject.setMaterial("metal_pipe")
tubeObject.setCollider("mesh")
print("Mesh: " + vertices.length + " verts, " + triangles.length + " tris")
}
// Dynamic mesh deformation on impact
on collision(other, point) {
if other.type == "projectile" {
// Deform mesh at impact point
mesh = self.getMesh()
verts = mesh.getVertices()
for i = 0 to verts.length {
dist = math.distance(verts[i], point)
if dist < 2.0 {
// Push vertex inward with falloff
deform = (1 - dist / 2.0) * 0.5
dir = math.normalize(verts[i] - point)
verts[i] -= dir * deform
}
}
mesh.setVertices(verts)
mesh.recalculateNormals()
mesh.upload() // Send updated data to GPU
}
}Node Graph Equivalent
The same system implemented as a Node Graph in Appley Node Scripting:
[On Start] --> [Define: spline path, segments, radius]
--> [For Each Segment along Spline]
--> [Sample: position, tangent, normal, binormal]
--> [For Each Ring Vertex]
--> [Calculate: position on ring circle]
--> [Push: vertex, normal, UV]
--> [Build Triangles: connect rings with quads (2 tris each)]
--> [Create Mesh: from vertex/normal/uv/triangle arrays]
--> [Upload to GPU] --> [Assign to object + collider]
[On Collision "projectile"]
--> [Get: mesh vertices]
--> [For Each Vertex near impact point]
--> [Branch: distance < 2.0]
+-- True --> [Deform: push inward with falloff]
--> [Recalculate Normals] --> [Upload to GPU]Professional Optimization Tips
How AAA studios optimize these systems — applicable to your Appley project:
- Use sub-mesh chunks for dynamic terrain. A 256x256 terrain mesh has 65k vertices. Updating the entire mesh for a small deformation is wasteful. Split into 16x16 chunks and only update the affected chunk.
- Recalculate normals efficiently. After deformation, recalculate normals for the affected region only, not the entire mesh. Use a spatial index to find nearby vertices quickly.
- Use 16-bit indices for meshes under 65k vertices. 32-bit indices double the index buffer size. Most runtime meshes are under 65k vertices — use 16-bit unless you explicitly need more.
- Batch mesh updates. If multiple deformations happen in one frame (explosion affecting 50 vertices), collect all changes and upload once. Per-vertex uploads are extremely expensive.
Common Mistakes
Recreating the mesh instead of updating
Use mesh.upload() to update vertex data in place. Recreating the entire mesh object causes GPU memory allocation and garbage collection spikes.
Wrong triangle winding order
Triangles must be counter-clockwise when viewed from the front. Wrong winding makes meshes invisible from one side (back-face culling).
