Author: Nikola Lukić 📧 zlatnaspirala@gmail.com 📅 2025
Logo includes the official WebGPU logo. WebGPU logo by W3C Licensed under Creative Commons Attribution 4.0
This project is a work-in-progress WebGPU engine inspired by the original matrix-engine for WebGL.
It uses the wgpu-matrix
npm package as a modern replacement for gl-matrix
to handle model-view-projection matrices.
Published on npm as: matrix-engine-wgpu
- ✔️ Support for 3D objects and scene transformations
- 🎯 Replicate matrix-engine (WebGL) features
- 📦 Based on the
shadowMapping
sample from webgpu-samples - ✔️ Ammo.js physics integration (basic cube)
-
Canvas is dynamically created in JavaScript—no
<canvas>
element needed in HTML. -
Access the main scene objects:
app.mainRenderBundle[0];
-
Add meshes with
.addMeshObj()
, supporting.obj
loading, unlit textures, cubes, spheres, etc. -
Cleanly destroy the scene:
app.destroyProgram();
Supported types: WASD
, arcball
mainCameraParams: {
type: 'WASD',
responseCoef: 1000
}
Control object position:
app.mainRenderBundle[0].position.translateByX(12);
Teleport / set directly:
app.mainRenderBundle[0].position.SetX(-2);
Adjust movement speed:
app.mainRenderBundle[0].position.thrust = 0.1;
⚠️ For physics-enabled objects, use Ammo.js functions —.position
and.rotation
are not visually applied but can be read.
Example:
app.matrixAmmo.rigidBodies[0].setAngularVelocity(new Ammo.btVector3(0, 2, 0));
app.matrixAmmo.rigidBodies[0].setLinearVelocity(new Ammo.btVector3(0, 7, 0));
Manual rotation:
app.mainRenderBundle[0].rotation.x = 45;
Auto-rotate:
app.mainRenderBundle[0].rotation.rotationSpeed.y = 10;
Stop rotation:
app.mainRenderBundle[0].rotation.rotationSpeed.y = 0;
⚠️ For physics-enabled objects, use Ammo.js methods (e.g.,.setLinearVelocity()
).
Manipulate WASD camera:
app.cameras.WASD.pitch = 0.2;
The raycast returns:
{
rayOrigin: [x, y, z],
rayDirection: [x, y, z] // normalized
}
Manual raycast example:
window.addEventListener('click', (event) => {
let canvas = document.querySelector('canvas');
let camera = app.cameras.WASD;
const { rayOrigin, rayDirection } = getRayFromMouse(event, canvas, camera);
for (const object of app.mainRenderBundle) {
if (rayIntersectsSphere(rayOrigin, rayDirection, object.position, object.raycast.radius)) {
console.log('Object clicked:', object.name);
}
}
});
Automatic raycast listener:
addRaycastListener();
window.addEventListener('ray.hit.event', (event) => {
console.log('Ray hit:', event.detail.hitObject);
});
import MatrixEngineWGPU from "./src/world.js";
import { downloadMeshes } from './src/engine/loader-obj.js';
export let application = new MatrixEngineWGPU({
useSingleRenderPass: true,
canvasSize: 'fullscreen',
mainCameraParams: {
type: 'WASD',
responseCoef: 1000
}
}, () => {
addEventListener('AmmoReady', () => {
downloadMeshes({
welcomeText: "./res/meshes/blender/piramyd.obj",
armor: "./res/meshes/obj/armor.obj",
sphere: "./res/meshes/blender/sphere.obj",
cube: "./res/meshes/blender/cube.obj",
}, onLoadObj);
});
function onLoadObj(meshes) {
application.myLoadedMeshes = meshes;
for (const key in meshes) {
console.log(`%c Loaded obj: ${key} `, LOG_MATRIX);
}
application.addMeshObj({
position: {x: 0, y: 2, z: -10},
rotation: {x: 0, y: 0, z: 0},
rotationSpeed: {x: 0, y: 0, z: 0},
texturesPaths: ['./res/meshes/blender/cube.png'],
name: 'CubePhysics',
mesh: meshes.cube,
physics: {
enabled: true,
geometry: "Cube"
}
});
application.addMeshObj({
position: {x: 0, y: 2, z: -10},
rotation: {x: 0, y: 0, z: 0},
rotationSpeed: {x: 0, y: 0, z: 0},
texturesPaths: ['./res/meshes/blender/cube.png'],
name: 'SpherePhysics',
mesh: meshes.sphere,
physics: {
enabled: true,
geometry: "Sphere"
}
});
}
});
window.app = application;
If this happen less then 15 times (Loading procces) then it is ok probably...
Draw func (err):TypeError: Failed to execute 'beginRenderPass' on 'GPUCommandEncoder': The provided value is not of type 'GPURenderPassDescriptor'.
I act according to the fact that there is only one canvas element on the page.
Buildin Url Param check for multiLang.
urlQuery.lang
main.js
is the main instance for the Ultimate Yahtzee game template.
It contains the game context, e.g., dices
.
For a clean startup without extra logic, use empty.js
.
This minimal build is ideal for online editors like CodePen or StackOverflow snippets.
Uses watchify
to bundle JavaScript.
"main-worker": "watchify app-worker.js -p [esmify --noImplicitAny] -o public/app-worker.js",
"examples": "watchify examples.js -p [esmify --noImplicitAny] -o public/examples.js",
"main": "watchify main.js -p [esmify --noImplicitAny] -o public/app.js",
"empty": "watchify empty.js -p [esmify --noImplicitAny] -o public/empty.js",
"build-all": "npm run main-worker && npm run examples && npm run main && npm run build-empty"
All resources and output go into the ./public
folder — everything you need in one place.
🎲 The first full app example will be a WebGPU-powered Ultimate Yahtzee game.
- Jamb WebGPU Demo (WIP)
-
CodePen Demo
→ Uses
empty.js
build from: https://maximumroulette.com/apps/megpu/empty.js - CodeSandbox Implementation
- 📘 Learning Resource: WebGPU Ray Tracing
You may use, modify, and sell projects based on this code — just keep this notice and included references intact.
- Engine design and scene structure inspired by: WebGPU Samples
- OBJ Loader adapted from: http://math.hws.edu/graphicsbook/source/webgl/cube-camera.html
- Dice roll sound
roll1.wav
sourced from: https://wavbvkery.com/dice-rolling-sound/ - Raycasting logic assisted by ChatGPT