Added NodeJS Demo.
98
demos/Graphics/GpuWorker.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// worker.js
|
||||
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
export class Controller {
|
||||
|
||||
setup( request ) {
|
||||
|
||||
var canvas = request.canvas;
|
||||
|
||||
console.log( canvas );
|
||||
|
||||
particleSimulation.setup( canvas, request.width, request.height );
|
||||
|
||||
//self.postMessage({ type: "pong", id: request.id });
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
mousemove( request ) {
|
||||
|
||||
particleSimulation.eventManager.mousemove( request );
|
||||
|
||||
}
|
||||
|
||||
mousedown( request ) {
|
||||
|
||||
console.log("onMouseDown");
|
||||
|
||||
particleSimulation.eventManager.mousedown( request );
|
||||
|
||||
}
|
||||
|
||||
mouseup( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseup( request );
|
||||
|
||||
}
|
||||
|
||||
mouseleave( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseleave( request );
|
||||
|
||||
}
|
||||
|
||||
wheel( request ) {
|
||||
|
||||
particleSimulation.eventManager.wheel( request );
|
||||
|
||||
}
|
||||
|
||||
resize( request ) {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions( );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new Controller();
|
||||
|
||||
self.onmessage = function (event) {
|
||||
|
||||
const data = event.data;
|
||||
|
||||
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||
console.warn("Invalid request received:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||
// const request = new Request(data.method, data.payload);
|
||||
|
||||
// Or just use plain data object
|
||||
const request = data;
|
||||
|
||||
const methodName = request.method;
|
||||
|
||||
if (typeof controller[methodName] !== "function") {
|
||||
console.warn("No method found for:", request.method);
|
||||
return;
|
||||
}
|
||||
|
||||
controller[methodName](request);
|
||||
};
|
||||
544
demos/Graphics/demo.js
Normal file
@@ -0,0 +1,544 @@
|
||||
|
||||
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 ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
171
demos/Graphics/index.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||
</head>
|
||||
<base href="Graphics/" />
|
||||
<link rel="stylesheet" href="./style/main.css" >
|
||||
|
||||
<script type="module">
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
|
||||
var useWebWorker = false;
|
||||
|
||||
const canvas = document.querySelector(".mainCanvas");
|
||||
|
||||
particleSimulation.setCanvas( canvas );
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
|
||||
var worker;
|
||||
|
||||
if ( !useWebWorker ) {
|
||||
|
||||
|
||||
|
||||
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||
|
||||
console.log("document.bufferMap", document.bufferMap);
|
||||
|
||||
} else if ( useWebWorker ) {
|
||||
|
||||
worker = new Worker("./GpuWorker.js", { type: "module" });
|
||||
|
||||
worker.onmessage = function ( event ) {
|
||||
|
||||
console.log("From worker:", event.data);
|
||||
|
||||
};
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
|
||||
worker.addEventListener("error", function ( event ) {
|
||||
|
||||
console.error( "Worker failed:",
|
||||
event.message, "at",
|
||||
event.filename + ":" +
|
||||
event.lineno + ":" +
|
||||
event.colno );
|
||||
|
||||
});
|
||||
|
||||
var request = {
|
||||
method: "setup",
|
||||
canvas: offscreen,
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
};
|
||||
|
||||
worker.postMessage( request, [offscreen] );
|
||||
|
||||
}
|
||||
|
||||
|
||||
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||
|
||||
for ( var i = 0; i < events.length; i++ ) {
|
||||
|
||||
let eventName = events[i];
|
||||
|
||||
console.log("createEvent", eventName);
|
||||
|
||||
canvas.addEventListener( eventName, function ( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
var request = {
|
||||
method: eventName,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
deltaY: event.deltaY
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager[ eventName ]( request );
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||
|
||||
var request = {
|
||||
method: "resetParticlePositions"
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function resizeCanvasToDisplaySize( canvas ) {
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
var request = {
|
||||
method: "resize",
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener( "resize", function () {
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<div id="controlPanel">
|
||||
<div class="inputRow">
|
||||
<button id="resetParticlesButton">Reset Spheres</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
114
demos/Graphics/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:#111111;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRow label {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputRow button,
|
||||
.inputRow input {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.inputRow button:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.inputRow select {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
appearance: none; /* Remove default arrow */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% - 3px),
|
||||
calc(100% - 15px) calc(50% - 3px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inputRow select:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow option {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
color: #f0f0f5;
|
||||
}
|
||||
114
demos/Graphics/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Simplifying WebGPU with This Framework
|
||||
|
||||
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||
|
||||
## 1. Loading and Using a Shader
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||
const pipeline = device.createComputePipeline({ ... });
|
||||
// Setup bind groups and command encoder manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const shader = new Shader( "myComputeShader.wgsl" );
|
||||
|
||||
await shader.compile();
|
||||
|
||||
shader.setVariable( "someUniform", 42 );
|
||||
|
||||
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||
```
|
||||
|
||||
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||
|
||||
## 2. Setting Buffers
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create GPU buffer, write data, create bind group manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||
|
||||
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||
```
|
||||
|
||||
Buffers are bound by name with a simple call.
|
||||
|
||||
## 3. Rendering to Canvas
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create render pipeline, set vertex buffers, encode commands manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
shader.setCanvas( canvasElement );
|
||||
|
||||
shader.setAttributes( vertexData );
|
||||
|
||||
shader.execute();
|
||||
```
|
||||
|
||||
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||
|
||||
## 4. Camera and Matrix Utilities
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const camera = new Camera();
|
||||
|
||||
camera.position.set( 0, 1, 5 );
|
||||
|
||||
camera.updateViewMatrix();
|
||||
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
```
|
||||
|
||||
The framework provides built-in classes for common math tasks.
|
||||
|
||||
## 5. Event Handling
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const events = new EventManager( canvasElement );
|
||||
|
||||
events.on( "mouseMove", (event) => {
|
||||
camera.rotate( event.deltaX, event.deltaY );
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
shader.execute();
|
||||
});
|
||||
```
|
||||
|
||||
Separates input handling cleanly from GPU logic.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | WebGPU Native API | This Framework |
|
||||
| --- | --- | --- |
|
||||
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||
|
||||
---
|
||||
|
||||
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||
304
demos/NodeJS/index.js
Normal file
@@ -0,0 +1,304 @@
|
||||
import sdl from '@kmamal/sdl'
|
||||
|
||||
import gpu from '@kmamal/gpu'
|
||||
|
||||
import Shader from "../../framework/WebGpu_node.js"
|
||||
|
||||
import Matrix4 from "../../framework/Matrix4.js"
|
||||
|
||||
import Vector3 from "../../framework/Vector3.js"
|
||||
|
||||
import Camera from "../../framework/Camera.js";
|
||||
|
||||
import EventManager from "../../framework/eventManager_node.js";
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
|
||||
const window = sdl.video.createWindow({ webgpu: true })
|
||||
|
||||
var canvas = window;
|
||||
|
||||
const instance = gpu.create([ 'verbose=1' ])
|
||||
|
||||
console.log("devices", gpu)
|
||||
|
||||
const adapter = await instance.requestAdapter()
|
||||
|
||||
const device = await adapter.requestDevice()
|
||||
|
||||
|
||||
const renderer = gpu.renderGPUDeviceToWindow({ device, window })
|
||||
|
||||
|
||||
canvas.getContext = function() {
|
||||
|
||||
return renderer;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
var renderShader = new Shader( device );
|
||||
|
||||
|
||||
renderShader.setCanvas( canvas );
|
||||
|
||||
renderShader.topology = "triangle-list";
|
||||
|
||||
await renderShader.setup( "../../shaders/triangle-list.wgsl");
|
||||
|
||||
async function loadJSON( pathName ) {
|
||||
|
||||
const json = await readFileSync( pathName, 'utf8' )
|
||||
|
||||
return JSON.parse( json );
|
||||
}
|
||||
|
||||
var camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
|
||||
|
||||
|
||||
|
||||
var eventManager = new EventManager( canvas );
|
||||
|
||||
eventManager.setup( canvas, camera );
|
||||
|
||||
eventManager.registerEventListenersNode();
|
||||
|
||||
var frameCount = 0;
|
||||
|
||||
|
||||
|
||||
var model = await loadJSON("../../models/demo.json");
|
||||
|
||||
var mesh = model.meshes[0];
|
||||
|
||||
|
||||
const instanceCount = 100;
|
||||
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||
|
||||
for (let i = 0; i < instanceCount; i++) {
|
||||
|
||||
const x = (i % 10) * 300.0;
|
||||
const y = Math.floor(i / 10) * 350.0;
|
||||
|
||||
instancePositions[i * 4 + 0] = x - 500;
|
||||
instancePositions[i * 4 + 1] = 0;
|
||||
instancePositions[i * 4 + 2] = y - 500;
|
||||
instancePositions[i * 4 + 3] = 0;
|
||||
}
|
||||
|
||||
|
||||
renderShader.setAttribute( "position", mesh.vertices );
|
||||
|
||||
renderShader.setAttribute( "normal", mesh.normals );
|
||||
|
||||
renderShader.setVariable( "instancePositions", instancePositions );
|
||||
|
||||
|
||||
var faces = mesh.faces;
|
||||
|
||||
const indexArray = new Uint32Array(faces.length * 3);
|
||||
|
||||
for (let i = 0; i < faces.length; i++) {
|
||||
|
||||
indexArray[i * 3 + 0] = faces[i][0];
|
||||
|
||||
indexArray[i * 3 + 1] = faces[i][1];
|
||||
|
||||
indexArray[i * 3 + 2] = faces[i][2];
|
||||
|
||||
}
|
||||
|
||||
renderShader.setIndices( indexArray );
|
||||
|
||||
var lastFrameTime = 0;
|
||||
|
||||
|
||||
|
||||
function updateTimeDelta() {
|
||||
|
||||
const now = performance.now();
|
||||
|
||||
deltaTimeValue = ( now - lastFrameTime ) / 1000;
|
||||
|
||||
lastFrameTime = now;
|
||||
|
||||
}
|
||||
|
||||
var frameCount = 0;
|
||||
|
||||
var deltaTimeValue = 0;
|
||||
|
||||
var vertexCount = 1;
|
||||
|
||||
const render = () => {
|
||||
|
||||
if (window.destroyed) { return }
|
||||
|
||||
updateTimeDelta();
|
||||
|
||||
const viewMatrixData = camera.getViewMatrix();
|
||||
|
||||
const projectionMatrixData = Matrix4.createProjectionMatrix( camera, canvas )
|
||||
|
||||
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
|
||||
|
||||
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
|
||||
|
||||
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
|
||||
|
||||
renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
|
||||
|
||||
renderShader.setVariable( "cameraPosition", cameraPosition );
|
||||
|
||||
frameCount++;
|
||||
|
||||
|
||||
renderShader.renderToCanvas( vertexCount, 60, 0, frameCount )
|
||||
|
||||
|
||||
renderer.swap()
|
||||
|
||||
|
||||
frameCount++;
|
||||
|
||||
setTimeout(render, 0)
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
|
||||
window.on('close', () => {
|
||||
device.destroy()
|
||||
gpu.destroy(instance)
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//console.log(model);
|
||||
|
||||
/*
|
||||
const renderer = gpu.renderGPUDeviceToWindow({ device, window })
|
||||
|
||||
const positions = new Float32Array([
|
||||
...[ 1.0, -1.0, 0.0 ],
|
||||
...[ -1.0, -1.0, 0.0 ],
|
||||
...[ 0.0, 1.0, 0.0 ],
|
||||
])
|
||||
|
||||
const colors = new Float32Array([
|
||||
...[ 1.0, 0.0, 0.0 ],
|
||||
...[ 0.0, 1.0, 0.0 ],
|
||||
...[ 0.0, 0.0, 1.0 ],
|
||||
])
|
||||
|
||||
const indices = new Uint16Array([ 0, 1, 2 ])
|
||||
|
||||
const createBuffer = (arr, usage) => {
|
||||
const buffer = device.createBuffer({
|
||||
size: (arr.byteLength + 3) & ~3,
|
||||
usage,
|
||||
mappedAtCreation: true,
|
||||
})
|
||||
|
||||
const writeArray = arr instanceof Uint16Array
|
||||
? new Uint16Array(buffer.getMappedRange())
|
||||
: new Float32Array(buffer.getMappedRange())
|
||||
writeArray.set(arr)
|
||||
buffer.unmap()
|
||||
return buffer
|
||||
}
|
||||
|
||||
const positionBuffer = createBuffer(positions, gpu.GPUBufferUsage.VERTEX)
|
||||
const colorBuffer = createBuffer(colors, gpu.GPUBufferUsage.VERTEX)
|
||||
const indexBuffer = createBuffer(indices, gpu.GPUBufferUsage.INDEX)
|
||||
|
||||
const vertexShaderFile = path.join(__dirname, 'vertex.wgsl')
|
||||
const vertexShaderCode = await fs.promises.readFile(vertexShaderFile, 'utf8')
|
||||
|
||||
const fragmentShaderFile = path.join(__dirname, 'fragment.wgsl')
|
||||
const fragmentShaderCode = await fs.promises.readFile(fragmentShaderFile, 'utf8')
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: 'auto',
|
||||
vertex: {
|
||||
module: device.createShaderModule({ code: vertexShaderCode }),
|
||||
entryPoint: 'main',
|
||||
buffers: [
|
||||
{
|
||||
attributes: [
|
||||
{
|
||||
shaderLocation: 0,
|
||||
offset: 0,
|
||||
format: 'float32x3',
|
||||
},
|
||||
],
|
||||
arrayStride: 3 * Float32Array.BYTES_PER_ELEMENT,
|
||||
stepMode: 'vertex',
|
||||
},
|
||||
{
|
||||
attributes: [
|
||||
{
|
||||
shaderLocation: 1,
|
||||
offset: 0,
|
||||
format: 'float32x3',
|
||||
},
|
||||
],
|
||||
arrayStride: 3 * Float32Array.BYTES_PER_ELEMENT,
|
||||
stepMode: 'vertex',
|
||||
},
|
||||
],
|
||||
},
|
||||
fragment: {
|
||||
module: device.createShaderModule({ code: fragmentShaderCode }),
|
||||
entryPoint: 'main',
|
||||
targets: [ { format: renderer.getPreferredFormat() } ],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-list',
|
||||
},
|
||||
})
|
||||
|
||||
const render = () => {
|
||||
if (window.destroyed) { return }
|
||||
|
||||
const commandEncoder = device.createCommandEncoder()
|
||||
|
||||
const renderPass = commandEncoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
{
|
||||
view: renderer.getCurrentTextureView(),
|
||||
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
})
|
||||
renderPass.setPipeline(pipeline)
|
||||
renderPass.setViewport(0, 0, width, height, 0, 1)
|
||||
renderPass.setScissorRect(0, 0, width, height)
|
||||
renderPass.setVertexBuffer(0, positionBuffer)
|
||||
renderPass.setVertexBuffer(1, colorBuffer)
|
||||
renderPass.setIndexBuffer(indexBuffer, 'uint16')
|
||||
renderPass.drawIndexed(3)
|
||||
renderPass.end()
|
||||
|
||||
device.queue.submit([ commandEncoder.finish() ])
|
||||
|
||||
renderer.swap()
|
||||
|
||||
setTimeout(render, 0)
|
||||
}
|
||||
|
||||
render()
|
||||
|
||||
window.on('close', () => {
|
||||
device.destroy()
|
||||
gpu.destroy(instance)
|
||||
})
|
||||
|
||||
*/
|
||||
97
demos/Texture/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// worker.js
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
export class Controller {
|
||||
|
||||
setup( request ) {
|
||||
|
||||
var canvas = request.canvas;
|
||||
|
||||
console.log( canvas );
|
||||
|
||||
particleSimulation.setup( canvas, request.width, request.height );
|
||||
|
||||
//self.postMessage({ type: "pong", id: request.id });
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
mousemove( request ) {
|
||||
|
||||
particleSimulation.eventManager.mousemove( request );
|
||||
|
||||
}
|
||||
|
||||
mousedown( request ) {
|
||||
|
||||
console.log("onMouseDown");
|
||||
|
||||
particleSimulation.eventManager.mousedown( request );
|
||||
|
||||
}
|
||||
|
||||
mouseup( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseup( request );
|
||||
|
||||
}
|
||||
|
||||
mouseleave( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseleave( request );
|
||||
|
||||
}
|
||||
|
||||
wheel( request ) {
|
||||
|
||||
particleSimulation.eventManager.wheel( request );
|
||||
|
||||
}
|
||||
|
||||
resize( request ) {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions( );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new Controller();
|
||||
|
||||
self.onmessage = function (event) {
|
||||
|
||||
const data = event.data;
|
||||
|
||||
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||
console.warn("Invalid request received:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||
// const request = new Request(data.method, data.payload);
|
||||
|
||||
// Or just use plain data object
|
||||
const request = data;
|
||||
|
||||
const methodName = request.method;
|
||||
|
||||
if (typeof controller[methodName] !== "function") {
|
||||
console.warn("No method found for:", request.method);
|
||||
return;
|
||||
}
|
||||
|
||||
controller[methodName](request);
|
||||
};
|
||||
300
demos/Texture/demo.js
Normal file
@@ -0,0 +1,300 @@
|
||||
|
||||
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 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.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();
|
||||
|
||||
this.renderShader = new Shader( this.device );
|
||||
|
||||
context.configure({
|
||||
device: this.device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "opaque"
|
||||
});
|
||||
const instanceCount = 100;
|
||||
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||
|
||||
for (let i = 0; i < instanceCount; i++) {
|
||||
|
||||
const x = (i % 10) * 300.0;
|
||||
const y = Math.floor(i / 10) * 350.0;
|
||||
|
||||
instancePositions[i * 4 + 0] = x - 1000;
|
||||
instancePositions[i * 4 + 1] = 0;
|
||||
instancePositions[i * 4 + 2] = y - 1000;
|
||||
instancePositions[i * 4 + 3] = 0;
|
||||
}
|
||||
|
||||
|
||||
var model = await this.loadJSON("../../models/demo.json");
|
||||
|
||||
var mesh = model.meshes[0];
|
||||
|
||||
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||
|
||||
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||
|
||||
this.renderShader.setAttribute( "uv", mesh.texturecoords[0] );
|
||||
|
||||
var faces = mesh.faces;
|
||||
|
||||
const indexArray = new Uint32Array(faces.length * 3);
|
||||
|
||||
for (let i = 0; i < faces.length; i++) {
|
||||
|
||||
indexArray[i * 3 + 0] = faces[i][0];
|
||||
|
||||
indexArray[i * 3 + 1] = faces[i][1];
|
||||
|
||||
indexArray[i * 3 + 2] = faces[i][2];
|
||||
|
||||
}
|
||||
|
||||
this.renderShader.setIndices( indexArray );
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||
|
||||
/*
|
||||
const { vertices, normals, uvs } = this.createPlane( 1000, 1000, 4, 4 );
|
||||
|
||||
this.renderShader.setAttribute( "position", vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", normals );
|
||||
|
||||
this.renderShader.setAttribute( "uv", uvs );
|
||||
|
||||
|
||||
this.vertexCount = vertices.length / 3
|
||||
|
||||
*/
|
||||
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||
|
||||
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 );
|
||||
|
||||
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( "cameraPosition", cameraPosition );
|
||||
|
||||
|
||||
this.renderShader.renderToCanvas( this.vertexCount, 74, 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
181
demos/Texture/index.html
Normal file
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||
</head>
|
||||
|
||||
<base href="Texture/" />
|
||||
|
||||
<link rel="stylesheet" href="./style/main.css" >
|
||||
|
||||
<script type="module">
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
|
||||
var useWebWorker = false;
|
||||
|
||||
const canvas = document.querySelector(".mainCanvas");
|
||||
|
||||
particleSimulation.setCanvas( canvas );
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var worker;
|
||||
|
||||
if ( !useWebWorker ) {
|
||||
|
||||
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||
|
||||
console.log("document.bufferMap", document.bufferMap);
|
||||
|
||||
} else if ( useWebWorker ) {
|
||||
|
||||
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||
|
||||
worker.onmessage = function ( event ) {
|
||||
|
||||
console.log("From worker:", event.data);
|
||||
|
||||
};
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
|
||||
worker.addEventListener("error", function ( event ) {
|
||||
|
||||
console.error( "Worker failed:",
|
||||
event.message, "at",
|
||||
event.filename + ":" +
|
||||
event.lineno + ":" +
|
||||
event.colno );
|
||||
|
||||
});
|
||||
|
||||
var request = {
|
||||
method: "setup",
|
||||
canvas: offscreen,
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
};
|
||||
|
||||
worker.postMessage( request, [offscreen] );
|
||||
|
||||
}
|
||||
|
||||
|
||||
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||
|
||||
for ( var i = 0; i < events.length; i++ ) {
|
||||
|
||||
let eventName = events[i];
|
||||
|
||||
console.log("createEvent", eventName);
|
||||
|
||||
canvas.addEventListener( eventName, function ( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
var request = {
|
||||
method: eventName,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
deltaY: event.deltaY
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager[ eventName ]( request );
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||
|
||||
var request = {
|
||||
method: "resetParticlePositions"
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function resizeCanvasToDisplaySize( canvas ) {
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
var request = {
|
||||
method: "resize",
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener( "resize", function () {
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<div id="controlPanel">
|
||||
<div class="inputRow">
|
||||
<button id="resetParticlesButton">Reset Spheres</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
114
demos/Texture/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:#111111;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRow label {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputRow button,
|
||||
.inputRow input {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.inputRow button:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.inputRow select {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
appearance: none; /* Remove default arrow */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% - 3px),
|
||||
calc(100% - 15px) calc(50% - 3px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inputRow select:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow option {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
color: #f0f0f5;
|
||||
}
|
||||
BIN
demos/Texture/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
demos/Texture/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
demos/Texture/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
demos/Texture/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Simplifying WebGPU with This Framework
|
||||
|
||||
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||
|
||||
## 1. Loading and Using a Shader
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||
const pipeline = device.createComputePipeline({ ... });
|
||||
// Setup bind groups and command encoder manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const shader = new Shader( "myComputeShader.wgsl" );
|
||||
|
||||
await shader.compile();
|
||||
|
||||
shader.setVariable( "someUniform", 42 );
|
||||
|
||||
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||
```
|
||||
|
||||
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||
|
||||
## 2. Setting Buffers
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create GPU buffer, write data, create bind group manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||
|
||||
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||
```
|
||||
|
||||
Buffers are bound by name with a simple call.
|
||||
|
||||
## 3. Rendering to Canvas
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create render pipeline, set vertex buffers, encode commands manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
shader.setCanvas( canvasElement );
|
||||
|
||||
shader.setAttributes( vertexData );
|
||||
|
||||
shader.execute();
|
||||
```
|
||||
|
||||
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||
|
||||
## 4. Camera and Matrix Utilities
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const camera = new Camera();
|
||||
|
||||
camera.position.set( 0, 1, 5 );
|
||||
|
||||
camera.updateViewMatrix();
|
||||
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
```
|
||||
|
||||
The framework provides built-in classes for common math tasks.
|
||||
|
||||
## 5. Event Handling
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const events = new EventManager( canvasElement );
|
||||
|
||||
events.on( "mouseMove", (event) => {
|
||||
camera.rotate( event.deltaX, event.deltaY );
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
shader.execute();
|
||||
});
|
||||
```
|
||||
|
||||
Separates input handling cleanly from GPU logic.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | WebGPU Native API | This Framework |
|
||||
| --- | --- | --- |
|
||||
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||
|
||||
---
|
||||
|
||||
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||
97
demos/Texture2/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// worker.js
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
export class Controller {
|
||||
|
||||
setup( request ) {
|
||||
|
||||
var canvas = request.canvas;
|
||||
|
||||
console.log( canvas );
|
||||
|
||||
particleSimulation.setup( canvas, request.width, request.height );
|
||||
|
||||
//self.postMessage({ type: "pong", id: request.id });
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
mousemove( request ) {
|
||||
|
||||
particleSimulation.eventManager.mousemove( request );
|
||||
|
||||
}
|
||||
|
||||
mousedown( request ) {
|
||||
|
||||
console.log("onMouseDown");
|
||||
|
||||
particleSimulation.eventManager.mousedown( request );
|
||||
|
||||
}
|
||||
|
||||
mouseup( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseup( request );
|
||||
|
||||
}
|
||||
|
||||
mouseleave( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseleave( request );
|
||||
|
||||
}
|
||||
|
||||
wheel( request ) {
|
||||
|
||||
particleSimulation.eventManager.wheel( request );
|
||||
|
||||
}
|
||||
|
||||
resize( request ) {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions( );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new Controller();
|
||||
|
||||
self.onmessage = function (event) {
|
||||
|
||||
const data = event.data;
|
||||
|
||||
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||
console.warn("Invalid request received:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||
// const request = new Request(data.method, data.payload);
|
||||
|
||||
// Or just use plain data object
|
||||
const request = data;
|
||||
|
||||
const methodName = request.method;
|
||||
|
||||
if (typeof controller[methodName] !== "function") {
|
||||
console.warn("No method found for:", request.method);
|
||||
return;
|
||||
}
|
||||
|
||||
controller[methodName](request);
|
||||
};
|
||||
300
demos/Texture2/demo.js
Normal file
@@ -0,0 +1,300 @@
|
||||
|
||||
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 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.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();
|
||||
|
||||
this.renderShader = new Shader( this.device );
|
||||
|
||||
context.configure({
|
||||
device: this.device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "opaque"
|
||||
});
|
||||
const instanceCount = 100;
|
||||
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||
|
||||
for (let i = 0; i < instanceCount; i++) {
|
||||
|
||||
const x = (i % 10) * 300.0;
|
||||
const y = Math.floor(i / 10) * 350.0;
|
||||
|
||||
instancePositions[i * 4 + 0] = x - 1000;
|
||||
instancePositions[i * 4 + 1] = 0;
|
||||
instancePositions[i * 4 + 2] = y - 1000;
|
||||
instancePositions[i * 4 + 3] = 0;
|
||||
}
|
||||
|
||||
|
||||
var model = await this.loadJSON("../../models/demo.json");
|
||||
|
||||
var mesh = model.meshes[0];
|
||||
|
||||
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||
|
||||
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||
|
||||
this.renderShader.setAttribute( "uv", mesh.texturecoords[0] );
|
||||
|
||||
var faces = mesh.faces;
|
||||
|
||||
const indexArray = new Uint32Array(faces.length * 3);
|
||||
|
||||
for (let i = 0; i < faces.length; i++) {
|
||||
|
||||
indexArray[i * 3 + 0] = faces[i][0];
|
||||
|
||||
indexArray[i * 3 + 1] = faces[i][1];
|
||||
|
||||
indexArray[i * 3 + 2] = faces[i][2];
|
||||
|
||||
}
|
||||
|
||||
this.renderShader.setIndices( indexArray );
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||
|
||||
/*
|
||||
const { vertices, normals, uvs } = this.createPlane( 1000, 1000, 4, 4 );
|
||||
|
||||
this.renderShader.setAttribute( "position", vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", normals );
|
||||
|
||||
this.renderShader.setAttribute( "uv", uvs );
|
||||
|
||||
|
||||
this.vertexCount = vertices.length / 3
|
||||
|
||||
*/
|
||||
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||
|
||||
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 );
|
||||
|
||||
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( "cameraPosition", cameraPosition );
|
||||
|
||||
|
||||
this.renderShader.renderToCanvas( this.vertexCount, 74, 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
179
demos/Texture2/index.html
Normal file
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||
</head>
|
||||
<base href="Texture2/" />
|
||||
<link rel="stylesheet" href="./style/main.css" >
|
||||
|
||||
<script type="module">
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
|
||||
var useWebWorker = false;
|
||||
|
||||
const canvas = document.querySelector(".mainCanvas");
|
||||
|
||||
particleSimulation.setCanvas( canvas );
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var worker;
|
||||
|
||||
if ( !useWebWorker ) {
|
||||
|
||||
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||
|
||||
console.log("document.bufferMap", document.bufferMap);
|
||||
|
||||
} else if ( useWebWorker ) {
|
||||
|
||||
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||
|
||||
worker.onmessage = function ( event ) {
|
||||
|
||||
console.log("From worker:", event.data);
|
||||
|
||||
};
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
|
||||
worker.addEventListener("error", function ( event ) {
|
||||
|
||||
console.error( "Worker failed:",
|
||||
event.message, "at",
|
||||
event.filename + ":" +
|
||||
event.lineno + ":" +
|
||||
event.colno );
|
||||
|
||||
});
|
||||
|
||||
var request = {
|
||||
method: "setup",
|
||||
canvas: offscreen,
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
};
|
||||
|
||||
worker.postMessage( request, [offscreen] );
|
||||
|
||||
}
|
||||
|
||||
|
||||
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||
|
||||
for ( var i = 0; i < events.length; i++ ) {
|
||||
|
||||
let eventName = events[i];
|
||||
|
||||
console.log("createEvent", eventName);
|
||||
|
||||
canvas.addEventListener( eventName, function ( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
var request = {
|
||||
method: eventName,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
deltaY: event.deltaY
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager[ eventName ]( request );
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||
|
||||
var request = {
|
||||
method: "resetParticlePositions"
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function resizeCanvasToDisplaySize( canvas ) {
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
var request = {
|
||||
method: "resize",
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener( "resize", function () {
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<div id="controlPanel">
|
||||
<div class="inputRow">
|
||||
<button id="resetParticlesButton">Reset Spheres</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
114
demos/Texture2/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:#111111;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRow label {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputRow button,
|
||||
.inputRow input {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.inputRow button:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.inputRow select {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
appearance: none; /* Remove default arrow */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% - 3px),
|
||||
calc(100% - 15px) calc(50% - 3px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inputRow select:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow option {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
color: #f0f0f5;
|
||||
}
|
||||
BIN
demos/Texture2/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
demos/Texture2/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
demos/Texture2/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
demos/Texture2/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Simplifying WebGPU with This Framework
|
||||
|
||||
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||
|
||||
## 1. Loading and Using a Shader
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||
const pipeline = device.createComputePipeline({ ... });
|
||||
// Setup bind groups and command encoder manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const shader = new Shader( "myComputeShader.wgsl" );
|
||||
|
||||
await shader.compile();
|
||||
|
||||
shader.setVariable( "someUniform", 42 );
|
||||
|
||||
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||
```
|
||||
|
||||
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||
|
||||
## 2. Setting Buffers
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create GPU buffer, write data, create bind group manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||
|
||||
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||
```
|
||||
|
||||
Buffers are bound by name with a simple call.
|
||||
|
||||
## 3. Rendering to Canvas
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create render pipeline, set vertex buffers, encode commands manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
shader.setCanvas( canvasElement );
|
||||
|
||||
shader.setAttributes( vertexData );
|
||||
|
||||
shader.execute();
|
||||
```
|
||||
|
||||
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||
|
||||
## 4. Camera and Matrix Utilities
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const camera = new Camera();
|
||||
|
||||
camera.position.set( 0, 1, 5 );
|
||||
|
||||
camera.updateViewMatrix();
|
||||
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
```
|
||||
|
||||
The framework provides built-in classes for common math tasks.
|
||||
|
||||
## 5. Event Handling
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const events = new EventManager( canvasElement );
|
||||
|
||||
events.on( "mouseMove", (event) => {
|
||||
camera.rotate( event.deltaX, event.deltaY );
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
shader.execute();
|
||||
});
|
||||
```
|
||||
|
||||
Separates input handling cleanly from GPU logic.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | WebGPU Native API | This Framework |
|
||||
| --- | --- | --- |
|
||||
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||
|
||||
---
|
||||
|
||||
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||
97
demos/Texture3/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// worker.js
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
export class Controller {
|
||||
|
||||
setup( request ) {
|
||||
|
||||
var canvas = request.canvas;
|
||||
|
||||
console.log( canvas );
|
||||
|
||||
particleSimulation.setup( canvas, request.width, request.height );
|
||||
|
||||
//self.postMessage({ type: "pong", id: request.id });
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
mousemove( request ) {
|
||||
|
||||
particleSimulation.eventManager.mousemove( request );
|
||||
|
||||
}
|
||||
|
||||
mousedown( request ) {
|
||||
|
||||
console.log("onMouseDown");
|
||||
|
||||
particleSimulation.eventManager.mousedown( request );
|
||||
|
||||
}
|
||||
|
||||
mouseup( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseup( request );
|
||||
|
||||
}
|
||||
|
||||
mouseleave( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseleave( request );
|
||||
|
||||
}
|
||||
|
||||
wheel( request ) {
|
||||
|
||||
particleSimulation.eventManager.wheel( request );
|
||||
|
||||
}
|
||||
|
||||
resize( request ) {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions( );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new Controller();
|
||||
|
||||
self.onmessage = function (event) {
|
||||
|
||||
const data = event.data;
|
||||
|
||||
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||
console.warn("Invalid request received:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||
// const request = new Request(data.method, data.payload);
|
||||
|
||||
// Or just use plain data object
|
||||
const request = data;
|
||||
|
||||
const methodName = request.method;
|
||||
|
||||
if (typeof controller[methodName] !== "function") {
|
||||
console.warn("No method found for:", request.method);
|
||||
return;
|
||||
}
|
||||
|
||||
controller[methodName](request);
|
||||
};
|
||||
307
demos/Texture3/demo.js
Normal file
@@ -0,0 +1,307 @@
|
||||
|
||||
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 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.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();
|
||||
|
||||
this.renderShader = new Shader( this.device );
|
||||
|
||||
context.configure({
|
||||
device: this.device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "opaque"
|
||||
});
|
||||
const instanceCount = 100;
|
||||
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||
|
||||
for (let i = 0; i < instanceCount; i++) {
|
||||
|
||||
const x = (i % 10) * 300.0;
|
||||
const y = Math.floor(i / 10) * 350.0;
|
||||
|
||||
instancePositions[i * 4 + 0] = x - 1000;
|
||||
instancePositions[i * 4 + 1] = 0;
|
||||
instancePositions[i * 4 + 2] = y - 1000;
|
||||
instancePositions[i * 4 + 3] = 0;
|
||||
}
|
||||
|
||||
|
||||
var model = await this.loadJSON("../../models/demo.json");
|
||||
|
||||
var mesh = model.meshes[0];
|
||||
|
||||
console.log("mesh", mesh);
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list-texture-normal.wgsl");
|
||||
|
||||
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||
|
||||
this.renderShader.setAttribute( "bitangent", mesh.bitangents );
|
||||
|
||||
this.renderShader.setAttribute( "uv", mesh.texturecoords[0] );
|
||||
|
||||
var faces = mesh.faces;
|
||||
|
||||
const indexArray = new Uint32Array(faces.length * 3);
|
||||
|
||||
for (let i = 0; i < faces.length; i++) {
|
||||
|
||||
indexArray[i * 3 + 0] = faces[i][0];
|
||||
|
||||
indexArray[i * 3 + 1] = faces[i][1];
|
||||
|
||||
indexArray[i * 3 + 2] = faces[i][2];
|
||||
|
||||
}
|
||||
|
||||
this.renderShader.setIndices( indexArray );
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list-texture-normal.wgsl");
|
||||
|
||||
/*
|
||||
const { vertices, normals, uvs } = this.createPlane( 1000, 1000, 4, 4 );
|
||||
|
||||
this.renderShader.setAttribute( "position", vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", normals );
|
||||
|
||||
this.renderShader.setAttribute( "uv", uvs );
|
||||
|
||||
|
||||
this.vertexCount = vertices.length / 3
|
||||
|
||||
*/
|
||||
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||
|
||||
var texture = await this.loadTexture("./textures/0_floorTiles_diff.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 );
|
||||
|
||||
var normalTexture = await this.loadTexture("./textures/0_floorTiles_ddn.png");
|
||||
|
||||
this.renderShader.setVariable( "normalMapTexture", normalTexture );
|
||||
|
||||
|
||||
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( "cameraPosition", cameraPosition );
|
||||
|
||||
|
||||
this.renderShader.renderToCanvas( this.vertexCount, 74, 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
180
demos/Texture3/index.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||
</head>
|
||||
|
||||
<link rel="stylesheet" href="./style/main.css" >
|
||||
|
||||
<base href="Texture3/" />
|
||||
<script type="module">
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
|
||||
var useWebWorker = false;
|
||||
|
||||
const canvas = document.querySelector(".mainCanvas");
|
||||
|
||||
particleSimulation.setCanvas( canvas );
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var worker;
|
||||
|
||||
if ( !useWebWorker ) {
|
||||
|
||||
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||
|
||||
console.log("document.bufferMap", document.bufferMap);
|
||||
|
||||
} else if ( useWebWorker ) {
|
||||
|
||||
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||
|
||||
worker.onmessage = function ( event ) {
|
||||
|
||||
console.log("From worker:", event.data);
|
||||
|
||||
};
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
|
||||
worker.addEventListener("error", function ( event ) {
|
||||
|
||||
console.error( "Worker failed:",
|
||||
event.message, "at",
|
||||
event.filename + ":" +
|
||||
event.lineno + ":" +
|
||||
event.colno );
|
||||
|
||||
});
|
||||
|
||||
var request = {
|
||||
method: "setup",
|
||||
canvas: offscreen,
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
};
|
||||
|
||||
worker.postMessage( request, [offscreen] );
|
||||
|
||||
}
|
||||
|
||||
|
||||
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||
|
||||
for ( var i = 0; i < events.length; i++ ) {
|
||||
|
||||
let eventName = events[i];
|
||||
|
||||
console.log("createEvent", eventName);
|
||||
|
||||
canvas.addEventListener( eventName, function ( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
var request = {
|
||||
method: eventName,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
deltaY: event.deltaY
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager[ eventName ]( request );
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||
|
||||
var request = {
|
||||
method: "resetParticlePositions"
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function resizeCanvasToDisplaySize( canvas ) {
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
var request = {
|
||||
method: "resize",
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener( "resize", function () {
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<div id="controlPanel">
|
||||
<div class="inputRow">
|
||||
<button id="resetParticlesButton">Reset Spheres</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
114
demos/Texture3/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:#111111;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRow label {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputRow button,
|
||||
.inputRow input {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.inputRow button:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.inputRow select {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
appearance: none; /* Remove default arrow */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% - 3px),
|
||||
calc(100% - 15px) calc(50% - 3px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inputRow select:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow option {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
color: #f0f0f5;
|
||||
}
|
||||
BIN
demos/Texture3/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
demos/Texture3/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
demos/Texture3/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
demos/Texture3/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Simplifying WebGPU with This Framework
|
||||
|
||||
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||
|
||||
## 1. Loading and Using a Shader
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||
const pipeline = device.createComputePipeline({ ... });
|
||||
// Setup bind groups and command encoder manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const shader = new Shader( "myComputeShader.wgsl" );
|
||||
|
||||
await shader.compile();
|
||||
|
||||
shader.setVariable( "someUniform", 42 );
|
||||
|
||||
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||
```
|
||||
|
||||
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||
|
||||
## 2. Setting Buffers
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create GPU buffer, write data, create bind group manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||
|
||||
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||
```
|
||||
|
||||
Buffers are bound by name with a simple call.
|
||||
|
||||
## 3. Rendering to Canvas
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create render pipeline, set vertex buffers, encode commands manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
shader.setCanvas( canvasElement );
|
||||
|
||||
shader.setAttributes( vertexData );
|
||||
|
||||
shader.execute();
|
||||
```
|
||||
|
||||
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||
|
||||
## 4. Camera and Matrix Utilities
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const camera = new Camera();
|
||||
|
||||
camera.position.set( 0, 1, 5 );
|
||||
|
||||
camera.updateViewMatrix();
|
||||
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
```
|
||||
|
||||
The framework provides built-in classes for common math tasks.
|
||||
|
||||
## 5. Event Handling
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const events = new EventManager( canvasElement );
|
||||
|
||||
events.on( "mouseMove", (event) => {
|
||||
camera.rotate( event.deltaX, event.deltaY );
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
shader.execute();
|
||||
});
|
||||
```
|
||||
|
||||
Separates input handling cleanly from GPU logic.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | WebGPU Native API | This Framework |
|
||||
| --- | --- | --- |
|
||||
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||
|
||||
---
|
||||
|
||||
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||
97
demos/TextureArray/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// worker.js
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
export class Controller {
|
||||
|
||||
setup( request ) {
|
||||
|
||||
var canvas = request.canvas;
|
||||
|
||||
console.log( canvas );
|
||||
|
||||
particleSimulation.setup( canvas, request.width, request.height );
|
||||
|
||||
//self.postMessage({ type: "pong", id: request.id });
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
mousemove( request ) {
|
||||
|
||||
particleSimulation.eventManager.mousemove( request );
|
||||
|
||||
}
|
||||
|
||||
mousedown( request ) {
|
||||
|
||||
console.log("onMouseDown");
|
||||
|
||||
particleSimulation.eventManager.mousedown( request );
|
||||
|
||||
}
|
||||
|
||||
mouseup( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseup( request );
|
||||
|
||||
}
|
||||
|
||||
mouseleave( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseleave( request );
|
||||
|
||||
}
|
||||
|
||||
wheel( request ) {
|
||||
|
||||
particleSimulation.eventManager.wheel( request );
|
||||
|
||||
}
|
||||
|
||||
resize( request ) {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions( );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new Controller();
|
||||
|
||||
self.onmessage = function (event) {
|
||||
|
||||
const data = event.data;
|
||||
|
||||
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||
console.warn("Invalid request received:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||
// const request = new Request(data.method, data.payload);
|
||||
|
||||
// Or just use plain data object
|
||||
const request = data;
|
||||
|
||||
const methodName = request.method;
|
||||
|
||||
if (typeof controller[methodName] !== "function") {
|
||||
console.warn("No method found for:", request.method);
|
||||
return;
|
||||
}
|
||||
|
||||
controller[methodName](request);
|
||||
};
|
||||
351
demos/TextureArray/demo.js
Normal file
@@ -0,0 +1,351 @@
|
||||
|
||||
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 loadImagesFromFile( filePaths ) {
|
||||
|
||||
var imageBitmaps = new Array();
|
||||
|
||||
for (var i = 0; i < filePaths.length; i++) {
|
||||
|
||||
var filePath = filePaths[i]
|
||||
|
||||
const imageBitmap = await this.loadImageBitmap( filePath );
|
||||
|
||||
imageBitmaps.push( imageBitmap );
|
||||
|
||||
}
|
||||
|
||||
return this.createTextureArray(this.device, 512, 512, imageBitmaps.length, imageBitmaps);
|
||||
|
||||
}
|
||||
|
||||
async createTextureArray(device, width, height, layerCount, imageBitmaps) {
|
||||
|
||||
const texture = device.createTexture({
|
||||
size: {
|
||||
width: width,
|
||||
height: height,
|
||||
depthOrArrayLayers: layerCount,
|
||||
},
|
||||
format: "rgba8unorm",
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.COPY_DST |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
for (let layer = 0; layer < layerCount; layer++) {
|
||||
const imageBitmap = imageBitmaps[layer];
|
||||
|
||||
device.queue.copyExternalImageToTexture(
|
||||
{ source: imageBitmap },
|
||||
{ texture: texture, origin: { x: 0, y: 0, z: layer } },
|
||||
[ imageBitmap.width, imageBitmap.height, 1 ]
|
||||
);
|
||||
}
|
||||
|
||||
const textureView = texture.createView({
|
||||
dimension: "2d-array",
|
||||
baseArrayLayer: 0,
|
||||
arrayLayerCount: layerCount,
|
||||
});
|
||||
|
||||
return { texture, textureView };
|
||||
}
|
||||
|
||||
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 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.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();
|
||||
|
||||
this.renderShader = new Shader( this.device );
|
||||
|
||||
context.configure({
|
||||
device: this.device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "opaque"
|
||||
});
|
||||
const instanceCount = 100;
|
||||
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||
|
||||
for (let i = 0; i < instanceCount; i++) {
|
||||
|
||||
const x = (i % 10) * 300.0;
|
||||
const y = Math.floor(i / 10) * 350.0;
|
||||
|
||||
instancePositions[i * 4 + 0] = x - 1000;
|
||||
instancePositions[i * 4 + 1] = 0;
|
||||
instancePositions[i * 4 + 2] = y - 1000;
|
||||
instancePositions[i * 4 + 3] = 0;
|
||||
}
|
||||
|
||||
|
||||
var model = await this.loadJSON("../../models/demo.json");
|
||||
|
||||
var mesh = model.meshes[0];
|
||||
|
||||
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list-texture-array.wgsl");
|
||||
|
||||
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||
|
||||
this.renderShader.setAttribute( "uv", mesh.texturecoords[0] );
|
||||
|
||||
var faces = mesh.faces;
|
||||
|
||||
const indexArray = new Uint32Array(faces.length * 3);
|
||||
|
||||
for (let i = 0; i < faces.length; i++) {
|
||||
|
||||
indexArray[i * 3 + 0] = faces[i][0];
|
||||
|
||||
indexArray[i * 3 + 1] = faces[i][1];
|
||||
|
||||
indexArray[i * 3 + 2] = faces[i][2];
|
||||
|
||||
}
|
||||
|
||||
this.renderShader.setIndices( indexArray );
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||
|
||||
/*
|
||||
const { vertices, normals, uvs } = this.createPlane( 1000, 1000, 4, 4 );
|
||||
|
||||
this.renderShader.setAttribute( "position", vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", normals );
|
||||
|
||||
this.renderShader.setAttribute( "uv", uvs );
|
||||
|
||||
|
||||
this.vertexCount = vertices.length / 3
|
||||
|
||||
*/
|
||||
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||
|
||||
//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( "myTextureArray", texture );
|
||||
|
||||
//var myTextureArray = this.loadImagesFromFile( ["./textures/defaultnouvs.png", "./textures/0_floorTiles_diff.png"] );
|
||||
//this.renderShader.setVariable( "myTextureArray", texture );
|
||||
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( "cameraPosition", cameraPosition );
|
||||
|
||||
|
||||
this.renderShader.renderToCanvas( this.vertexCount, 74, 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
844040
demos/TextureArray/demo.json
Executable file
179
demos/TextureArray/index.html
Normal file
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||
</head>
|
||||
<base href="TextureArray/" />
|
||||
<link rel="stylesheet" href="./style/main.css" >
|
||||
|
||||
<script type="module">
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
|
||||
var useWebWorker = false;
|
||||
|
||||
const canvas = document.querySelector(".mainCanvas");
|
||||
|
||||
particleSimulation.setCanvas( canvas );
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var worker;
|
||||
|
||||
if ( !useWebWorker ) {
|
||||
|
||||
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||
|
||||
console.log("document.bufferMap", document.bufferMap);
|
||||
|
||||
} else if ( useWebWorker ) {
|
||||
|
||||
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||
|
||||
worker.onmessage = function ( event ) {
|
||||
|
||||
console.log("From worker:", event.data);
|
||||
|
||||
};
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
|
||||
worker.addEventListener("error", function ( event ) {
|
||||
|
||||
console.error( "Worker failed:",
|
||||
event.message, "at",
|
||||
event.filename + ":" +
|
||||
event.lineno + ":" +
|
||||
event.colno );
|
||||
|
||||
});
|
||||
|
||||
var request = {
|
||||
method: "setup",
|
||||
canvas: offscreen,
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
};
|
||||
|
||||
worker.postMessage( request, [offscreen] );
|
||||
|
||||
}
|
||||
|
||||
|
||||
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||
|
||||
for ( var i = 0; i < events.length; i++ ) {
|
||||
|
||||
let eventName = events[i];
|
||||
|
||||
console.log("createEvent", eventName);
|
||||
|
||||
canvas.addEventListener( eventName, function ( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
var request = {
|
||||
method: eventName,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
deltaY: event.deltaY
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager[ eventName ]( request );
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||
|
||||
var request = {
|
||||
method: "resetParticlePositions"
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function resizeCanvasToDisplaySize( canvas ) {
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
var request = {
|
||||
method: "resize",
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener( "resize", function () {
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<div id="controlPanel">
|
||||
<div class="inputRow">
|
||||
<button id="resetParticlesButton">Reset Spheres</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
114
demos/TextureArray/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:#111111;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRow label {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputRow button,
|
||||
.inputRow input {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.inputRow button:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.inputRow select {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
appearance: none; /* Remove default arrow */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% - 3px),
|
||||
calc(100% - 15px) calc(50% - 3px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inputRow select:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow option {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
color: #f0f0f5;
|
||||
}
|
||||
BIN
demos/TextureArray/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
demos/TextureArray/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
demos/TextureArray/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
demos/TextureArray/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Simplifying WebGPU with This Framework
|
||||
|
||||
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||
|
||||
## 1. Loading and Using a Shader
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||
const pipeline = device.createComputePipeline({ ... });
|
||||
// Setup bind groups and command encoder manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const shader = new Shader( "myComputeShader.wgsl" );
|
||||
|
||||
await shader.compile();
|
||||
|
||||
shader.setVariable( "someUniform", 42 );
|
||||
|
||||
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||
```
|
||||
|
||||
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||
|
||||
## 2. Setting Buffers
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create GPU buffer, write data, create bind group manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||
|
||||
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||
```
|
||||
|
||||
Buffers are bound by name with a simple call.
|
||||
|
||||
## 3. Rendering to Canvas
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create render pipeline, set vertex buffers, encode commands manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
shader.setCanvas( canvasElement );
|
||||
|
||||
shader.setAttributes( vertexData );
|
||||
|
||||
shader.execute();
|
||||
```
|
||||
|
||||
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||
|
||||
## 4. Camera and Matrix Utilities
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const camera = new Camera();
|
||||
|
||||
camera.position.set( 0, 1, 5 );
|
||||
|
||||
camera.updateViewMatrix();
|
||||
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
```
|
||||
|
||||
The framework provides built-in classes for common math tasks.
|
||||
|
||||
## 5. Event Handling
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const events = new EventManager( canvasElement );
|
||||
|
||||
events.on( "mouseMove", (event) => {
|
||||
camera.rotate( event.deltaX, event.deltaY );
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
shader.execute();
|
||||
});
|
||||
```
|
||||
|
||||
Separates input handling cleanly from GPU logic.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | WebGPU Native API | This Framework |
|
||||
| --- | --- | --- |
|
||||
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||
|
||||
---
|
||||
|
||||
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||
97
demos/Triangles/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// worker.js
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
export class Controller {
|
||||
|
||||
setup( request ) {
|
||||
|
||||
var canvas = request.canvas;
|
||||
|
||||
console.log( canvas );
|
||||
|
||||
particleSimulation.setup( canvas, request.width, request.height );
|
||||
|
||||
//self.postMessage({ type: "pong", id: request.id });
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
mousemove( request ) {
|
||||
|
||||
particleSimulation.eventManager.mousemove( request );
|
||||
|
||||
}
|
||||
|
||||
mousedown( request ) {
|
||||
|
||||
console.log("onMouseDown");
|
||||
|
||||
particleSimulation.eventManager.mousedown( request );
|
||||
|
||||
}
|
||||
|
||||
mouseup( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseup( request );
|
||||
|
||||
}
|
||||
|
||||
mouseleave( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseleave( request );
|
||||
|
||||
}
|
||||
|
||||
wheel( request ) {
|
||||
|
||||
particleSimulation.eventManager.wheel( request );
|
||||
|
||||
}
|
||||
|
||||
resize( request ) {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions( );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new Controller();
|
||||
|
||||
self.onmessage = function (event) {
|
||||
|
||||
const data = event.data;
|
||||
|
||||
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||
console.warn("Invalid request received:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||
// const request = new Request(data.method, data.payload);
|
||||
|
||||
// Or just use plain data object
|
||||
const request = data;
|
||||
|
||||
const methodName = request.method;
|
||||
|
||||
if (typeof controller[methodName] !== "function") {
|
||||
console.warn("No method found for:", request.method);
|
||||
return;
|
||||
}
|
||||
|
||||
controller[methodName](request);
|
||||
};
|
||||
200
demos/Triangles/demo.js
Normal file
@@ -0,0 +1,200 @@
|
||||
|
||||
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;
|
||||
|
||||
const context = offscreenCanvas.getContext("webgpu");
|
||||
|
||||
this.camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
|
||||
|
||||
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();
|
||||
|
||||
this.renderShader = new Shader( this.device );
|
||||
|
||||
context.configure({
|
||||
device: this.device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "opaque"
|
||||
});
|
||||
|
||||
var model = await this.loadJSON("../../models/demo.json");
|
||||
|
||||
var mesh = model.meshes[0];
|
||||
|
||||
console.log( mesh );
|
||||
|
||||
const vertices = new Float32Array([
|
||||
0.0, 0.5, // 0 - top outer point
|
||||
0.1123, 0.1545, // 1 - inner point between top and right
|
||||
0.4755, 0.1545, // 2 - right outer point
|
||||
0.1816, -0.0590, // 3 - inner point between right and bottom right
|
||||
0.2939, -0.4045, // 4 - bottom right outer point
|
||||
0.0, -0.1909, // 5 - inner bottom point
|
||||
-0.2939,-0.4045, // 6 - bottom left outer point
|
||||
-0.1816, -0.0590,// 7 - inner point between bottom left and left
|
||||
-0.4755, 0.1545, // 8 - left outer point
|
||||
-0.1123, 0.1545, // 9 - inner point between left and top
|
||||
]);
|
||||
|
||||
const indices = new Uint16Array([
|
||||
0, 1, 9,
|
||||
1, 2, 3,
|
||||
3, 4, 5,
|
||||
5, 6, 7,
|
||||
7, 8, 9,
|
||||
1, 3, 5,
|
||||
1, 5, 7,
|
||||
1, 7, 9,
|
||||
]);
|
||||
|
||||
const instanceCount = 100;
|
||||
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||
|
||||
for (let i = 0; i < instanceCount; i++) {
|
||||
|
||||
const x = (i % 10) * 300.0;
|
||||
const y = Math.floor(i / 10) * 350.0;
|
||||
|
||||
instancePositions[i * 4 + 0] = x - 500;
|
||||
instancePositions[i * 4 + 1] = 0;
|
||||
instancePositions[i * 4 + 2] = y - 500;
|
||||
instancePositions[i * 4 + 3] = 0;
|
||||
}
|
||||
|
||||
this.renderShader.setCanvas( this.canvas );
|
||||
|
||||
this.renderShader.topology = "triangle-list";
|
||||
|
||||
await this.renderShader.setup( "../../shaders/triangle-list.wgsl");
|
||||
|
||||
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||
|
||||
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||
|
||||
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||
|
||||
|
||||
var faces = mesh.faces;
|
||||
|
||||
const indexArray = new Uint32Array(faces.length * 3);
|
||||
|
||||
for (let i = 0; i < faces.length; i++) {
|
||||
|
||||
indexArray[i * 3 + 0] = faces[i][0];
|
||||
|
||||
indexArray[i * 3 + 1] = faces[i][1];
|
||||
|
||||
indexArray[i * 3 + 2] = faces[i][2];
|
||||
|
||||
}
|
||||
|
||||
this.renderShader.setIndices( indexArray );
|
||||
|
||||
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( "cameraPosition", cameraPosition );
|
||||
|
||||
|
||||
this.renderShader.renderToCanvas( this.vertexCount, 60, 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
180
demos/Triangles/index.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||
<base href="Triangles/" />
|
||||
</head>
|
||||
|
||||
<link rel="stylesheet" href="./style/main.css" >
|
||||
|
||||
<script type="module">
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
|
||||
var useWebWorker = false;
|
||||
|
||||
const canvas = document.querySelector(".mainCanvas");
|
||||
|
||||
particleSimulation.setCanvas( canvas );
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var worker;
|
||||
|
||||
if ( !useWebWorker ) {
|
||||
|
||||
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||
|
||||
console.log("document.bufferMap", document.bufferMap);
|
||||
|
||||
} else if ( useWebWorker ) {
|
||||
|
||||
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||
|
||||
worker.onmessage = function ( event ) {
|
||||
|
||||
console.log("From worker:", event.data);
|
||||
|
||||
};
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
|
||||
worker.addEventListener("error", function ( event ) {
|
||||
|
||||
console.error( "Worker failed:",
|
||||
event.message, "at",
|
||||
event.filename + ":" +
|
||||
event.lineno + ":" +
|
||||
event.colno );
|
||||
|
||||
});
|
||||
|
||||
var request = {
|
||||
method: "setup",
|
||||
canvas: offscreen,
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
};
|
||||
|
||||
worker.postMessage( request, [offscreen] );
|
||||
|
||||
}
|
||||
|
||||
|
||||
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||
|
||||
for ( var i = 0; i < events.length; i++ ) {
|
||||
|
||||
let eventName = events[i];
|
||||
|
||||
console.log("createEvent", eventName);
|
||||
|
||||
canvas.addEventListener( eventName, function ( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
var request = {
|
||||
method: eventName,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
deltaY: event.deltaY
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager[ eventName ]( request );
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||
|
||||
var request = {
|
||||
method: "resetParticlePositions"
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function resizeCanvasToDisplaySize( canvas ) {
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
var request = {
|
||||
method: "resize",
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener( "resize", function () {
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<div id="controlPanel">
|
||||
<div class="inputRow">
|
||||
<button id="resetParticlesButton">Reset Spheres</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
114
demos/Triangles/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:#111111;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRow label {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputRow button,
|
||||
.inputRow input {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.inputRow button:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.inputRow select {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
appearance: none; /* Remove default arrow */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% - 3px),
|
||||
calc(100% - 15px) calc(50% - 3px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inputRow select:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow option {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
color: #f0f0f5;
|
||||
}
|
||||
114
demos/Triangles/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Simplifying WebGPU with This Framework
|
||||
|
||||
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||
|
||||
## 1. Loading and Using a Shader
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||
const pipeline = device.createComputePipeline({ ... });
|
||||
// Setup bind groups and command encoder manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const shader = new Shader( "myComputeShader.wgsl" );
|
||||
|
||||
await shader.compile();
|
||||
|
||||
shader.setVariable( "someUniform", 42 );
|
||||
|
||||
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||
```
|
||||
|
||||
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||
|
||||
## 2. Setting Buffers
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create GPU buffer, write data, create bind group manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||
|
||||
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||
```
|
||||
|
||||
Buffers are bound by name with a simple call.
|
||||
|
||||
## 3. Rendering to Canvas
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create render pipeline, set vertex buffers, encode commands manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
shader.setCanvas( canvasElement );
|
||||
|
||||
shader.setAttributes( vertexData );
|
||||
|
||||
shader.execute();
|
||||
```
|
||||
|
||||
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||
|
||||
## 4. Camera and Matrix Utilities
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const camera = new Camera();
|
||||
|
||||
camera.position.set( 0, 1, 5 );
|
||||
|
||||
camera.updateViewMatrix();
|
||||
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
```
|
||||
|
||||
The framework provides built-in classes for common math tasks.
|
||||
|
||||
## 5. Event Handling
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const events = new EventManager( canvasElement );
|
||||
|
||||
events.on( "mouseMove", (event) => {
|
||||
camera.rotate( event.deltaX, event.deltaY );
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
shader.execute();
|
||||
});
|
||||
```
|
||||
|
||||
Separates input handling cleanly from GPU logic.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | WebGPU Native API | This Framework |
|
||||
| --- | --- | --- |
|
||||
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||
|
||||
---
|
||||
|
||||
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||
97
demos/particleHeader/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// worker.js
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
export class Controller {
|
||||
|
||||
setup( request ) {
|
||||
|
||||
var canvas = request.canvas;
|
||||
|
||||
console.log( canvas );
|
||||
|
||||
particleSimulation.setup( canvas, request.width, request.height );
|
||||
|
||||
//self.postMessage({ type: "pong", id: request.id });
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
mousemove( request ) {
|
||||
|
||||
particleSimulation.eventManager.mousemove( request );
|
||||
|
||||
}
|
||||
|
||||
mousedown( request ) {
|
||||
|
||||
console.log("onMouseDown");
|
||||
|
||||
particleSimulation.eventManager.mousedown( request );
|
||||
|
||||
}
|
||||
|
||||
mouseup( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseup( request );
|
||||
|
||||
}
|
||||
|
||||
mouseleave( request ) {
|
||||
|
||||
particleSimulation.eventManager.mouseleave( request );
|
||||
|
||||
}
|
||||
|
||||
wheel( request ) {
|
||||
|
||||
particleSimulation.eventManager.wheel( request );
|
||||
|
||||
}
|
||||
|
||||
resize( request ) {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
resetParticlePositions() {
|
||||
|
||||
particleSimulation.resetParticlePositions( );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new Controller();
|
||||
|
||||
self.onmessage = function (event) {
|
||||
|
||||
const data = event.data;
|
||||
|
||||
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||
console.warn("Invalid request received:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||
// const request = new Request(data.method, data.payload);
|
||||
|
||||
// Or just use plain data object
|
||||
const request = data;
|
||||
|
||||
const methodName = request.method;
|
||||
|
||||
if (typeof controller[methodName] !== "function") {
|
||||
console.warn("No method found for:", request.method);
|
||||
return;
|
||||
}
|
||||
|
||||
controller[methodName](request);
|
||||
};
|
||||
431
demos/particleHeader/demo.js
Normal file
@@ -0,0 +1,431 @@
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
195
demos/particleHeader/index.html
Normal file
@@ -0,0 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||
</head>
|
||||
<base href="particleHeader/" />
|
||||
<link rel="stylesheet" href="./style/main.css" >
|
||||
|
||||
<script type="module">
|
||||
|
||||
import { ParticleSimulation } from "./demo.js";
|
||||
|
||||
|
||||
var particleSimulation = new ParticleSimulation();
|
||||
|
||||
|
||||
var useWebWorker = false;
|
||||
|
||||
const canvas = document.querySelector(".mainCanvas");
|
||||
|
||||
particleSimulation.setCanvas( canvas );
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var worker;
|
||||
|
||||
if ( !useWebWorker ) {
|
||||
|
||||
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||
|
||||
console.log("document.bufferMap", document.bufferMap);
|
||||
|
||||
} else if ( useWebWorker ) {
|
||||
|
||||
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||
|
||||
worker.onmessage = function ( event ) {
|
||||
|
||||
console.log("From worker:", event.data);
|
||||
|
||||
};
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
|
||||
worker.addEventListener("error", function ( event ) {
|
||||
|
||||
console.error( "Worker failed:",
|
||||
event.message, "at",
|
||||
event.filename + ":" +
|
||||
event.lineno + ":" +
|
||||
event.colno );
|
||||
|
||||
});
|
||||
|
||||
var request = {
|
||||
method: "setup",
|
||||
canvas: offscreen,
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
};
|
||||
|
||||
worker.postMessage( request, [offscreen] );
|
||||
|
||||
}
|
||||
|
||||
|
||||
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||
|
||||
for ( var i = 0; i < events.length; i++ ) {
|
||||
|
||||
let eventName = events[i];
|
||||
|
||||
console.log("createEvent", eventName);
|
||||
|
||||
canvas.addEventListener( eventName, function ( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
var request = {
|
||||
method: eventName,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
deltaY: event.deltaY
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager[ eventName ]( request );
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||
|
||||
var request = {
|
||||
method: "resetParticlePositions"
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.resetParticlePositions();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function resizeCanvasToDisplaySize( canvas ) {
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
var request = {
|
||||
method: "resize",
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
if ( useWebWorker ) {
|
||||
|
||||
worker.postMessage( request );
|
||||
|
||||
} else {
|
||||
|
||||
particleSimulation.eventManager.resize( request );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener( "resize", function () {
|
||||
|
||||
resizeCanvasToDisplaySize( canvas );
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
|
||||
<body>
|
||||
<div id="controlPanel">
|
||||
<div class="inputRow">
|
||||
<button id="resetParticlesButton">Reset Spheres</button>
|
||||
</div>
|
||||
</div><div id="particleSource" xmlns="http://www.w3.org/1999/xhtml" style="
|
||||
width: 882px;
|
||||
height: 67px;
|
||||
background: linear-gradient(135deg, #ff6600, #ffcc00);
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.3);
|
||||
">
|
||||
Hello GPU!
|
||||
</div>
|
||||
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
114
demos/particleHeader/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:#111111;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRow label {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputRow button,
|
||||
.inputRow input {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.inputRow button:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.inputRow select {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
appearance: none; /* Remove default arrow */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% - 3px),
|
||||
calc(100% - 15px) calc(50% - 3px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inputRow select:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow option {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
color: #f0f0f5;
|
||||
}
|
||||
BIN
demos/particleHeader/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
demos/particleHeader/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
demos/particleHeader/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
demos/particleHeader/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Simplifying WebGPU with This Framework
|
||||
|
||||
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||
|
||||
## 1. Loading and Using a Shader
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||
const pipeline = device.createComputePipeline({ ... });
|
||||
// Setup bind groups and command encoder manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const shader = new Shader( "myComputeShader.wgsl" );
|
||||
|
||||
await shader.compile();
|
||||
|
||||
shader.setVariable( "someUniform", 42 );
|
||||
|
||||
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||
```
|
||||
|
||||
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||
|
||||
## 2. Setting Buffers
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create GPU buffer, write data, create bind group manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||
|
||||
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||
```
|
||||
|
||||
Buffers are bound by name with a simple call.
|
||||
|
||||
## 3. Rendering to Canvas
|
||||
|
||||
**Without Framework:**
|
||||
|
||||
```
|
||||
Create render pipeline, set vertex buffers, encode commands manually
|
||||
```
|
||||
|
||||
**With Framework:**
|
||||
|
||||
```
|
||||
shader.setCanvas( canvasElement );
|
||||
|
||||
shader.setAttributes( vertexData );
|
||||
|
||||
shader.execute();
|
||||
```
|
||||
|
||||
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||
|
||||
## 4. Camera and Matrix Utilities
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const camera = new Camera();
|
||||
|
||||
camera.position.set( 0, 1, 5 );
|
||||
|
||||
camera.updateViewMatrix();
|
||||
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
```
|
||||
|
||||
The framework provides built-in classes for common math tasks.
|
||||
|
||||
## 5. Event Handling
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
const events = new EventManager( canvasElement );
|
||||
|
||||
events.on( "mouseMove", (event) => {
|
||||
camera.rotate( event.deltaX, event.deltaY );
|
||||
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||
shader.execute();
|
||||
});
|
||||
```
|
||||
|
||||
Separates input handling cleanly from GPU logic.
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Task | WebGPU Native API | This Framework |
|
||||
| --- | --- | --- |
|
||||
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||
|
||||
---
|
||||
|
||||
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||
170
demos/sort/demo.js
Normal file
@@ -0,0 +1,170 @@
|
||||
|
||||
import Shader from "../../framework/WebGpu.js"
|
||||
|
||||
import Measure from "../../framework/Measure.js"
|
||||
|
||||
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
|
||||
if (!adapter ) {
|
||||
|
||||
throw new Error("Failed to get GPU adapter");
|
||||
|
||||
}
|
||||
|
||||
const particleCount = 131072;
|
||||
|
||||
const workgroupSize = 256;
|
||||
|
||||
const numWorkgroups = Math.ceil( particleCount / workgroupSize );
|
||||
|
||||
const device = await adapter.requestDevice();
|
||||
|
||||
|
||||
var passIndex = 0;
|
||||
|
||||
var jArray = new Array();
|
||||
|
||||
var kArray = new Array();
|
||||
|
||||
|
||||
for ( let k = 512; k <= particleCount; k <<= 1 ) {
|
||||
|
||||
for ( let j = k >> 1; j > 0; j >>= 1 ) {
|
||||
|
||||
jArray.push( j );
|
||||
|
||||
kArray.push( k );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const indices = new Uint32Array( particleCount );
|
||||
|
||||
const threadPassIndices = new Uint32Array( particleCount );
|
||||
|
||||
for (var i = 0; i < particleCount; i++) {
|
||||
|
||||
indices[particleCount-i-1] = i;
|
||||
|
||||
threadPassIndices[0] = 0;
|
||||
|
||||
}
|
||||
|
||||
const localSortShader = new Shader( device, "./shaders/localSort.wgsl");
|
||||
|
||||
await localSortShader.addStage("main", GPUShaderStage.COMPUTE );
|
||||
|
||||
localSortShader.setVariable( "compare", indices );
|
||||
|
||||
localSortShader.setVariable( "totalCount", particleCount );
|
||||
|
||||
|
||||
const bitonicSortGridHashShader = new Shader( device, "./shaders/bitonicSortUIntMultiPass.wgsl");
|
||||
|
||||
await bitonicSortGridHashShader.addStage("main", GPUShaderStage.COMPUTE );
|
||||
|
||||
bitonicSortGridHashShader.setVariable( "totalCount", particleCount );
|
||||
|
||||
bitonicSortGridHashShader.setBuffer( "compare", localSortShader.getBuffer("compare") );
|
||||
|
||||
bitonicSortGridHashShader.setVariable( "jArray", jArray );
|
||||
|
||||
bitonicSortGridHashShader.setVariable( "kArray", kArray );
|
||||
|
||||
|
||||
const measure = new Measure();
|
||||
|
||||
measure.writeToPage = true;
|
||||
|
||||
measure.element = document.querySelector(".result");
|
||||
|
||||
for (var i = 3; i < 59; i++) {
|
||||
|
||||
bitonicSortGridHashShader.setVariable("threadPassIndices", threadPassIndices);
|
||||
|
||||
measure.start( "sort"+i );
|
||||
|
||||
//await localSortShader.execute( numWorkgroups );
|
||||
|
||||
const commandEncoder = device.createCommandEncoder();
|
||||
|
||||
|
||||
// === Local sort ===
|
||||
{
|
||||
const passEncoder = commandEncoder.beginComputePass();
|
||||
|
||||
passEncoder.setPipeline( localSortShader.pipeline );
|
||||
|
||||
for ( const [ index, bindGroup ] of localSortShader.bindGroups.entries() ) {
|
||||
|
||||
passEncoder.setBindGroup( index, bindGroup );
|
||||
|
||||
}
|
||||
|
||||
passEncoder.dispatchWorkgroups( numWorkgroups );
|
||||
|
||||
passEncoder.end();
|
||||
}
|
||||
|
||||
// === Bitonic global merge ===
|
||||
{
|
||||
const passEncoder = commandEncoder.beginComputePass();
|
||||
|
||||
passEncoder.setPipeline( bitonicSortGridHashShader.pipeline );
|
||||
|
||||
for ( const [ index, bindGroup ] of bitonicSortGridHashShader.bindGroups.entries() ) {
|
||||
|
||||
passEncoder.setBindGroup( index, bindGroup );
|
||||
|
||||
}
|
||||
|
||||
var numberOfPasses = 0;
|
||||
|
||||
for ( let k = 512; k <= particleCount; k <<= 1 ) {
|
||||
|
||||
for ( let j = k >> 1; j > 0; j >>= 1 ) {
|
||||
|
||||
numberOfPasses++;
|
||||
|
||||
passEncoder.dispatchWorkgroups( numWorkgroups );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
passEncoder.end();
|
||||
|
||||
}
|
||||
|
||||
|
||||
const commandBuffer = commandEncoder.finish();
|
||||
|
||||
device.queue.submit( [ commandBuffer ] );
|
||||
|
||||
await device.queue.onSubmittedWorkDone();
|
||||
|
||||
measure.end( "sort"+i );
|
||||
|
||||
}
|
||||
|
||||
|
||||
var result = await bitonicSortGridHashShader.debugBuffer("compare");
|
||||
|
||||
await bitonicSortGridHashShader.debugBuffer("threadPassIndices");
|
||||
|
||||
var error = false;
|
||||
|
||||
for (var i = 0; i < particleCount; i++ ) {
|
||||
|
||||
if( result[i] != i ) {
|
||||
|
||||
error = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
console.log("There is error?", error, particleCount);
|
||||
|
||||
206
demos/sort/index.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>WebGPU Attention Example</title>
|
||||
<style>
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
.result{
|
||||
color: #d9d9d9;
|
||||
margin:0 auto;
|
||||
width: 400px;
|
||||
|
||||
padding: 100px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 10;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.8);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 8px rgba(28, 28, 30, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.6),
|
||||
0 0 15px rgba(28, 28, 30, 0.9);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background: rgba(28, 28, 30, 1);
|
||||
box-shadow:
|
||||
0 2px 6px rgba(0, 0, 0, 0.8),
|
||||
0 0 10px rgba(28, 28, 30, 1);
|
||||
}
|
||||
#controlPanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRow label {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputRow button,
|
||||
.inputRow input {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.inputRow button:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.inputRow select {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f0f0f5;
|
||||
background: rgba(28, 28, 30, 0.9);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||
0 0 6px rgba(28, 28, 30, 0.5);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
appearance: none; /* Remove default arrow */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 20px) calc(50% - 3px),
|
||||
calc(100% - 15px) calc(50% - 3px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.inputRow select:hover {
|
||||
background: rgba(40, 40, 44, 0.95);
|
||||
}
|
||||
|
||||
.inputRow option {
|
||||
background: rgba(28, 28, 30, 0.95);
|
||||
color: #f0f0f5;
|
||||
}
|
||||
|
||||
.top-panel{
|
||||
justify-content: flex-start;
|
||||
border-bottom: 1px solid #292929;
|
||||
display: flex;
|
||||
|
||||
gap: 2.5rem;
|
||||
padding: 1.75rem 2rem;
|
||||
background: rgb(20 20 20 / 95%);
|
||||
backdrop-filter: blur(35px);
|
||||
-webkit-backdrop-filter: blur(35px);
|
||||
color: white;
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.7);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
user-select: none;
|
||||
|
||||
|
||||
width: calc(100vw);
|
||||
z-index: 10;
|
||||
|
||||
}
|
||||
|
||||
.top-panel button{
|
||||
cursor:pointer;
|
||||
}
|
||||
body {
|
||||
background: #111111;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
</style>
|
||||
<base href="sort/" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="top-panel"></div>
|
||||
|
||||
<div class="result">
|
||||
|
||||
<p>LocalSort -> Bitonic Sort Multi step</p>
|
||||
<p> 131 072 items under 10 ms in WebGpu</p>
|
||||
<p>Open Your console to see the results.</p>
|
||||
|
||||
</div>
|
||||
<canvas width="1000" height="1000"></canvas>
|
||||
<script type="module" src="demo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
64
demos/sort/shaders/bitonicSort.wgsl
Normal file
@@ -0,0 +1,64 @@
|
||||
@group(0) @binding(0)
|
||||
var<storage, read_write> compare: array<f32>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var<storage, read_write> indices: array<u32>;
|
||||
|
||||
@group(0) @binding(2)
|
||||
var<uniform> k: u32; // current stage size (power of two)
|
||||
|
||||
@group(0) @binding(3)
|
||||
var<uniform> j: u32; // current subsequence size
|
||||
|
||||
@group(0) @binding(4)
|
||||
var<uniform> totalCount: u32;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
let idx = global_id.x;
|
||||
|
||||
|
||||
let ixj = idx ^ j;
|
||||
|
||||
|
||||
if (idx >= totalCount || ixj >= totalCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (ixj > idx && ixj < totalCount) {
|
||||
let ascending = (idx & k) == 0u;
|
||||
|
||||
let dist_idx = compare[idx];
|
||||
let dist_ixj = compare[ixj];
|
||||
|
||||
var swap = false;
|
||||
|
||||
if (ascending) {
|
||||
if (dist_idx < dist_ixj) {
|
||||
swap = true;
|
||||
}
|
||||
} else {
|
||||
if (dist_idx > dist_ixj) {
|
||||
swap = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (swap) {
|
||||
|
||||
let tempDist = compare[idx];
|
||||
let tempDist2 = compare[ixj];
|
||||
|
||||
let tempIndex = indices[idx];
|
||||
let tempIndex2 = indices[ixj];
|
||||
|
||||
compare[idx] = tempDist2;
|
||||
compare[ixj] = tempDist;
|
||||
|
||||
indices[idx] = tempIndex2;
|
||||
indices[ixj] = tempIndex;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
53
demos/sort/shaders/bitonicSortUInt.wgsl
Normal file
@@ -0,0 +1,53 @@
|
||||
@group(0) @binding(0)
|
||||
var<storage, read_write> compare: array<u32>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var<storage, read_write> indices: array<u32>;
|
||||
|
||||
@group(0) @binding(2)
|
||||
var<uniform> k: u32;
|
||||
|
||||
@group(0) @binding(3)
|
||||
var<uniform> j: u32;
|
||||
|
||||
@group(0) @binding(4)
|
||||
var<uniform> totalCount: u32;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
let idx = global_id.x;
|
||||
|
||||
let ixj = idx ^ j;
|
||||
|
||||
if (idx >= totalCount || ixj <= idx || ixj >= totalCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ixj > idx) {
|
||||
let ascending = (idx & k) == 0u;
|
||||
|
||||
let dist_idx = compare[idx];
|
||||
let dist_ixj = compare[ixj];
|
||||
|
||||
var swap = false;
|
||||
|
||||
if (ascending) {
|
||||
if (dist_idx > dist_ixj) {
|
||||
swap = true;
|
||||
}
|
||||
} else {
|
||||
if (dist_idx < dist_ixj) {
|
||||
swap = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (swap) {
|
||||
|
||||
let tempDist = compare[idx];
|
||||
|
||||
compare[idx] = compare[ixj];
|
||||
compare[ixj] = tempDist;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
68
demos/sort/shaders/bitonicSortUIntMultiPass.wgsl
Normal file
@@ -0,0 +1,68 @@
|
||||
@group(0) @binding(0)
|
||||
var<storage, read_write> compare: array<u32>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var<storage, read_write> threadPassIndices: array<u32>;
|
||||
|
||||
@group(0) @binding(2)
|
||||
var<storage, read_write> kArray: array<u32>;
|
||||
|
||||
@group(0) @binding(3)
|
||||
var<storage, read_write> jArray: array<u32>;
|
||||
|
||||
@group(0) @binding(4)
|
||||
var<uniform> totalCount: u32;
|
||||
|
||||
|
||||
|
||||
@compute @workgroup_size( 256 )
|
||||
fn main( @builtin(global_invocation_id) global_id : vec3<u32> ) {
|
||||
|
||||
let idx = global_id.x;
|
||||
|
||||
let threadPassIndex = threadPassIndices[ idx ];
|
||||
|
||||
threadPassIndices[idx] = threadPassIndices[idx] + 1;
|
||||
|
||||
let j = jArray[ threadPassIndex ];
|
||||
|
||||
let k = kArray[ threadPassIndex ];
|
||||
|
||||
let ixj = idx ^ j;
|
||||
|
||||
|
||||
|
||||
if (idx >= totalCount || ixj <= idx || ixj >= totalCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ixj > idx) {
|
||||
let ascending = (idx & k) == 0u;
|
||||
|
||||
let dist_idx = compare[idx];
|
||||
let dist_ixj = compare[ixj];
|
||||
|
||||
var swap = false;
|
||||
|
||||
if (ascending) {
|
||||
if (dist_idx > dist_ixj) {
|
||||
swap = true;
|
||||
}
|
||||
} else {
|
||||
if (dist_idx < dist_ixj) {
|
||||
swap = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (swap) {
|
||||
|
||||
let tempDist = compare[idx];
|
||||
|
||||
compare[idx] = compare[ixj];
|
||||
compare[ixj] = tempDist;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
68
demos/sort/shaders/localSort.wgsl
Normal file
@@ -0,0 +1,68 @@
|
||||
@group(0) @binding(0)
|
||||
var<storage, read_write> compare: array<u32>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var<uniform> totalCount: u32;
|
||||
|
||||
var<workgroup> sharedData: array<u32, 256>;
|
||||
|
||||
@compute @workgroup_size(256)
|
||||
fn main(@builtin(local_invocation_id) local_id : vec3<u32>,
|
||||
@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||
|
||||
let localIndex = local_id.x;
|
||||
let globalIndex = global_id.x;
|
||||
|
||||
// Load element from global memory into shared memory if in range
|
||||
if (globalIndex < totalCount) {
|
||||
sharedData[localIndex] = compare[globalIndex];
|
||||
} else {
|
||||
sharedData[localIndex] = 0xffffffffu; // Max uint to push invalid values to the end
|
||||
}
|
||||
|
||||
workgroupBarrier();
|
||||
|
||||
// Bitonic sort in shared memory on 256 elements
|
||||
var size = 2u;
|
||||
while (size <= 256u) {
|
||||
var stride = size >> 1u;
|
||||
|
||||
var j = stride;
|
||||
while (j > 0u) {
|
||||
let ixj = localIndex ^ j;
|
||||
|
||||
if (ixj > localIndex) {
|
||||
let ascending = ((localIndex & size) == 0u);
|
||||
|
||||
let valLocal = sharedData[localIndex];
|
||||
let valIxj = sharedData[ixj];
|
||||
|
||||
var swap = false;
|
||||
if (ascending) {
|
||||
if (valLocal > valIxj) {
|
||||
swap = true;
|
||||
}
|
||||
} else {
|
||||
if (valLocal < valIxj) {
|
||||
swap = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (swap) {
|
||||
sharedData[localIndex] = valIxj;
|
||||
sharedData[ixj] = valLocal;
|
||||
}
|
||||
}
|
||||
|
||||
workgroupBarrier();
|
||||
j = j >> 1u;
|
||||
}
|
||||
|
||||
size = size << 1u;
|
||||
}
|
||||
|
||||
// Write sorted results back to global memory
|
||||
if (globalIndex < totalCount) {
|
||||
compare[globalIndex] = sharedData[localIndex];
|
||||
}
|
||||
}
|
||||