Added NodeJS Demo.

This commit is contained in:
2025-11-18 11:45:56 +01:00
parent 4b8bd4366a
commit b3ab6bd314
69 changed files with 2531 additions and 4220223 deletions

View 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
View 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
View 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>

View 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;
}

View File

@@ -0,0 +1,114 @@
# Simplifying WebGPU with This Framework
WebGPUs 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
View 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)
})
*/

View 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
View 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
View 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>

View 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;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,114 @@
# Simplifying WebGPU with This Framework
WebGPUs 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.

View 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
View 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
View 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>

View 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;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,114 @@
# Simplifying WebGPU with This Framework
WebGPUs 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.

View 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
View 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
View 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>

View 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;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,114 @@
# Simplifying WebGPU with This Framework
WebGPUs 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.

View 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
View 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
View File

File diff suppressed because it is too large Load Diff

View 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>

View 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;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,114 @@
# Simplifying WebGPU with This Framework
WebGPUs 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.

View 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
View 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
View 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>

View 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;
}

View File

@@ -0,0 +1,114 @@
# Simplifying WebGPU with This Framework
WebGPUs 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.

View 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);
};

View 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();
}
}

View 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>

View 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;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,114 @@
# Simplifying WebGPU with This Framework
WebGPUs 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
View 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
View 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>

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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];
}
}