As I explained in the previous article we are refactoring this ShaderToy example.

The code used by this example has a common function library. This may look like a lot of work to transcode from GLSL to TSL. But ThreeJS has a transpiler that takes GLSL and turns it into TSL. Here is a screengrab of some of the code.

Common tab from ShaderToy example

I simply copied this to the transpiler. Initially this caused an error, but this was easily fixed. The transpiler does not understand the #define preprocessor. Commenting this out fixed the error, lines 6-10.

I grabbed the transpiled code and saved this to a file TSL-Common.js. I replaced the #defines with this code. That sorted the common functions library.

const UI0 = uint(1597334673);
const  UI1 = uint(3812015801);
const  UI2 = uvec2(UI0, UI1);
const  UI3 = uvec3(UI0, UI1, 2798796415);
const  UIF = float(1.0).div(float(0xffffffff));

Then in the ShaderToy example a texture is created with different noise layers saved to the red, green, blue and alpha channels. The great thing about this texture is the noise is tileable. The right edge blends perfectly with the left edge and top with bottom.

Here is the texture we create. Use the Gui to see the content of each channel.

As usual use the JS button to view the code. Notice we import perlinFbm, worleyFbm and remap from the TSL-Common.js library.

import { perlinFbm, worleyFbm, remap } from "https://assets.codepen.io/2666677/TSL-Common.js";

Here is the code we need to refactor to create the tileable texture. Ultimately we will create a texture containing 128 copies of the texture with different z values used for each copy when calling the noise functions perlinFbm and worleyFbm. Collectively they will allow us to render the clouds as a 3d object.

In the code uv is generated using the screen position of the pixel being set and the screen resolution. The z value is based on the mouse position. For this example we will simply set z as 0.

We need to adapt the createNoiseTexture function. The sample point, pt, will be (posX, posY, 0) divided by size. We need a freq variable. For our sample point floor(m.y*slices)/slices is just zero.

The two functions perlinFbm and worleyFbm create tileable textures when passed values for the x and y axes in the range 0-1. The return value for pt.x = 1 will blend with pt.x = 0.

async function createNoiseTexture(size = 512) {
  const storageTexture = new THREE.StorageTexture(size, size);

  const computeTexture = Fn(({ texture }) => {
    const posX = instanceIndex.modInt(size);
    const posY = instanceIndex.div(size);
    const indexUV = uvec2(posX, posY);

    const pt = vec3(posX, posY, 0).div(size);
    const freq = float(4).toVar();

    const pfbm = mix(1, perlinFbm(pt, 4, 7), 0.5).toVar();
    pfbm.assign(abs(pfbm.mul(2).sub(1))); // billowy perlin noise

    const g = worleyFbm(pt, freq);
    const b = worleyFbm(pt, freq.mul(2));
    const a = worleyFbm(pt, freq.mul(4));
    const r = remap(pfbm, 0, 1, g, 1); // perlin-worley

    textureStore(texture, indexUV, vec4(r, g, b, a)).toWriteOnly();
  })({ texture: storageTexture }).compute(size * size);

  await renderer.computeAsync(computeTexture);

  return storageTexture;
}

To allow the user to view individual channels, I created a GUI and a colorTSL function. The GUI simply let’s the user choose from a dropdown menu. There is an onChange event that gets the index value of the string in the channels array and uses it to set the value of the channel uniform. If the user chooses red then the index value of this in the array is 0, so the channel uniform will be set to 0.

const options = {
    channel: "all"
};
const gui = new GUI();
const channels = ["red", "green", "blue", "alpha", "all"];

gui.add(options, "channel", channels).onChange((value) => {
  channel.value = channels.indexOf(value);
});

The colorTSL function receives the storageTexture and grabs the texel at the current uv, the second parameter to the TSL texture function is uv() by default. We create a result vector. If channel uniform is lessThan 1 we assign texel.r to each channel of the result vector. I did try equals but this currently, v172, gives an error expecting the second parameter to be a bool. I suspect this is a bug. With various ElseIf options we cover each option for the channel uniform.

const channel = uniform( uint(4) );

const colorTSL = Fn( ( { storageTexture } ) => {
  const texel = texture( storageTexture );
  const result = vec4().toVar();

  If( ( channel.lessThan( uint( 1 ) ) ), () => { result.assign( vec4( texel.r ) ) } )
  .ElseIf( ( channel.lessThan( uint(2) ) ), () => { result.assign( vec4( texel.g ) ) } )
  .ElseIf( ( channel.lessThan( uint(3) ) ), () => { result.assign( vec4( texel.b ) ) })
  .ElseIf( ( channel.lessThan( uint(4) ) ), () => { result.assign( vec4( texel.a ) ) })
  .Else(  () => { result.assign( texel ) });

  return result;
});

material.colorNode = colorTSL( { storageTexture } );

Now we have a tileable texture, it’s time to use it to display something that starts to look like a cloud. We’ll do that in the next part. Coming soon.

If you find these articles useful, perhaps you can buy me a coffee by clicking the button below. It might encourage me to write more.

Signup to my mailing list.