Added NodeJS Demo.
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
python/__pycache__/
|
||||
models/
|
||||
*.pyc
|
||||
package-lock.json
|
||||
844040
Demos/Texture2/demo.json
844040
Demos/Texture3/demo.json
844040
Demos/TextureArray/demo.json
844040
Demos/Triangles/demo.json
51
README.md
@@ -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
@@ -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)
|
||||
})
|
||||
|
||||
*/
|
||||
@@ -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];
|
||||
|
||||
|
Before Width: | Height: | Size: 447 KiB After Width: | Height: | Size: 447 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -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];
|
||||
|
||||
|
Before Width: | Height: | Size: 447 KiB After Width: | Height: | Size: 447 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -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];
|
||||
|
||||
|
Before Width: | Height: | Size: 447 KiB After Width: | Height: | Size: 447 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -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];
|
||||
|
||||
|
Before Width: | Height: | Size: 447 KiB After Width: | Height: | Size: 447 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -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];
|
||||
|
||||
|
Before Width: | Height: | Size: 447 KiB After Width: | Height: | Size: 447 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
1960
framework/WebGpu_node.js
Normal file
214
framework/eventManager_node.js
Normal 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 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
{"type": "module" }
|
||||
{"type":"module","dependencies":{
|
||||
|
||||
"@kmamal/gpu": "^0.2.0",
|
||||
"@kmamal/sdl": "^0.11.12"
|
||||
}}
|
||||