AppleyAppley Docs
Docs/Optimization

Runtime Mesh Generation

Generate and modify 3D meshes dynamically during gameplay.

17 min read Advanced Updated 2026-07-01 v2.0 ~60 min to complete

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

This is part of the Advanced Documentation collection. Category: Optimization. Estimated completion: 60 minutes. Difficulty: Advanced/Professional.

Screenshot

Runtime Mesh Generation
Runtime Mesh Generation

How Professional Games Do It

1

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.

2

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.

3

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.

4

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).

5

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.

6

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 — FluxScript
// 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:

Runtime Mesh Generation — Node Graph
[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).