Image for post
Image for post
This effect is used to make the fish move organically and efficiently in the koi garden demo.

Curve Modifiers in Three.js

My first major PR to Three.js

Ada Rose Cannon
Oct 29 · 5 min read

I recently made a relaxing koi garden demo, where koi fish swim around a VR environment. The two most notable parts of the scene are the 3D positioned audio which I wrote about previously and the 100s of fish which appear to organically swim around the trees.

The 3D models of the fish bend as they turn tight corners. This effect is great for organic models that travel along fixed paths such as birds or fish.

For my fish scene I had found a really cool demo which could be adapted for my needs. The author was willing to make a THREE.js example, but hadn’t got around to it. When I adapted it for myself I made it a general purpose module which can be brought in to abstract away lots of the complexities. I then submitted this to THREE.js as an example to be used by anyone.

This technique works by encoding the curves into a texture. A special shader then distorts the vertices as the model moves around the curve.

The technique relies on floating point values in the texture. In WebGL 1 this is an optional feature for implementors, so if you use the first WebGL it may not work on many devices, but certain android phones don’t have this feature available. You can get around this by using WebGL2. WebGL2 has pretty good browser support now.

You can use WebGL2 in THREEjs by explicitly getting the WebGL2 context and passing it to the renderer:

const canvas = document.querySelector('canvas');
const context = canvas.getContext( 'webgl2', { antialias: true } );
const renderer = new WebGLRenderer({ canvas, context });

If you want to optimize support you could detect whether WebGL2 is supported, and use it if it is available.

This is an expensive technique, so if you want to have many of the same objects you need to take advantage of instancing to render the same object many times along the curve.

The code example I made for Three.js is designed to take advantage of instancing. Instancing lets you have many copies of the same object rendered in a single draw call. This allows you to define multiple curves in the texture and each instance of a model, set which curve it is on as well as set its position on the curve. This can let you have a very large amount of objects that get rendered in a single draw call.

In the demo I made with the fish you can control the number of fish by setting the ?fish=300 query parameter. On mobile devices it can render almost 100 fish whilst maintaining a 60fps frame rate. On laptops it can do 1000s of fish and on powerful desktop computers it can do over 10000 fish!

Image for post
Image for post
The Koi Garden demo with 300 fish

The simplified demonstrations I made for Three.js have a mesh with complex geometry, generated using the Three.js TextGeometry which travels round the curves.

The first example has a single object, which is not instanced, traveling around a single curve.

The second example has a single object instanced 8 times traveling around 2 separate curves.

Image for post
Image for post
3D text duplicated 8 times traveling along two paths.

The not-instanced method is the simpler method for having a single object.

  • Pick your mesh
  • Define the curve to use
  • Create a new flow from the mesh
  • Add the curve to the mesh at index 0
  • Add the curves object to the scene
import { Flow } from "three/examples/jsm/modifiers/CurveModifier.js";const points = [
new Vector3( 1, 0, z: -1 ),
new Vector3( 1, 0, z: 1 ),
new Vector3( -1, 0, z: 1 ),
new Vector3( -1, 0, z: -1 ),
const curve = new THREE.CatmullRomCurve3(points);
curve.curveType = "centripetal";
curve.closed = true;
const mesh = // some mesh I made earlier;// You may need to tweak the geometry beforehand to get it to
// Display with the orientation you expect.
mesh.geometry.rotateX( Math.PI );
const flow = new Flow( objectToCurve );
flow.updateCurve( 0, curve );
scene.add( flow.object3D );

Note: you do not need to add the mesh to the scene. The flow object clones one from the mesh.

The instanced method is how to performantly have many many objects drawn in a single draw call.

import { InstancedFlow } from "three/examples/jsm/modifiers/CurveModifier.js";const material = // some material
const geometry = // some geometry
const curve1 = // A curve
const curve2 = // A curve
const curve3 = // A curve
const curve4 = // A curve
geometry.rotateX( Math.PI );const numberOfInstances = 8;
const numberOfCurves = 4;
const flow = new InstancedFlow( numberOfInstances, numberOfCurves, geometry, material );// Add the flow object to the scene
scene.add( flow.object3D );
flow.updateCurve( 0, curve1 );
flow.updateCurve( 1, curve2 );
flow.updateCurve( 2, curve3 );
flow.updateCurve( 3, curve4 );
// Do each step below for each numberOfInstances// Set the first instance to be on the first curve
flow.setCurve( 0, 0 );
// Move the first instance along the curve by a random amount
flow.moveIndividualAlongCurve( 0, Math.random() );
// Give the first instance a random Color
flow.object3D.setColorAt( 0, new THREE.Color( 0xffffff * Math.random() ) );

By calling updateCurve() at a later point you can change the curves on the fly. This is an expensive operation, especially if you have many curves — try to avoid changing it every frame if it can be avoided.

If you need more curves or instanced objects than you had allocated for, you will have to make a new curve instance with space for the new assets.

If you are a very advanced developer then you don’t need to use the Flow instances, they are just to make your life easier. The helper functions they use are exported as well so you can use them to create your own shaders and materials. Here is the source code for those interested in diving in and learning more:

Samsung Internet Developers

Writings from the Samsung Internet Developer Relations…

Ada Rose Cannon

Written by

Co-chair of the W3C Immersive Web Working Group, Developer Advocate for Samsung.

Samsung Internet Developers

Writings from the Samsung Internet Developer Relations Team. For more info see our disclaimer:

Ada Rose Cannon

Written by

Co-chair of the W3C Immersive Web Working Group, Developer Advocate for Samsung.

Samsung Internet Developers

Writings from the Samsung Internet Developer Relations Team. For more info see our disclaimer:

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store