Author: Nikola LukiΔ π§ [email protected] π 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];
or
app.getSceneObjectByName("Sphere1");
-
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
}
Best way for access physics body object: app.matrixAmmo.getBodyByName(name) also app.matrixAmmo.getNameByBody
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);
});
Engine also exports (box):
- addRaycastsAABBListener
- rayIntersectsAABB,
- computeAABB,
- computeWorldVertsAndAABB,
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;
This example shows how to load and animate a sequence of .obj files to simulate mesh-based animation (e.g. walking character).
import MatrixEngineWGPU from "../src/world.js";
import { downloadMeshes, makeObjSeqArg } from "../src/engine/loader-obj.js";
import { LOG_MATRIX } from "../src/engine/utils.js";
export var loadObjsSequence = function () {
let loadObjFile = new MatrixEngineWGPU({
useSingleRenderPass: true,
canvasSize: "fullscreen",
mainCameraParams: {
type: "WASD",
responseCoef: 1000,
},
}, () => {
addEventListener("AmmoReady", () => {
downloadMeshes(
makeObjSeqArg({
id: "swat-walk-pistol",
path: "res/meshes/objs-sequence/swat-walk-pistol",
from: 1,
to: 20,
}),
onLoadObj,
{ scale: [10, 10, 10] }
);
});
function onLoadObj(m) {
console.log(`%c Loaded objs: ${m} `, LOG_MATRIX);
var objAnim = {
id: "swat-walk-pistol",
meshList: m,
currentAni: 1,
animations: {
active: "walk",
walk: { from: 1, to: 20, speed: 3 },
walkPistol: { from: 36, to: 60, speed: 3 },
},
};
loadObjFile.addMeshObj({
position: { x: 0, y: 2, z: -10 },
rotation: { x: 0, y: 0, z: 0 },
rotationSpeed: { x: 0, y: 0, z: 0 },
scale: [100, 100, 100],
texturesPaths: ["./res/meshes/blender/cube.png"],
name: "swat",
mesh: m["swat-walk-pistol"],
physics: {
enabled: false,
geometry: "Cube",
},
objAnim: objAnim,
});
app.mainRenderBundle[0].objAnim.play("walk");
}
});
window.app = loadObjFile;
};
TEST.loadVideoTexture({
type: 'video', // video , camera //not tested yet canvas2d , canvas2dinline
src: 'res/videos/tunel.mp4'
});
For canvasinline attach this to arg (example for direct draw on canvas2d and passing intro webgpu pipeline):
canvaInlineProgram: (ctx, canvas) => {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = '20px Orbitron';
ctx.fillText(`FPS: ${Math.round(performance.now() % 60)}`, 10, 30);
}
| Scenario | Best Approach | | ------------------------------ | ---------------------------------- | | Dynamic 2D canvas animation | `canvas.captureStream()` β `video` | | Static canvas snapshot | `createImageBitmap(canvas)` | | Replaying real video or webcam | Direct `video` element |
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'.
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.
This is static file storage.
π² 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
Top level main.js instance (Ultimate Yahtzee)