<script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@v0.172.0/build/three.webgpu.js",
      "three/webgpu": "https://cdn.jsdelivr.net/npm/three@v0.172.0/build/three.webgpu.js",
      "three/tsl": "https://cdn.jsdelivr.net/npm/three@v0.172.0/build/three.tsl.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@v0.171.0/examples/jsm/"
    }
  }
</script>

<div id="courses"><a href="https://niklever.com/courses" target="_blank">niklever.com/courses</a></div>
<div id="slice">0</div>
body {
  padding: 0;
  margin: 0;
}

#courses {
  font: bold 30px "Arial";
  position: fixed;
  left: 20px;
  top: 20px;
  color: #ffffff;
  text-decoration: none;
}

#slice {
  font: bold 30px "Arial";
  position: fixed;
  right: 20px;
  top: 20px;
  color: #ffffff;
}

a:link {
  color: white;
  text-decoration: none;
}

a:hover {
  color: #dddd33;
  text-decoration: underline;
}

a:visited {
  color: white;
  text-decoration: none;
}
import * as THREE from "three";
import {
  uv,
  uvec2,
  float,
  uint,
  vec2,
  vec3,
  vec4,
  instanceIndex,
  Fn,
  abs,
  mix,
  texture,
  textureStore,
  time,
  floor,
  saturate,
  reciprocal,
  uniform
} from "three/tsl";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import {
  perlinFbm,
  worleyFbm,
  remap
} from "https://assets.codepen.io/2666677/TSL-Common.js";
import { GUI } from "three/addons/libs/lil-gui.module.min.js";

let container;

let camera, scene, renderer, mesh;

init();

function init() {
  renderer = new THREE.WebGPURenderer({ antialias: true });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setAnimationLoop(render);
  document.body.appendChild(renderer.domElement);

  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x0561a0);

  camera = new THREE.PerspectiveCamera(
    40,
    window.innerWidth / window.innerHeight,
    1,
    40
  );
  camera.position.set(0, 0, 2);

  //new OrbitControls( camera, renderer.domElement );

  tsl();

  window.addEventListener("resize", onWindowResize);
}

async function createNoiseTexture(size = 256, cellsX = 16, cellsY = 8) {
  const storageTexture = new THREE.StorageTexture(size * cellsX, size * cellsY);
  storageTexture.wrapS = THREE.RepeatWrapping;
  storageTexture.wrapT = THREE.RepeatWrapping;
  storageTexture.minFilter = THREE.NearestFilter;
  storageTexture.maxFilter = THREE.NearestFilter;
  storageTexture.generateMinMaps = false;

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

    const slices = cellsX * cellsY;
    const row = uint(posY.div(size)).toVar();
    const col = uint(posX.div(size)).toVar();
    const slice = row.mul(cellsX).add(col).toVar(); //uint(cellsX).sub(col) ).toVar();

    const pt = vec3(posX.sub(col.mul(size)), posY.sub(row.mul(size)), 0).div(
      size
    );
    pt.z = float(slice).div(slices);

    const freq = float(4);

    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(storageTexture, indexUV, vec4(r, g, b, a)).toWriteOnly();
  })({ storageTexture }).compute(size * size * cellsY * cellsX);

  await renderer.computeAsync(computeTexture);

  return storageTexture;
}

async function tsl() {
  const geometry = new THREE.PlaneGeometry();
  const material = new THREE.MeshBasicNodeMaterial({ transparent: true });
  mesh = new THREE.Mesh(geometry, material);

  const cellsX = 16;
  const cellsY = 8;
  const slices = cellsX * cellsY;

  const storageTexture = await createNoiseTexture(256, cellsX, cellsY);

  const speed = uniform(float(1000));
  const slice = uniform(0);
  let pointerDown = false;

  function setSlice(x) {
    slice.value = ~~(((window.innerHeight - x) / window.innerHeight) * slices);
    const elm = document.getElementById("slice");
    elm.innerHTML = slice.value;
  }

  window.addEventListener("pointerdown", () => {
    pointerDown = true;
    setSlice(event.pageY);
  });
  window.addEventListener("pointerup", () => {
    pointerDown = false;
  });
  window.addEventListener("pointermove", (event) => {
    if (pointerDown) setSlice(event.pageY);
  });

  const fragTSL = Fn(({ storageTexture }) => {
    const slices = cellsX * cellsY;

    const col = slice.modInt(cellsX).toVar();
    const row = uint(slice.div(cellsX)).toVar();
    const origin = vec2(float(col).div(cellsX), float(row).div(cellsY));

    const uv1 = vec2(uv()).div(vec2(cellsX, cellsY)).toVar();
    uv1.addAssign(origin);

    const texel = texture(storageTexture, uv1, 0).toVar();
    const perlinWorley = texel.x.toVar();
    const worley = texel.yzw.toVar();
    
    // worley fbms with different frequencies
    const wfbm = worley.x
      .mul(0.625)
      .add(worley.y.mul(0.125))
      .add(worley.z.mul(0.25))
      .toVar();

    // cloud shape modeled after the GPU Pro 7 chapter
    const cloud = remap(perlinWorley, wfbm.sub(1), 1, 0, 1).toVar();
    cloud.assign(saturate(remap(cloud, 0.85, 1, 0, 1))); // fake cloud coverage

    return vec4(1, 1, 1, cloud);
  });

  material.fragmentNode = fragTSL({ storageTexture });
  
  scene.add(mesh);

  window.addEventListener("resize", onWindowResize);
}

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

//

function render() {
  renderer.render(scene, camera);
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.