Added NodeJS Demo.

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

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
python/__pycache__/
models/
*.pyc
package-lock.json

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,40 @@ Just GPU work.
### Installation
#### Browser demos
node server.js
then browse to localhost:3030/demos/
#### Nodejs demo
first install. in this project root.
```html
npm i
```
To start the demo:
Open the directory
```command
cd ./demos/NodeJS
```
then execute:
```command
node index.js
```
#### How to use the code
Browser:
```html
@@ -52,7 +86,7 @@ Browser:
Node.js (requires headless or windowed WebGPU runtime):
```bash
npm install webgpu-framework
npm install
```
```js
@@ -207,18 +241,3 @@ 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.

304
demos/NodeJS/index.js Normal file
View File

@@ -0,0 +1,304 @@
import sdl from '@kmamal/sdl'
import gpu from '@kmamal/gpu'
import Shader from "../../framework/WebGpu_node.js"
import Matrix4 from "../../framework/Matrix4.js"
import Vector3 from "../../framework/Vector3.js"
import Camera from "../../framework/Camera.js";
import EventManager from "../../framework/eventManager_node.js";
import { readFileSync } from "node:fs";
const window = sdl.video.createWindow({ webgpu: true })
var canvas = window;
const instance = gpu.create([ 'verbose=1' ])
console.log("devices", gpu)
const adapter = await instance.requestAdapter()
const device = await adapter.requestDevice()
const renderer = gpu.renderGPUDeviceToWindow({ device, window })
canvas.getContext = function() {
return renderer;
}
var renderShader = new Shader( device );
renderShader.setCanvas( canvas );
renderShader.topology = "triangle-list";
await renderShader.setup( "../../shaders/triangle-list.wgsl");
async function loadJSON( pathName ) {
const json = await readFileSync( pathName, 'utf8' )
return JSON.parse( json );
}
var camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
var eventManager = new EventManager( canvas );
eventManager.setup( canvas, camera );
eventManager.registerEventListenersNode();
var frameCount = 0;
var model = await loadJSON("../../models/demo.json");
var mesh = model.meshes[0];
const instanceCount = 100;
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
for (let i = 0; i < instanceCount; i++) {
const x = (i % 10) * 300.0;
const y = Math.floor(i / 10) * 350.0;
instancePositions[i * 4 + 0] = x - 500;
instancePositions[i * 4 + 1] = 0;
instancePositions[i * 4 + 2] = y - 500;
instancePositions[i * 4 + 3] = 0;
}
renderShader.setAttribute( "position", mesh.vertices );
renderShader.setAttribute( "normal", mesh.normals );
renderShader.setVariable( "instancePositions", instancePositions );
var faces = mesh.faces;
const indexArray = new Uint32Array(faces.length * 3);
for (let i = 0; i < faces.length; i++) {
indexArray[i * 3 + 0] = faces[i][0];
indexArray[i * 3 + 1] = faces[i][1];
indexArray[i * 3 + 2] = faces[i][2];
}
renderShader.setIndices( indexArray );
var lastFrameTime = 0;
function updateTimeDelta() {
const now = performance.now();
deltaTimeValue = ( now - lastFrameTime ) / 1000;
lastFrameTime = now;
}
var frameCount = 0;
var deltaTimeValue = 0;
var vertexCount = 1;
const render = () => {
if (window.destroyed) { return }
updateTimeDelta();
const viewMatrixData = camera.getViewMatrix();
const projectionMatrixData = Matrix4.createProjectionMatrix( camera, canvas )
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
renderShader.setVariable( "cameraPosition", cameraPosition );
frameCount++;
renderShader.renderToCanvas( vertexCount, 60, 0, frameCount )
renderer.swap()
frameCount++;
setTimeout(render, 0)
}
render();
window.on('close', () => {
device.destroy()
gpu.destroy(instance)
})
//console.log(model);
/*
const renderer = gpu.renderGPUDeviceToWindow({ device, window })
const positions = new Float32Array([
...[ 1.0, -1.0, 0.0 ],
...[ -1.0, -1.0, 0.0 ],
...[ 0.0, 1.0, 0.0 ],
])
const colors = new Float32Array([
...[ 1.0, 0.0, 0.0 ],
...[ 0.0, 1.0, 0.0 ],
...[ 0.0, 0.0, 1.0 ],
])
const indices = new Uint16Array([ 0, 1, 2 ])
const createBuffer = (arr, usage) => {
const buffer = device.createBuffer({
size: (arr.byteLength + 3) & ~3,
usage,
mappedAtCreation: true,
})
const writeArray = arr instanceof Uint16Array
? new Uint16Array(buffer.getMappedRange())
: new Float32Array(buffer.getMappedRange())
writeArray.set(arr)
buffer.unmap()
return buffer
}
const positionBuffer = createBuffer(positions, gpu.GPUBufferUsage.VERTEX)
const colorBuffer = createBuffer(colors, gpu.GPUBufferUsage.VERTEX)
const indexBuffer = createBuffer(indices, gpu.GPUBufferUsage.INDEX)
const vertexShaderFile = path.join(__dirname, 'vertex.wgsl')
const vertexShaderCode = await fs.promises.readFile(vertexShaderFile, 'utf8')
const fragmentShaderFile = path.join(__dirname, 'fragment.wgsl')
const fragmentShaderCode = await fs.promises.readFile(fragmentShaderFile, 'utf8')
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: device.createShaderModule({ code: vertexShaderCode }),
entryPoint: 'main',
buffers: [
{
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x3',
},
],
arrayStride: 3 * Float32Array.BYTES_PER_ELEMENT,
stepMode: 'vertex',
},
{
attributes: [
{
shaderLocation: 1,
offset: 0,
format: 'float32x3',
},
],
arrayStride: 3 * Float32Array.BYTES_PER_ELEMENT,
stepMode: 'vertex',
},
],
},
fragment: {
module: device.createShaderModule({ code: fragmentShaderCode }),
entryPoint: 'main',
targets: [ { format: renderer.getPreferredFormat() } ],
},
primitive: {
topology: 'triangle-list',
},
})
const render = () => {
if (window.destroyed) { return }
const commandEncoder = device.createCommandEncoder()
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: renderer.getCurrentTextureView(),
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
})
renderPass.setPipeline(pipeline)
renderPass.setViewport(0, 0, width, height, 0, 1)
renderPass.setScissorRect(0, 0, width, height)
renderPass.setVertexBuffer(0, positionBuffer)
renderPass.setVertexBuffer(1, colorBuffer)
renderPass.setIndexBuffer(indexBuffer, 'uint16')
renderPass.drawIndexed(3)
renderPass.end()
device.queue.submit([ commandEncoder.finish() ])
renderer.swap()
setTimeout(render, 0)
}
render()
window.on('close', () => {
device.destroy()
gpu.destroy(instance)
})
*/

View File

@@ -173,7 +173,7 @@ export class ParticleSimulation {
}
var model = await this.loadJSON("demo.json");
var model = await this.loadJSON("../../models/demo.json");
var mesh = model.meshes[0];

View File

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 447 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -173,7 +173,7 @@ export class ParticleSimulation {
}
var model = await this.loadJSON("demo.json");
var model = await this.loadJSON("../../models/demo.json");
var mesh = model.meshes[0];

View File

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 447 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -173,7 +173,7 @@ export class ParticleSimulation {
}
var model = await this.loadJSON("demo.json");
var model = await this.loadJSON("../../models/demo.json");
var mesh = model.meshes[0];

View File

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 447 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -222,7 +222,7 @@ export class ParticleSimulation {
}
var model = await this.loadJSON("demo.json");
var model = await this.loadJSON("../../models/demo.json");
var mesh = model.meshes[0];

View File

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 447 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -69,7 +69,7 @@ export class ParticleSimulation {
alphaMode: "opaque"
});
var model = await this.loadJSON("demo.json");
var model = await this.loadJSON("../../models/demo.json");
var mesh = model.meshes[0];

View File

Before

Width:  |  Height:  |  Size: 447 KiB

After

Width:  |  Height:  |  Size: 447 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

1960
framework/WebGpu_node.js Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
// eventManager.js
import sdl from '@kmamal/sdl'
var isNode = true;
if ( typeof window === 'undefined' ) {
//isNode = false;
}
console.log("isNode", isNode);
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 } );
}
registerEventListenersNode() {
var that = this;
this.canvas.on('mouseMove', function( event ) {
that.mousemove( event )
});
this.canvas.on('mouseButtonDown', function( event ) {
that.mousedown( event )
});
this.canvas.on('mouseButtonUp', function( event ) {
that.mouseup( event )
});
/*
this.canvas.on( "mouseButtonDown", this.onMouseDown.bind(this) );
this.canvas.on( "mouseButtonUp", this.onMouseUp.bind(this) );
//this.canvas.on( "mouseleave", this.onMouseLeave.bind(this) );
this.canvas.on( "mouseMove", this.onMouseMove.bind(this) );
this.canvas.on( "mouseWheel", 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 ) {
this.isDragging = true;
if( isNode ) {
var mouseX = event.x;
var mouseY = event.y;
} else {
var mouseX = event.clientX;
var mouseY = event.clientY;
}
//console.log("mouseDownHandler", mouseX, mouseY);
if( isNode ) {
this.lastX = mouseX;
this.lastY = mouseY;
} else {
this.lastX = mouseX;
this.lastY = mouseY;
}
}
mouseup( event ) {
this.isDragging = false;
}
mouseleave( event ) {
this.isDragging = false;
}
mousemove( event ) {
if( isNode ) {
var mouseX = event.x;
var mouseY = event.y;
} else {
var mouseX = event.clientX;
var mouseY = event.clientY;
}
if ( !this.isDragging ) return;
const deltaX = ( mouseX - this.lastX ) * 0.005;
const deltaY = ( mouseY - this.lastY ) * 0.005;
//console.log("mousemove", mouseX, mouseY);
this.camera.rotate( deltaX, -deltaY );
this.lastX = mouseX;
this.lastY = mouseY;
}
wheel( event ) {
const delta = event.deltaY * 0.01;
this.camera.zoom( delta );
}
}

View File

@@ -1 +1,5 @@
{"type": "module" }
{"type":"module","dependencies":{
"@kmamal/gpu": "^0.2.0",
"@kmamal/sdl": "^0.11.12"
}}

View File

@@ -28,7 +28,9 @@ class App
this.httpServer = http.createServer( this.handleRequest.bind( this ) );
this.httpServer.listen( 3000 );
this.httpServer.listen( 3030 );
console.log("Server started on port http://localhost:3030/");
}