Skip to content

zlatnaspirala/matrix-engine-wgpu

Repository files navigation

matrix-engine-wgpu

Author: Nikola LukiΔ‡ πŸ“§ [email protected] πŸ“… 2025


Logo

Logo includes the official WebGPU logo. WebGPU logo by W3C Licensed under Creative Commons Attribution 4.0


Description

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


Goals

  • βœ”οΈ 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)

Features

Scene Management

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

Camera Options

Supported types: WASD, arcball

mainCameraParams: {
  type: 'WASD',
  responseCoef: 1000
}

Object Position

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

Object Rotation

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()).


3D Camera Example

Manipulate WASD camera:

app.cameras.WASD.pitch = 0.2;

Object Interaction (Raycasting)

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,

How to Load .obj Models

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;

πŸ” Load OBJ Sequence Animation

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

πŸ“½οΈ Video textures

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             |

Note

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'.

About URLParams

Buildin Url Param check for multiLang.

urlQuery.lang;

About main.js

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.


NPM Scripts

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"

Resources

All resources and output go into the ./public folder β€” everything you need in one place. This is static file storage.


Proof of Concept

🎲 The first full app example will be a WebGPU-powered Ultimate Yahtzee game.


Live Demos & Dev Links


License

Usage Note

You may use, modify, and sell projects based on this code β€” just keep this notice and included references intact.


Attribution & Credits


BSD 3-Clause License (from WebGPU Samples)

Full License Text

Top level main.js instance (Ultimate Yahtzee)


About

webGpu powered pwa application. Crazy fast rendering solution. Scene object based.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages