Author: Nikola LukiΔ
π§ zlatnaspirala@gmail.com
π
Version: 1.11.3 2026 The beast
This project is a stable but also work-in-progress WebGPU engine inspired by the original matrix-engine for WebGL.
It uses the wgpu-matrix npm package as a replacement for gl-matrix to handle model-view-projection matrices.
Published on npm as: matrix-engine-wgpu
Backend editor (works in local env - desktop browsers) support list:
HOT: For 1.1x.0 Firefox render not FIXED yet.
shadowMapping sample from webgpu-samples.npm i matrix-engine-wgpu.EditorX has two main parts:
./src/tools/editor)./src/tools/editor/backend)Before running anything, install dependencies with
npm i:
- in the root folder
- and also inside
./src/tools/editor/backend
The backend is built using Node.js π’
onTargetReach eventsexport const aiConfig = {
ollama: "***********",
qrok: "************",
};
For now i create graphs with simple tasks.
In future it will be able to create whole scenes or simple games.Visual Scripting is only available when running the engine from source
(not from npm i matrix-engine-wgpu).
You must clone or download the engine source from the GitHub repository.
Editor use esbuild compiler but still present browserify, both works perfect.
npm run editorx
from the engine root directory. EditorX is an alias for FluxCodexVertex backend part (needed three words to keep the name unique) Run the scene by pressing F6 or by clicking Run in the left panel If you delete all objects from the scene, you must refresh the page and add at least one object again Before importing a graph, delete all nodes from the FluxCodexVertex graph After deleting everything, click Save to store an empty [] array. All changes in graph must be saved manually/clicking for now πΎ (no autosave for graphs).
Live : https://maximumroulette.com/apps/webgpu/api-docs/
Canvas is dynamically created in JavaScriptβno <canvas> element needed in HTML.
Access the main scene objects:
app.mainRenderBundle[0];
or by name:
app.getSceneObjectByName("Sphere1");
Add meshes with .addMeshObj(), supporting .obj loading, unlit textures, cubes, spheres, etc.
Destroy sceneObj:
app.removeSceneObjectByName("Sphere1");
Also interest for clearing physics body and render part:
app.destroyByPrefix("towers");
// OR
app.destroyBySufix("_001");
// Destroy objects become true after calling flushDestroyQueue function.
app.flushDestroyQueue();
Cleanly destroy the scene:
app.destroyProgram();
Add in args for instancing main MEWGPU class.
mainCameraParams: {
type: 'WASD',
responseCoef: 1000
}
Access camera:
let cam = app.getCamera();
cam.setPitch(-0.26);
cam.setYaw(-0.06);
cam.setY(15);
cam.setZ(11);
cam._dirtyAngle = true;
For now translation is only with WASD keyboard keys.
Supported types: WASD, RPGCamera, FirstPersonCamera, cinematicCamera
WASD also use 'c' and 'v' for up and down camera position.
To change position dont use direct edit on position array. Use methods:
maze.cameras.firstPersonCamera.movementSpeed = 0.1;
maze.collisionSystem.registerCamera(maze.cameras.firstPersonCamera.position, 1.0);
maze.cameras.firstPersonCamera.setPosition(-49, 10.40, -49);
maze.cameras.firstPersonCamera.setTarget(-49, 0.0, -49);
or
const cam = app.getCamera();
cam.setYaw(-0.03);
cam.setPitch(-0.49);
cam.setZ(0);
cam.setY(33);
cam._dirtyAngle = true;
Cinematic camera:
const introPath = new CameraPath([
{position: [0, 5, 20], target: [0, 0, 0]},
{position: [10, 12, 10], target: [0, 1, 0]},
{position: [0, 15, -22], target: [0, 0, 0]},
], {parameterization: 'arc'});
if(cam.setPath) cam.setPath(introPath).play({
speed: 0.3,
onEnd: () => console.log('done'),
});
Best way for access physics body object:
app.matrixPhysics.getBodyByName(name)
Return idx of body.Use that idx for any call of worker fn.
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 matrixPhysics functions β
.positionand.rotationare not visually applied but can be read.from 1.11.0 physics works from webworkers getBodyByName on cleaver way return body id not body inself but all funck now use ids not real body ref. interface stay same. see examples for physics like flipper game.
Example for apply physics on scene object click.
physicsPlayground.canvas.addEventListener("ray.hit.event", e => {
console.log("ray.hit.event:", e.detail);
const physics = app.matrixPhysics; // The engine instance
const body = physics.getBodyByName(e.detail.hitObject.name);
if (!body) return;
// 1. Apply Impulse up
// physics.applyImpulse(body, new PVector(0, 5, 0));
// 2. Set Angular Velocity
// physics.setAngularVelocity(body, new PVector(0, 9, 9));
// 3. Directional hit based on ray
// const dir = e.detail.rayDirection; // assuming [x, y, z]
// const strength = 20;
// physics.applyImpulse(body, new PVector(
// dir[0] * strength,
// dir[1] * strength,
// dir[2] * strength
// ));
// 4. Explosion example
const hitPos = new PVector(
e.detail.hitPoint.x,
e.detail.hitPoint.y,
e.detail.hitPoint.z,
);
physics.explode(idx, hitPos, 10, 50);
// 5. Change Materials
// const metal = {friction: 0.4, restitution: 0.1};
// physics.setMaterial(body, metal.friction, metal.restitution);
});
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 matrixPhysics methods (e.g.,
.applyImpulse).
π‘ Lighting System
Matrix Engine WGPU now supports independent light entities, meaning lights are no longer tied to the camera. You can freely place and configure lights in the scene, and they will affect objects based on their type and parameters.
Supported Light Types Ambient global also per material SpotLight β Emits light in a cone shape with configurable cutoff angles.
(Planned: PointLight)
Features
β Supports multiple lights (4 max), ~20 for next update. β Shadow-ready (spotlight0 shadows implemented, extendable to others)
## Important Required to be added manual:
engine.addLight();
Access lights with array lightContainer:
app.lightContainer[0];
app.getSceneLightByName("light1");
Small behavior object.
Make light move by x.
loadObjFile.addLight();
loadObjFile.lightContainer[0].behavior.setOsc0(-1, 1, 0.01);
loadObjFile.lightContainer[0].behavior.value_ = -1;
loadObjFile.lightContainer[0].updater.push(light => {
light.setPosX(light.behavior.setPath0());
// setTargetX ...
});
With last glb feature materials become part of engine also.
material: {
type: "standard";
}
material: {
type: "pong";
}
material: {
type: "power";
}
material: {
type: "water";
}
material: {
type: "metal";
}
material: {
type: "mirror";
}
// Also for addMeshObj
TEST_ANIM.addGlbObj({
material: {type: 'power'},
...
}, null, glbFile);
// Mirror example
TEST_ANIM.addGlbObj({
material: {type: 'mirror'},
envMapParams: {
baseColorMix: 0.55,
mirrorTint: [0.9, 0.95, 1.0], // Slight cool tint
reflectivity: 0.95, // 25% reflection blend
illuminateColor: [0.3, 0.7, 1.0], // Soft cyan
illuminateStrength: 0.1, // Gentle rim
illuminatePulse: 0.001, // No pulse (static)
fresnelPower: 5.0, // Medium-sharp edge
envLodBias: 2.5,
usePlanarReflection: false, // β
FOr now only false - Env map mode
},
...
}, null, glbFile);
// Access some object with mirror mat
app.mainRenderBundle[5].writeParamsMirror({
baseColorMix: 0.9,
mirrorTint: [0.9, 0.95, 1.0],
reflectivity: 0.25,
illuminateColor: [1.3, 0.7, 1.0],
illuminateStrength: 11.4,
illuminatePulse: 1.1,
fresnelPower: 2.5,
envLodBias: 2.5,
usePlanarReflection: false,
});
await app.mainRenderBundle[0].loadTex0(["res/icons/editor/chatgpt-gen-bg.png"]);
app.mainRenderBundle[0].changeTexture(app.mainRenderBundle[0].texture0);
app.mainRenderBundle[0].setBlend(0.5);
Examples for setup water params:
app.mainRenderBundle[0].updateWaterParams(
[0.0, 0.1, 0.3], // Deep: navy
[0.2, 0.6, 0.9], // Shallow: blue
2.0, // Wave speed: fast continuous
1.8, // Wave scale: rolling waves
0.5, // Wave height: tall active waves
1.5, // Fresnel: strong
50.0, // Specular: soft highlights
);
app.mainRenderBundle[0].updateWaterParams(
[0.0, 0.3, 0.5], // Deep: medium blue
[0.3, 0.8, 1.0], // Shallow: bright cyan
1.2, // Wave speed: gentle continuous (changed from 0.6)
2.5, // Wave scale: smooth ripples (changed from 5.0)
0.3, // Wave height: visible movement
2.5, // Fresnel: moderate reflection
100.0, // Specular: sharp sparkles
);
Activation for prebuild(buildin) effects
TEST_ANIM.addGlbObj({
...
pointerEffect: {
enabled: true,
flameEffect: true,
flameEmitter: true,
},
...
}, null, glbFile);
// Access in runtime :
// app.mainRenderBundle[0].effects
Use random for creation than when you find perfect crazzy fancy case just collect input data see: https://gist.github.com/zlatnaspirala/8758bcfb8be81d65c2c428e222abbef2
Activete with :
app.activateBloomEffect();
Manipulate with app.bloomPass:
setKnee
setIntensity
setThreshold
setBlurRadius
Fancy results:

Call from app app.activateVolumetricEffect()
!Note volumetric works only if bloom is activated. Bloom can work alone.
Passing arguments:
app.activateVolumetricEffect({
density: 0.03,
steps: 32,
scatterStrength: 1.2,
heightFalloff: 0.08,
lightColor: [1.0, 0.88, 0.65],
});
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;
// let camera = app.getCamera();
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();
// Must be app.canvas or [Program name].canvas
app.canvas.addEventListener("ray.hit.event", event => {
console.log("Ray hit:", event.detail.hitObject);
});
Engine also exports (box):
.obj Modelsimport 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("PhysicsReady", () => {
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/textures/cube-g1-extra_low.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/textures/cube-g1-extra_low.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("PhysicsReady", () => {
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/textures/cube-g1-extra_low.png"],
name: "swat",
mesh: m["swat-walk-pistol"],
physics: {
enabled: false,
geometry: "Cube",
},
objAnim: objAnim,
});
app.mainRenderBundle[0].objAnim.play("walk");
}
},
);
window.app = loadObjFile;
};
π‘ GLB binary loading bvh(rig)animations.
From 1.6.0 glb support multi skinned mesh + mutli primitives cases.
Limitation: glb loader not handled for non animation case. Use obj loader for static mesh.
Must powerfull call is new class MEMeshObjInstances. MEMeshObj is good now for optimised call(less conditionals). You can add instanced draws and modify basic color for each individualy also transformation good for fantazy or any game dev.
Example:
var glbFile01 = await fetch(p).then(res =>
res.arrayBuffer().then(buf => uploadGLBModel(buf, this.core.device)),
);
this.core.addGlbObjInctance(
{
material: {type: "standard", useTextureFromGlb: true},
scale: [20, 20, 20],
position: {x: 0, y: -4, z: -220},
name: this.name,
texturesPaths: ["./res/meshes/glb/textures/mutant_origin.webp"],
raycast: {enabled: true, radius: 1.5},
pointerEffect: {enabled: true},
},
null,
glbFile01,
);
// access - index -0 is BASE MESH ! I added maxLimit = 5 you can change this from engine source.
// added lepr smoot translate , also color+.
app.mainRenderBundle[1].instanceTargets[1].position[2] = 10;
// This recreate buffer it is not for loop call space
app.mainRenderBundle[1].updateInstances(5);
export const geoTypesForMorph = {
cube: "cube",
sphere: "sphere",
mobius: "mobius",
plane: "plane",
capsule: "capsule",
cone: "cone",
torus: "torus",
cylinder: "cylinder",
wavePlane: "wavePlane",
supershape: "supershape",
pyramid: "pyramid",
diamond: "diamond",
icosahedron: "icosahedron",
circlePlane: "circlePlane",
rock: "rock",
star: "star",
star3d: "star3d",
littleStar: "littleStar",
flatStar: "flatStar",
klein: "klein",
shell: "shell",
rippleSphere: "rippleSphere",
twistedTorus: "twistedTorus",
tornado: "tornado",
galaxySpiral: "galaxySpiral",
};
Draw all buildin proceduralMesh shapes:
const spacing = 3;
const keys = Object.keys(geoTypesForMorph);
let col = 0;
let row = 0;
for (let i = 0; i < keys.length - 1; i++) {
const typeA = keys[i];
const typeB = keys[i + 1];
loadObjFile.addProceduralMeshObj({
material: {type: "power"},
position: {x: col * spacing - 5, y: 1, z: -15 + row * spacing},
rotation: {x: 0, y: 0, z: 0},
scale: [1, 1, 1],
rotationSpeed: {x: 0, y: 0, z: 0},
texturesPaths: ["./res/textures/cube-g1_low.webp"],
meshA: MeshMorpher[typeA](1),
meshB: MeshMorpher[typeB](1),
name: `morph_${typeA}_to_${typeB}`,
physics: {
enabled: false,
geometry: "Sphere",
},
});
col++;
if (col % 4 === 0) {
row++;
col = 0;
}
}
// Also works - but tested just two cube.
// need to improve and est more
// let test = MeshMorpher.compose(
// {shape: MeshMorpher.cube(1), offset: [-2, 0, 0]},
// {shape: MeshMorpher.cube(1), offset: [2, 0, 0]},
// );
// loadObjFile.addProceduralMeshObj({
// material: {type: 'power'},
// position: {x: 0 , y: 5, z: -15},
// rotation: {x: 0, y: 0, z: 0},
// scale: [1, 1, 1],
// rotationSpeed: {x: 0, y: 0, z: 0},
// texturesPaths: ['./res/textures/cube-g1_low.webp'],
// meshA: test,
// meshB: test,
// name: `morph_1`,
// physics: {
// enabled: false,
// geometry: "Sphere"
// }
// });
Morph between two shapes
sceneObject.morphTo(1.0, 2000, () => {
/*callback*/
});
sceneObject.morphTo(0.0, 2000, () => {
/*callback*/
});
TEST.loadVideoTexture({
type: "video", // video , camera //not tested 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 |
Warn can be disabled from MECongif.logLoopError (bool). 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'.
It is possible for 1 or 2 warn in middle time when mesh switch to the videoTexture. Will be fixxed in next update.
Dimension (TextureViewDimension::e2DArray) of [TextureView of Texture "shadowTextureArray[GLOBAL] num of light 1"] doesn't match the expected dimension (TextureViewDimension::e2D).
From 1.7.0 engine powered by networking. Used kurento&Openvidu server for backend. Very good for handling streams, channels etc...
All you needed to start kurento and openvidu and interconnect this nodejs server https://github.com/zlatnaspirala/matrix-stream/blob/main/middleware/server.js
See example code at ./examples/games/moba/
Buildin net sync basic: Let's say app is engine root object and net is networking object. webRTC tech with openvidu server middleware server
app.net = new MatrixStream({
active: true,
domain: "maximumroulette.com",
port: 2020,
sessionName: "forestOfHollowBlood-free-for-all",
resolution: "160x240",
isDataOnly: urlQuery.camera || urlQuery.audio ? false : true,
customData: forestOfHollowBlood.player.data,
});
// `customData` if you wanna pass some extra meta data on connection-created event
// No need always camera and mic we can use is like data signaling only.
How to use buildin network operations:
// Activate emiting remote position, on remote side adapted on scene object with same name
sceneObject.position.netObject = sceneObject[0].name;
// For now net view for rot is axis separated - cost is ok for orientaion remote pass
sceneObject.rotation.emitY = sceneObject.name;
// If you need oposite remote/local situation. For example:
// you friendly object is enemy object at remote machine that just setup another flag
sceneObject.position.netObject = sceneObject[0].name; // we still need this setup!
sceneObject.position.remoteName = sceneObjecOposite[0].name;
Use toRemote arg prop in send pass, if not it is default [] emit to all.
Intelegent emit for teams (two teams implemented). Position will be emited for teams[0] and received for uniq scene obj name. Position will be emited for teams[1] and received for oposite (eg. enemy) uniq scene obj name. In this case toRemote is overrided (Don't pass it). Used for moba project.
mesh.position.teams[0] = [connId0, connId1];
mesh.position.teams[1] = [connId2, connId3];
// emiter in core engine file
if (this.teams.length > 0)
if (this.teams[0].length > 0)
app.net.send({
toRemote: this.teams[0], // default null remote conns
sceneName: this.netObject, // origin scene name to receive
netPos: {x: this.x, y: this.y, z: this.z},
});
if (this.teams.length > 0)
if (this.teams[1].length > 0)
app.net.send({
toRemote: this.teams[1], // default null remote conns
remoteName: this.remoteName, // to enemy players
sceneName: this.netObject, // now not important
netPos: {x: this.x, y: this.y, z: this.z},
});
Buildin Url Param check for multiLang. MultiLang feature is also buildin options.
Load multilang json file data.
Access from code:
urlQuery.lang;
Also render config can be controlled:
?PHYSICS_GROUND_Y=1 ?fs=true // Force-Fullscreen ?MAX_BONES=0 // optimisation [use this for mobile devices] ?MAX_SPOTLIGHTS=1 // optimisation [use this for mobile devices] ?SHADOW_RES=250 // optimisation [use this for mobile devices] ?GRAVITY_Y_AXIS=10 // optimisation [use this for mobile devices]
Use it :
matrix-engine-wgpu/public/examples?demo=1&SHADOW_RES=250
If you wanna full performance on mobile devices than use in constructor:
fastRender = anynumber;
In this case FORCE_FULL_SCREEN is true by default. Pushed notification click anywhere to start the engine.
Note for MAX_SPOTLIGHTS : If you wanna override values from MECOnfig or even urlParam than use in main engine arg pass:
new MatrixEngineWGPU({
canvasSize: 'fullscreen',
fastRender: 0.9,
render: 'culling',
dontUsePhysics: true,
MAX_SPOTLIGHTS: 1,
MAX_BONES: 0,
mainCameraParams: {
type: 'firstPersonCamera',
// type: 'WASD',
responseCoef: 1000
},
clearColor: {r: 0, b: 0.122, g: 0.122, a: 1}
}, () => { ... })
floor.ignoreCulling = true;empty.jsFor a clean startup without extra logic, use engine build empty.js.
This minimal build is ideal for online editors like CodePen or StackOverflow snippets.
It is the plain text JavaScript ready for exec.
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 Jamb 3d deluxe game.
Features done:
Install it on your desktop with one click.

Invest in Forest Of Hollow Blood 9,660$ See more details at FOHB Wiki
π²π²π² Support JAMB 3D this project on itch.io
MOBA Forest Of Hollow Blood Live
CodePen Demo
β Uses empty.js build from:
https://maximumroulette.com/apps/megpu/empty.js
Performance for Jamb game:
Special licence for MOBA example:
Creative Commons Attribution 4.0 International (CC BY 4.0)
You are free to share and adapt this project, provided that you give appropriate credit.
Attribution requirement:
Include the following notice (with working link) in any distributed version or about page:
"Forest Of Hollow Blood β an MOBA example made with MatrixEngineWGPU (https://github.com/zlatnaspirala/matrix-engine-wgpu)"
Run editor
npm run editorx
Navigate to matrix-engine.html it is landing page for editor.
After create new project or load project page will be redirect to
./public/<PROJECT_NAME>.html
Source location : ./projects/<PROJECT_NAME>
Features :
Create new project/ load project - only on landing page
Create cubeMesh
Properties box for selected sceneObj
Events system (Create func and attach to sceneObj)
Resource navigation
Visual Scripting
@Note License for fluxCodexVertex.js (MPL 2.0) Affect only this file! Just leave comment licence part in header of file.
YT video promotion :
About 'In fly' regime: Editor can be activated even without backend node but in that case no saves.
You may use, modify, and sell projects based on this code β just keep this notice and included references intact (whole licence paragraph - but you can always remove assets -> remove asset back link).
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 and glb loader assisted by ChatGPT.
GLTF Loader: https://github.com/Twinklebear/webgpu-gltf, improved with chatgpt.
Music by Mykola Sosin from Pixabay
Characters used from great mixamo.com -β What you can do You can use Mixamo characters and animations royalty-free in commercial, personal, or nonβprofit projects (games, films, prints, etc.).You own your creations / how you use them.No requirement to credit Adobe / Mixamo (though allowed). -π«What you cannot do You cannot redistribute or sell the raw Mixamo character or animation files βas isβ (i.e. as standalone assets) to others.You canβt use Mixamo content to create a competing library of characters / animations (i.e. you canβt just package them and sell them to others). You canβt use Mixamoβs content (or outputs) to train AI / machine learning models.
Music used: BLACK FLY by Audionautix | http://audionautix.com Music promoted by https://www.free-stock-music.com Creative Commons Attribution-ShareAlike 3.0 Unported
Used free assets from great https://craftpix.net Magic icons : https://craftpix.net/freebies/free-rpg-splash-game-512x512-icons/
For background music in rpg template used: Music by Dvir Silverstone from Pixabay Sound Effect by Crunchpix Studio from Pixabay Music by Emmraan from Pixabay
'Ruined rock fence' (https://skfb.ly/6RLwN) by VladNeko is licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/).
// test "fantasy rock" (https://skfb.ly/oHXAz) by duckcracker02 is licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/).
"Fantasy Rock" (https://skfb.ly/oHZSq) by lalune is licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/).
Invertory images https://djinnbestiary.itch.io/ancient-oddities-vol-1-13-free-potions
"Dancing girl" (https://skfb.ly/p7vNU) by planetrey.com is licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/).
Skeletal system by: Aidan Sanderson - from blender addson blenderKit.
Top level main.js instance (Jamb 3d deluxe)
WebGPU Ray Tracing ChatGPT , claude ai