Raphaël Améaume

Freelance Creative Technologist.

Based in Paris, France.

Until Dawn

A procedural forest for Christmas Experiments

  • Three.js
  • WebGL
  • Procedural

See it live

In 2018, I was invited for the third time by David Rosnais and Florian Zumbrunn to take part in Christmas Experiments , a digital advent calendar revealing a WebGL experience made by different developers once a day. Along these developers was my friend Nicolas Daniel and we decided to partner for this one after having made individual contributions in 2016 and 2017.

We decided to create an entirely procedural forest with THREE.js, meaning we wouldn’t load external models or textures but instead create them on the fly, allowing for a bit of change between each refresh.

Procedural geometries and textures

Trees were generated with the Space Colonization algorithm for the branches. We used instanced meshes based on the slightly distorted circle geometry to create the leaves and trunks are TubeGeometry enlarged at the base in a vertex shader. The bark texture is a simple mix() in the fragment shader between two colors computed from a simplex noise. We computed their positions with the Poisson Disk algorithm to make sure they would not overlap.

In order to render more of them without losing performances, we took inspiration from the imposters technique in game development. We created a texture based on those procedural trees and render sprites (textured meshes always facing the camera) in the distance to create the illusion of a bigger forest.

Rocks were made with a quickhull implementation. We generated a single ConvexHull geometry and then used instanced meshes with random positions and rotations to create what looks like different rocks.

Thanks to these techniques and a bit of randomness in these procedural generations, we were able to create a different forest on each reload.

The pond surface is a CircleBufferGeometry with a lot of vertices so we would be able to slightly offset the edges with a noise. This was done once on the CPU side at the geometry generation step and not in the vertex shader to avoid recomputation on every frame and save some performances.

Reflection, godrays and performances

We wanted the pond to reflect the environment but the scene was already getting pretty heavy and doing a second render on every frame was breaking the framerate. We solved this by hiding what would never be reflected in the pond anyways and disabling shadowmap generation before rendering to a RenderTarget.

// hide elements not visible in the pond reflection
trees.leaves.visible = false;
trees.imposters.visible = false
sky.visible = false;

// disable shadow computations
renderer.shadowMap.autoUpdate = false;

// render scene to a render target for pond reflections
renderer.render(scene, camera);

// re-enable shadow computations
renderer.shadowMap.autoUpdate = true;

trees.leaves.visible = true;
trees.imposters.visible = true
sky.visible = true;

// render scene on screen
renderer.render(scene, camera);

We really wanted to have some kind of godrays in our scene, as we would know it would get us closer to our inspirations moodboard. They can be pretty heavy and complex to compute so we quickly decided to fake them to save some performance budget.

The final “godrays” are a single fragment shader on top of the scene, which fakes a light position based on the rotation of the camera.


Once we had every piece of our scene, we started to play with different parameters to create 4 atmospheres that would reflect different times of the day.


Interactions and sound design

We wanted the experiment to be a contemplative moment for the viewer with tiny bits of interactions, so we added with water lilies reactions to the mouse cursor, the ability to click in the pond to generate waves and a highlight effect on the grass if close to the cursor.

We added some royalty-free sounds for the pond and the ambient track. We first wanted to create a procedural theme music as well but time was missing and we asked our friend Quentin Hocdé to create one for us.


We added a custom pass of processing to handle the antialiasing, a vignette effect and a subtle noise layer on top of everything.


  • Nicolas Daniel
  • Quentin Hocdé