import Shader from "../../framework/WebGpu.js" import Matrix4 from "../../framework/Matrix4.js" import Vector3 from "../../framework/Vector3.js" import Camera from "../../framework/Camera.js"; import EventManager from "../../framework/eventManager.js"; import ShaderInpector from "../../framework/ShaderInpector.js"; export class ParticleSimulation { canvas; device; camera; useLocalSort = true; eventManager = new EventManager(); frameCount = 0; setCanvas( canvas ) { this.canvas = canvas; this.eventManager.setCanvas( canvas ); } async setup( offscreenCanvas, width, height ) { offscreenCanvas.width = width; offscreenCanvas.height = height; this.canvas = offscreenCanvas; //console.log( this.canvas.width, this.canvas.height ); const context = offscreenCanvas.getContext("webgpu"); this.camera = new Camera( [0, 0, 5], [0, -.3, 0], [0, 1, 0] ); this.eventManager.setup( offscreenCanvas, this.camera ); //this.eventManager.registerEventListenersNode(); const adapter = await self.navigator.gpu.requestAdapter(); if ( !adapter ) { throw new Error("Failed to get GPU adapter"); } this.device = await adapter.requestDevice(); this.particleCount = 8192 * 2; const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device: this.device, format: presentationFormat, alphaMode: "opaque" }); let boundsMinimum = new Vector3( -2, -2, -2 ); let boundsMaximum = new Vector3( 2, 2, 2 ); let cellsPerDimension = 12; let gravity = -2.3; this.workgroupSize = 64; this.numWorkgroups = Math.ceil( this.particleCount / this.workgroupSize ); var jArray = new Array(); var kArray = new Array(); if( this.useLocalSort ) { var startK = this.workgroupSize * 2; // 2 * 256; 256 = this.workgroupSize } else { var startK = 2; } for (let k = startK; k <= this.particleCount; k <<= 1 ) { for (let j = k >> 1; j > 0; j >>= 1 ) { jArray.push( j ); kArray.push( k ); } } this.gravityShader = new Shader( this.device ); await this.gravityShader.setup( "../../shaders/gravity.wgsl" ); this.gravityShader.setVariable("gravity", gravity ); this.gravityShader.setVariable("updateDistancesAndIndices", 1); this.resetParticlePositions(); this.copyIndicesShader = new Shader( this.device ); await this.copyIndicesShader.setup( "../../shaders/copyBuffer.wgsl" ); this.renderShader = new Shader( this.device ); this.renderShader.setCanvas( this.canvas ); await this.renderShader.setup("../../shaders/points.wgsl"); const quadOffsets = new Float32Array( [ // Triangle 1 -1, -1, // bottom-left 1, -1, // bottom-right 1, 1, // top-right // Triangle 2 -1, -1, // bottom-left 1, 1, // top-right -1, 1 // top-left ]); this.renderShader.setAttribute( "quadOffset", quadOffsets ); this.findGridHashShader = new Shader( this.device ); await this.findGridHashShader.setup( "../../shaders/findGridHash.wgsl" ); //console.log("this.gravityShader.getBuffer(\"positions\")", this.gravityShader.getBuffer("positions")); this.findGridHashShader.setBuffer( "positions", this.gravityShader.getBuffer("positions") ); this.findGridHashShader.setVariable( "cellCount", cellsPerDimension ); this.findGridHashShader.setVariable( "gridMin", boundsMinimum ); this.findGridHashShader.setVariable( "gridMax", boundsMaximum ); if( this.useLocalSort ) { this.localSortShader = new Shader( this.device ); await this.localSortShader.setup( "../../shaders/localSort.wgsl" ); this.localSortShader.setBuffer( "gridHashes", this.findGridHashShader.getBuffer("gridHashes" ) ); this.localSortShader.setBuffer( "indices", this.findGridHashShader.getBuffer("indices" ) ); this.localSortShader.setVariable( "totalCount", this.particleCount ); } this.bitonicSortGridHashShader = new Shader( this.device ); await this.bitonicSortGridHashShader.setup( "../../shaders/bitonicSortUIntMultiPass.wgsl" ); if( this.useLocalSort ) { this.bitonicSortGridHashShader.setBuffer( "gridHashes", this.localSortShader.getBuffer("gridHashes") ); this.bitonicSortGridHashShader.setBuffer( "indices", this.localSortShader.getBuffer("indices") ); } else { this.bitonicSortGridHashShader.setBuffer( "gridHashes", this.findGridHashShader.getBuffer("gridHashes" ) ); this.bitonicSortGridHashShader.setBuffer( "indices", this.findGridHashShader.getBuffer("indices") ); } this.bitonicSortGridHashShader.setVariable( "totalCount", this.particleCount ); this.bitonicSortGridHashShader.setVariable( "jArray", jArray ); this.bitonicSortGridHashShader.setVariable( "kArray", kArray ); this.findGridHashRangeShader = new Shader( this.device ); await this.findGridHashRangeShader.setup( "../../shaders/findGridHashRanges.wgsl" ); this.collisionDetectionShader = new Shader( this.device ); await this.collisionDetectionShader.setup( "../../shaders/collisionDetection.wgsl" ); this.collisionDetectionShader.setBuffer( "positions", this.gravityShader.getBuffer("positions" ) ); this.collisionDetectionShader.setBuffer( "velocities", this.gravityShader.getBuffer("velocities") ); this.collisionDetectionShader.setBuffer( "positions", this.gravityShader.getBuffer("positions" ) ); this.collisionDetectionShader.setBuffer( "velocities", this.gravityShader.getBuffer("velocities") ); this.collisionDetectionShader.setBuffer( "gridHashes", this.findGridHashShader.getBuffer("gridHashes") ); if( this.useLocalSort ) { this.collisionDetectionShader.setBuffer( "hashSortedIndices", this.localSortShader.getBuffer("indices") ); } else { this.collisionDetectionShader.setBuffer( "hashSortedIndices", this.bitonicSortGridHashShader.getBuffer("indices") ); } this.collisionDetectionShader.setVariable( "cellCount", cellsPerDimension ); this.collisionDetectionShader.setVariable( "gridMin", boundsMinimum ); this.collisionDetectionShader.setVariable( "gridMax", boundsMaximum ); this.collisionDetectionShader.setBuffer( "startIndices", this.findGridHashRangeShader.getBuffer("startIndices") ); this.collisionDetectionShader.setBuffer( "endIndices", this.findGridHashRangeShader.getBuffer("endIndices") ); this.collisionDetectionShader.setVariable( "collisionRadius", 0.06 ); this.bitonicSortShader = new Shader(this.device, "../../shaders/bitonicSort.wgsl"); await this.bitonicSortShader.addStage("main", GPUShaderStage.COMPUTE ); this.bitonicSortShader.setBuffer( "compare", this.gravityShader.getBuffer("distances") ); this.bitonicSortShader.setBuffer( "indices", this.gravityShader.getBuffer("indices") ); this.bitonicSortShader.setVariable( "totalCount", this.particleCount ); this.findGridHashRangeShader.setBuffer( "gridHashes", this.bitonicSortGridHashShader.getBuffer("gridHashes") ); this.findGridHashRangeShader.setBuffer( "indices", this.bitonicSortGridHashShader.getBuffer("indices") ); this.findGridHashRangeShader.setVariable( "totalCount", this.particleCount ); this.copyIndicesShader.setBuffer( "indices", this.bitonicSortShader.getBuffer("indices") ); this.renderShader.setBuffer( "positions", this.gravityShader.getBuffer("positions") ); this.renderShader.setBuffer( "sortedIndices", this.copyIndicesShader.getBuffer("sortedIndices") ); this.renderShader.setCanvas( this.canvas ); //var simulation = new particleSimulation(); this.render(); return; //document.getElementById("resetParticlesButton").addEventListener("click", resetParticlePositions); } resetParticlePositions() { const positions = new Float32Array( this.particleCount * 3 ); const velocities = new Float32Array( this.particleCount * 3 ); for (var i = 0; i < this.particleCount; i++) { positions[ i * 3 + 0 ] = Math.random() * 2.0 - 1.0; positions[ i * 3 + 1 ] = Math.random() * 2.0 - 1.0; positions[ i * 3 + 2 ] = Math.random() * 2.0 - 1.0; velocities[ i * 3 + 0 ] = 0; velocities[ i * 3 + 1 ] = 0; velocities[ i * 3 + 2 ] = 0; } this.gravityShader.setVariable("positions", positions); this.gravityShader.setVariable("velocities", velocities); } updateTimeDelta() { const now = performance.now(); this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000; this.lastFrameTime = now; } async sortGridHash() { const commandEncoder = this.device.createCommandEncoder(); // Local sort 256 items if( this.useLocalSort ) { const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline( this.localSortShader.pipeline ); for ( const [ index, bindGroup ] of this.localSortShader.bindGroups.entries() ) { passEncoder.setBindGroup( index, bindGroup ); } passEncoder.dispatchWorkgroups( this.numWorkgroups ); passEncoder.end(); } // Bitonic global merge { const threadPassIndices = new Uint32Array( this.particleCount ); for (var i = 0; i < this.particleCount; i++) { threadPassIndices[0] = 0; } this.bitonicSortGridHashShader.setVariable("threadPassIndices", threadPassIndices); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline( this.bitonicSortGridHashShader.pipeline ); for ( const [ index, bindGroup ] of this.bitonicSortGridHashShader.bindGroups.entries() ) { passEncoder.setBindGroup( index, bindGroup ); } if( this.useLocalSort ) { var startK = this.workgroupSize * 2; } else { var startK = 2; } for (let k = startK; k <= this.particleCount; k <<= 1 ) { for (let j = k >> 1; j > 0; j >>= 1 ) { passEncoder.dispatchWorkgroups( this.numWorkgroups ); } } passEncoder.end(); } const commandBuffer = commandEncoder.finish(); await this.device.queue.submit( [ commandBuffer ] ); await this.device.queue.onSubmittedWorkDone(); } async sortDistance() { await this.gravityShader.execute( this.numWorkgroups ); this.gravityShader.setVariable("updateDistancesAndIndices", 0); for (let k = 2; k <= this.particleCount; k <<= 1) { for (let j = k >> 1; j > 0; j >>= 1) { const commandBuffers = new Array(); await this.bitonicSortShader.setVariable( "j", j ); // Must await uniform update await this.bitonicSortShader.setVariable( "k", k ); const commandEncoder = this.device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline( this.bitonicSortShader.pipeline ); for ( const [ index, bindGroup ] of this.bitonicSortShader.bindGroups.entries() ) { passEncoder.setBindGroup( index, bindGroup ); } passEncoder.dispatchWorkgroups( this.numWorkgroups ); passEncoder.end(); commandBuffers.push( commandEncoder.finish() ); this.device.queue.submit( commandBuffers ); } } this.gravityShader.setVariable( "updateDistancesAndIndices", 1 ); await this.copyIndicesShader.execute( this.numWorkgroups ); } async findStartEndIndices( logBuffers ) { await this.gravityShader.execute( this.numWorkgroups ); await this.gravityShader.setVariable("updateDistancesAndIndices", 0); await this.findGridHashShader.execute( this.numWorkgroups ); await this.sortGridHash(); await this.findGridHashRangeShader.execute( this.workgroupSize ); if( logBuffers ) { await this.bitonicSortGridHashShader.debugBuffer("indices"); await this.bitonicSortGridHashShader.debugBuffer("gridHashes"); await this.findGridHashRangeShader.debugBuffer("startIndices"); await this.findGridHashRangeShader.debugBuffer("endIndices"); } await this.gravityShader.setVariable("updateDistancesAndIndices", 1); } async render() { this.updateTimeDelta(); const viewMatrixData = this.camera.getViewMatrix(); const projectionMatrixData = Matrix4.createProjectionMatrix( this.camera, this.canvas ) const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData ); const cameraWorldMatrix = Matrix4.invert( viewMatrixData ); const cameraRight = Matrix4.getColumn( cameraWorldMatrix, 0 ); const cameraUp = Matrix4.getColumn( cameraWorldMatrix, 1 ); const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 ); this.gravityShader.execute( this.numWorkgroups ); this.gravityShader.setVariable( "cameraPosition", cameraPosition ); this.gravityShader.setVariable( "deltaTimeSeconds", this.deltaTimeValue ); if ( this.frameCount % 20 === 0 ) { this.sortDistance(); } this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix ); this.renderShader.setVariable( "cameraRight", cameraRight ); this.renderShader.setVariable( "cameraUp", cameraUp ); this.renderShader.renderToCanvas( 6, this.particleCount, 0 ); this.frameCount++; if ( this.frameCount % 6 === 0 ) { this.findStartEndIndices(); } this.collisionDetectionShader.setVariable( "deltaTimeSeconds", this.deltaTimeValue ); this.collisionDetectionShader.execute( this.numWorkgroups ); requestAnimationFrame( this.render.bind( this ) ); } }