<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">

  {

    "imports": {

      "three": "https://unpkg.com/three@0.155/build/three.module.js", 

      "three/addons/": "https://unpkg.com/three@0.155/examples/jsm/",

      "three/nodes": "https://unpkg.com/three@0.155/examples/jsm/nodes/Nodes.js"

    }

  }

</script>
body{

  overflow: hidden;

  margin: 0;

}
import * as THREE from "three";
import {texture, MeshBasicNodeMaterial, attribute, uniform, vec2, vec3, vec4, wgslFn, float, ShaderNode, storage, PointsNodeMaterial, instanceIndex, color } from 'three/nodes';
import {OrbitControls} from "three/addons/controls/OrbitControls.js";
import Stats from "three/addons/libs/stats.module.js";
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';


let computeNode;
let camera;
const pointerVector = new THREE.Vector2( - 10.0, - 10.0 );
const scaleVector = new THREE.Vector2( 1, 1 );

let scene = new THREE.Scene();
scene.background = new THREE.Color(0x00001f);
let renderer = new WebGPURenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth,window.innerHeight);
document.body.appendChild(renderer.domElement);
//let camera = new THREE.PerspectiveCamera(50.0, window.innerWidth/window.innerHeight, 0.5, 10000);

//camera.position.set(3, 3, 3);

camera = new THREE.OrthographicCamera( - 1.0, 1.0, 1.0, - 1.0, 0, 1 );
camera.position.z = 1;



//let controls = new OrbitControls(camera, renderer.domElement);


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

init();
render();


function init() {
  
 
  const particleNum = 300000;
	const particleSize = 2; // vec2

  const particleArray = new Float32Array(particleNum*particleSize);
  const velocityArray = new Float32Array(particleNum*particleSize);

  // create buffers
	const particleBuffer = new THREE.InstancedBufferAttribute( particleArray, 2);
	const velocityBuffer = new THREE.InstancedBufferAttribute( velocityArray, 2);

  const particleBufferNode=storage(particleBuffer,'vec2',particleNum);
	const velocityBufferNode=storage(velocityBuffer,'vec2',particleNum);



	const computeShaderNode = new ShaderNode((stack) => {

		const particle = particleBufferNode.element(instanceIndex);
		const velocity = velocityBufferNode.element(instanceIndex);

		const pointer = uniform(pointerVector);
		const limit = uniform(scaleVector);

		const position = particle.add(velocity);

		stack.assign(velocity.x, position.x.abs().greaterThanEqual(limit.x).cond(velocity.x.negate(), velocity.x));
		stack.assign(velocity.y, position.y.abs().greaterThanEqual(limit.y).cond(velocity.y.negate(), velocity.y));

		stack.assign(position, position.min(limit).max(limit.negate()));

		const pointerSize = 0.1;
		const distanceFromPointer = pointer.sub(position).length();

		stack.assign(particle, distanceFromPointer.lessThanEqual(pointerSize).cond( vec3(), position));
	});




	//compute
  //default limits for WebGPU @workgroup_size are as follows
  //maxComputeInvocationsPerWorkgroup: 256
  //maxComputeWorkgroupSizeX: 256
  //maxComputeWorkgroupSizeY: 256
  //maxComputeWorkgroupSizeZ: 64
  
  //@workgroup_size(256, 1, 1)  //ok -> [256, 1, 1]
  //@workgroup_size(128, 2, 1)  //ok -> [128, 2, 1]
  //@workgroup_size(16, 16, 1)  //ok -> [16, 16, 1]
  //@workgroup_size(16, 16, 2)  //bad! 16 * 16 * 2 = 512
  
  //The perfect size is GPU dependent and WebGPU can not provide that info!
  //The general advice for WebGPU is to choose a workgroup size of 64 
  //unless you have some reason to choose another size
  const workgroup_size = [8, 8, 1];  // 8 * 8 * 1 = 64
	computeNode = computeShaderNode.compute(particleNum, workgroup_size);
				
	computeNode.onInit = ({renderer}) => {

		const precomputeShaderNode = new ShaderNode((stack) => {

			const particleIndex = float(instanceIndex);
			const randomAngle = particleIndex.mul(.005).mul(Math.PI * 2);
			const randomSpeed = particleIndex.mul(0.0000001).add(0.000001);
			const velX = randomAngle.sin().mul(randomSpeed);
			const velY = randomAngle.cos().mul(randomSpeed);

			const velocity = velocityBufferNode.element(instanceIndex);

			stack.assign(velocity.xy, vec2(velX, velY));
		});

		renderer.compute(precomputeShaderNode.compute(particleNum, workgroup_size));
	};


  
  
  
  // use a compute shader to animate the point cloud's vertex data.
  const particleNode = attribute( 'particle', 'vec2' );

  const pointsGeometry = new THREE.BufferGeometry();
pointsGeometry.setAttribute( 'position', new THREE.BufferAttribute( new Float32Array( 3 ), 3 ) ); // single vertex ( not triangle )
  pointsGeometry.setAttribute( 'particle', particleBuffer ); // dummy the position points as instances
  pointsGeometry.drawRange.count = 1; // force render points as instances ( not triangle )

  const pointsMaterial = new PointsNodeMaterial();
  pointsMaterial.colorNode = particleNode.add( color( 0xFFFFFF ) );
  pointsMaterial.positionNode = particleNode;

  const mesh = new THREE.Points( pointsGeometry, pointsMaterial );
  mesh.isInstancedMesh = true;
  mesh.count = particleNum;
  scene.add( mesh );
   
  
}


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


function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.