TL;DR: Stop downloading 3D model files. Generate geometry on-the-fly using math functions. Get infinite variety, zero file storage, and 2-10x performance gains. Production-ready patterns included.
The Problem: Heavy 3D Assets
Your web app needs 3D models. Your options:
Option A (Traditional): Download .gltf/.fbx files (10–500 MB each). Wait for download. Store on S3 (costs money). Models are static.
Result: Slow page loads, high bandwidth costs, limited variety.
Option B (Procedural): Generate geometry at runtime from TypeScript code. Instant generation. Zero files. Infinite variations.
Result: Fast loads, zero storage costs, dynamic models.
This is why elite developers (Tom Krcha at Warp, Chiro Visuals) generate models instead of downloading them.
What Is Procedural 3D Generation?
Procedural generation = math functions that output 3D geometry.
Instead of:
Model file (downloaded) → renderYou do:
JavaScript function → BufferGeometry → renderReal example:
// Download approach: Large file, download wait time
const model = await loadGLTF('train.gltf');
// Procedural approach: 0 KB file, instant generation
const trainGeometry = generateTrainGeometry({
wheelRadius: 5,
bodyLength: 20,
carriages: 3
});The train doesn't exist as a file. It's computed from parameters in real-time.
Why This Matters: Real Numbers
Scenario: You're building a SaaS product design tool. Users customize 3D objects (trains, buildings, furniture).
Old way (asset files):
- Store 100 model variations: 5,000 MB on S3
- Monthly S3 costs: ~$100+
- Download latency: 2–5 seconds per model
- Can't generate new combinations on-the-fly New way (procedural generation):
- Generate any combination: 0 MB storage
- Monthly S3 costs: ~$5 (just code)
- Generation latency: milliseconds (instant to user)
- Infinite variations from single code Performance metrics (verified, 2026):
On a 3D model visualization (like procedural train generation):
- Asset-based: Large file download + rendering wait time
- Procedural: 0 MB + instant runtime generation
- Savings: No asset storage, instant generation For 1,000 concurrent users:
- Asset approach: 45 GB bandwidth bill
- Procedural approach: negligible network cost
How It Works: The Pattern
Step 1: Define Parameters
const boxParams = {
width: 10,
height: 20,
depth: 5,
color: 0xff0000
};Step 2: Generate Geometry (Math Functions)
const geometry = new THREE.BoxGeometry(
boxParams.width,
boxParams.height,
boxParams.depth
);Step 3: Create Mesh & Render
const material = new THREE.MeshStandardMaterial({
color: boxParams.color
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);The key insight: The geometry is created by math functions, not downloaded files. Change parameters → different geometry, instantly.
Real-World Pattern: Procedural Wheels
Problem: A wheel is just a cylinder with specific parameters (radius, thickness, spoke count).
Why download a 5 MB wheel model when you can compute it?
function generateWheel(radius, thickness, spokes) {
// Create cylinder for rim
const rimGeometry = new THREE.CylinderGeometry(
radius, // outer radius
radius, // inner radius
thickness, // thickness
32 // segments
);
// Create spokes (repeated cylinders)
const spokeGeometry = new THREE.CylinderGeometry(0.5, 0.5, radius, 8);
// Create spokes array
const spokes = [];
for (let i = 0; i < spokes; i++) {
const angle = (i / spokes) * Math.PI * 2;
const spoke = new THREE.Mesh(spokeGeometry, material);
spoke.position.set(
Math.cos(angle) * radius / 2,
0,
Math.sin(angle) * radius / 2
);
spoke.rotation.z = angle;
spokes.push(spoke);
}
// Merge geometries
const merged = BufferGeometryUtils.mergeGeometries([
rimGeometry,
...spokes.map(s => s.geometry)
]);
return merged;
}
// Usage: Change parameters → different wheels
const smallWheel = generateWheel(5, 1, 6); // 6-spoke wheel
const largeWheel = generateWheel(15, 2, 8); // 8-spoke wheelFile size: 0 bytes for both wheels (just code)
Generation time: Instant (no file download needed)
Variations possible: Infinite (any radius, thickness, spoke count)
Advanced Pattern: Noise-Based Geometry
For organic shapes (terrain, plants, crystals), use Perlin noise or Simplex noise.
Concept: Noise generates random values that look natural (not random-looking).
import { Perlin } from 'three/examples/jsm/math/Perlin.js';
function generateTerrain(width, height, scale) {
const geometry = new THREE.BufferGeometry();
const perlin = new Perlin();
// Create height values using Perlin noise
const vertices = [];
for (let x = 0; x < width; x++) {
for (let z = 0; z < height; z++) {
const y = perlin.noise(x / scale, z / scale) * 10; // Scale to 0-10 height
vertices.push(x, y, z);
}
}
// Create mesh
geometry.setAttribute('position', new THREE.BufferAttribute(
new Float32Array(vertices),
3
));
// Add faces (triangles connecting vertices)
const indices = [];
for (let x = 0; x < width - 1; x++) {
for (let z = 0; z < height - 1; z++) {
const a = x * height + z;
const b = (x + 1) * height + z;
const c = x * height + (z + 1);
const d = (x + 1) * height + (z + 1);
indices.push(a, c, b);
indices.push(b, c, d);
}
}
geometry.setIndex(new THREE.BufferAttribute(
new Uint16Array(indices),
1
));
return geometry;
}
// Generate infinite terrain variations
const terrain1 = generateTerrain(100, 100, 10); // Smooth rolling hills
const terrain2 = generateTerrain(100, 100, 3); // Sharp rocky terrainReal performance: Perlin noise terrain generation is fast enough for real-time applications, enabling procedural terrain to regenerate on-the-fly without lag.
File size: 0 bytes (pure code)
Variations: Infinite (adjust scale, octaves, persistence)
Performance: Procedural vs Assets
WebGPU Advantage (2026 Update)
With WebGPU, procedural generation gets dramatically faster:
Particle system comparison:
- WebGL: 10,000 particles updates at 30ms per frame
- WebGPU with compute shaders: 100,000 particles in 2ms
- Improvement: 150x faster and 10x more particles Why? WebGPU compute shaders run directly on GPU, not CPU.
Instancing for Repeated Objects
If you need 1,000 identical wheels on a conveyor belt:
const wheelGeometry = generateWheel(5, 1, 6);
const material = new THREE.MeshStandardMaterial();
// Create 1,000 wheels with single draw call
const instancedMesh = new THREE.InstancedMesh(
wheelGeometry,
material,
1000
);
for (let i = 0; i < 1000; i++) {
const matrix = new THREE.Matrix4();
matrix.setPosition(i * 10, 0, 0); // Space them out
instancedMesh.setMatrixAt(i, matrix);
}
scene.add(instancedMesh);Performance:
- Naive approach (1,000 separate meshes): 1,000 draw calls, 60 FPS = impossible
- Instanced approach: 1 draw call, 1,000 meshes, 60 FPS = easy This is the technique Tom Krcha used for his train demo.
When to Use Procedural vs Assets
| Need | Use Procedural | Use Assets |
|---|---|---|
| Simple geometric shapes (boxes, wheels, cylinders) | ✅ Yes | ❌ No |
| Highly detailed faces or organic forms | ❌ No | ✅ Yes |
| User-customizable variations (adjust size, color, style) | ✅ Yes | ❌ No |
| Fixed, hand-crafted artwork | ❌ No | ✅ Yes |
| Performance-critical (1000+ objects) | ✅ Yes | ❌ No |
| Real-time generation (new on each frame) | ✅ Yes | ❌ No |
| Offline 3D art | ❌ No | ✅ Yes |
Hybrid approach (best): Use procedural for structure, assets for detail.
Example: Procedural building outline + asset textures.
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Generate geometry every frame | Huge CPU waste | Generate once, reuse mesh |
| Too many vertices | WebGL crashes | Optimize with LOD (level of detail) |
| No BufferGeometry | Terrible performance | Always use BufferGeometry, never Geometry |
| Forgetting to compute normals | Lighting looks wrong | Call geometry.computeVertexNormals() |
| Creating 1,000 separate meshes | Draw call hell | Use InstancedMesh or BatchedMesh |
Start This Week
import * as THREE from 'three';
// 1. Create basic box geometry
const geometry = new THREE.BoxGeometry(10, 20, 5);
// 2. Create material
const material = new THREE.MeshStandardMaterial({ color: 0x0088ff });
// 3. Create mesh
const mesh = new THREE.Mesh(geometry, material);
// 4. Add to scene
scene.add(mesh);
// 5. Customize: Just change parameters
const customBox = new THREE.BoxGeometry(5, 15, 10); // Different dimensionsNext step: Write a function that generates geometry based on user input.
function generateCustomBox(width, height, depth, color) {
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshStandardMaterial({ color });
return new THREE.Mesh(geometry, material);
}
// User clicks "make it bigger" → regenerate
const box = generateCustomBox(10, 20, 5, 0xff0000);Your Competitive Edge
Teams using asset files:
- Download large model files
- Limit variations to what they pre-modeled
- Can't customize in real-time
- High bandwidth bills Teams using procedural generation:
- Generate models instantly
- Infinite variations from code
- Full runtime customization
- Minimal bandwidth costs The difference in user experience is enormous.
Next Steps
- Learn Three.js: Follow the official docs (threejs.org)
- Start simple: Box, cylinder, plane geometries (built-in)
- Add parameters: Width, height, color inputs
- Scale up: Noise functions for organic shapes
- Optimize: Use InstancedMesh for thousands of objects
- WebGPU: Migrate to WebGPURenderer for 2-10x performance
Tools & Libraries
Built-in (no install):
- Three.js BoxGeometry, CylinderGeometry, etc.
- Perlin noise (three/examples/jsm/math/Perlin.js) Open-source:
- THREE.Terrain (procedural terrain engine)
- Babylon.js (similar to Three.js, great procedural support)
- TresJS (TypeScript-first Three.js wrapper) Learn:
- Three.js documentation (threejs.org)
- Codrops procedural geometry tutorial (August 2026)
- Chiro Visuals WebGPU experiments (GitHub)
Got stuck, or want this shipped end-to-end for you? bitroot.club builds custom products for founders. →