Initial commit

This commit is contained in:
2025-11-17 15:06:39 +01:00
parent 2015516eca
commit 4d2432a1c2
91 changed files with 5074894 additions and 2 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.

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

844040
Demos/Texture/demo.json Executable file
View File

File diff suppressed because it is too large Load Diff

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

844040
Demos/Texture2/demo.json Executable file
View File

File diff suppressed because it is too large Load Diff

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

844040
Demos/Texture3/demo.json Executable file
View File

File diff suppressed because it is too large Load Diff

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

844040
Demos/Triangles/demo.json Executable file
View File

File diff suppressed because it is too large Load Diff

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

844040
Demos/particleHeader/demo.json Executable file
View File

File diff suppressed because it is too large Load Diff

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

231
README.md
View File

@@ -1,3 +1,230 @@
# WebGPU-Framework
Here is a full **professional** README for your WebGPU Framework — polished, structured, and hire-worthy.
High-level GPU Compute + Rendering for Browser & Node.js.
If you want, I can also produce a shorter “NPM package style” version or include diagrams/screenshots.
---
# WebGPU Framework
**High-level GPU Compute + Rendering for Browser & Node.js**
**Write once — run on WebGPU everywhere.**
WebGPU exposes massive GPU power — but also a very low-level API full of pipelines, descriptor layouts, bind groups, command buffers, and sync issues.
This framework brings **clarity and simplicity**:
```js
const shader = new Shader(device);
await shader.setup("path/to/shader.wgsl");
shader.setVariable("gravity", -2.3);
shader.setBuffer("positions", buffer);
await shader.execute(numWorkgroups);
```
No boilerplate.
No bind group confusion.
Just GPU work.
---
## ✨ Key Features
| Feature | Description |
| ------------------------------------------ | --------------------------------------------------------------------- |
| **Unified API** | Same code works in browser or native Node.js GPU contexts |
| **Shader-first design** | Bind uniforms & buffers by name — the framework auto-builds pipelines |
| **Automatic bind group management** | No descriptor juggling |
| **Compute & Rendering** | Full support for GPU compute pipelines & mesh rendering |
| **Camera, Matrix & Vector math utilities** | Real 3D scenes ready to go |
| **Event system (browser & desktop)** | Camera & input control mapped seamlessly |
| **Debug utilities** | Inspect GPU buffers during runtime |
| **Fast iteration** | Live shader reload (optional extension) |
---
## Getting Started
### Installation
Browser:
```html
<script type="module" src="./framework/WebGpu.js"></script>
```
Node.js (requires headless or windowed WebGPU runtime):
```bash
npm install webgpu-framework
```
```js
import Shader from "webgpu-framework";
```
> Rendering works **in Node** using a native window — same code as browser.
---
## Quick Example — Compute Shader
```js
const device = await navigator.gpu.requestDevice();
const shader = new Shader(device);
await shader.setup("./shaders/addVectors.wgsl");
shader.setBuffer("inputA", bufferA);
shader.setBuffer("inputB", bufferB);
shader.setBuffer("output", bufferOut);
await shader.execute(256);
```
---
## Quick Example — Rendering Particles
```js
const shader = new Shader(device, "./shaders/points.wgsl");
shader.setCanvas(canvas);
shader.setBuffer("positions", particleBuffer);
shader.renderToCanvas(
vertexCount = particleCount,
instanceCount = 1
);
```
Thats all you need.
---
## Example Project Included: **3D Particle Simulation**
* 16K moving particles
* Gravity, spatial hashing, collision detection
* Bitonic GPU sorting
* Full compute → render → compute loop
Demo code:
```js
import { ParticleSimulation } from "./demo/ParticleSimulation.js";
const sim = new ParticleSimulation();
await sim.setup(canvas, 1280, 720);
sim.render();
```
---
## Under the Hood
This framework builds and manages:
* GPUDevice, SwapChain, Render Passes
* Pipeline creation (compute + render)
* Bind groups and layouts
* Buffer creation + dynamic sized uniform arrays
* Command submission + synchronization
The developer writes:
* WGSL shaders
* Minimal JavaScript to bind variables
**All complexity stays internal.**
---
## Architecture
```
Your Code
|
| setVariable(), setBuffer(), execute(), render()
v
WebGPU Framework
|
| Auto-bind pipelines, sync, command buffers
v
Native WebGPU (Browser or Node.js)
|
v
GPU 🚀
```
---
## API Summary
| Method | Purpose |
| ---------------------------------- | --------------------------------------------------- |
| `setup(path)` | Load & compile a WGSL shader |
| `setVariable(name, value)` | Upload uniform values (numbers, vectors, matrices…) |
| `setBuffer(name, gpuBuffer)` | Bind device buffers by name |
| `execute(workgroups)` | Dispatch compute workloads |
| `renderToCanvas(verts, instances)` | Draw geometry to output surface |
| `debugBuffer(name)` | Inspect GPU buffer contents |
Camera/Math/Event utilities:
* `Matrix4`, `Vector3`, `Camera`
* `EventManager` for mouse/touch/keyboard (browser + Node)
---
## Requirements
* Modern GPU with WebGPU support
* Browser support: Chromium-based + Firefox Nightly
* Node.js support: wgpu-native or compatible environments
---
## Status
| Component | State |
| -------------------- | --------------- |
| Core Shader API | ✓ Stable |
| Browser rendering | ✓ Fully working |
| Node.js rendering | ✓ Fully working |
| Advanced pipelines | 🚧 In progress |
| Cross-platform demos | 🚧 More coming |
---
## License
MIT License
© 2025 Kaj Dijkstra
---
## Author
**Kaj Dijkstra**
Leeuwarden, Netherlands
Portfolio: [https://kajdijkstra.com](https://kajdijkstra.com)
Email: [kajdijkstra@protonmail.com](mailto:kajdijkstra@protonmail.com)
---
Would you like me to:
✔ Add diagrams illustrating compute → sort → grid hashing pipeline
✔ Add screenshots from your particle simulation
✔ Add performance benchmarks
✔ Add a “Why use this instead of raw WebGPU?” section
✔ Publish this to npm with clean package layout
Also — should we link this repo from your CV under **Projects** with a short bullet like:
> *High-level WebGPU framework enabling real-time GPU compute + rendering in browser and native runtimes with a minimal API*
If you're ready, I can push this README into your Gitea repository automatically.

83
framework/Camera.js Normal file
View File

@@ -0,0 +1,83 @@
import Vector3 from "./Vector3.js";
import Matrix4 from "./Matrix4.js";
export default class Camera {
eye = new Vector3();
target = new Vector3();
up = new Vector3( 0, 1, 0 );
yaw = 0;
pitch = 0;
fovRadians = Math.PI / 4;
near = 0.1;
far = 3000.0;
distance = 10;
viewMatrix = new Float32Array( 16 );
constructor( eye = [0, 0, 5], target = [0, 0, 0], up = [0, 1, 0] ) {
this.eye = new Vector3( ...eye );
this.target = new Vector3( ...target );
this.up = new Vector3( ...up );
this.distance = Vector3.subtract( this.eye, this.target ).length();
this.viewMatrix = Matrix4.lookAt( this.eye, this.target, this.up );
}
update() {
const x = this.distance * Math.cos( this.pitch ) * Math.sin( this.yaw );
const y = this.distance * Math.sin( this.pitch );
const z = this.distance * Math.cos( this.pitch ) * Math.cos( this.yaw );
this.eye = new Vector3(
x + this.target.x,
y + this.target.y,
z + this.target.z
);
this.viewMatrix = Matrix4.lookAt( this.eye, this.target, this.up );
}
getViewMatrix() {
return this.viewMatrix;
}
rotate( deltaYaw, deltaPitch ) {
this.yaw += deltaYaw;
this.pitch -= deltaPitch;
const maxPitch = Math.PI / 2 - 0.01;
if ( this.pitch > maxPitch ) this.pitch = maxPitch;
if ( this.pitch < -maxPitch ) this.pitch = -maxPitch;
this.update();
}
zoom( delta ) {
this.distance += delta * 1;
if ( this.distance < 0.1 ) this.distance = 0.1;
this.update();
}
setTarget( target ) {
this.target = new Vector3( ...target );
this.update();
}
}

125
framework/Matrix4.js Normal file
View File

@@ -0,0 +1,125 @@
import Vector3 from "./Vector3.js";
export default class Matrix4 {
static lookAt( eye, target, up ) {
const zAxis = Vector3.normalize( Vector3.subtract( eye, target ) );
const xAxis = Vector3.normalize( Vector3.cross( up, zAxis ) );
const yAxis = Vector3.cross( zAxis, xAxis );
return new Float32Array([
xAxis.x, yAxis.x, zAxis.x, 0,
xAxis.y, yAxis.y, zAxis.y, 0,
xAxis.z, yAxis.z, zAxis.z, 0,
-Vector3.dot( xAxis, eye ), -Vector3.dot( yAxis, eye ), -Vector3.dot( zAxis, eye ), 1,
]);
}
static getColumn( matrix, index ) {
const i = index * 4;
return new Vector3(
matrix[ i + 0 ],
matrix[ i + 1 ],
matrix[ i + 2 ]
);
}
static createProjectionMatrix( camera, canvas ) {
return Matrix4.perspective(
camera.fovRadians,
canvas.width / canvas.height,
camera.near,
camera.far
);
}
static invert( m ) {
const out = new Float32Array(16);
const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
const a0 = m00 * m11 - m01 * m10;
const a1 = m00 * m12 - m02 * m10;
const a2 = m00 * m13 - m03 * m10;
const a3 = m01 * m12 - m02 * m11;
const a4 = m01 * m13 - m03 * m11;
const a5 = m02 * m13 - m03 * m12;
const b0 = m20 * m31 - m21 * m30;
const b1 = m20 * m32 - m22 * m30;
const b2 = m20 * m33 - m23 * m30;
const b3 = m21 * m32 - m22 * m31;
const b4 = m21 * m33 - m23 * m31;
const b5 = m22 * m33 - m23 * m32;
const det = a0 * b5 - a1 * b4 + a2 * b3 + a3 * b2 - a4 * b1 + a5 * b0;
if (det === 0) return null;
const invDet = 1 / det;
out[0] = ( m11 * b5 - m12 * b4 + m13 * b3) * invDet;
out[1] = (-m01 * b5 + m02 * b4 - m03 * b3) * invDet;
out[2] = ( m31 * a5 - m32 * a4 + m33 * a3) * invDet;
out[3] = (-m21 * a5 + m22 * a4 - m23 * a3) * invDet;
out[4] = (-m10 * b5 + m12 * b2 - m13 * b1) * invDet;
out[5] = ( m00 * b5 - m02 * b2 + m03 * b1) * invDet;
out[6] = (-m30 * a5 + m32 * a2 - m33 * a1) * invDet;
out[7] = ( m20 * a5 - m22 * a2 + m23 * a1) * invDet;
out[8] = ( m10 * b4 - m11 * b2 + m13 * b0) * invDet;
out[9] = (-m00 * b4 + m01 * b2 - m03 * b0) * invDet;
out[10] = ( m30 * a4 - m31 * a2 + m33 * a0) * invDet;
out[11] = (-m20 * a4 + m21 * a2 - m23 * a0) * invDet;
out[12] = (-m10 * b3 + m11 * b1 - m12 * b0) * invDet;
out[13] = ( m00 * b3 - m01 * b1 + m02 * b0) * invDet;
out[14] = (-m30 * a3 + m31 * a1 - m32 * a0) * invDet;
out[15] = ( m20 * a3 - m21 * a1 + m22 * a0) * invDet;
return out;
}
static perspective( fovRadians, aspect, near, far ) {
const f = 1.0 / Math.tan( fovRadians / 2 );
const nf = 1 / ( near - far );
return new Float32Array([
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) * nf, -1,
0, 0, (2 * far * near) * nf, 0,
]);
}
static multiply( a, b ) {
const out = new Float32Array(16);
for ( let col = 0; col < 4; col++ ) {
for ( let row = 0; row < 4; row++ ) {
let sum = 0;
for ( let k = 0; k < 4; k++ ) {
// a is column-major: element at col k, row row => a[k*4 + row]
// b is column-major: element at col col, row k => b[col*4 + k]
sum += a[k * 4 + row] * b[col * 4 + k];
}
out[col * 4 + row] = sum;
}
}
return out;
}
}

49
framework/Measure.js Normal file
View File

@@ -0,0 +1,49 @@
export default class Measure {
startTimes = {};
endTimes = {};
writeToPage = false;
element = false;
start ( label ) {
this.startTimes[ label ] = performance.now();
}
end ( label ) {
this.endTimes[ label ] = performance.now();
this.log( label );
}
getElapsed ( label ) {
if ( this.startTimes[ label ] === undefined || this.endTimes[ label ] === undefined ) {
throw new Error( "Start or end time missing for label: " + label );
}
return this.endTimes[ label ] - this.startTimes[ label ];
}
log ( label ) {
const elapsed = this.getElapsed( label );
if( this.writeToPage ) {
var p = document.createElement("p")
p.innerText = label + " took " + elapsed.toFixed(3) + " ms";
this.element.appendChild( p );
}
console.log( label + " took " + elapsed.toFixed(3) + " ms" );
}
}

11
framework/Request.js Normal file
View File

@@ -0,0 +1,11 @@
export class Request {
constructor( method, payload = {} ) {
this.method = method; // method name to call on Controller, e.g. "Ping"
this.payload = payload; // any data for the method
}
}

View File

@@ -0,0 +1,67 @@
class shaderDebugger{
setup() {
var shaders = document.shaders;
var select = document.querySelector(".selectDebugShader");
for (var i = 0; i < shaders.length; i++) {
var currentShader = shaders[i];
var option = document.createElement("option");
option.innerText = currentShader.path;
option.id = i;
select.appendChild( option );
}
document.querySelector( "#showBuffers" ).addEventListener( "click", async function() {
var select = document.querySelector(".selectDebugShader");
var selectedIndex = select.selectedIndex;
var selectedShader = document.shaders[ selectedIndex ]
const keysArray = Array.from( selectedShader.buffers );
console.log("\n\n\n\n -------------------- Debugging Shader --------------- \n\n\n\n");
console.log( "Shader Path: ", selectedShader.path );
console.log( selectedShader );
for (var i = 0; i < keysArray.length; i++) {
const bindingInfo = selectedShader.bindings.find( b => b.varName === keysArray[i][0] );
if( bindingInfo ) {
if( bindingInfo.type == "storage" ) {
await selectedShader.debugBuffer( keysArray[i][0] );
}
} else {
console.log("this is a Uniform", keysArray, selectedShader.bindings);
}
}
});
}
}
export default shaderDebugger;

60
framework/Vector3.js Normal file
View File

@@ -0,0 +1,60 @@
export default class Vector3 {
x = 0;
y = 0;
z = 0;
constructor( x = 0, y = 0, z = 0 ) {
this.x = x;
this.y = y;
this.z = z;
}
static subtract( a, b ) {
return new Vector3( a.x - b.x, a.y - b.y, a.z - b.z );
}
length() {
return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );
}
static cross( a, b ) {
return new Vector3(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
);
}
static dot( a, b ) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
static normalize( v ) {
const length = Math.sqrt( v.x * v.x + v.y * v.y + v.z * v.z );
if ( length > 0.00001 ) {
return new Vector3( v.x / length, v.y / length, v.z / length );
} else {
return new Vector3( 0, 0, 0 );
}
}
}

1893
framework/WebGpu.js Normal file
View File

File diff suppressed because it is too large Load Diff

113
framework/eventManager.js Normal file
View File

@@ -0,0 +1,113 @@
// eventManager.js
export default class EventManager {
isDragging = false;
lastX = 0;
lastY = 0;
camera;
canvas;
setCanvas( canvas ) {
this.canvas = canvas;
//this.registerEventListeners();
//this.handleResize();
}
setup( canvas, camera ) {
this.canvas = canvas;
this.camera = camera;
//this.registerEventListeners();
//this.handleResize();
}
registerEventListeners() {
this.canvas.addEventListener( "mousedown", this.onMouseDown.bind(this) );
this.canvas.addEventListener( "mouseup", this.onMouseUp.bind(this) );
this.canvas.addEventListener( "mouseleave", this.onMouseLeave.bind(this) );
this.canvas.addEventListener( "mousemove", this.onMouseMove.bind(this) );
this.canvas.addEventListener( "wheel", this.onWheel.bind(this), { passive: false } );
}
resize( event ) {
this.canvas.width = event.width;
this.canvas.height = event.height;
//this.canvas.width = window.innerWidth;
//this.canvas.height = window.innerHeight;
}
mousedown( event ) {
console.log("mouseDownHandler");
this.isDragging = true;
this.lastX = event.clientX;
this.lastY = event.clientY;
}
mouseup( event ) {
this.isDragging = false;
}
mouseleave( event ) {
this.isDragging = false;
}
mousemove( event ) {
if ( !this.isDragging ) return;
const deltaX = ( event.clientX - this.lastX ) * 0.005;
const deltaY = ( event.clientY - this.lastY ) * 0.005;
this.camera.rotate( deltaX, -deltaY );
this.lastX = event.clientX;
this.lastY = event.clientY;
}
wheel( event ) {
const delta = event.deltaY * 0.01;
this.camera.zoom( delta );
}
}

6
framework/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"type": "module",
"dependencies": {
}
}

1
package.json Normal file
View File

@@ -0,0 +1 @@
{"type": "module" }

166
server.js Normal file
View File

@@ -0,0 +1,166 @@
import http from "http";
import { readdir } from "fs/promises";
import { stat } from "fs/promises";
import { readFile } from "fs/promises";
import { join } from "path";
import { dirname } from "path";
import { fileURLToPath } from "url";
class App
{
constructor( )
{
const selfPath = fileURLToPath( import.meta.url );
this.rootPath = dirname( selfPath );
this.httpServer = null;
}
async start( )
{
this.httpServer = http.createServer( this.handleRequest.bind( this ) );
this.httpServer.listen( 3000 );
}
async handleRequest( req, res )
{
const requestedPath = decodeURI( req.url );
const fullPath = join( this.rootPath, requestedPath );
const exists = await this.checkFileExists( fullPath );
if ( !exists )
{
res.statusCode = 404;
res.end( "Not Found" );
return;
}
const stats = await stat( fullPath );
if ( stats.isDirectory( ) )
{
const indexPath = join( fullPath, "index.html" );
const indexExists = await this.checkFileExists( indexPath );
if ( indexExists )
{
await this.sendFile( indexPath, res );
return;
}
await this.sendDirectoryListing( fullPath, requestedPath, res );
return;
}
await this.sendFile( fullPath, res );
}
async sendFile( path, res )
{
const contentType = this.getContentType( path );
const fileData = await readFile( path );
res.setHeader( "Content-Type", contentType );
res.statusCode = 200;
res.end( fileData );
}
async sendDirectoryListing( dirPath, urlPath, res )
{
const entries = await readdir( dirPath, { withFileTypes : true } );
let html = "<html><body><h1>Index of " + urlPath + "</h1><ul>";
let i = 0;
while ( i < entries.length )
{
const e = entries[ i ].name;
const link = urlPath.endsWith( "/" )
? urlPath + e
: urlPath + "/" + e;
html = html + "<li><a href=\"" + link + "\">" + e + "</a></li>";
i = i + 1;
}
html = html + "</ul></body></html>";
res.setHeader( "Content-Type", "text/html" );
res.statusCode = 200;
res.end( html );
}
async checkFileExists( path )
{
const exists = await stat( path )
.then( function( ) { return true; } )
.catch( function( ) { return false; } );
return exists;
}
getContentType( path )
{
const lower = path.toLowerCase( );
if ( lower.endsWith( ".html" ) ) return "text/html";
if ( lower.endsWith( ".css" ) ) return "text/css";
if ( lower.endsWith( ".js" ) ) return "text/javascript";
if ( lower.endsWith( ".json" ) ) return "application/json";
if ( lower.endsWith( ".wasm" ) ) return "application/wasm";
if ( lower.endsWith( ".png" ) ) return "image/png";
if ( lower.endsWith( ".jpg" ) ) return "image/jpeg";
if ( lower.endsWith( ".jpeg" ) ) return "image/jpeg";
if ( lower.endsWith( ".gif" ) ) return "image/gif";
if ( lower.endsWith( ".svg" ) ) return "image/svg+xml";
if ( lower.endsWith( ".wgsl" ) ) return "text/plain";
if ( lower.endsWith( ".txt" ) ) return "text/plain";
return "application/octet-stream";
}
}
const app = new App( );
await app.start( );

64
shaders/bitonicSort.wgsl Normal file
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,54 @@
@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(256)
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];
let tempIndex = indices[idx];
compare[idx] = compare[ixj];
compare[ixj] = tempDist;
indices[idx] = indices[ixj];
indices[ixj] = tempIndex;
}
}
}

View File

@@ -0,0 +1,67 @@
@group(0) @binding(0)
var<storage, read_write> gridHashes: 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<storage, read_write> indices: array<u32>;
@group(0) @binding(5)
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] + 1u;
let j = jArray[threadPassIndex];
let k = kArray[threadPassIndex];
let ixj = idx ^ j;
if (ixj <= idx || ixj >= totalCount) {
return;
}
if (ixj > idx) {
let ascending = (idx & k) == 0u;
let dist_idx = gridHashes[idx];
let dist_ixj = gridHashes[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 = gridHashes[idx];
let tempIndex = indices[idx];
gridHashes[idx] = gridHashes[ixj];
gridHashes[ixj] = tempDist;
indices[idx] = indices[ixj];
indices[ixj] = tempIndex;
}
}
}

View File

@@ -0,0 +1,187 @@
@group(0) @binding(0)
var<storage, read_write> positions: array<vec3<f32>>;
@group(0) @binding(1)
var<storage, read_write> velocities: array<vec3<f32>>;
@group(0) @binding(2)
var<storage, read_write> gridHashes: array<u32>;
@group(0) @binding(3)
var<storage, read_write> hashSortedIndices: array<u32>;
@group(0) @binding(4)
var<uniform> cellCount: u32;
@group(0) @binding(5)
var<uniform> gridMin: vec3<f32>;
@group(0) @binding(6)
var<storage, read> startIndices: array<u32>; // start index of particles in each cell
@group(0) @binding(7)
var<storage, read> endIndices: array<u32>; // end index (exclusive) of particles in each cell
@group(0) @binding(8)
var<uniform> collisionRadius: f32;
@group(0) @binding(9)
var<uniform> deltaTimeSeconds: f32;
@group(0) @binding(10) var<uniform> gridMax: vec3<f32>;
// particleIndex = hashSortedIndices[ startIndices[ i ] ]
fn getHash(gridCoordinate: vec3<i32>, cellCount: u32) -> u32 {
let maxIndex = i32(cellCount) - 1;
let x = max(0, min(gridCoordinate.x, maxIndex));
let y = max(0, min(gridCoordinate.y, maxIndex));
let z = max(0, min(gridCoordinate.z, maxIndex));
return u32(x + y * i32(cellCount) + z * i32(cellCount) * i32(cellCount));
}
@compute @workgroup_size(256)
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3<u32>) {
let index = globalInvocationId.x;
let particleIndex = hashSortedIndices[ index ];
if ( particleIndex >= arrayLength(&positions) ) {
return;
}
var currentPosition = positions[ particleIndex ];
let cellSize = (gridMax - gridMin) / f32(cellCount); // 2.0 / 16 = 0.125
let relativePos = currentPosition - gridMin; // currentPosition + 1
let gridCoord = vec3<i32>(floor(relativePos / cellSize)); // relativePos divided by cellSize, then floored
let hash = getHash(gridCoord, cellCount);
var currentVelocity = velocities[particleIndex];
var push = vec3<f32>(0.0);
var count = 0u;
let collisionRadiusSquared = collisionRadius * collisionRadius;
for (var dz = -1; dz <= 1; dz = dz + 1) {
for (var dy = -1; dy <= 1; dy = dy + 1) {
for (var dx = -1; dx <= 1; dx = dx + 1) {
let neighborCell = gridCoord + vec3<i32>(dx, dy, dz);
// Compute hash of neighbor cell, with clamping to valid range inside getHash()
let neighborHash = getHash(neighborCell, cellCount);
let startIndex = startIndices[neighborHash];
let endIndex = endIndices[neighborHash];
for (var i = startIndex; i < endIndex; i = i + 1u) {
let otherIndex = hashSortedIndices[i];
if (otherIndex == particleIndex) {
continue;
}
let otherPosition = positions[otherIndex];
let offset = currentPosition - otherPosition;
let distSquared = dot(offset, offset);
if (distSquared < collisionRadiusSquared && distSquared > 0.00001) {
let distance = sqrt(distSquared);
let direction = offset / distance;
let overlap = collisionRadius - distance;
push += direction * overlap;
count += 1u;
}
}
}
}
}
if ( count > 0u ) {
let averagePush = push / f32(count);
currentPosition += averagePush * .9;
currentVelocity += averagePush * 3.0;
let pushDir = normalize(averagePush);
// Project current velocity onto push direction
let velAlongPush = dot(currentVelocity, pushDir);
// Damping factor (energy loss on collision)
let dampingFactor = 0.25;
// Reduce velocity along push direction
let velAlongPushDamped = velAlongPush * dampingFactor;
// Velocity perpendicular to push direction remains unchanged
let velPerp = currentVelocity - velAlongPush * pushDir;
// Combine damped velocity components
currentVelocity = velPerp + velAlongPushDamped * pushDir;
}
let deltaTimeClamped = min( deltaTimeSeconds, 0.01 );
let gridExtent = vec3<f32>(f32(cellCount)) * cellSize;
//let gridMax = gridMin + gridExtent;
// Enforce hardcoded bounding box from -1 to +1 on all axes
if (currentPosition.x < gridMin.x) {
currentPosition.x = gridMin.x;
currentVelocity.x = abs(currentVelocity.x) * 0.2;
} else if (currentPosition.x > gridMax.x) {
currentPosition.x = gridMax.x;
currentVelocity.x = -abs(currentVelocity.x) * 0.2;
}
if (currentPosition.y < gridMin.y) {
currentPosition.y = gridMin.y;
currentVelocity.y = abs(currentVelocity.y) * 0.2;
} else if (currentPosition.y > gridMax.y) {
currentPosition.y = gridMax.y;
currentVelocity.y = -abs(currentVelocity.y) * 0.2;
}
if (currentPosition.z < gridMin.z) {
currentPosition.z = gridMin.z;
currentVelocity.z = abs(currentVelocity.z) * 0.2;
} else if (currentPosition.z > gridMax.z) {
currentPosition.z = gridMax.z;
currentVelocity.z = -abs(currentVelocity.z) * 0.2;
}
if (currentPosition.y < -1.0) {
currentPosition.y = -1.0;
currentVelocity.y *= -0.2;
}
currentPosition += currentVelocity * deltaTimeClamped;
positions[ particleIndex ] = currentPosition;
velocities[ particleIndex ] = currentVelocity;
}

14
shaders/copyBuffer.wgsl Normal file
View File

@@ -0,0 +1,14 @@
@group(0) @binding(0)
var<storage, read> indices: array<u32>;
@group(0) @binding(1)
var<storage, read_write> sortedIndices: array<u32>;
@compute @workgroup_size(64)
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3<u32>) {
let particleIndex = globalInvocationId.x;
sortedIndices[particleIndex] = indices[particleIndex];
}

49
shaders/findGridHash.wgsl Normal file
View File

@@ -0,0 +1,49 @@
@group(0) @binding(0) var<storage, read_write> positions: array<vec3<f32>>;
@group(0) @binding(1) var<storage, read_write> gridHashes: array<u32>;
@group(0) @binding(2) var<storage, read_write> indices: array<u32>;
@group(0) @binding(3) var<uniform> cellCount: u32;
@group(0) @binding(4) var<uniform> gridMin: vec3<f32>;
@group(0) @binding(5) var<uniform> gridMax: vec3<f32>;
fn getHash(gridCoordinate: vec3<i32>, cellCount: u32) -> u32 {
let maxIndex = i32(cellCount) - 1;
let x = max(0, min(gridCoordinate.x, maxIndex));
let y = max(0, min(gridCoordinate.y, maxIndex));
let z = max(0, min(gridCoordinate.z, maxIndex));
return u32(x + y * i32(cellCount) + z * i32(cellCount) * i32(cellCount));
}
@compute @workgroup_size(256)
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3<u32>) {
let particleIndex = globalInvocationId.x;
if ( particleIndex >= arrayLength(&positions) ) {
return;
}
var currentPosition = positions[particleIndex];
let cellSize = (gridMax - gridMin) / f32(cellCount); // 2.0 / 16 = 0.125
let relativePos = currentPosition - gridMin; // currentPosition + 1
let gridCoord = vec3<i32>(floor(relativePos / cellSize)); // relativePos divided by cellSize, then floored
let hash = getHash(gridCoord, cellCount);
gridHashes[ particleIndex ] = hash;
indices[ particleIndex ] = particleIndex;
}

View File

@@ -0,0 +1,40 @@
@group(0) @binding(0)
var<storage, read> gridHashes: array<u32>;
@group(0) @binding(1)
var<storage, read> indices: array<u32>;
@group(0) @binding(2)
var<storage, read_write> startIndices: array<u32>;
@group(0) @binding(3)
var<storage, read_write> endIndices: array<u32>;
@group(0) @binding(4)
var<uniform> totalCount: u32;
@compute @workgroup_size(256)
fn findStartEndIndices(@builtin(global_invocation_id) globalId: vec3<u32>) {
let i = globalId.x;
if (i >= totalCount) {
return;
}
let currentHash = gridHashes[i];
if (i == 0u || gridHashes[i - 1u] != currentHash) {
startIndices[currentHash] = i;
}
if (i == totalCount - 1u || gridHashes[i + 1u] != currentHash) {
endIndices[currentHash] = i;
}
}

69
shaders/gravity.wgsl Normal file
View File

@@ -0,0 +1,69 @@
@group(0) @binding(0)
var<storage, read_write> positions: array<vec3<f32>>;
@group(0) @binding(1)
var<storage, read_write> velocities: array<vec3<f32>>;
@group(0) @binding(2)
var<storage, read_write> distances: array<f32>;
@group(0) @binding(3)
var<storage, read_write> indices: array<u32>;
@group(0) @binding(4)
var<uniform> deltaTimeSeconds: f32;
@group(0) @binding(5)
var<uniform> cameraPosition: vec3<f32>;
@group(0) @binding(6)
var<uniform> updateDistancesAndIndices: u32;
@group(0) @binding(7)
var<uniform> cellCount: u32;
@group(0) @binding(8)
var<uniform> gravity: f32;
@compute @workgroup_size(64)
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3<u32>) {
let particleIndex = globalInvocationId.x;
if (particleIndex >= arrayLength(&positions)) {
return;
}
let gravityAcceleration = vec3<f32>(0.0, gravity, 0.0);
var currentPosition = positions[particleIndex];
var currentVelocity = velocities[particleIndex];
let deltaTimeClamped = min(deltaTimeSeconds, 0.01);
currentVelocity += gravityAcceleration * deltaTimeClamped;
currentPosition += currentVelocity * deltaTimeClamped;
let friction = 0.98;
currentVelocity *= friction;
positions[particleIndex] = currentPosition;
velocities[particleIndex] = currentVelocity;
if ( updateDistancesAndIndices == 1u ) {
let diff = currentPosition - cameraPosition;
let dist = length(diff);
distances[ particleIndex ] = dist;
indices[ particleIndex ] = particleIndex;
positions[ particleIndex ] = currentPosition;
}
}

View File

@@ -0,0 +1,18 @@
@group(0) @binding(0)
var<storage, read> initiationPositions : array<vec4<f32>>;
@group(0) @binding(1)
var<storage, read_write> positions : array<vec4<f32>>;
@group(0) @binding(2)
var<storage, read_write> velocities : array<vec2<f32>>;
@compute @workgroup_size(64)
fn initialize(@builtin(global_invocation_id) id : vec3<u32>) {
let i = id.x;
positions[i] = initiationPositions[i];
velocities[i] = vec2<f32>(0.0, 0.0);
}

88
shaders/localSort.wgsl Normal file
View File

@@ -0,0 +1,88 @@
@group(0) @binding(0)
var<storage, read_write> gridHashes: array<u32>;
@group(0) @binding(1)
var<storage, read_write> indices: array<u32>;
@group(0) @binding(2)
var<uniform> totalCount: u32;
var<workgroup> sharedData: array<u32, 256>;
var<workgroup> sharedIndices: 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] = gridHashes[globalIndex];
sharedIndices[localIndex] = indices[globalIndex];
} else {
sharedData[localIndex] = 0xffffffffu; // Max uint to push invalid values to the end
sharedIndices[localIndex] = 0xffffffffu; // or some invalid index
}
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) {
// Swap values
sharedData[localIndex] = valIxj;
sharedData[ixj] = valLocal;
// Swap indices as well
let idxLocal = sharedIndices[localIndex];
let idxIxj = sharedIndices[ixj];
sharedIndices[localIndex] = idxIxj;
sharedIndices[ixj] = idxLocal;
}
}
workgroupBarrier();
j = j >> 1u;
}
size = size << 1u;
}
// Write sorted results back to global memory
if (globalIndex < totalCount) {
gridHashes[globalIndex] = sharedData[localIndex];
indices[globalIndex] = sharedIndices[localIndex];
}
}

View File

@@ -0,0 +1,60 @@
@group(0) @binding(0)
var<storage, read> positions : array<vec4<f32>>;
@group(0) @binding(1)
var<storage, read> colors : array<vec4<f32>>;
@group(0) @binding(2)
var<uniform> viewProjectionMatrix : mat4x4<f32>;
@group(0) @binding(3)
var<uniform> aspectRatio : f32;
@group(0) @binding(4)
var<uniform> mousePos : vec2<f32>;
@group(0) @binding(5)
var<uniform> hoverRadius : f32;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) color : vec4<f32>,
};
fn smoothStep(edge0: f32, edge1: f32, x: f32) -> f32 {
let t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
@vertex
fn vertex_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput {
var output : VertexOutput;
let pos = positions[vertexIndex];
var color = colors[vertexIndex];
let correctedPosition = vec4<f32>(
pos.x * aspectRatio,
pos.y,
pos.z,
pos.w
);
// Change color if near mousePos:
// mousePos is passed as uniform vec2<f32> in clip space (-aspectRatio..aspectRatio, -1..1)
let dist = distance(vec2<f32>(pos.x* aspectRatio, pos.y), mousePos);
if (dist < hoverRadius) {
color = vec4<f32>(1.0, 0.0, 0.0, 1.0); // Red highlight
}
output.Position = viewProjectionMatrix * correctedPosition;
output.color = color;
return output;
}
@fragment
fn fragment_main(input : VertexOutput) -> @location(0) vec4<f32> {
return input.color;
}

73
shaders/points.wgsl Normal file
View File

@@ -0,0 +1,73 @@
struct Point {
pos: vec3<f32>,
_pad: f32,
};
struct BillboardAxis {
vector : vec3<f32>,
_pad : f32,
};
struct VSOut {
@builtin(position) Position : vec4<f32>,
@location(0) uv : vec2<f32>,
@location(1) color : vec3<f32>,
};
@group(0) @binding(0) var<storage, read> positions: array<Point>;
@group(0) @binding(1) var<storage, read> sortedIndices: array<u32>; // New binding for sorted indices
@group(0) @binding(2) var<uniform> viewProjectionMatrix: mat4x4<f32>;
@group(0) @binding(3) var<uniform> cameraRight : BillboardAxis;
@group(0) @binding(4) var<uniform> cameraUp : BillboardAxis;
@vertex
fn vertexEntryPoint(
@builtin(vertex_index) vertexIndex: u32,
@builtin(instance_index) instanceIndex: u32,
@location(0) quadOffset: vec2<f32>
) -> VSOut {
var output: VSOut;
// Use the sorted index to get the actual particle index
let actualIndex = sortedIndices[instanceIndex];
let point = positions[actualIndex];
let center = point.pos;
let radius = 0.03;
let rightOffset = cameraRight.vector * quadOffset.x * radius;
let upOffset = cameraUp.vector * quadOffset.y * radius;
let worldPos = vec4<f32>(center + rightOffset + upOffset, 1.0);
output.Position = viewProjectionMatrix * worldPos;
output.uv = quadOffset;
output.color = (center + vec3<f32>(1.0, 1.0, 1.0)) * 0.5;
return output;
}
@fragment
fn fragmentEntryPoint(
@location(0) uv: vec2<f32>,
@location(1) color: vec3<f32>
) -> @location(0) vec4<f32> {
let dist = length(uv);
if (dist > 1.0) {
discard;
}
let z = sqrt(1.0 - dist * dist);
let normal = normalize(vec3<f32>(uv.x, uv.y, z));
let light = normalize(vec3<f32>(1.0, 1.0, 1.0));
let diffuse = max(dot(normal, light), 0.0);
return vec4<f32>(color * diffuse, 1.0);
}

View File

@@ -0,0 +1,72 @@
@group(0) @binding(0)
var<storage, read_write> positions : array<vec4<f32>>;
@group(0) @binding(1)
var<storage, read_write> velocities : array<vec4<f32>>;
@group(0) @binding(2)
var<uniform> deltaTime : f32;
@group(0) @binding(3)
var<uniform> aspectRatio : f32;
@group(0) @binding(4)
var<uniform> mousePos : vec2<f32>;
@group(0) @binding(5)
var<uniform> hoverRadius : f32;
@group(0) @binding(6)
var<storage, read_write> states : array<u32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id : vec3<u32>) {
let index = id.x;
let gravity = vec3<f32>(0.0, -29.61, 0.0);
var pos = positions[index];
var vel = velocities[index];
// Apply gravity to velocity
// Integrate position
let dist = distance(vec2<f32>(pos.x*aspectRatio, pos.y), mousePos);
var newPos = pos.xyz;
if (dist < hoverRadius) {
states[index] = 1;
}
if ( states[index] == 1 ) {
let newVel = vel.xyz + gravity * deltaTime;
newPos = pos.xyz + newVel * 0.001;
velocities[index] = vec4<f32>(newVel, vel.w);
positions[index] = vec4<f32>(newPos, pos.w);
} else {
let newVel = vel.xyz;
newPos = pos.xyz + newVel * 0.001;
velocities[index] = vec4<f32>(newVel, vel.w);
positions[index] = vec4<f32>(newPos, pos.w);
}
// Store updated values
}

View File

@@ -0,0 +1,95 @@
@group(0) @binding(0)
var<uniform> viewProjectionMatrix : mat4x4<f32>;
@group(0) @binding(1)
var<storage, read> instancePositions : array<vec4<f32>>;
@group(0) @binding(2)
var<uniform> cameraPosition : vec3<f32>;
@group(0) @binding(3)
var myTextureArray: texture_2d_array<f32>;
@group(0) @binding(4)
var mySampler : sampler;
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) worldPosition : vec3<f32>,
@location(1) worldNormal : vec3<f32>,
@location(2) uv : vec2<f32>,
};
@vertex
fn vertexEntryPoint(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>,
@location(2) uv : vec2<f32>,
@builtin(instance_index) instanceIndex : u32
) -> VertexOutput {
var output : VertexOutput;
let instanceOffset = instancePositions[instanceIndex].xyz;
let worldPosition = position + instanceOffset;
output.worldPosition = worldPosition;
output.worldNormal = normalize(normal);
output.position = viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
output.uv = uv;
return output;
}
@fragment
fn fragmentEntryPoint(
@location(0) worldPosition : vec3<f32>,
@location(1) worldNormal : vec3<f32>,
@location(2) uv : vec2<f32>
) -> @location(0) vec4<f32> {
// For test: encode UV as color (no texture sampling)
//let baseColor = vec3<f32>(uv, 0.0);
let baseColor = textureSampleLevel(myTextureArray, mySampler, uv, 0, 0).rgb;
let pi = 3.14159265;
let invPi = 0.318309886;
let N = normalize(worldNormal);
let V = normalize(cameraPosition - worldPosition);
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
let H = normalize(V + L);
let metallic = 0.2;
let roughness = 0.4;
let rough2 = roughness * roughness;
let lightColor = vec3<f32>(1.0);
let NdotV = max(dot(N, V), 0.001);
let NdotL = max(dot(N, L), 0.001);
let NdotH = max(dot(N, H), 0.001);
let HdotV = max(dot(H, V), 0.001);
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
let f = pow(1.0 - HdotV, 5.0);
let F = F0 + (1.0 - F0) * f;
let a2 = rough2 * rough2;
let NdotH2 = NdotH * NdotH;
let denom = NdotH2 * (a2 - 1.0) + 1.0;
let NDF = a2 / (pi * denom * denom);
let k = (roughness + 1.0);
let k2 = (k * k) / 8.0;
let Gv = NdotV / (NdotV * (1.0 - k2) + k2);
let Gl = NdotL / (NdotL * (1.0 - k2) + k2);
let G = Gv * Gl;
let spec = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.001);
let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
let diff = kd * baseColor * invPi;
let color = (diff + spec) * lightColor * NdotL + vec3<f32>(0.03) * baseColor;
return vec4<f32>(pow(color, vec3<f32>(1.0 / 2.2)), 1.0);
}

View File

@@ -0,0 +1,127 @@
@group(0) @binding(0)
var<uniform> viewProjectionMatrix : mat4x4<f32>;
@group(0) @binding(1)
var<storage, read> instancePositions : array<vec4<f32>>;
@group(0) @binding(2)
var<uniform> cameraPosition : vec3<f32>;
@group(0) @binding(3)
var myTexture : texture_2d<f32>;
@group(0) @binding(4)
var mySampler : sampler;
@group(0) @binding(5)
var normalMapTexture : texture_2d<f32>;
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) worldPosition : vec3<f32>,
@location(1) worldNormal : vec3<f32>,
@location(2) worldBitangent : vec3<f32>,
@location(3) uv : vec2<f32>,
@location(4) meshIndex : f32
};
@vertex
fn vertexEntryPoint(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>,
@location(2) bitangent : vec3<f32>,
@location(3) uv : vec2<f32>,
@builtin(instance_index) instanceIndex : u32
) -> VertexOutput {
var output : VertexOutput;
let instanceOffset = instancePositions[instanceIndex].xyz;
let worldPosition = position + instanceOffset;
output.worldPosition = worldPosition;
output.worldNormal = normalize(normal);
output.worldBitangent = normalize(bitangent);
output.position = viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
output.uv = uv;
output.meshIndex = f32( instanceIndex );
return output;
}
@fragment
fn fragmentEntryPoint(
@location(0) worldPosition : vec3<f32>,
@location(1) worldNormal : vec3<f32>,
@location(2) worldBitangent : vec3<f32>,
@location(3) uv : vec2<f32>,
@location(4) meshIndex : f32
) -> @location(0) vec4<f32> {
// For test: encode UV as color (no texture sampling)
//let baseColor = vec3<f32>(uv, 0.0);
let baseColor = textureSample(myTexture, mySampler, uv).rgb;
// Sample normal map and decode
let normalMapSample = textureSample(normalMapTexture, mySampler, uv).rgb;
let tangentSpaceNormal = normalMapSample * 2.0 - 1.0; // Convert [0,1] to [-1,1]
// Construct TBN matrix
let n = normalize(worldNormal);
let B = normalize(worldBitangent);
let T = normalize(cross(B, n));
let TBN = mat3x3<f32>(T, B, n);
// Transform normal to world space
let mappedNormal = normalize(TBN * tangentSpaceNormal);
let pi = 3.14159265;
let invPi = 0.318309886;
let N = normalize(mappedNormal);
let V = normalize(cameraPosition - worldPosition);
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
let H = normalize(V + L);
let roughnessIndex = meshIndex / 30.0 % 1.0;
let metallic = 0.2;
let roughness = roughnessIndex;
let rough2 = roughness * roughness;
let lightColor = vec3<f32>(1.0);
let NdotV = max(dot(N, V), 0.001);
let NdotL = max(dot(N, L), 0.001);
let NdotH = max(dot(N, H), 0.001);
let HdotV = max(dot(H, V), 0.001);
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
let f = pow(1.0 - HdotV, 5.0);
let F = F0 + (1.0 - F0) * f;
let a2 = rough2 * rough2;
let NdotH2 = NdotH * NdotH;
let denom = NdotH2 * (a2 - 1.0) + 1.0;
let NDF = a2 / (pi * denom * denom);
let k = (roughness + 1.0);
let k2 = (k * k) / 8.0;
let Gv = NdotV / (NdotV * (1.0 - k2) + k2);
let Gl = NdotL / (NdotL * (1.0 - k2) + k2);
let G = Gv * Gl;
let spec = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.001);
let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
let diff = kd * baseColor * invPi;
let color = (diff + spec) * lightColor * NdotL + vec3<f32>(0.03) * baseColor;
return vec4<f32>(pow(color, vec3<f32>(1.0 / 2.2)), 1.0);
}

View File

@@ -0,0 +1,93 @@
@group(0) @binding(0)
var<uniform> viewProjectionMatrix : mat4x4<f32>;
@group(0) @binding(1)
var<storage, read> instancePositions : array<vec4<f32>>;
@group(0) @binding(2)
var<uniform> cameraPosition : vec3<f32>;
@group(0) @binding(3)
var myTexture : texture_2d<f32>;
@group(0) @binding(4)
var mySampler : sampler;
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) worldPosition : vec3<f32>,
@location(1) worldNormal : vec3<f32>,
@location(2) uv : vec2<f32>,
};
@vertex
fn vertexEntryPoint(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>,
@location(2) uv : vec2<f32>,
@builtin(instance_index) instanceIndex : u32
) -> VertexOutput {
var output : VertexOutput;
let instanceOffset = instancePositions[instanceIndex].xyz;
let worldPosition = position + instanceOffset;
output.worldPosition = worldPosition;
output.worldNormal = normalize(normal);
output.position = viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
output.uv = uv;
return output;
}
@fragment
fn fragmentEntryPoint(
@location(0) worldPosition : vec3<f32>,
@location(1) worldNormal : vec3<f32>,
@location(2) uv : vec2<f32>
) -> @location(0) vec4<f32> {
// For test: encode UV as color (no texture sampling)
//let baseColor = vec3<f32>(uv, 0.0);
let baseColor = textureSample(myTexture, mySampler, uv).rgb;
let pi = 3.14159265;
let invPi = 0.318309886;
let N = normalize(worldNormal);
let V = normalize(cameraPosition - worldPosition);
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
let H = normalize(V + L);
let metallic = 0.2;
let roughness = 0.4;
let rough2 = roughness * roughness;
let lightColor = vec3<f32>(1.0);
let NdotV = max(dot(N, V), 0.001);
let NdotL = max(dot(N, L), 0.001);
let NdotH = max(dot(N, H), 0.001);
let HdotV = max(dot(H, V), 0.001);
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
let f = pow(1.0 - HdotV, 5.0);
let F = F0 + (1.0 - F0) * f;
let a2 = rough2 * rough2;
let NdotH2 = NdotH * NdotH;
let denom = NdotH2 * (a2 - 1.0) + 1.0;
let NDF = a2 / (pi * denom * denom);
let k = (roughness + 1.0);
let k2 = (k * k) / 8.0;
let Gv = NdotV / (NdotV * (1.0 - k2) + k2);
let Gl = NdotL / (NdotL * (1.0 - k2) + k2);
let G = Gv * Gl;
let spec = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.001);
let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
let diff = kd * baseColor * invPi;
let color = (diff + spec) * lightColor * NdotL + vec3<f32>(0.03) * baseColor;
return vec4<f32>(pow(color, vec3<f32>(1.0 / 2.2)), 1.0);
}

View File

@@ -0,0 +1,78 @@
@group(0) @binding(0)
var<uniform> viewProjectionMatrix : mat4x4<f32>;
@group(0) @binding(1)
var<storage, read> instancePositions : array<vec4<f32>>;
@group(0) @binding(2)
var<uniform> cameraPosition : vec3<f32>;
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) worldPosition : vec3<f32>,
@location(1) worldNormal : vec3<f32>,
};
@vertex
fn vertexEntryPoint(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>,
@builtin(instance_index) instanceIndex : u32
) -> VertexOutput {
var output : VertexOutput;
let instanceOffset = instancePositions[instanceIndex].xyz;
let worldPosition = position + instanceOffset;
output.worldPosition = worldPosition;
output.worldNormal = normalize(normal);
output.position = viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
return output;
}
@fragment
fn fragmentEntryPoint(
@location(0) worldPosition : vec3<f32>,
@location(1) worldNormal : vec3<f32>
) -> @location(0) vec4<f32> {
let pi = 3.14159265;
let invPi = 0.318309886;
let N = normalize(worldNormal);
let V = normalize(cameraPosition - worldPosition);
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
let H = normalize(V + L);
let baseColor = vec3<f32>(1.0);
let metallic = 0.2;
let roughness = 0.4;
let rough2 = roughness * roughness;
let lightColor = vec3<f32>(1.0);
let NdotV = max(dot(N, V), 0.001);
let NdotL = max(dot(N, L), 0.001);
let NdotH = max(dot(N, H), 0.001);
let HdotV = max(dot(H, V), 0.001);
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
let f = pow(1.0 - HdotV, 5.0);
let F = F0 + (1.0 - F0) * f;
let a2 = rough2 * rough2;
let NdotH2 = NdotH * NdotH;
let denom = NdotH2 * (a2 - 1.0) + 1.0;
let NDF = a2 / (pi * denom * denom);
let k = (roughness + 1.0);
let k2 = (k * k) / 8.0;
let Gv = NdotV / (NdotV * (1.0 - k2) + k2);
let Gl = NdotL / (NdotL * (1.0 - k2) + k2);
let G = Gv * Gl;
let spec = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.001);
let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
let diff = kd * baseColor * invPi;
let color = (diff + spec) * lightColor * NdotL + vec3<f32>(0.03) * baseColor;
return vec4<f32>(pow(color, vec3<f32>(1.0 / 2.2)), 1.0);
}