Did you know that you can create functions that run on the GPU that do not directly relate to a material’s shader? Remember the advantage of a GPU is to run many copies of a function in parallel. So you need to design your function to do one thing. When you run the function you give an integer value which is the number of copies of the function you want to run. Each copy gets a instanceIndex. If you run 10 copies of the function then each version will have a unique instanceIndex with values from 0 to 9.

Take a look at the CodePen example below. Try dragging the radius and delta sliders. The positioning of the boids is animated over time. But this is a single mesh and we’re not using an InstancedMesh. Instead the motion is created using a ComputeShader. A small TSL function.

A ComputeShader is simply an instance of a TSL Fn. But you add ().compute(COUNT). Similar syntax to a JavaScript immediately invoked function expression (IIFE).

The content of the function creates two possible positions for the value to be stored as instanceIndex element in the positionStorage array. We learnt about attributeArrays in the previous lesson. Each boid is positioned based on its instanceID using an offset value stored in this buffer.

When delta is 0 it will use the value v1, which uses sin, cos and radius for the positioning. If you’re unfamiliar with positioning meshes on a circular path then take a look at this.

If delta is 1 then the boids are positioned as in the previous lesson. We need to calculate the positioning again because the values in positionStorage are overwritten by thus function.

Finally we use the mix method to blend between the circle positioning and the grid positioning using the value of delta.

computePosition = Fn( () => {
    const PI2 = float( 6.2832 );
    const theta = PI2.div( BOIDS ).toVar();
    const posx = cos(time.add(theta.mul(instanceIndex))).mul( radius ).toVar();
    const posy = sin(time.add(theta.mul(instanceIndex))).mul( radius ).toVar();
    const cellSize = float(0.5).toVar();
    const row = float(instanceIndex).mod(3.0).sub(1.0);
    const col = floor(float(instanceIndex).div(3.0)).sub(1.0);
    const position = positionStorage.element( instanceIndex );
    const v1 = vec3( posx, posy, 0).toVar();
    const v2 = vec3( col.mul(cellSize), row.mul(cellSize), 0).toVar();
    position.assign( mix( v1, v2, delta ) );
  })().compute(BOIDS);

If you’re new to shaders consider doing my GLSL shader course on Udemy.

It just remains to call this function in the render function. Now the position of each boid is adjusted before the scene is rendered.

if (computePosition) renderer.compute(computePosition);

In the next lesson we looking at flocking with 2000 boids!