featured image
ThreeJS

Procedural Geometry: Trees

   - 

I’m not a 3d artist. I don’t know how to use Blender or Maya. Instead I create 3D shapes with code. Making geometry from scratch is fairly hard though. You have to create a series of points in 3D without being able to visualize them first. A much easier solution is to start with some existing geometry and modify it. Fortunately ThreeJS comes with lots of built in geometry primitives that are easy to modify.

This article is part of my ongoing series of medium difficulty ThreeJS tutorials. I’ve long wanted something in between the intro “How to draw a cube” and “Let’s fill the screen with shader madness” levels. So here it is.

Let’s build a little pine tree from some primitives

Let’s build a little pine tree. This is what we want it to look like:

Pine Tree

Clearly it’s composed of a few cones and a cylinder. Here’s the code to stack them together:

const group = new THREE.Group()
const level1 = new THREE.Mesh(
    new THREE.ConeGeometry(1.5,2,8),
    new THREE.MeshLambertMaterial({color:0x00ff00})
)
level1.position.y = 4
group.add(level1)
const level2 = new THREE.Mesh(
    new THREE.ConeGeometry(2,2,8),
    new THREE.MeshLambertMaterial({color:0x00ff00})
)
level2.position.y = 3
group.add(level2)
const level3 = new THREE.Mesh(
    new THREE.ConeGeometry(3,2,8),
    new THREE.MeshLambertMaterial({color:0x00ff00})
)
level3.position.y = 2
group.add(level3)
const trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(0.5,0.5,2),
    new THREE.MeshLambertMaterial({color:0xbb6600})
)
trunk.position.y = 0
group.add(trunk)
return group

Optimizing Draw Calls

The code above works but we probably don’t want to add all of those pieces each time we want a tree. We could bundle them up in a Group() but that highlights a new problem. This will use up several draw calls, one for each object. That’s sort of a waste. Instead let’s combine them.

If you remember your first Three JS tutorial you may recall that meshes are made of material for the color & textures, and the geometry for the actual shape. Three JS geometry has a cool ability. You can merge multiple geometries together to create a new object, then add a single material. This has the advantage of using up only a single draw call. Let’s do it.

const geo = new THREE.Geometry()
const level1 = new THREE.ConeGeometry(1.5,2,8)
level1.translate(0,4,0)
geo.merge(level1)
const level2 = new THREE.ConeGeometry(2,2,8)
level2.translate(0,3,0)
geo.merge(level2)
const level3 = new THREE.ConeGeometry(3,2,8)
level3.translate(0,2,0)
geo.merge(level3)
const trunk = new THREE.CylinderGeometry(0.5,0.5,2)
trunk.translate(0,0,0)
geo.merge(trunk)
return group = new THREE.Mesh(
    geo,
    new THREE.MeshLambertMaterial({color:0x00ff00})
)
return group

Now we have only a single draw call (plus the one for the stats viewer).

Tree with merged geometry

Merging geometry introduces a new problem. We want the leaves to be green and the trunk to be brown. That’s two colors but we have only one material for our one geometry.

Multiple Colors

There are two ways to have multiple colors on an object. We could use a texture, which can obviously use as many colors as we want, but for procedural geometry that’s often a pain. We would have to load the object into a 3D modeler, un-wrap it, then draw colors into the texture. Annoying.

The alternative is vertex colors. A mesh is really a series of buffers. One buffer contains the vertex points that make up the shape. Another one contains the normals (like we used in this blog). The standard Three JS primitives also support another buffer: per-face colors. This lets us set any color we want on each face in the geometry. That’s what we need to use.

const geo = new THREE.Geometry()
const level1 = new THREE.ConeGeometry(1.5,2,8)
level1.faces.forEach(f => f.color.set(0x00ff00))
level1.translate(0,4,0)
geo.merge(level1)
const level2 = new THREE.ConeGeometry(2,2,8)
level2.faces.forEach(f => f.color.set(0x00ff00))
level2.translate(0,3,0)
geo.merge(level2)
const level3 = new THREE.ConeGeometry(3,2,8)
level3.faces.forEach(f => f.color.set(0x00ff00))
level3.translate(0,2,0)
geo.merge(level3)
const trunk = new THREE.CylinderGeometry(0.5,0.5,2)
trunk.faces.forEach(f => f.color.set(0xbb6600))
trunk.translate(0,0,0)
geo.merge(trunk)
return group = new THREE.Mesh(
    geo,
    new THREE.MeshLambertMaterial({
        vertexColors: THREE.VertexColors,
    })
)
return group

We also need to tell the material to use the colors in the vertexes instead of using the material color.

const mesh = new THREE.Mesh(tree, new THREE.MeshLambertMaterial({
      vertexColors: THREE.VertexColors,
}))

Live Demo

Tree with Vertex Colors

That’s all there is to merged geometry with vertex colors. Next time we’ll make some cute low-poly clouds.

Get Noticed

By the way, if you are working on a cool WebVR experience that you’d like to have showcased right inside Firefox Reality, let us know.