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
20 changes: 15 additions & 5 deletions packages/examples/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,28 @@ courtesy to the original creator.

Spacecraft 3D models (`craft_speederA`, `craft_speederB`, `craft_racer`,
`craft_miner`) in `public/assets/multiMaterialMesh/` are taken from
**"Space Kit (2.0)"** published by Kenney:

<https://www.kenney.nl/assets/space-kit>

Released under **CC0 1.0 Universal (Public Domain Dedication)** — no
attribution legally required, credited here as a courtesy.

### `waterOverworld` example

The Water Overworld example (`waterOverworld/`) uses the
**"Free Pixel Art Side Scroller Asset Pack (32x32) Overworld"** published by
**GandalfHardcore**, free to use:

<https://gandalfhardcore.itch.io/free-pixel-art-sidescroller-asset-pack-32x32-overworld>

**"Space Kit (2.0)"** published by Kenney:

<https://www.kenney.nl/assets/space-kit>
### `materialTextures` example

Released under **CC0 1.0 Universal (Public Domain Dedication)** — no
attribution legally required, credited here as a courtesy.
The models (`crate.obj`, `panel.obj`), their material files and every
texture in `public/assets/materialTextures/` were authored for this
repository — hand-written geometry and procedurally generated maps. No
third-party assets, no attribution required. The ball re-uses the shared
`assets/mesh3d/sphere.obj` primitive from the mesh3d example.

### `gltf` examples

Expand Down
Binary file modified packages/examples/public/assets/materialTextures/crate-label.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/examples/public/assets/materialTextures/crate-metal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/examples/public/assets/materialTextures/crate-wood.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions packages/examples/public/assets/materialTextures/panel.obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# A double-sided plate for the per-texel opacity demo (#1575). Two quads
# back to back so the cut-outs read from either side without relying on
# back-face culling being off.
mtllib props.mtl

v -1 -1 0
v 1 -1 0
v 1 1 0
v -1 1 0

vt 0 0
vt 1 0
vt 1 1
vt 0 1

vn 0 0 1
vn 0 0 -1

usemtl vent
f 1/1/1 2/2/1 3/3/1
f 1/1/1 3/3/1 4/4/1
f 1/1/2 3/3/2 2/2/2
f 1/1/2 4/4/2 3/3/2
20 changes: 20 additions & 0 deletions packages/examples/public/assets/materialTextures/props.mtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Two materials exercising the MTL properties added in #1575.
#
# `chrome` is the specular case: a dark base so the highlight is the only
# bright thing on it, a near-white `Ks` and a tight `Ns`. Note `Ks` alone
# does nothing — `Ns` of 0 is the format's "no highlight", which is why a
# matte material can safely carry a bright specular colour.
newmtl chrome
Kd 0.10 0.11 0.14
Ks 1.0 0.97 0.90
Ns 180

# `vent` is the per-texel opacity case: a solid steel diffuse punched
# through by `map_d`, so one material cuts to the shape of the holes rather
# than at a single threshold across the whole surface.
newmtl vent
Kd 1.0 1.0 1.0
Ks 0.4 0.45 0.5
Ns 40
map_Kd crate-metal.png
map_d vent-mask.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
/**
* melonJS — Per-material diffuse textures on a multi-material OBJ.
* melonJS — What an MTL can carry: three props, three material features.
*
* One supply crate, built from a `crate.obj` + `crate.mtl` pair whose three
* materials each declare their own `map_Kd`: wooden boards on the sides,
* steel plate on the lid and floor, a shipping label on the front. All the
* example does is preload the pair and construct a `Mesh` — the `Mesh`
* resolves each material's own texture and reduces them to the shortest list
* of index ranges that need switching (`mesh.textureGroups`), which the GPU
* batchers draw as one indexed range each over the same buffers. Adjacent
* materials sharing a map are merged, so this crate costs three draws rather
* than one per material.
* All three are plain OBJ + MTL pairs under a `Camera3d` with one directional
* `Light3d` and an ambient fill. Nothing is wired by hand — every material
* property below comes out of the `.mtl`.
*
* Companion to the `multiMaterialMesh` example, which shows the other half of
* the multi-material path: per-material diffuse *colour* (`Kd`), baked into a
* per-vertex colour buffer at construction and multiplied by a runtime tint.
* - **Crate** — three materials, three different `map_Kd` maps (wood boards,
* steel plate, a shipping label). The `Mesh` reduces them to the shortest
* list of index ranges that need a different texture and the GPU backends
* draw one range each, so this costs three draws rather than one per
* material (#1573).
* - **Ball** — `Ks` + `Ns`, a specular highlight on the lit mesh path, which
* was previously half-Lambert diffuse plus an ambient floor and read as
* chalk whatever the material said (#1575). Its normals are *generated*:
* the sphere ships without `vn`, and the parser accumulates them
* angle-weighted per source position, which is what makes it smooth rather
* than faceted (#1572).
* - **Panel** — `map_d`, per-texel opacity. `alphaCutoff` on its own can only
* threshold uniformly across a material; the map cuts to the shape of the
* perforations (#1575).
*
* Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License.
*/
import { DebugPanelPlugin } from "@melonjs/debug-plugin";
import type { CanvasRenderer, WebGLRenderer } from "melonjs";
import {
Application,
Camera3d,
Light3d,
loader,
Mesh,
plugin,
Renderable,
state,
Vector3d,
video,
} from "melonjs";
Expand All @@ -34,25 +41,33 @@ import { createExampleComponent } from "../utils";

const CANVAS_W = 1024;
const CANVAS_H = 768;
const CRATE_SIZE = 250;
const CRATE_Y = 400;
// caption sits in the empty band above the crate
const CAPTION_Y_PCT = 13;

const ASSET_BASE = `${import.meta.env.BASE_URL}assets/materialTextures/`;
const MESH_BASE = `${import.meta.env.BASE_URL}assets/mesh3d/`;

// the three props, left to right along X at a common depth
const PROP_Y = 0;
const PROP_Z = 520;
const SPACING = 210;

const AXIS_Y = new Vector3d(0, 1, 0);

// ─── entry point ──────────────────────────────────────────────────

const createGame = async () => {
// per-material texture switching is a GPU-backend feature: the Canvas
// renderer solid-fills multi-material meshes per triangle and never
// samples a texture at all, so there would be nothing to see.
// Lit meshes need a GPU backend AND a Camera3d: world normals are only
// written on the 3D path, so a lit mesh under a Camera2d degrades to
// unlit (melonJS/melonJS#1576) and there would be no highlight to see.
let app: Application;
try {
app = new Application(CANVAS_W, CANVAS_H, {
parent: "screen",
renderer: video.AUTO,
scale: "auto",
cameraClass: Camera3d,
// smooth magnification (these maps are stretched across a face
// several times their own size) plus MSAA on the silhouettes —
// the cut-out panel in particular has a lot of edge to alias
antiAlias: true,
});
await app.init();
if (!app.renderer.supportsDepthBuffer) {
Expand All @@ -62,79 +77,137 @@ const createGame = async () => {
const reason = err instanceof Error ? err.message : String(err);
globalThis.alert(
"This example couldn't start: no GPU rendering is available in this browser.\n\n" +
"Per-material mesh textures need a WebGPU- or WebGL-capable " +
"Lit meshes and per-material textures need a WebGPU- or WebGL-capable " +
"browser/GPU. Try enabling hardware acceleration in your browser " +
"settings, or open this example in a different browser.\n\n" +
`Details: ${reason}`,
);
throw err;
}

app.world.backgroundColor.parseCSS("#12161d");
app.world.backgroundColor.parseCSS("#0e1117");
plugin.register(DebugPanelPlugin, "debugPanel");

// only the .obj and .mtl are listed: the MTL loader fetches the three
// `map_Kd` images itself, relative to the .mtl
// only the models and their .mtl are listed: the MTL loader fetches every
// map_Kd AND map_d it references, relative to the .mtl
loader.preload(
[
{ name: "crate", type: "obj", src: `${ASSET_BASE}crate.obj` },
{ name: "crate", type: "mtl", src: `${ASSET_BASE}crate.mtl` },
{ name: "panel", type: "obj", src: `${ASSET_BASE}panel.obj` },
{ name: "props", type: "mtl", src: `${ASSET_BASE}props.mtl` },
// the sphere is shared with the mesh3d example; it carries no `vn`,
// so its normals are generated at parse
{ name: "ball", type: "obj", src: `${MESH_BASE}sphere.obj` },
],
() => {
app.world.addChild(new SpinningCrate(CANVAS_W / 2, CRATE_Y));
// preload transitions to the loading screen, which pins itself to a
// Camera2d — come back to the default stage so the app's Camera3d
// becomes the active viewport again
state.change(state.DEFAULT, true);
buildScene(app);
spawnCaption(app);
},
);
};

// ─── per-crate renderable ─────────────────────────────────────────
// ─── the scene ────────────────────────────────────────────────────

function buildScene(app: Application) {
const world = app.world;

// One directional key light plus a dim ambient fill. The key is what the
// specular highlight rides: a highlight needs a direction to reflect, so
// an ambient-only scene would show none however glossy the material.
const key = new Light3d(0, 0, {
type: "directional",
direction: new Vector3d(-0.45, -0.75, 0.5),
color: "#fff6e8",
intensity: 1.15,
});
key.name = "key";
world.addChild(key);
world.addChild(new Light3d(0, 0, { type: "ambient", color: "#3b4870" }));

const props: Mesh[] = [];

// ── crate: three diffuse maps, one per material ──────────────
const crate = new Mesh(-SPACING, PROP_Y, {
model: "crate",
material: "crate",
width: 150,
lit: true,
});
crate.depth = PROP_Z;
props.push(crate);

// ── ball: Ks + Ns, and generated normals ─────────────────────
const ball = new Mesh(0, PROP_Y, {
model: "ball",
// no `usemtl` in sphere.obj, so the first MTL entry wins — `chrome`
material: "props",
width: 170,
lit: true,
});
ball.depth = PROP_Z;
props.push(ball);

// ── panel: per-texel cutout ──────────────────────────────────
const panel = new Mesh(SPACING, PROP_Y, {
model: "panel",
material: "props",
width: 150,
lit: true,
// `map_d` scales alpha; this is the threshold it is scaled against.
// Without a cutoff there is nothing to discard and the map does
// nothing — the two are a pair.
alphaCutoff: 0.5,
cullBackFaces: false,
});
panel.depth = PROP_Z;
props.push(panel);

for (const prop of props) {
world.addChild(prop);
// depth is assigned before addChild above; Container.autoDepth would
// overwrite it otherwise
prop.depth = PROP_Z;
}

const AXIS_Y = new Vector3d(0, 1, 0);
const AXIS_X = new Vector3d(1, 0, 0);
// slow turn so the highlight sweeps across the ball and every face of the
// crate comes round — a static shot would hide both
world.addChild(new Spinner(props));

// looking slightly down, so the crate's steel lid is in view alongside
// its wooden sides — the split is the point, and a level shot hides half
const camera = app.viewport as Camera3d;
camera.pos.set(0, -170, 0);
camera.lookAt(0, 10, PROP_Z);
}

/**
* The slowly turning crate. No texture wiring at all: `material:` names the
* preloaded MTL, and each material's `map_Kd` follows from it. Passing an
* explicit `texture:` here would pin one binding over the whole model
* instead — that is how a caller opts out of the split.
* Turns the props. A Renderable that draws nothing — the meshes are children
* of the world in their own right, so this only advances them.
*/
class SpinningCrate extends Renderable {
mesh: Mesh;

constructor(x: number, y: number) {
super(0, 0, CANVAS_W, CANVAS_H);
this.anchorPoint.set(0, 0);
this.mesh = new Mesh(x, y, {
model: "crate",
material: "crate",
width: CRATE_SIZE,
height: CRATE_SIZE,
cullBackFaces: true,
});
// tilted forward so the steel lid is in view alongside the boards and
// the label — all three materials on screen from the first frame
this.mesh.rotate(0.5, AXIS_X);
class Spinner extends Renderable {
props: Mesh[];

constructor(props: Mesh[]) {
super(0, 0, 1, 1);
this.props = props;
this.alwaysUpdate = true;
}

override update(dt: number): boolean {
this.mesh.rotate(dt * 0.0005, AXIS_Y);
for (const prop of this.props) {
prop.rotate(dt * 0.0004, AXIS_Y);
}
return true;
}

override draw(renderer: WebGLRenderer | CanvasRenderer): void {
this.mesh.preDraw(renderer);
this.mesh.draw(renderer);
this.mesh.postDraw(renderer);
}
}

// ─── caption ──────────────────────────────────────────────────────

/**
* An HTML caption over the canvas, naming the three materials in view. The
* canvas is scaled by `scale: "auto"`, so the position is a percentage of
* the wrapper element and stays put at any display size.
*/
function spawnCaption(app: Application) {
const parent = app.renderer.getCanvas().parentElement;
if (!parent) {
Expand All @@ -144,14 +217,14 @@ function spawnCaption(app: Application) {

const el = document.createElement("div");
el.innerHTML =
'<div style="font-size:16px;font-weight:bold">three materials, three diffuse maps</div>' +
'<div style="font-size:12px;opacity:0.72;margin-top:5px">' +
"wood boards · steel plate · shipping label — one .obj, one .mtl, three draw ranges" +
'<div style="font-size:16px;font-weight:bold">everything below comes out of the .mtl</div>' +
'<div style="font-size:12px;opacity:0.75;margin-top:6px">' +
"map_Kd per material · Ks + Ns specular · map_d per-texel cutout" +
"</div>";
el.style.cssText =
"position:absolute;color:#e6e9ef;font-family:'Courier New',monospace;" +
"text-align:center;text-shadow:0 0 5px #000;z-index:1000;pointer-events:none;" +
`transform:translate(-50%,-50%);left:50%;top:${CAPTION_Y_PCT}%;`;
"transform:translate(-50%,-50%);left:50%;top:11%;";
parent.appendChild(el);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/examples/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ const examples: {
path: "material-textures",
sourceDir: "materialTextures",
description:
"A crate whose wood, steel and label materials each carry their own diffuse map from the .mtl — resolved into one indexed draw range per texture.",
"Three props, three MTL material features: per-material diffuse maps on a crate, a Ks/Ns specular highlight on a chrome ball, and a map_d per-texel cutout on a perforated panel.",
},
{
component: <ExamplePlatformer />,
Expand Down
Loading
Loading