import { getPath } from "./common.js"; function test(position) { const x = position[0] * 4 * Math.PI; const y = position[1] * 4 * Math.PI; return Math.cos(y - 3 + Math.sin(x)) * 0.5 + 0.5; } function length(vector) { return Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]); } function sub(a, b) { return [ a[0] - b[0], a[1] - b[1], ]; } function smoothing(radius, distance) { const volume = Math.PI * Math.pow(radius, 8) / 4; const value = Math.max(0, radius * radius - distance * distance); return value * value * value / volume; } function smoothingSlope(radius, distance) { if (distance >= radius) return 0; const value = radius * radius - distance * distance; const scale = -24.0 / (Math.PI * Math.pow(radius, 8)); return scale * distance * value * value; } function densityToPressure(density, targetDensity, pressure) { return (density - targetDensity) * pressure; } class FlatRenderer { positionProperty(position, circleRadius, particleCount) { var property = 0.0; for (var i = 0; i < particleCount; i++) { const particlePosition = [this.particles[i * this.particleStride + 0], this.particles[i * this.particleStride + 1]]; const particleProperty = this.particles[i * this.particleStride + 4]; const particleDensity = this.particles[i * this.particleStride + 5]; const distance = length(sub(position, particlePosition)); const influence = smoothing(circleRadius, distance); property += particleProperty * influence / particleDensity; } return property; } positionGradient(position, circleRadius, particleCount) { var property = new Float32Array([0, 0]); for (var i = 0; i < particleCount; i++) { const particlePosition = [this.particles[i * this.particleStride + 0], this.particles[i * this.particleStride + 1]]; const signedDistance = sub(position, particlePosition); const distance = length(signedDistance); const direction = [signedDistance[0] / distance, signedDistance[1] / distance]; const particleProperty = this.particles[i * this.particleStride + 4]; const particleDensity = this.particles[i * this.particleStride + 5]; const slope = smoothingSlope(circleRadius, distance); const weight = particleProperty * slope / particleDensity; property[0] += direction[0] * weight; property[1] += direction[1] * weight; } return property; } positionPressureForce(index, circleRadius, particleCount, targetDensity, pressure, mass) { var pressureForce = new Float32Array([0, 0]); const position = [this.particles[index * this.particleStride + 0], this.particles[index * this.particleStride + 1]]; for (var i = 0; i < particleCount; i++) { if (i === index) continue; const particlePosition = [this.particles[i * this.particleStride + 0], this.particles[i * this.particleStride + 1]]; const signedDistance = sub(position, particlePosition); const distance = length(signedDistance); var direction; if (distance === 0) { const x = Math.random(); const y = Math.random(); const l = length([x, y]); direction = [x / l, y / l]; } else { direction = [signedDistance[0] / distance, signedDistance[1] / distance]; } const particleDensity = this.particles[i * this.particleStride + 5]; const slope = smoothingSlope(circleRadius, distance); const sharedPressure = ( densityToPressure(this.particles[index * this.particleStride * 5], targetDensity, pressure) + densityToPressure(particleDensity, targetDensity, pressure) ) / 2; const weight = sharedPressure * slope * mass / particleDensity; pressureForce[0] += direction[0] * weight; pressureForce[1] += direction[1] * weight; } return pressureForce; } particleDensity(aPosition, circleRadius, particleCount) { var density = 0.0; for (var i = 0; i < particleCount; i++) { const bPosition = [this.particles[i * this.particleStride + 0], this.particles[i * this.particleStride + 1]]; const distance = length(sub(aPosition, bPosition)); const influence = smoothing(circleRadius, distance); density += influence; } return density; } createBuffers(device) { this.maxDim = 32; this.maxParticles = this.maxDim * this.maxDim; this.particleStride = this.module.instance.exports.particle__stride(); // bytes this.particlesSize = this.maxParticles * this.particleStride; // bytes this.maxGradientsDim = 32; this.maxGradients = this.maxGradientsDim * this.maxGradientsDim; this.gradientStride = 4 * 2; this.particleConfigurationStride = this.module.instance.exports.particle_configuration__stride(); this.particleBuffers = []; this.gradientBuffers = []; this.particleConfigurationBuffers = []; this.particleBindGroups = []; for (let i = 0; i < 2; i++) { const particleBuffer = device.createBuffer({ label: `particle buffer ${i}`, size: this.particlesSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this.particleBuffers.push(particleBuffer); const gradientBuffer = device.createBuffer({ label: `gradient buffer ${i}`, size: 4 * 4 * 2, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this.gradientBuffers.push(gradientBuffer); const configBuffer = device.createBuffer({ label: `particle configuration buffer ${i}`, size: this.particleConfigurationStride, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); this.particleConfigurationBuffers.push(configBuffer); const bindGroup = device.createBindGroup({ label: `particle buffer bind group ${i}`, layout: this.bindGroupLayout.particle, entries: [{ binding: 0, resource: { buffer: configBuffer }, }, { binding: 1, resource: { buffer: particleBuffer }, }, { binding: 2, resource: { buffer: gradientBuffer }, }], }); this.particleBindGroups.push(bindGroup); } } initializeState() { } constructor(device, canvasFormat, viewUniformBuffer, shaderModule, module) { this.module = module; const label = "flat"; ////////////////////////////////////////////////////////////////////// // buffer ////////////////////////////////////////////////////////////////////// const array = new Uint16Array(6); array[0] = 0; array[1] = 1; array[2] = 2; array[3] = 0; array[4] = 3; array[5] = 1; this.buffer = device.createBuffer({ label: `${label} renderer buffer`, size: array.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, }); device.queue.writeBuffer(this.buffer, 0, array, 0, array.length); ////////////////////////////////////////////////////////////////////// // pipeline ////////////////////////////////////////////////////////////////////// this.bindGroupLayout = { view: device.createBindGroupLayout({ label: `${label} bind group layout view`, entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } }] }), particle: device.createBindGroupLayout({ label: `${label} bind group layout particle`, entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }] }), }; const pipelineLayout = device.createPipelineLayout({ label: `${label} pipeline layout`, bindGroupLayouts: [ this.bindGroupLayout.view, this.bindGroupLayout.particle ], }); this.renderPipeline = device.createRenderPipeline({ label: `${label} pipeline`, layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vertexMain", }, fragment: { module: shaderModule, entryPoint: "fragmentMain", targets: [{ format: canvasFormat, }] }, primitive: { topology: "triangle-list", }, depthStencil: { depthWriteEnabled: true, depthCompare: "less", format: "depth24plus", }, }); ////////////////////////////////////////////////////////////////////// // particles ////////////////////////////////////////////////////////////////////// this.createBuffers(device); ////////////////////////////////////////////////////////////////////// // bind group ////////////////////////////////////////////////////////////////////// this.bindGroups = [ device.createBindGroup({ label: `${label} bind group`, layout: this.bindGroupLayout.view, entries: [{ binding: 0, resource: { buffer: viewUniformBuffer }, }], }), ]; } updateGradients(device, frameNumber, configuration) { const radius = configuration.circleRadius; const count = configuration.particleCount; // calculate particle gradients for (let y = 0; y < this.maxGradientsDim; y++) { for (let x = 0; x < this.maxGradientsDim; x++) { const i = y * this.maxGradientsDim + x; const position = [(x / this.maxGradientsDim) + 0.5 / this.maxGradientsDim, (y / this.maxGradientsDim) + 0.5 / this.maxGradientsDim]; const gradient = this.positionGradient(position, radius, count); this.gradients[i * this.gradientStride + 0] = position[0]; this.gradients[i * this.gradientStride + 1] = position[1]; this.gradients[i * this.gradientStride + 4] = gradient[0]; this.gradients[i * this.gradientStride + 5] = gradient[1]; } } device.queue.writeBuffer(this.gradientBuffers[frameNumber], 0, this.gradients, 0, this.gradients.length); } updateParticles(device, frameNumber, configuration) { for (var i = 0; i < configuration.particleCount; i++) { const position = [this.particles[i * this.particleStride + 0], this.particles[i * this.particleStride + 1]]; const density = this.particleDensity(position, configuration.circleRadius, configuration.particleCount); // value.y this.particles[i * this.particleStride + 5] = density; } device.queue.writeBuffer(this.particleBuffers[frameNumber], 0, this.particles, 0, this.particles.length); } updateSimulation(device, frameNumber, configuration) { for (var i = 0; i < configuration.particleCount; i++) { const force = this.positionPressureForce(i, configuration.circleRadius, configuration.particleCount, configuration.targetDensity, configuration.pressure, configuration.particleMass1); if (force[0] == NaN) throw new Error("force"); const particleDensity = this.particles[i * this.particleStride + 5]; const acceleration = [force[0] / particleDensity, force[1] / particleDensity]; this.particles[i * this.particleStride + 8] += acceleration[0]; this.particles[i * this.particleStride + 9] += acceleration[1] + 0.001; // collisions } for (var i = 0; i < configuration.particleCount; i++) { this.particles[i * this.particleStride + 0] -= this.particles[i * this.particleStride + 8]; this.particles[i * this.particleStride + 1] -= this.particles[i * this.particleStride + 9]; const position = [this.particles[i * this.particleStride + 0], this.particles[i * this.particleStride + 1]]; for (var c = 0; c < 2; c++) { if (position[c] < 0) { this.particles[i * this.particleStride + c] = 0.01; this.particles[i * this.particleStride + c + 8] *= -1 * 0.9; } if (position[c] > 1) { this.particles[i * this.particleStride + c] = 0.99; this.particles[i * this.particleStride + c + 8] *= -1 * 0.9; } } } } updateConfiguration(device, frameNumber, configuration) { const PARTICLE = 0; const CIRCLE = 1; const MOUSE = 2; const D = 3; const config = this.particleConfiguration; config[PARTICLE * 4 + 0] = configuration.particleCount; config[PARTICLE * 4 + 1] = configuration.particleSize; config[PARTICLE * 4 + 2] = configuration.particleMass1; config[PARTICLE * 4 + 3] = configuration.particleMass2; config[CIRCLE * 4 + 0] = configuration.circleRadius; config[CIRCLE * 4 + 1] = configuration.circleThickness; config[CIRCLE * 4 + 2] = configuration.testIntensity; config[CIRCLE * 4 + 3] = configuration.mass12Mix; config[MOUSE * 4 + 0] = configuration.mousePosition[0]; config[MOUSE * 4 + 1] = configuration.mousePosition[1]; config[MOUSE * 4 + 2] = configuration.lineThickness; config[MOUSE * 4 + 3] = configuration.lineLength; config[D * 4 + 0] = configuration.targetDensity; config[D * 4 + 1] = configuration.pressure; config[D * 4 + 2] = 0; config[D * 4 + 3] = 0; device.queue.writeBuffer(this.particleConfigurationBuffers[frameNumber], 0, this.particleConfiguration, 0, this.particleConfiguration.length) } render(device, renderPass, frameNumber, configuration) { // particle update must be before gradient update //this.updateParticles(device, frameNumber, configuration); //this.updateGradients(device, frameNumber, configuration); //this.updateSimulation(device, frameNumber, configuration); //this.updateConfiguration(device, frameNumber, configuration); renderPass.setPipeline(this.renderPipeline); renderPass.setIndexBuffer(this.buffer, "uint16"); renderPass.setBindGroup(0, this.bindGroups[0]); renderPass.setBindGroup(1, this.particleBindGroups[frameNumber]); renderPass.drawIndexed(6); } } async function loadFlat(device, canvasFormat, viewUniformBuffer, module) { const flatWgsl = await getPath("flat.wgsl"); const shaderModule = device.createShaderModule({ label: "flag shader", code: flatWgsl, }); const renderer = new FlatRenderer(device, canvasFormat, viewUniformBuffer, shaderModule, module); return renderer; } export { loadFlat };