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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions packages/examples/public/assets/materialTextures/crate.mtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Three materials, three diffuse maps. `Kd` stays near-white on each so the
# textures speak for themselves — the per-material tint path is already shown
# by the multiMaterialMesh example.
newmtl wood
Kd 1.0 0.95 0.9
map_Kd crate-wood.png

newmtl metal
Kd 0.95 0.97 1.0
map_Kd crate-metal.png

newmtl label
Kd 1.0 1.0 1.0
map_Kd crate-label.png
53 changes: 53 additions & 0 deletions packages/examples/public/assets/materialTextures/crate.obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# melonJS example asset — a supply crate with three textured materials.
#
# Authored for the per-material texture example (#1573): the four sides are
# wooden boards, the lid and floor are steel plate, and a shipping label sits
# just proud of the front face. Three `usemtl` groups, three different
# `map_Kd` maps — which is exactly what a single shared texture binding
# cannot render.
mtllib crate.mtl

v -1 -1 1
v 1 -1 1
v 1 1 1
v -1 1 1
v -1 -1 -1
v 1 -1 -1
v 1 1 -1
v -1 1 -1
v -0.62 -0.62 1.02
v 0.62 -0.62 1.02
v 0.62 0.62 1.02
v -0.62 0.62 1.02

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

vn 0 0 1
vn 0 0 -1
vn -1 0 0
vn 1 0 0
vn 0 1 0
vn 0 -1 0

usemtl wood
f 1/1/1 2/2/1 3/3/1
f 1/1/1 3/3/1 4/4/1
f 2/1/4 6/2/4 7/3/4
f 2/1/4 7/3/4 3/4/4
f 6/1/2 5/2/2 8/3/2
f 6/1/2 8/3/2 7/4/2
f 5/1/3 1/2/3 4/3/3
f 5/1/3 4/3/3 8/4/3

usemtl metal
f 4/1/5 3/2/5 7/3/5
f 4/1/5 7/3/5 8/4/5
f 5/1/6 6/2/6 2/3/6
f 5/1/6 2/3/6 1/4/6

usemtl label
f 9/1/1 10/2/1 11/3/1
f 9/1/1 11/3/1 12/4/1
24 changes: 24 additions & 0 deletions packages/examples/src/examples/afterBurner/ExampleAfterBurner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ import {
Application,
audio,
Camera3d,
Color,
event,
input,
Light3d,
loader,
state,
Vector3d,
video,
} from "melonjs";
import { createExampleComponent } from "../utils";
Expand Down Expand Up @@ -131,6 +134,27 @@ const createGame = async () => {
const controller = new GameController(app);
app.world.addChild(controller);

// Lights for the OBJ craft. They shade against the vertex normals
// the OBJ loader now supplies (#1572) — without them a `lit` mesh
// has no surface to shade and reads flat whatever the lighting.
// A key light plus an ambient fill so the shadowed side stays
// readable rather than black.
app.world.addChild(
new Light3d(0, 0, 0, {
type: "directional",
direction: new Vector3d(-0.35, -0.7, -0.6),
color: new Color(255, 244, 226),
intensity: 1.2,
}),
);
app.world.addChild(
new Light3d(0, 0, 0, {
type: "ambient",
color: new Color(128, 146, 178),
intensity: 0.6,
}),
);

// Start the music loop via `playTrack` (not `play`) — that
// registers BGM_NAME as the engine's currentTrack, which
// in turn hooks the engine's built-in pause-on-blur
Expand Down
5 changes: 5 additions & 0 deletions packages/examples/src/examples/afterBurner/Plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export class Plane extends Mesh {
width: settings.size,
height: settings.size,
cullBackFaces: true,
// OBJ models carry vertex normals since #1572 — authored `vn`
// where the file has them, generated from face geometry where it
// does not — so they shade against the modelled surface instead
// of reading flat under the scene lights.
lit: true,
});
// Mesh defaults anchor to (0.5, 0.5), but Renderable.preDraw
// applies `translate(-anchorPoint * width)` on top of the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* melonJS — Per-material diffuse textures on a multi-material OBJ.
*
* 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.
*
* 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.
*
* Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License.
*/
import { DebugPanelPlugin } from "@melonjs/debug-plugin";
import type { CanvasRenderer, WebGLRenderer } from "melonjs";
import {
Application,
loader,
Mesh,
plugin,
Renderable,
Vector3d,
video,
} from "melonjs";
import { createExampleComponent } from "../utils";

// ─── layout & content ─────────────────────────────────────────────

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/`;

// ─── 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.
let app: Application;
try {
app = new Application(CANVAS_W, CANVAS_H, {
parent: "screen",
renderer: video.AUTO,
scale: "auto",
});
await app.init();
if (!app.renderer.supportsDepthBuffer) {
throw new Error("no GPU backend available (Canvas fallback)");
}
} catch (err) {
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 " +
"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");
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
loader.preload(
[
{ name: "crate", type: "obj", src: `${ASSET_BASE}crate.obj` },
{ name: "crate", type: "mtl", src: `${ASSET_BASE}crate.mtl` },
],
() => {
app.world.addChild(new SpinningCrate(CANVAS_W / 2, CRATE_Y));
spawnCaption(app);
},
);
};

// ─── per-crate renderable ─────────────────────────────────────────

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

/**
* 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.
*/
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);
}

override update(dt: number): boolean {
this.mesh.rotate(dt * 0.0005, 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) {
return;
}
parent.style.position = "relative";

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>";
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}%;`;
parent.appendChild(el);
}

export const ExampleMaterialTextures = 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 @@ -163,6 +163,11 @@ const ExampleMultiMaterialMesh = lazy(() =>
default: m.ExampleMultiMaterialMesh,
})),
);
const ExampleMaterialTextures = lazy(() =>
import("./examples/materialTextures/ExampleMaterialTextures").then((m) => ({
default: m.ExampleMaterialTextures,
})),
);
const ExamplePlatformer = lazy(() =>
import("./examples/platformer/ExamplePlatformer").then((m) => ({
default: m.ExamplePlatformer,
Expand Down Expand Up @@ -485,6 +490,14 @@ const examples: {
description:
"Rotating 3D models with multiple materials and per-mesh tinting — each material region picks up its diffuse color from the .mtl file, multiplied by a runtime tint.",
},
{
component: <ExampleMaterialTextures />,
label: "Per-material Textures",
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.",
},
{
component: <ExamplePlatformer />,
label: "Platformer",
Expand Down
Loading
Loading