/** * Compute Shader Template * * A template for GPU compute shaders with: * - Storage buffer setup * - Initialize and update shaders * - Visualization with instanced mesh * * Usage: * 1. Modify PARTICLE_COUNT and buffer types * 2. Implement your initialization logic * 3. Implement your update logic * 4. Customize visualization */ import * as THREE from 'three/webgpu'; import { Fn, If, Loop, float, int, vec2, vec3, vec4, color, uniform, instancedArray, instanceIndex, hash, time, deltaTime } from 'three/tsl'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // ============================================ // CONFIGURATION // ============================================ const PARTICLE_COUNT = 50000; // ============================================ // STORAGE BUFFERS // ============================================ // Define your storage buffers here // Available types: 'float', 'vec2', 'vec3', 'vec4', 'int', 'uint' const positions = instancedArray(PARTICLE_COUNT, 'vec3'); const velocities = instancedArray(PARTICLE_COUNT, 'vec3'); // Add more buffers as needed: // const colors = instancedArray(PARTICLE_COUNT, 'vec3'); // const lifetimes = instancedArray(PARTICLE_COUNT, 'float'); // const states = instancedArray(PARTICLE_COUNT, 'uint'); // ============================================ // UNIFORMS // ============================================ const dt = uniform(0); // Add your uniforms here: // const gravity = uniform(-9.8); // const attractorPosition = uniform(new THREE.Vector3()); // const forceStrength = uniform(1.0); // ============================================ // COMPUTE SHADERS // ============================================ /** * Initialize particles * Called once at startup */ const computeInit = Fn(() => { const position = positions.element(instanceIndex); const velocity = velocities.element(instanceIndex); // ======================================== // IMPLEMENT YOUR INITIALIZATION HERE // ======================================== // Example: Random positions in a cube position.x.assign(hash(instanceIndex).sub(0.5).mul(10)); position.y.assign(hash(instanceIndex.add(1)).sub(0.5).mul(10)); position.z.assign(hash(instanceIndex.add(2)).sub(0.5).mul(10)); // Example: Zero velocity velocity.assign(vec3(0)); })().compute(PARTICLE_COUNT); /** * Update particles each frame * Called every frame in animation loop */ const computeUpdate = Fn(() => { const position = positions.element(instanceIndex); const velocity = velocities.element(instanceIndex); // ======================================== // IMPLEMENT YOUR UPDATE LOGIC HERE // ======================================== // Example: Simple gravity velocity.y.addAssign(float(-9.8).mul(dt)); // Example: Update position position.addAssign(velocity.mul(dt)); // Example: Ground bounce If(position.y.lessThan(0), () => { position.y.assign(0); velocity.y.assign(velocity.y.negate().mul(0.8)); }); // Example: Boundary wrapping // If(position.x.abs().greaterThan(5), () => { // position.x.assign(position.x.negate()); // }); })().compute(PARTICLE_COUNT); /** * Optional: Additional compute pass (e.g., for interactions) */ const computeInteraction = Fn(() => { const position = positions.element(instanceIndex); const velocity = velocities.element(instanceIndex); // ======================================== // IMPLEMENT INTERACTION LOGIC HERE // ======================================== // Example: Attract to point // const toTarget = attractorPosition.sub(position); // const dist = toTarget.length(); // const force = toTarget.normalize().mul(forceStrength).div(dist.add(0.1)); // velocity.addAssign(force.mul(dt)); })().compute(PARTICLE_COUNT); // ============================================ // VISUALIZATION // ============================================ function createVisualization(scene) { // Choose visualization type: // - Points (fastest, simplest) // - Instanced Mesh (more control) // Option 1: Points // return createPointsVisualization(scene); // Option 2: Instanced Mesh return createInstancedVisualization(scene); } function createPointsVisualization(scene) { const geometry = new THREE.BufferGeometry(); geometry.setAttribute( 'position', new THREE.Float32BufferAttribute(new Float32Array(PARTICLE_COUNT * 3), 3) ); const material = new THREE.PointsNodeMaterial(); // Position from compute buffer material.positionNode = positions.element(instanceIndex); // ======================================== // CUSTOMIZE POINT APPEARANCE HERE // ======================================== material.sizeNode = float(3.0); material.colorNode = Fn(() => { // Example: Color based on velocity const velocity = velocities.element(instanceIndex); const speed = velocity.length(); return mix(color(0x0066ff), color(0xff6600), speed.div(5).saturate()); })(); const points = new THREE.Points(geometry, material); scene.add(points); return points; } function createInstancedVisualization(scene) { // Geometry for each instance const geometry = new THREE.SphereGeometry(0.05, 8, 8); // Or use simpler geometry for better performance: // const geometry = new THREE.IcosahedronGeometry(0.05, 0); const material = new THREE.MeshStandardNodeMaterial(); // Position from compute buffer material.positionNode = positions.element(instanceIndex); // ======================================== // CUSTOMIZE MESH APPEARANCE HERE // ======================================== material.colorNode = Fn(() => { // Example: Color based on position const position = positions.element(instanceIndex); return color(0x0088ff).add(position.mul(0.05)); })(); material.roughnessNode = float(0.5); material.metalnessNode = float(0.2); const mesh = new THREE.InstancedMesh(geometry, material, PARTICLE_COUNT); scene.add(mesh); return mesh; } // ============================================ // MAIN SETUP // ============================================ let camera, scene, renderer, controls; let visualization; async function init() { // Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x111122); // Camera camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); camera.position.set(0, 5, 15); // Lights const ambientLight = new THREE.AmbientLight(0x404040); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1); directionalLight.position.set(5, 10, 5); scene.add(directionalLight); // Optional: Ground plane const ground = new THREE.Mesh( new THREE.PlaneGeometry(20, 20), new THREE.MeshStandardNodeMaterial({ color: 0x333333 }) ); ground.rotation.x = -Math.PI / 2; scene.add(ground); // Renderer renderer = new THREE.WebGPURenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); await renderer.init(); // Initialize particles await renderer.computeAsync(computeInit); // Create visualization visualization = createVisualization(scene); // Controls controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.target.set(0, 2, 0); // Events window.addEventListener('resize', onWindowResize); // Start renderer.setAnimationLoop(animate); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } const clock = new THREE.Clock(); function animate() { // Update delta time uniform dt.value = Math.min(clock.getDelta(), 0.1); // Run compute shaders renderer.compute(computeUpdate); // renderer.compute(computeInteraction); // Update controls controls.update(); // Render renderer.render(scene, camera); } init().catch(console.error); // Export for external control export { positions, velocities, dt, computeInit, computeUpdate, computeInteraction };