Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ melonJS is designed so you can **focus on making games, not on graphics plumbing

- **Complete engine, minimal footprint** — Physics, tilemaps, audio, input, cameras, tweens, particles, UI — a full game stack in a single tree-shakeable ES module. No dependency sprawl, no library stitching.

- **Scenes, loaded in one call** — `me.level.load(name)` brings an authored scene straight into your world. [Tiled](https://www.mapeditor.org) is a first-class citizen for **2D** — orthogonal, isometric, hexagonal & staggered maps, animated tilesets, collision shapes, object properties, compressed formats, with GPU-accelerated tile rendering under WebGL 2 — and **glTF / GLB** is the equivalent for **3D scenes**: author in Blender (or any DCC tool), export a `.glb`, and the whole scene — meshes, materials, cameras, and lights — loads under a `Camera3d`, no per-mesh wiring.
- **Scenes, loaded in one call** — `level.load(name)` brings an authored scene straight into your world. [Tiled](https://www.mapeditor.org) is a first-class citizen for **2D** — orthogonal, isometric, hexagonal & staggered maps, animated tilesets, collision shapes, object properties, compressed formats, with GPU-accelerated tile rendering under WebGL 2 — and **glTF / GLB** is the equivalent for **3D scenes**: author in Blender (or any DCC tool), export a `.glb`, and the whole scene — meshes, materials, cameras, lights, and node animation — loads under a `Camera3d`, no per-mesh wiring. Animated models play back through the same animation API as a 2D `Sprite`.

- **Batteries included, hackable by design** — Get started in minutes with minimal setup. When you need to go deeper: ES6 classes throughout, a plugin system for engine extensions, and a clean architecture that's easy to extend without fighting the framework.

Expand All @@ -56,7 +56,7 @@ Graphics
- 3D mesh rendering with OBJ/MTL model loading, multi-material support, hardware depth testing, and perspective projection via `Camera3d`
- Lighting, in 2D and 3D:
- **2D** — `Light2d` as a first-class `Renderable` (multiple dynamic lights, radial-gradient falloff, illumination-only mode, procedural rendering via `drawLight`), plus optional per-pixel normal-map shading on sprites for 3D-looking dynamic lights
- **3D** — directional lights via `Light3d` / `LightingEnvironment` (half-Lambert diffuse + ambient floor), auto-loaded from a glTF scene's authored sun
- **3D** — `Light3d` directional + ambient lights, added to the world like `Light2d` (half-Lambert diffuse + ambient fill, runtime-manipulable for day/night), auto-loaded from a glTF scene's authored sun
- Built-in shader effects (Flash, Outline, Glow, Dissolve, CRT, Hologram, etc.) with multi-pass chaining via `postEffects`, plus custom shader support via `ShaderEffect` for per-sprite fragment effects (WebGL)
- Trail renderable for fading, tapering ribbons behind moving objects (speed lines, sword slashes, magic trails)
- System & Bitmap Text with built-in typewriter effect
Expand Down Expand Up @@ -95,7 +95,7 @@ UI
- `UITextButton` text button with hover, press, and key-bind support — built on `BitmapText`

Scenes
- Load a scene in one call with `me.level.load(name)` — 2D Tiled maps and 3D glTF scenes alike, auto-registered on preload
- Load a scene in one call with `level.load(name)` — 2D Tiled maps and 3D glTF scenes alike, auto-registered on preload
- [Tiled](https://www.mapeditor.org) map format [up to 1.12](https://doc.mapeditor.org/en/stable/reference/tmx-changelog/) built-in support for easy level design
- **GPU-accelerated tile rendering** for orthogonal maps under WebGL 2 — each layer draws as a single quad with no per-tile loop, ~5–8× faster than the legacy CPU renderer on dense maps. Honors animated tiles, flip bits, per-layer opacity/tint/blend, and oversized bottom-aligned tiles; falls back transparently to the CPU renderer on isometric/staggered/hexagonal layers or non-WebGL-2 contexts
- Uncompressed and [compressed](https://github.com/melonjs/melonJS/tree/master/packages/tiled-inflate-plugin) Plain, Base64, CSV and JSON encoded XML tilemap loading
Expand All @@ -113,11 +113,12 @@ Scenes
- Dynamic Layer and Object/Group ordering
- Dynamic Entity loading via an extensible object factory registry — register custom handlers for any Tiled class name without modifying engine code
- Shape based Tile collision support
- glTF / GLB 3D scenes — load an authored 3D scene with `me.level.load(...)`, the same one call as a Tiled map
- glTF / GLB 3D scenes — load an authored 3D scene with `level.load(...)`, the same one call as a Tiled map
- The whole scene loads at once — meshes, materials, cameras and lights — viewed under a `Camera3d`
- Automatically lit by the scene's directional lights (the sun set up in the authoring tool)
- Textured, solid-colored, and vertex-colored materials
- `.glb` and self-contained `.gltf` files
- Node animation — walk/idle/sprint characters, spinning pickups, doors, lifts — played through the same `setCurrentAnimation` / `play` / `pause` / `stop` API as a 2D `Sprite`
- `.glb` and `.gltf` files, with embedded *or* external buffers & textures
- Works with any glTF authoring tool (Blender, Maya, 3ds Max, Cinema 4D, …)

Assets
Expand Down Expand Up @@ -182,7 +183,8 @@ Examples
* [3D Mesh](https://melonjs.github.io/melonJS/examples/#/mesh-3d) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/mesh3d))
* [3D Mesh Material](https://melonjs.github.io/melonJS/examples/#/mesh-3d-material) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/mesh3dMaterial))
* [AfterBurner Clone](https://melonjs.github.io/melonJS/examples/#/after-burner) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/afterBurner)) — `Camera3d` + 3D Mesh arcade shooter
* [glTF Scene](https://melonjs.github.io/melonJS/examples/#/gltf) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/gltf)) — a Blender-authored, lit 3D scene loaded with `me.level.load`
* [glTF Scene](https://melonjs.github.io/melonJS/examples/#/gltf) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/gltf)) — a Blender-authored, lit 3D scene loaded with `level.load`
* [glTF Animated Model](https://melonjs.github.io/melonJS/examples/#/gltf-character) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/gltf)) — a rigged character (Kenney Blocky Characters) with node animation, driven by the Sprite-aligned `setCurrentAnimation` / `play` / `pause` / `stop` API
* [Trail](https://melonjs.github.io/melonJS/examples/#/trail) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/trail))
* [Shader Effects](https://melonjs.github.io/melonJS/examples/#/shader-effects) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/shaderEffects))
* [Spine](https://melonjs.github.io/melonJS/examples/#/spine) ([source](https://github.com/melonjs/melonJS/tree/master/packages/examples/src/examples/spine))
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
283 changes: 283 additions & 0 deletions packages/examples/src/examples/gltf/ExampleGltfCharacter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
/**
* melonJS — glTF/GLB animated model example.
* Loads a rigged blocky character (Kenney Blocky Characters, CC0) exported as
* GLB via the level director via `level.load`. The asset defines node-TRS
* animation clips (walk, idle, sprint, …) over a rigid node hierarchy — no
* skinning — driven through the Sprite-aligned animation API
* (`setCurrentAnimation` / `play` / `pause` / `stop`).
* Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License.
* See `packages/examples/LICENSE.md` for full license + asset credits.
*/
import { DebugPanelPlugin } from "@melonjs/debug-plugin";
import {
Application,
Camera3d as Camera3dClass,
type CanvasRenderer,
type GLTFModel,
input,
level,
loader,
type Pointer,
plugin,
Renderable,
state,
video,
type WebGLRenderer,
} from "melonjs";
import { createExampleComponent } from "../utils";

const base = `${import.meta.env.BASE_URL}assets/gltf/`;

// pixels per glTF unit — the character is ~1.8 units tall, so this puts it at a
// few hundred pixels on screen.
const SCALE = 200;

/**
* A screen-fixed sky gradient drawn behind the model. `Camera3d` doesn't clear
* to the world `backgroundColor`, so we paint our own sky as a `floating`
* (screen-space, perspective-exempt) renderable.
*/
function bakeSky() {
const c = document.createElement("canvas");
c.width = 1;
c.height = 512;
const ctx = c.getContext("2d");
if (ctx) {
const g = ctx.createLinearGradient(0, 0, 0, 512);
g.addColorStop(0, "#2b5876");
g.addColorStop(0.6, "#5b86a8");
g.addColorStop(1, "#c7dceb");
ctx.fillStyle = g;
ctx.fillRect(0, 0, 1, 512);
}
return c;
}

class SkyBackdrop extends Renderable {
private sky = bakeSky();

constructor() {
super(0, 0, 1, 1);
this.floating = true; // screen-space — ignore the perspective camera
this.anchorPoint.set(0, 0);
}

override draw(renderer: CanvasRenderer | WebGLRenderer) {
renderer.drawImage(
this.sky,
0,
0,
1,
512,
0,
0,
renderer.width,
renderer.height,
);
}
}

const createGame = () => {
let app: Application;
try {
app = new Application(1024, 768, {
parent: "screen",
renderer: video.WEBGL, // Mesh rendering requires WebGL
scale: "auto",
cameraClass: Camera3dClass,
});
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
globalThis.alert(
"This example couldn't start: WebGL isn't available.\n\n" +
"glTF mesh rendering requires a WebGL-capable browser/GPU.\n\n" +
`Details: ${reason}`,
);
throw err;
}

plugin.register(DebugPanelPlugin, "debugPanel");

let domCleanup: (() => void) | null = null;
let pointerCleanup: (() => void) | null = null;

// frame the camera + add the sky + wire the animation controls once the
// model has been instantiated into the world (runs from level.load's
// onLoaded, after the container reset + model creation)
const setupScene = () => {
app.world.addChild(new SkyBackdrop(), -10000);

const scene = loader.getGLTF("character");
// the animated asset loads as a single GLTFModel named after the asset
const model = app.world.getChildByName("character")[0] as GLTFModel;
if (!scene || !model) {
return;
}

// frame a Camera3d on the model: center on its bounds, look down a touch
// at a 3/4 yaw, pulled back to fit the model height.
const { min, max } = scene.bounds;
const cx = ((min[0] + max[0]) / 2) * SCALE;
const cy = -((min[1] + max[1]) / 2) * SCALE; // render space: -Y is up
const cz = -((min[2] + max[2]) / 2) * SCALE;
const spanY = (max[1] - min[1]) * SCALE;

const camera = app.viewport as InstanceType<typeof Camera3dClass>;
camera.setClipPlanes(SCALE * 0.1, 8000);
const clamp = (v: number, lo: number, hi: number) =>
Math.max(lo, Math.min(hi, v));

// orbit state — drag to rotate around the character
let yaw = 0.5;
let pitch = -0.12;
let distance = spanY * 2.4 + 200;
const updateCam = () => {
pitch = clamp(pitch, -1.45, 1.45);
distance = clamp(distance, 120, 4000);
camera.pos.set(
cx + Math.sin(yaw) * Math.cos(pitch) * -distance,
cy + Math.sin(pitch) * distance, // up = -Y
cz - Math.cos(yaw) * Math.cos(pitch) * distance,
);
camera.lookAt(cx, cy, cz);
};
updateCam();

// drag to orbit — radians per pixel dragged. Use the camera-independent
// screen coords (gameScreenX/Y), NOT gameX/gameY: the latter are
// projected through the viewport, so since orbiting moves the camera
// every frame the same pixel would map to a different world point each
// move — a feedback loop that makes the drag jump. gameScreenX/Y come
// straight from the canvas/scale transform and stay stable. (Same
// approach as the glTF Scene example.)
const ORBIT_SENSITIVITY = 0.0022;
let dragging = false;
let lastX = 0;
let lastY = 0;
input.registerPointerEvent("pointerdown", camera, (ev: Pointer) => {
dragging = true;
lastX = ev.gameScreenX;
lastY = ev.gameScreenY;
});
input.registerPointerEvent("pointerup", camera, () => {
dragging = false;
});
input.registerPointerEvent("pointermove", camera, (ev: Pointer) => {
if (!dragging) {
return;
}
yaw += (ev.gameScreenX - lastX) * ORBIT_SENSITIVITY;
pitch -= (ev.gameScreenY - lastY) * ORBIT_SENSITIVITY;
lastX = ev.gameScreenX;
lastY = ev.gameScreenY;
updateCam();
});
pointerCleanup = () => {
input.releasePointerEvent("pointerdown", camera);
input.releasePointerEvent("pointerup", camera);
input.releasePointerEvent("pointermove", camera);
};

// start walking
const clips = model.getAnimationNames();
const initial = clips.includes("walk") ? "walk" : clips[0];
model.setCurrentAnimation(initial);

// ── on-screen animation controls ──────────────────────────────────
const panel = document.createElement("div");
panel.style.cssText =
"position:absolute;top:60px;left:16px;z-index:1000;" +
"font-family:sans-serif;font-size:13px;color:#e0e0e0;" +
"background:rgba(20,20,28,0.72);padding:10px 12px;border-radius:8px;" +
"display:flex;flex-direction:column;gap:8px;min-width:170px;";

// clip selector — every clip the asset defines
const select = document.createElement("select");
select.style.cssText =
"background:#1a1a1a;color:#e0e0e0;border:1px solid #555;" +
"border-radius:4px;padding:4px;font-size:13px;";
for (const name of clips) {
const opt = document.createElement("option");
opt.value = name;
opt.textContent = name;
select.appendChild(opt);
}
select.value = initial;
select.addEventListener("change", () => {
model.play(select.value);
});
panel.appendChild(select);

// transport: play / pause / stop (the Sprite-aligned API)
const row = document.createElement("div");
row.style.cssText = "display:flex;gap:6px;";
const mkBtn = (label: string, fn: () => void) => {
const b = document.createElement("button");
b.textContent = label;
b.style.cssText =
"flex:1;background:#2a2a3a;color:#e0e0e0;border:1px solid #555;" +
"border-radius:4px;cursor:pointer;padding:5px 0;font-size:13px;";
b.addEventListener("click", fn);
row.appendChild(b);
};
mkBtn("▶ play", () => model.play(select.value));
mkBtn("⏸ pause", () => model.pause());
mkBtn("⏹ stop", () => model.stop());
panel.appendChild(row);

// speed multiplier
const speedLabel = document.createElement("label");
speedLabel.style.cssText = "display:flex;align-items:center;gap:8px;";
const speedValue = document.createElement("span");
speedValue.textContent = "1.0×";
speedValue.style.minWidth = "34px";
const speed = document.createElement("input");
speed.type = "range";
speed.min = "0";
speed.max = "3";
speed.step = "0.1";
speed.value = "1";
speed.style.flex = "1";
speed.addEventListener("input", () => {
model.animationspeed = Number.parseFloat(speed.value);
speedValue.textContent = `${model.animationspeed.toFixed(1)}×`;
});
speedLabel.append("speed", speed, speedValue);
panel.appendChild(speedLabel);

const hint = document.createElement("div");
hint.textContent = "drag to rotate · pick a clip · play / pause / stop";
hint.style.cssText = "font-size:11px;color:#9fc3e0;";
panel.appendChild(hint);

const parent = app.renderer.getCanvas().parentElement;
if (parent) {
parent.style.position = "relative";
parent.appendChild(panel);
}
domCleanup = () => {
panel.remove();
};
};

loader.preload(
// the GLB references an external texture (Textures/texture-a.png),
// resolved relative to the asset URL by the loader — no repackaging.
[{ name: "character", type: "glb", src: `${base}character.glb` }],
() => {
state.change(state.DEFAULT, true);
level.load("character", { scale: SCALE, onLoaded: setupScene });
},
);

return () => {
if (pointerCleanup) {
pointerCleanup();
}
if (domCleanup) {
domCleanup();
}
};
};

export const ExampleGltfCharacter = createExampleComponent(createGame);
13 changes: 13 additions & 0 deletions packages/examples/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ const ExampleGltf = lazy(() =>
default: m.ExampleGltf,
})),
);
const ExampleGltfCharacter = lazy(() =>
import("./examples/gltf/ExampleGltfCharacter").then((m) => ({
default: m.ExampleGltfCharacter,
})),
);
const ExampleMesh3d = lazy(() =>
import("./examples/mesh3d/ExampleMesh3d").then((m) => ({
default: m.ExampleMesh3d,
Expand Down Expand Up @@ -373,6 +378,14 @@ const examples: {
description:
"A Blender-authored scene (Kenney Platformer Kit, CC0) exported to GLB and loaded via the glTF Tier-1 importer — each node instantiated as a Mesh under a Camera3d.",
},
{
component: <ExampleGltfCharacter />,
label: "glTF Animated Model",
path: "gltf-character",
sourceDir: "gltf",
description:
"A rigged blocky character (Kenney Blocky Characters, CC0) loaded from GLB — node-TRS animation over a rigid hierarchy (walk, idle, sprint, …) driven through the Sprite-aligned setCurrentAnimation / play / pause / stop API.",
},
{
component: <ExampleMesh3dMaterial />,
label: "3D Material",
Expand Down
Loading
Loading