432 lines
9.4 KiB
JavaScript
432 lines
9.4 KiB
JavaScript
|
|
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();
|
|
|
|
}
|
|
|
|
}
|