Web Development

3D Tree Viz with JS: My 5-Step Three.js Guide for 2025

Ready to build stunning 3D tree visualizations? My 2025 guide breaks down creating procedural trees with Three.js and JavaScript in 5 simple steps.

L

Liam Carter

Creative technologist and JavaScript enthusiast specializing in WebGL and interactive data visualizations.

7 min read14 views

Ever looked at a complex data visualization and thought, "How on earth did they build that?" You're not alone. Bringing data to life in 3D can seem like a dark art, but with modern JavaScript libraries, it's more accessible than ever. Welcome to my updated 2025 guide on creating beautiful, procedural 3D trees with the magic of Three.js.

Step 1: Setting Up Your 3D World

Before we can grow a tree, we need to prepare the soil. In Three.js, this means setting up the foundational trio: the Scene, the Camera, and the Renderer.

  • Scene: Think of this as the universe where all your 3D objects will live. It's the top-level container.
  • Camera: This is your eye into the scene. We'll use a PerspectiveCamera, which mimics how the human eye sees things (objects farther away appear smaller).
  • Renderer: This is the engine that takes the scene and camera information and draws it onto a <canvas> element in your HTML. We'll use the WebGLRenderer for hardware-accelerated graphics.

Here’s how you can set up a basic HTML file and the initial JavaScript. We'll use modern ES modules for cleaner code.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Tree with Three.js</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
            "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
        }
    }
    </script>
    <script type="module" src="/main.js"></script>
</body>
</html>

main.js

import * as THREE from 'three';

// 1. Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xabcdef);

// 2. Camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 50;

// 3. Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Basic animation loop
function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}

// Handle window resizing
window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

animate();

With this, you have a blank 3D canvas, ready for creation!

Step 2: Modeling the Tree's Logic

A tree is a fractal structure—a trunk splits into branches, which split into smaller branches, and so on. The best way to model this in code is with a recursive function. Our function will define a single branch and then call itself to create the next set of smaller, angled branches.

The Recursive Branching Logic

Imagine a function called createBranch. Here's what it needs to know:

  • Start Point: A Vector3 defining where the branch begins.
  • Direction: A Vector3 defining the angle and orientation of the branch.
  • Length: How long the branch is.
  • Level: The current depth of recursion (e.g., trunk is level 0, main branches are level 1).
Advertisement

The process inside the function will be:

  1. Base Case: If the level is too deep (e.g., > 10), stop recursing. This prevents an infinite loop and creates the 'tips' of the tree.
  2. Draw Current Branch: Create the geometry for the current branch based on its parameters.
  3. Calculate New Parameters: Determine the start point, direction, and length for the *next* set of branches. The new start point is the end point of the current branch. The new directions will be variations of the current direction (e.g., tilted up and rotated).
  4. Recurse: Call createBranch for each new set of parameters.

This approach gives us a powerful, procedural way to define an infinitely complex tree with just a few rules.

Step 3: Generating the Tree's Geometry

Now, let's turn that logic into visible shapes. For branches, a CylinderGeometry is a perfect starting point. It takes parameters like radius, height, and segments.

We'll implement our recursive function. This function won't just calculate logic; it will create Mesh objects (the combination of a geometry and a material) and add them to the scene.

function createBranch(scene, startPoint, direction, length, radius, level, maxLevels) {
    if (level > maxLevels) return;

    // Create the branch geometry (a cylinder)
    const branchGeometry = new THREE.CylinderGeometry(radius * 0.8, radius, length, 8);
    // We'll add a material in the next step
    const branchMaterial = new THREE.MeshStandardMaterial({ color: 0x553311 });
    const branch = new THREE.Mesh(branchGeometry, branchMaterial);

    // Position and orient the branch
    branch.position.copy(startPoint);
    // Align the cylinder with the direction vector
    const a = new THREE.Vector3(0, 1, 0); // Default cylinder orientation
    branch.quaternion.setFromUnitVectors(a, direction.clone().normalize());
    // Move the branch to its correct position (cylinders are centered by default)
    branch.position.addScaledVector(direction, 0.5);

    scene.add(branch);

    // Calculate the end point of the current branch
    const endPoint = new THREE.Vector3().addVectors(startPoint, direction);

    // --- Recurse for new branches ---
    const newLength = length * 0.8; // Branches get shorter
    const newRadius = radius * 0.7; // Branches get thinner

    // Branch 1 (e.g., turn left)
    const newDirection1 = direction.clone()
        .applyAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 4) // Rotate around Z
        .applyAxisAngle(new THREE.Vector3(1, 0, 0), Math.random() * 0.2); // Add some randomness
    createBranch(scene, endPoint, newDirection1, newLength, newRadius, level + 1, maxLevels);

    // Branch 2 (e.g., turn right)
    const newDirection2 = direction.clone()
        .applyAxisAngle(new THREE.Vector3(0, 0, 1), -Math.PI / 4) // Rotate other way
        .applyAxisAngle(new THREE.Vector3(0, 1, 0), Math.random() * 0.2);
    createBranch(scene, endPoint, newDirection2, newLength, newRadius, level + 1, maxLevels);
}

// Initial call to start the tree
const startPoint = new THREE.Vector3(0, -25, 0);
const initialDirection = new THREE.Vector3(0, 1, 0);
createBranch(scene, startPoint, initialDirection.clone().setLength(10), 1.5, 0, 5);

While cylinders are great, there are other options depending on your needs for performance and realism.

Branch Geometry Options

Method Pros Cons Best For
CylinderGeometry Simple to use, good performance. Can look uniform and 'low-poly'. Joins between branches aren't seamless. Rapid prototyping, stylized trees, background elements.
ExtrudeGeometry Allows for custom cross-sections and tapering for a more organic look. More complex to set up; can be slower than cylinders. Hero trees where detail matters.
Custom BufferGeometry Maximum performance by defining vertices manually. Can merge all branches into one geometry. Very difficult to implement and debug. Requires deep WebGL knowledge. Forests with thousands of trees or highly optimized scenes.

Step 4: Adding Materials and Lighting

A shape without material is invisible. A scene without light is black. Let's fix that.

Choosing Your Materials

The MeshStandardMaterial is the workhorse for realistic-looking objects in Three.js. It's a physically-based rendering (PBR) material, meaning it interacts with light in a plausible way. You can control properties like color, roughness (how matte or shiny it is), and metalness.

In the code from Step 3, we already added a basic brown material. You could enhance this by varying the color slightly for each branch to add more visual depth.

Illuminating the Scene

For good results, you typically need at least two types of light:

  • AmbientLight: This light provides a base illumination to the entire scene, ensuring that even parts in shadow are not completely black.
  • DirectionalLight: This light simulates a distant light source like the sun. It casts parallel rays, creating distinct highlights and, importantly, shadows.

Let's add them to our setup:

// Add some light to the scene
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Soft white light
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); // Brighter, directional light
directionalLight.position.set(50, 100, 25);
scene.add(directionalLight);

// To get shadows, you need to enable them on the renderer and the light
renderer.shadowMap.enabled = true;
directionalLight.castShadow = true; 

// And objects need to be set to cast/receive shadows
// (You'd add this inside the createBranch function for each branch mesh)
// branch.castShadow = true;
// branch.receiveShadow = true;

Step 5: Making It Interactive

A 3D object you can't inspect is just a fancy image. Let's add camera controls so the user can explore our creation. The easiest way is with OrbitControls, an add-on that provides intuitive rotate, pan, and zoom functionality.

First, import it, then instantiate it with your camera and the renderer's DOM element.

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// ... after setting up camera and renderer

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.maxPolarAngle = Math.PI / 2;

// The animation loop needs to be updated to include the controls
function animate() {
    requestAnimationFrame(animate);

    // required if controls.enableDamping or controls.autoRotate are set to true
    controls.update();

    renderer.render(scene, camera);
}

animate(); // Call the updated loop

And that's it! With these controls, you can now freely move around your procedurally generated tree, admiring its structure from every angle.

Conclusion & Key Takeaways

We've gone from a blank canvas to a fully interactive, procedurally generated 3D tree in five steps. Let's recap the journey:

  1. Setup: We established the core Three.js scene, camera, and renderer.
  2. Logic: We designed a recursive algorithm to define the tree's fractal structure.
  3. Geometry: We translated that logic into 3D shapes using CylinderGeometry.
  4. Lighting: We made our tree visible and realistic with materials and lights.
  5. Interaction: We added OrbitControls to allow users to explore the model.

This is just the beginning. The real fun is in experimenting and building upon this foundation. Here are a few ideas to take your project to the next level:

  • Add Leaves: Use THREE.InstancedMesh for performance when rendering thousands of leaves.
  • Animate Growth: Modify the recursive function to build the tree over time.
  • Data-Driven Trees: Use an external dataset to control branching angles, lengths, or colors.
  • GUI Controls: Add a library like lil-gui to let users change parameters like branch length, angle, and recursion depth in real-time.

The world of 3D on the web is vast and exciting. I hope this guide has demystified the process and inspired you to start creating your own interactive visualizations. Happy coding!

You May Also Like