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 ); } createTextureFromImageBitmap( device, imageBitmap ) { const texture = device.createTexture( { size: [ imageBitmap.width, imageBitmap.height, 1 ], format: 'rgba8unorm', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT } ); device.queue.copyExternalImageToTexture( { source: imageBitmap }, { texture: texture }, [ imageBitmap.width, imageBitmap.height, 1 ] ); return texture; } async loadImageBitmap(url) { const response = await fetch( url ); const blob = await response.blob(); const imageBitmap = await createImageBitmap( blob ); return imageBitmap; } async loadTexture( url ) { const imageBitmap = await this.loadImageBitmap( url ); const texture = this.createTextureFromImageBitmap( this.device, imageBitmap ); return texture; } createPlane(width, height, repeatU, repeatV) { const vertices = new Float32Array( 18 ); // 6 vertices (2 triangles) * 3 coords const normals = new Float32Array( 18 ); // same count as vertices const uvs = new Float32Array( 12 ); // 6 vertices * 2 coords // Positions (two triangles forming a plane on XY plane at z=0) // Large plane from (-width/2, -height/2) to (width/2, height/2) vertices.set([ -width / 2, -height / 2, 0, width / 2, -height / 2, 0, -width / 2, height / 2, 0, -width / 2, height / 2, 0, width / 2, -height / 2, 0, width / 2, height / 2, 0 ]); // Normals all pointing +Z for (let i = 0; i < 6; i++) { normals[i * 3 + 0] = 0; normals[i * 3 + 1] = 0; normals[i * 3 + 2] = 1; } // UVs scaled by repeatU, repeatV to repeat texture over the plane uvs.set([ 0, 0, repeatU, 0, 0, repeatV, 0, repeatV, repeatU, 0, repeatU, repeatV ]); return { vertices, normals, uvs }; } async getParticlePositionsAndIndicesFromDiv(div, canvasSize = 256) { const rect = div.getBoundingClientRect(); console.log(rect.width, rect.height); const divWidth = rect.width; const divHeight = rect.height; const canvas = await html2canvas(div, { backgroundColor: null, width: divWidth, height: divHeight, scale: .6, useCORS: true }); const ctx = canvas.getContext("2d"); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height ); const data = imageData.data; console.log(imageData); const positions = []; const samplesPerPixel = 8; const colors = []; for (let y = 0; y < canvas.height; y++) { for (let x = 0; x < canvas.width; x++) { const i = (y * canvas.width + x) * 4; const r = data[i + 0]; const g = data[i + 1]; const b = data[i + 2]; const a = data[i + 3]; if (a > 10) { for (let s = 0; s < samplesPerPixel; s++) { const u = (x + Math.random()) / (canvas.width - 1); const v = (y + Math.random()) / (canvas.height - 1); const clipX = u * 2 - 1; const clipY = 1 - v * 2; positions.push(clipX, clipY, 0, 1); colors.push(r / 255, g / 255, b / 255, a / 255); } } } } console.log("positions", positions.length); const positionsArray = new Float32Array(positions); const indexCount = positionsArray.length / 4; const indices = new Uint32Array(indexCount); for (let i = 0; i < indexCount; i++) { indices[i] = i; } return { positions: positionsArray, indices: indices, colors: new Float32Array(colors) }; } async setup( offscreenCanvas, width, height ) { offscreenCanvas.width = width; offscreenCanvas.height = height; this.canvas = offscreenCanvas; const context = offscreenCanvas.getContext("webgpu"); this.camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] ); this.camera.distance = 2 this.eventManager.setup( offscreenCanvas, this.camera ); const adapter = await self.navigator.gpu.requestAdapter(); if ( !adapter ) { throw new Error("Failed to get GPU adapter"); } const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); this.device = await adapter.requestDevice(); context.configure({ device: this.device, format: presentationFormat, alphaMode: "opaque" }); //var model = await this.loadJSON("demo.json"); //var mesh = model.meshes[0]; var div = document.querySelector("#particleSource"); const { positions, indices, colors } = await this.getParticlePositionsAndIndicesFromDiv( div ); const rect = div.getBoundingClientRect(); const divWidth = rect.width; const divHeight = rect.height; const aspectRatio = divWidth / divHeight; this.initiateParticlesShader = new Shader( this.device ); await this.initiateParticlesShader.setup( "../../shaders/initiateParticles.wgsl"); //this.initiateParticlesShader.setVariable( "aspectRatio", aspectRatio ); this.initiateParticlesShader.setVariable( "initiationPositions", positions ); this.initiateParticlesShader.setVariable( "positions", positions ); this.simpleGravityShader = new Shader( this.device ); await this.simpleGravityShader.setup( "../../shaders/simpleGravity.wgsl"); this.simpleGravityShader.setBuffer( "velocities", this.initiateParticlesShader.getBuffer("velocities") ); this.simpleGravityShader.setBuffer( "positions", this.initiateParticlesShader.getBuffer("positions") ); this.simpleGravityShader.setVariable( "aspectRatio", aspectRatio ); //this.simpleGravityShader.setVariable( "gravity", [0, -7, 0] ); this.simpleGravityShader.setVariable( "hoverRadius", .2 ); this.renderShader = new Shader( this.device ); this.renderShader.setCanvas( this.canvas ); this.renderShader.topology = "point-list"; console.log("vertices", positions); this.vertexCount = positions.length / 4; //this.renderShader.setAttribute( "normal", mesh.normals ); this.renderShader.setAttribute( "indices",indices ); this.renderShader.setCanvas( this.canvas ); this.renderShader.topology = "point-list"; await this.renderShader.setup( "../../shaders/particle-header.wgsl"); this.renderShader.setVariable( "hoverRadius", .2 ); this.renderShader.setBuffer( "positions", this.simpleGravityShader.getBuffer("positions") ); //this.renderShader.setVariable( "positions", positions ); this.renderShader.setVariable( "colors", colors ); this.renderShader.setVariable( "aspectRatio", aspectRatio ); div.addEventListener("mousemove", function(event) { const rect = div.getBoundingClientRect(); const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; const aspectRatio = rect.width / rect.height; // Normalize to 0..1 const normX = mouseX / rect.width; const normY = mouseY / rect.height; // Map to clip space coordinates const clipX = normX * 2 * aspectRatio - aspectRatio; // -aspectRatio to +aspectRatio const clipY = 1 - normY * 2; // +1 to -1 (top to bottom) // Update uniform buffer for mousePos this.renderShader.setVariable("mousePos", [clipX, clipY]); this.simpleGravityShader.setVariable("mousePos", [clipX, clipY]); }.bind(this)); var texture = await this.loadTexture("./textures/defaultnouvs.png"); const sampler = this.device.createSampler({ minFilter: 'linear', magFilter: 'linear', mipmapFilter: 'linear', addressModeU: 'repeat', addressModeV: 'repeat', }); //this.renderShader.setVariable( "mySampler", sampler ); //this.renderShader.setVariable( "myTexture", texture ); const workgroupSize = 64; const particleCount = positions.length / 4; this.dispatchCount = Math.ceil(particleCount / workgroupSize); if( this.dispatchCount * workgroupSize <= particleCount ) { console.error( "this.dispatchCount is to small", this.dispatchCount); } this.initiateParticlesShader.execute( this.dispatchCount ); this.render(); } updateTimeDelta() { const now = performance.now(); this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000; this.lastFrameTime = now; } 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 cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 ); this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix ); //this.renderShader.setVariable( "time", this.lastFrameTime*.001 ); this.simpleGravityShader.setVariable("deltaTime", this.deltaTimeValue); this.simpleGravityShader.execute( this.dispatchCount ); this.renderShader.renderToCanvas( this.vertexCount , 1, 0 ); this.frameCount++; requestAnimationFrame( this.render.bind( this ) ); } async loadJSON( pathName ) { const response = await fetch( pathName ); if ( !response.ok ){ throw new Error( `Failed to load shader: ${ pathName }` ); } return await response.json(); } }