<script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@v0.171.0/build/three.webgpu.js",
      "three/webgpu": "https://cdn.jsdelivr.net/npm/three@v0.171.0/build/three.webgpu.js",
      "three/tsl": "https://cdn.jsdelivr.net/npm/three@v0.171.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>
body {
  padding: 0;
  margin: 0;
}

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

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 {
  positionLocal,
  normalLocal,
  normalize,
  modelWorldMatrix,
  cameraProjectionMatrix,
  cameraViewMatrix,
  mix,
  attributeArray,
  clamp,
  time,
  mx_noise_float,
  Fn,
  uint,
  float,
  cross,
  If,
  Continue,
  distance,
  length,
  attribute,
  max,
  exp,
  mat3,
  vec3,
  select,
  Loop,
  instanceIndex,
  uniform
} from "three/tsl";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
import Stats from 'three/addons/libs/stats.module.js';

class FlockGeometry extends THREE.BufferGeometry {
  constructor(geo) {
    super();

    const geometry = geo.toNonIndexed();
    const srcPosAttr = geometry.getAttribute( "position" );
    const srcNormAttr = geometry.getAttribute( "normal" );
    let count = srcPosAttr.count;
    const total = count * BOIDS;
    
    const posAttr = new THREE.BufferAttribute(new Float32Array(total * 3), 3); 
    const normAttr = new THREE.BufferAttribute(new Float32Array(total * 3), 3); 
    const instanceIDAttr = new THREE.BufferAttribute(new Uint32Array(total), 1);

    this.setAttribute("instanceID", instanceIDAttr);
    this.setAttribute("position", posAttr);
    this.setAttribute("normal", normAttr);


    for (let b = 0; b < BOIDS; b++) {
      let offset = b * count * 3;
      for (let i = 0; i < count * 3; i++) {
        posAttr.array[offset + i] = srcPosAttr.array[i];
        normAttr.array[offset + i] = srcNormAttr.array[i];
      }
  
      offset = b * count;
      for (let i = 0; i < count; i++) {
        instanceIDAttr.array[offset + i] = b
      }
    }
  }
}

let container;

let camera,
  scene,
  renderer,
  options,
  material,
  assetPath,
  clock,
  boid,
  flock,
  deltaTime,
  stats,
  computeVelocity,
  computePosition,
  computeTest;

const BOIDS = 2000;
const BOUNDS = 20,
  BOUNDS_HALF = BOUNDS / 2;

init();

function init() {
  container = document.createElement("div");
  document.body.appendChild(container);
  
  stats = new Stats();
  stats.domElement.style.right = "0px";
	container.appendChild( stats.dom );

  camera = new THREE.PerspectiveCamera(
    40,
    window.innerWidth / window.innerHeight,
    1,
    100
  );
  camera.position.set(0.0, 1, 12);

  //

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

  //

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

  //

  //content

  const ambient = new THREE.HemisphereLight(0xaaaaaa, 0x333333);
  const light = new THREE.DirectionalLight(0xffffff, 3);
  light.position.set(3, 3, 1);
  scene.add(ambient);
  scene.add(light);

  clock = new THREE.Clock();
  
  const controls = new OrbitControls(camera, renderer.domElement);

  assetPath = "https://assets.codepen.io/2666677/";

  loadGLB("boid");

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

function loadGLB(name) {
  const loader = new GLTFLoader().setPath(assetPath);
  const dracoLoader = new DRACOLoader();
  dracoLoader.setDecoderPath(
    "https://cdn.jsdelivr.net/npm/three@v0.170.0/examples/jsm/libs/draco/gltf/"
  );
  loader.setDRACOLoader(dracoLoader);

  loader.load(`${name}.glb`, (gltf) => {
    boid = gltf.scene.children[0];
    const scale = 0.2;
    boid.geometry.scale( scale, scale, scale );

    tsl();
    //scene.add(boid);
  });
}

function initStorage() {
  const positionArray = new Float32Array(BOIDS * 3);
  const directionArray = new Float32Array(BOIDS * 3);
  const noiseArray = new Float32Array(BOIDS);

  const q = new THREE.Quaternion();
  const v = new THREE.Euler();

  for (let i = 0; i < BOIDS; i++) {
    const offset = i * 3;

    for( let j=0; j<3; j++){
      positionArray[offset + j] = Math.random() * BOUNDS - BOUNDS_HALF;
    }

    q.random();
    v.setFromQuaternion(q);

    directionArray[offset + 0] = v.x;
    directionArray[offset + 1] = v.y;
    directionArray[offset + 2] = v.z;
    
    noiseArray[i] = Math.random() * 1000.0;
  }

  const positionStorage = attributeArray(positionArray, "vec3").label(
    "positionStorage"
  );
  const directionStorage = attributeArray(directionArray, "vec3").label(
    "directionStorage"
  );
  const noiseStorage = attributeArray(noiseArray, "float").label(
    "noiseStorage"
  );

  // The Pixel Buffer Object (PBO) is required to get the GPU computed data in the WebGL2 fallback.
  positionStorage.setPBO(true);
  directionStorage.setPBO(true);
  noiseStorage.setPBO(true);

  return [positionStorage, directionStorage, noiseStorage];
}

function tsl() {
  const [positionStorage, directionStorage, noiseStorage] = initStorage();

  deltaTime = uniform(float());
  const boidSpeed = uniform(3);
  const flockPosition = uniform(vec3());
  const neighbourDistance = uniform(5);
  const rotationSpeed = uniform(1);

  const flockVertexTSL = Fn(() => {
    const instanceID = attribute("instanceID");
    const normal = normalLocal.toVar();
    const dir = normalize(directionStorage.element(instanceID)).toVar();
    
    //Create matrix
    //float4x4 create_matrix(float3 pos, float3 dir, float3 up) {
    const zaxis = dir.negate().normalize().toVar();
    const xaxis = cross(vec3(0, 1, 0), zaxis).normalize().toVar();
    const yaxis = cross(zaxis, xaxis).toVar();
    const mat = mat3( 
      xaxis.x,
      yaxis.x,
      zaxis.x,
      xaxis.y,
      yaxis.y,
      zaxis.y,
      xaxis.z,
      yaxis.z,
      zaxis.z
    ).toVar();
    
    const finalVert = modelWorldMatrix.mul(mat.mul(positionLocal)).add(positionStorage.element(instanceID));

    return cameraProjectionMatrix.mul(cameraViewMatrix).mul(finalVert);
  });

  computeVelocity = Fn(() => {
    const boid_pos = positionStorage.element(instanceIndex).toVar();
    const boid_dir = directionStorage.element(instanceIndex).toVar();
    
    const separation = vec3(0).toVar();
    const alignment = vec3(0).toVar();
    const cohesion = vec3(flockPosition).toVar();

    const nearbyCount = uint(1).toVar(); // Add self that is ignored in loop
    
    Loop(
      { start: uint(0), end: uint(BOIDS), type: "uint", condition: "<" },
      ({ i }) => {
        If(i == instanceIndex, () => {
          Continue();
        });

        const tempBoid_pos = positionStorage.element(i).toVar();
        const tempBoid_dir = directionStorage.element(i).toVar();

        const offset = boid_pos.sub(tempBoid_pos).toVar();
        const dist = length(offset).toVar();
        
        If( dist.lessThan(neighbourDistance), () => {
          If( dist.lessThan( 0.0001 ), () => {
							Continue();
						} ); 
          
          //separation += offset * (1.0/dist - 1.0/neighbourDistance);
          const s = offset.mul(float(1.0).div(dist).sub(float(1.0).div(neighbourDistance))).toVar();
          separation.addAssign( s );
          alignment.addAssign(tempBoid_dir);
          cohesion.addAssign(tempBoid_pos);

          nearbyCount.addAssign(1);
        }); //If
      }); //Loop
    
    const avg = float(1.0).div(nearbyCount).toVar();
    alignment.mulAssign(avg);
    cohesion.mulAssign(avg);
    cohesion.assign(cohesion.normalize().sub(boid_pos));

    const direction = alignment.add(separation).add(cohesion).toVar();

    const ip = exp(rotationSpeed.mul(-1).mul(deltaTime));
    boid_dir.assign(mix(direction, boid_dir.normalize(), ip));
    directionStorage.element(instanceIndex).assign(boid_dir);
  })().compute(BOIDS);

  computePosition = Fn( () => {
    const boid_pos = positionStorage.element(instanceIndex).toVar();
    const boid_dir = directionStorage.element(instanceIndex).toVar();
    const noise_offset = noiseStorage.element(instanceIndex).toVar();
    const noise = mx_noise_float( boid_pos.mul( time.div(100.0).add(noise_offset))).add(1).div(2.0).toVar();
	const velocity = boidSpeed.mul(float(1.0).add(noise)).toVar();// * boidSpeedVariation);
    
    boid_pos.addAssign( boid_dir.mul( velocity ).mul(deltaTime));
    positionStorage.element(instanceIndex).assign(boid_pos);
  })().compute(BOIDS);
  
  computeTest = Fn( () => {
    const position = positionStorage.element( instanceIndex );
    position.addAssign( vec3( 0, deltaTime.mul(100), 0) );
  })().compute(BOIDS);
  
  const geometry = new FlockGeometry(boid.geometry);
  const material = new THREE.MeshStandardNodeMaterial();

  flock = new THREE.Mesh(geometry, material);
  scene.add(flock);

  material.vertexNode = flockVertexTSL();
  /*options = {
    delta: 0
  };

  const gui = new GUI();
  gui.add(options, "delta", 0, 1).onChange((value) => {
    deltaUniform.value = value;
  });*/
}

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

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

//

function render() {
  if (deltaTime && clock) deltaTime.value = clock.getDelta();
  if (computeVelocity) renderer.compute(computeVelocity);
  if (computePosition) renderer.compute(computePosition);
  stats.update();
  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.