Vibe-coded fractal trees with threejs
My son Ivan is currently into No Man’s Sky, an exploration game with a huge procedurally-generated universe. We have also been watching the wonderful Sebastian Lague’s coding adventures. So Ivan is really into the idea of procedurally generating landscapes, animals and plants.
Today we have been doing some vibe coding with o3-mini-high. First we created a very simple fractal shape — a simple regular branching structure — and then gradually added more features and randomness to it. o3-mini-high did a great job at choosing libraries to work with, and I love that you can see its Chain of Thought process before it answers. However, it did take a bit of messing around at the start to get threejs to work locally — for example it needed to run on a local web server — and I expect Ivan wouldn’t have been able to set that up by himself even with the AI’s help. As Simon Willison often points out, chat-driven generative AI tools are still very difficult to use to program if you are not an experienced programmer.
After about a dozen iterations, this is the final little app we came up with:
Doing this kind of thing is a great educational exercise. All life is fractal, and actually generating fractal shapes like this is a great way to demonstrate that. I’ve given Ivan the challenge to see if he can create alien insects using the same methodology.
There are lots of possibilities with this:
- Create other types of fractal life, such as coral, jelly fish, mosses and lichens.
- Create virtual organisms with DNA which you can breed and evolve.
- Explore the process of embryo development, where you get a different type of fractal process in 3D.
- Use it to create your own virtual Cambrian Explosion.
I expect Ivan and I will be doing a lot more of this. It’s great fun. I gave ChatGPT some stars and smiley faces which it seemed to appreciate… 🙂🙂🙂🌟🌟⭐️
Here’s the final code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fractal Tree with Leaves on Terminal Branches</title>
<style>
margin: 0; overflow: hidden; }
body { -container { position: absolute; top: 0; right: 0; }
#gui</style>
<!-- Import map to resolve the module specifier "three" -->
<script type="importmap">
{"imports": {
"three": "./three/build/three.module.js"
}
}</script>
</head>
<body>
<div id="gui-container"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from './three/addons/controls/OrbitControls.js';
import GUI from './three/addons/libs/lil-gui.module.min.js';
let scene, camera, renderer, treeGroup, controls;
// Updated parameters with new defaults.
const params = {
branchLength: 7, // initial branch length
branchDiameter: 1, // initial branch diameter
numDivisions: 3, // maximum number of child branches per branch
scaleDecrease: 0.7, // factor by which branch dimensions decrease
branchAngle: 90, // azimuth spacing (in degrees) between child branches
divisionsRandomness: 0.5, // 0 = always max; 1 = fully random per branch node (between 1 and max)
lengthRandomness: 0.15, // 0 = no randomness; 1 = maximum randomness for branch length
noBranchChance: 0.1, // probability (0 to 1) that a branch produces no children
randomSeed: 123456789 // seed for the fast random generator
;
}
// Move the tree base further down the page.
const treeBaseOffset = -15;
// --- Fast pseudo-random generator using a simple LCG ---
let seed = params.randomSeed;
function fastRandom() {
= (1664525 * seed + 1013904223) % 4294967296;
seed return seed / 4294967296;
}// ---------------------------------------------------------
init();
function init() {
// Create scene and camera.
= new THREE.Scene();
scene .background = new THREE.Color(0xffffff);
scene= new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera .position.set(0, 20, 30);
camera.lookAt(new THREE.Vector3(0, 0, 0));
camera
// Set up renderer.
= new THREE.WebGLRenderer({ antialias: true });
renderer .setSize(window.innerWidth, window.innerHeight);
rendererdocument.body.appendChild(renderer.domElement);
// Setup OrbitControls for mouse rotation.
= new OrbitControls(camera, renderer.domElement);
controls
// Lighting.
.add(new THREE.AmbientLight(0xcccccc, 0.8));
sceneconst directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
.position.set(0, 50, 0);
directionalLight.add(directionalLight);
scene
// Group to hold all tree branches.
= new THREE.Group();
treeGroup .add(treeGroup);
scene
// Setup GUI controls using lil-gui.
const gui = new GUI({ container: document.getElementById('gui-container') });
.add(params, 'branchLength', 5, 20).name("Branch Length").onChange(generateTree);
gui.add(params, 'branchDiameter', 0.1, 5).name("Branch Diameter").onChange(generateTree);
gui.add(params, 'numDivisions', 1, 5, 1).name("Max Divisions").onChange(generateTree);
gui.add(params, 'scaleDecrease', 0.5, 0.9).name("Scale Decrease").onChange(generateTree);
gui.add(params, 'branchAngle', 5, 180).name("Branch Angle").onChange(generateTree);
gui.add(params, 'divisionsRandomness', 0, 1).name("Divisions Randomness").onChange(generateTree);
gui.add(params, 'lengthRandomness', 0, 1).name("Length Randomness").onChange(generateTree);
gui.add(params, 'noBranchChance', 0, 1).name("No Branch Chance").onChange(generateTree);
gui
// Add a controller for the seed.
const randomSeedController = gui.add(params, 'randomSeed').name("Random Seed").onChange(() => {
= params.randomSeed;
seed generateTree();
;
})// Add a button to randomize the seed.
.add({ randomizeSeed: function(){
gui.randomSeed = Math.floor(Math.random() * 4294967296);
params= params.randomSeed;
seed generateTree();
.updateDisplay();
randomSeedController, 'randomizeSeed').name("Randomize Seed");
}}
generateTree();
animate();
window.addEventListener('resize', onWindowResize, false);
}
// Adjust canvas on window resize.
function onWindowResize() {
.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
camera.setSize(window.innerWidth, window.innerHeight);
renderer
}
// Generates (or regenerates) the fractal tree.
function generateTree() {
// Reset the seed to the current seed parameter.
= params.randomSeed;
seed while (treeGroup.children.length > 0) {
.remove(treeGroup.children[0]);
treeGroup
}// Start from the base offset so the trunk begins further down.
const startPos = new THREE.Vector3(0, treeBaseOffset, 0);
const direction = new THREE.Vector3(0, 1, 0);
drawBranch(startPos, direction, params.branchLength, params.branchDiameter);
}
// Recursively draws branches and adds leaves to terminal branches.
function drawBranch(startPos, direction, length, diameter) {
// If branch length is very small, create the branch and add a leaf.
if (length < 0.5) {
const geometry = new THREE.CylinderGeometry(diameter * 0.5, diameter * 0.5, length, 8);
const material = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
const branchMesh = new THREE.Mesh(geometry, material);
.position.y = length / 2;
branchMeshconst branch = new THREE.Group();
.add(branchMesh);
branch.position.copy(startPos);
branch.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize());
branch.add(branch);
treeGroup
// Add a leaf at the top of this terminal branch.
const leafGeometry = new THREE.SphereGeometry(0.3, 8, 8);
const leafMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 });
const leafMesh = new THREE.Mesh(leafGeometry, leafMaterial);
.position.set(0, length, 0);
leafMesh.add(leafMesh);
branchreturn;
}
// Create the branch as a cylinder.
const geometry = new THREE.CylinderGeometry(diameter * 0.5, diameter * 0.5, length, 8);
const material = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
const branchMesh = new THREE.Mesh(geometry, material);
.position.y = length / 2;
branchMeshconst branch = new THREE.Group();
.add(branchMesh);
branch.position.copy(startPos);
branch.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize());
branch.add(branch);
treeGroup
// Compute the end position of this branch.
const endPos = startPos.clone().add(direction.clone().normalize().multiplyScalar(length));
// Update branch dimensions for children.
let newLength = length * params.scaleDecrease;
if (params.lengthRandomness > 0) {
const variation = 1 + (fastRandom() * 2 - 1) * params.lengthRandomness;
*= variation;
newLength
}const newDiameter = diameter * params.scaleDecrease;
// If the no-branch chance is triggered, add a leaf and do not generate children.
if (fastRandom() < params.noBranchChance) {
const leafGeometry = new THREE.SphereGeometry(0.3, 8, 8);
const leafMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 });
const leafMesh = new THREE.Mesh(leafGeometry, leafMaterial);
.position.set(0, length, 0);
leafMesh.add(leafMesh);
branchreturn;
}
// Compute effective number of child branches.
const randomDivs = Math.floor(fastRandom() * params.numDivisions) + 1;
const effectiveDivisions = Math.round(THREE.MathUtils.lerp(params.numDivisions, randomDivs, params.divisionsRandomness));
// Fixed tilt for children (30°).
const fixedTilt = THREE.MathUtils.degToRad(30);
const parentDir = direction.clone().normalize();
let arbitrary = new THREE.Vector3(0, 1, 0);
if (Math.abs(parentDir.dot(arbitrary)) > 0.99) {
= new THREE.Vector3(1, 0, 0);
arbitrary
}const right = new THREE.Vector3().crossVectors(parentDir, arbitrary).normalize();
const tiltQuat = new THREE.Quaternion().setFromAxisAngle(right, fixedTilt);
const baseDir = parentDir.clone().applyQuaternion(tiltQuat).normalize();
// Recursively create child branches.
for (let i = 0; i < effectiveDivisions; i++) {
const azimuth = (i - (effectiveDivisions - 1) / 2) * THREE.MathUtils.degToRad(params.branchAngle);
const azimuthQuat = new THREE.Quaternion().setFromAxisAngle(parentDir, azimuth);
const newDir = baseDir.clone().applyQuaternion(azimuthQuat).normalize();
drawBranch(endPos, newDir, newLength, newDiameter);
}
}
// Animation loop.
function animate() {
requestAnimationFrame(animate);
.update();
controls.render(scene, camera);
renderer
}</script>
</body>
</html>