From 34604886866ce327b59c2e24f961e940f018bb02 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 07:45:04 +0800 Subject: [PATCH 01/13] feat(mesh): multi-material OBJ rendering via Three.js-style groups[] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OBJ files with multiple `usemtl` directives (Kenney asset packs, Blender / Maya exports, any model with painted panels) previously rendered as a single uniform color in melonJS because `Mesh` bound one material per mesh and the parser threw away `usemtl` boundaries. OBJ parser now emits `groups: Array<{materialName, start, count}>` alongside the existing geometry — each entry is a contiguous slice of the shared index buffer that draws as one submesh against one material. Field shape (`start`, `count`, plus a name pointing into a material table) matches the Three.js / glTF "groups" convention so the structure is familiar to anyone porting from those engines. Single- material OBJs still produce a `groups` array of length 1, so downstream code doesn't need a special case. `Mesh` detects multi-material OBJs (multiple groups + at least one named + an MTL bound via `material:`) and builds a per-group descriptor carrying texture, tint, and opacity resolved from each named MTL entry. `Mesh.draw()` iterates the descriptors, swapping `renderer.currentTint` + `mesh.texture` per draw call — one draw per material region, all sharing the same projected vertex buffer (no geometry duplication, no per-frame allocation). `renderer.drawMesh(mesh, group?)` gains an optional group argument (matches Three.js's `renderer.renderBufferDirect(..., group)`) — when provided, only the index slice `[group.start, group.start + group.count)` is pushed to the GPU. WebGL and Canvas renderers updated. Depth clear moved to "first group only" so per-material draws within one mesh compose against each other for correct cross-material occlusion. Kd-only Kenney-style models without `map_Kd` get a shared 1×1 white texture fallback so the GPU pipeline still has something to sample — the per-group `tint` does all the visible coloring. Allocated lazily on first use, shared across every Mesh that needs it. 8 new parser tests cover: single-material default group, multi-usemtl emission, anonymous null-material chunk for pre-usemtl faces, triangulated quads inside groups, winding-fix preserves boundaries, empty OBJ, trailing usemtl with no faces. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/src/loader/parsers/obj.js | 65 ++++- packages/melonjs/src/renderable/mesh.js | 242 ++++++++++++++---- .../src/video/canvas/canvas_renderer.js | 27 +- .../src/video/webgl/batchers/mesh_batcher.js | 18 +- .../melonjs/src/video/webgl/webgl_renderer.js | 30 ++- packages/melonjs/tests/mesh.spec.js | 122 +++++++++ 6 files changed, 434 insertions(+), 70 deletions(-) diff --git a/packages/melonjs/src/loader/parsers/obj.js b/packages/melonjs/src/loader/parsers/obj.js index b8dcee7fb..53221ce5c 100644 --- a/packages/melonjs/src/loader/parsers/obj.js +++ b/packages/melonjs/src/loader/parsers/obj.js @@ -26,20 +26,33 @@ const OBJ_INDEX_OFFSET = 1; * Parse a Wavefront OBJ file into geometry data. * Supports: `v` (vertex positions), `vt` (texture coordinates), * `f` (faces in `v`, `v/vt`, `v/vt/vn`, or `v//vn` format), - * `mtllib` (material library reference). + * `mtllib` (material library reference), + * `usemtl` (material group boundaries — emitted as `groups[]`). * * Features: * - Quad and n-gon triangulation (fan from first vertex) * - Automatic CW → CCW winding correction via signed volume test * - V texture coordinate flipped for OpenGL convention (OBJ has origin at bottom-left) * - Single-pass parsing with direct vertex unification (no intermediate arrays) + * - Material grouping: each `usemtl` switch emits a new `groups[]` entry + * pointing to a slice of the unified `indices` buffer, so callers + * (e.g. `Mesh`) can render each group with its own material without + * touching the geometry. A model with no `usemtl` directives produces + * a single group with `material: null`. * - * Parsed but ignored: `vn` (normals), `g` (groups), `usemtl` (material assignment), - * `s` (smooth shading), `o` (object name). + * Parsed but ignored: `vn` (normals), `g` (groups), `s` (smooth shading), + * `o` (object name). * * @param {string} text - raw OBJ file contents - * @returns {object} parsed geometry with `vertices` (Float32Array), `uvs` (Float32Array), - * `indices` (Uint16Array), `vertexCount` (number), and `mtllib` (string|null) + * @returns {object} parsed geometry with `vertices` (Float32Array), + * `uvs` (Float32Array), `indices` (Uint16Array), `vertexCount` (number), + * `mtllib` (string|null), and `groups` + * (Array<{materialName: string|null, start: number, count: number}>). + * `groups` follows the Three.js / glTF convention — each entry is a + * contiguous slice of the shared `indices` buffer that draws as one + * submesh against a single material. Single-material models still + * produce a `groups` array of length 1, so consumers don't need a + * special case. * @ignore */ function parseOBJ(text) { @@ -89,6 +102,31 @@ function parseOBJ(text) { // mtllib reference (if present) let mtllib = null; + // Material grouping. Each `usemtl` switch closes the running group + // (recording its index count) and opens a new one. Models without + // any `usemtl` produce a single group spanning all indices with + // `materialName: null` — consumers can treat that uniformly with + // the multi-material path. Field name `materialName` matches the + // Three.js / glTF convention for "name of the material this + // submesh wants to be drawn with"; renderers / mesh objects look + // it up in their own material table. + const groups = []; + const startGroup = (materialName) => { + // close the previous group if it has any indices + const prev = groups[groups.length - 1]; + if (prev) { + prev.count = indices.length - prev.start; + } else if (indices.length > 0) { + // pre-usemtl indices belong to an anonymous group + groups.push({ + materialName: null, + start: 0, + count: indices.length, + }); + } + groups.push({ materialName, start: indices.length, count: 0 }); + }; + // parse lines and build geometry in a single pass const lines = text.split("\n"); for (let i = 0; i < lines.length; i++) { @@ -102,6 +140,10 @@ function parseOBJ(text) { mtllib = line.substring(7).trim(); continue; } + if (first === "u" && line.startsWith("usemtl ")) { + startGroup(line.substring(7).trim()); + continue; + } if (first === VERTEX_PREFIX) { const parts = line.split(/\s+/); if (parts[0] === VERTEX_PREFIX) { @@ -164,12 +206,25 @@ function parseOBJ(text) { } } + // finalize the last open group (or, if no `usemtl` was ever seen, + // emit a single material-less group covering all indices so the + // `groups[]` contract is always non-empty for non-empty OBJs) + if (groups.length === 0) { + if (indices.length > 0) { + groups.push({ materialName: null, start: 0, count: indices.length }); + } + } else { + const last = groups[groups.length - 1]; + last.count = indices.length - last.start; + } + return { vertices: new Float32Array(vertices), uvs: new Float32Array(uvs), indices: new Uint16Array(indices), vertexCount, mtllib, + groups, }; } diff --git a/packages/melonjs/src/renderable/mesh.js b/packages/melonjs/src/renderable/mesh.js index f7ea5348b..a54794517 100644 --- a/packages/melonjs/src/renderable/mesh.js +++ b/packages/melonjs/src/renderable/mesh.js @@ -1,6 +1,7 @@ import { game } from "../application/application.ts"; import { Polygon } from "../geometries/polygon.ts"; import { getImage, getMTL, getOBJ } from "./../loader/loader.js"; +import { Color } from "../math/color.ts"; import { Matrix3d } from "../math/matrix3d.ts"; import { Vector2d } from "../math/vector2d.ts"; import { @@ -20,6 +21,88 @@ import Renderable from "./renderable.js"; // reusable matrix for combining projection × model in draw() const _combinedMatrix = new Matrix3d(); +// reusable color used by `draw()` to save/restore `this.tint` around +// the multi-material per-group swap. Module-scoped so we don't +// allocate every frame; safe because draw is single-threaded. +const _savedTint = new Color(255, 255, 255, 1); + +// Lazily-allocated 1×1 white pixel used as the texture fallback for +// flat-color (Kd-only, no `map_Kd`) MTL materials. One canvas shared +// across every Mesh that needs it — no per-instance allocation. +let _whitePixel = null; +function getOrCreateWhitePixel() { + if (!_whitePixel) { + const c = document.createElement("canvas"); + c.width = 1; + c.height = 1; + const ctx = c.getContext("2d"); + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, 1, 1); + _whitePixel = c; + } + return _whitePixel; +} + +// Resolve any acceptable texture input (TextureAtlas, image / canvas +// object, or asset name) to a cached `TextureAtlas`. Throws if nothing +// resolves — Mesh requires a texture binding for its GL pipeline. +function resolveTextureAtlas(src) { + if (src instanceof TextureAtlas) { + return src; + } + const image = typeof src === "object" ? src : getImage(src); + if (!image) { + throw new Error("Mesh: '" + src + "' image/texture not found!"); + } + return game.renderer.cache.get(image, { + framewidth: image.width, + frameheight: image.height, + }); +} + +/** + * Resolve an OBJ material group into a draw descriptor. Builds the + * group's tint from the MTL's `Kd` (defaults to white if missing) and + * picks the texture from `map_Kd` if the MTL provides one, else falls + * back to the explicit `textureSource`. Returns a self-contained + * record that {@link Mesh#draw} can iterate without touching the MTL + * cache or re-resolving anything per frame. + * @param {{material: string|null, start: number, count: number}} group + * @param {object} materials - MTL material table keyed by material name + * @param {string|HTMLImageElement|TextureAtlas|undefined} textureSource + * @returns {object} draw descriptor for this group + * @ignore + */ +function resolveGroupMaterial(group, materials, textureSource) { + const mat = group.materialName ? materials[group.materialName] : null; + const tint = new Color(255, 255, 255, 1); + let opacity = 1; + let texture = textureSource; + if (mat) { + if (mat.map_Kd) { + texture = mat.map_Kd; + } + if (mat.Kd) { + tint.setColor( + Math.round(mat.Kd[0] * 255), + Math.round(mat.Kd[1] * 255), + Math.round(mat.Kd[2] * 255), + ); + } + if (typeof mat.d === "number" && mat.d < 1) { + opacity = mat.d; + } + } + return { + materialName: group.materialName, + start: group.start, + count: group.count, + texture, // resolved to TextureAtlas in the Mesh constructor once the cache is reachable + tint, + opacity, + }; +} + /** * A renderable object for displaying textured triangle meshes. * Supports loading from Wavefront OBJ models (via `loader.preload` with type "obj") @@ -70,6 +153,7 @@ export default class Mesh extends Renderable { super(x, y, settings.width, settings.height); // load geometry from OBJ model or raw data + let objGroups = null; if (typeof settings.model === "string") { const objData = getOBJ(settings.model); if (!objData) { @@ -98,6 +182,11 @@ export default class Mesh extends Renderable { * @type {number} */ this.vertexCount = objData.vertexCount; + + // pick up the material-group list emitted by the OBJ parser + // (always non-null for non-empty models thanks to the + // parser's "anonymous group" fallback) + objGroups = objData.groups; } else { this.originalVertices = settings.vertices instanceof Float32Array @@ -129,55 +218,92 @@ export default class Mesh extends Renderable { this.cullBackFaces = settings.cullBackFaces !== undefined ? settings.cullBackFaces : true; - // resolve material (MTL) — applies texture, tint, and opacity + // resolve material (MTL) — applies texture, tint, and opacity. + // Two paths: + // - Single-material: pick the first MTL entry, apply to the whole + // mesh (legacy behavior, unchanged). + // - Multi-material: build a per-group descriptor with each + // group's texture, tint, and opacity resolved from its own MTL + // entry. `draw()` later iterates the groups and swaps state + // per draw. let textureSource = settings.texture; - if (typeof settings.material === "string") { - const materials = getMTL(settings.material); - if (materials) { - // use the first material's properties - const mat = materials[Object.keys(materials)[0]]; - if (mat) { - // auto-resolve texture from map_Kd if no explicit texture - if (!textureSource && mat.map_Kd) { - textureSource = mat.map_Kd; - } - // apply diffuse color as tint - if (mat.Kd) { - this.tint.setColor( - Math.round(mat.Kd[0] * 255), - Math.round(mat.Kd[1] * 255), - Math.round(mat.Kd[2] * 255), - ); - } - // apply opacity - if (mat.d < 1.0) { - this.setOpacity(mat.d); - } + const materials = + typeof settings.material === "string" ? getMTL(settings.material) : null; + const isMultiMaterial = + materials !== null && + objGroups !== null && + objGroups.length > 1 && + objGroups.some((g) => { + return g.materialName !== null; + }); + + if (isMultiMaterial) { + /** + * Per-material submesh groups, populated when the OBJ + * contains multiple `usemtl` directives AND a matching MTL + * is bound via the `material` setting. Each entry slices + * the shared `indices` buffer and carries its own texture + + * tint + opacity, so `draw()` can render one material per + * draw call without touching geometry. + * + * Field shape (`start`, `count`, `materialName`) matches the + * Three.js / glTF "groups" convention so the structure is + * familiar to anyone coming from those engines. + * @type {Array<{materialName: string|null, start: number, + * count: number, texture: TextureAtlas, tint: Color, + * opacity: number}>} + */ + this.groups = objGroups.map((g) => { + return resolveGroupMaterial(g, materials, textureSource); + }); + // the legacy `texture` / `tint` / opacity stay set from the + // FIRST group so single-material code paths (e.g. + // `toCanvas()`) still produce a sensible default + const first = this.groups[0]; + textureSource = first.texture; + this.tint.copy(first.tint); + if (first.opacity < 1) { + this.setOpacity(first.opacity); + } + } else if (materials) { + // single-material path — pick the first MTL entry + const mat = materials[Object.keys(materials)[0]]; + if (mat) { + if (!textureSource && mat.map_Kd) { + textureSource = mat.map_Kd; + } + if (mat.Kd) { + this.tint.setColor( + Math.round(mat.Kd[0] * 255), + Math.round(mat.Kd[1] * 255), + Math.round(mat.Kd[2] * 255), + ); + } + if (mat.d < 1.0) { + this.setOpacity(mat.d); } } } - // resolve texture - if (textureSource instanceof TextureAtlas) { - /** - * the texture atlas used by this mesh - * @type {TextureAtlas} - */ - this.texture = textureSource; - } else { - const image = - typeof textureSource === "object" - ? textureSource - : getImage(textureSource); - if (!image) { - throw new Error( - "Mesh: '" + textureSource + "' image/texture not found!", - ); + // resolve texture. For multi-material meshes that have NO + // per-material textures (Kenney-style flat-color models that + // only set `Kd`), fall back to a shared 1×1 white pixel so + // the GPU pipeline still has something to sample — the per- + // group `tint` does all the visible coloring. + if (!textureSource && isMultiMaterial) { + textureSource = getOrCreateWhitePixel(); + } + this.texture = resolveTextureAtlas(textureSource); + + // resolve every multi-material group's texture into a real + // `TextureAtlas` (cached per image) so `draw()` can swap + // bindings without per-frame allocation. Groups whose MTL had + // no `map_Kd` fall back to the shared `this.texture` (the 1×1 + // white pixel above for Kenney-style models). + if (isMultiMaterial) { + for (const g of this.groups) { + g.texture = g.texture ? resolveTextureAtlas(g.texture) : this.texture; } - this.texture = game.renderer.cache.get(image, { - framewidth: image.width, - frameheight: image.height, - }); } /** @@ -236,12 +362,38 @@ export default class Mesh extends Renderable { /** * Draw the mesh (automatically called by melonJS). - * Projects vertices through projectionMatrix × currentTransform and calls renderer.drawMesh(). + * Projects vertices through projectionMatrix × currentTransform and + * calls `renderer.drawMesh()`. For multi-material meshes (OBJ files + * with multiple `usemtl` directives + a bound MTL), iterates the + * per-material `groups` array, swapping texture and tint per draw + * so each material region renders with its own appearance. * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance */ draw(renderer) { this._projectVertices(this.pos.x, this.pos.y, 1000); - renderer.drawMesh(this); + if (this.groups && this.groups.length > 1) { + // Save the mesh's primary tint / texture so the per-group + // swaps don't leak into reads of `this.tint` / `this.texture` + // between frames (e.g. `toCanvas`, picking, debug overlays). + // `Renderable.preDraw` already called `renderer.setTint(this.tint)`, + // locking the renderer to the first group's color — we mutate + // `renderer.currentTint` directly per group instead of going + // through `setTint` because `setTint` multiplies into alpha, + // which would compound across iterations. + const savedTexture = this.texture; + _savedTint.copy(this.tint); + for (const g of this.groups) { + this.texture = g.texture; + this.tint.copy(g.tint); + renderer.currentTint.copy(g.tint); + renderer.drawMesh(this, g); + } + this.texture = savedTexture; + this.tint.copy(_savedTint); + renderer.currentTint.copy(_savedTint); + } else { + renderer.drawMesh(this); + } } /** diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index e5428170c..2bda3068d 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -392,15 +392,22 @@ export default class CanvasRenderer extends Renderer { /** * Draw a textured triangle mesh. - * Uses per-triangle affine texture mapping with back-to-front depth sorting - * (painter's algorithm) and optional backface culling. - * Note: the painter's algorithm works well for convex shapes but may produce - * visual artifacts with concave or self-overlapping geometry (e.g. a torus), - * as Canvas 2D has no hardware depth buffer. Use the WebGL renderer for - * correct depth ordering on complex meshes. + * Uses per-triangle affine texture mapping with back-to-front depth + * sorting (painter's algorithm) and optional backface culling. + * Note: the painter's algorithm works well for convex shapes but + * may produce visual artifacts with concave or self-overlapping + * geometry (e.g. a torus), as Canvas 2D has no hardware depth + * buffer. Use the WebGL renderer for correct depth ordering on + * complex meshes. + * + * When `group` is provided, only the index slice + * `[group.start, group.start + group.count)` is drawn — used by + * multi-material OBJs to render one material at a time. * @param {Mesh} mesh - a Mesh renderable or compatible object + * @param {{start: number, count: number}} [group] - optional index + * buffer slice to draw (defaults to the whole mesh) */ - drawMesh(mesh) { + drawMesh(mesh, group) { if (this.getGlobalAlpha() < 1 / 255) { return; } @@ -408,6 +415,8 @@ export default class CanvasRenderer extends Renderer { const vertices = mesh.vertices; const uvs = mesh.uvs; const indices = mesh.indices; + const startIdx = group ? group.start : 0; + const endLimit = group ? group.start + group.count : indices.length; // apply tint if set let image = mesh.texture.getTexture(); @@ -418,7 +427,7 @@ export default class CanvasRenderer extends Renderer { const imgW = image.width; const imgH = image.height; const cullBack = mesh.cullBackFaces === true; - const triCount = indices.length / 3; + const triCount = (endLimit - startIdx) / 3; // pre-allocate flat sort array (reuse across frames via closure) // each entry stores: [sortKey, originalIndex] @@ -429,7 +438,7 @@ export default class CanvasRenderer extends Renderer { let visCount = 0; // build sort keys for visible triangles (no object allocation) - for (let j = 0; j < indices.length; j += 3) { + for (let j = startIdx; j < endLimit; j += 3) { const i0 = indices[j]; const i1 = indices[j + 1]; const i2 = indices[j + 2]; diff --git a/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js b/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js index 970dff847..db8c446bc 100644 --- a/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js +++ b/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js @@ -50,14 +50,22 @@ export default class MeshBatcher extends MaterialBatcher { } /** - * Add a textured mesh to the batch + * Add a textured mesh to the batch. When `group` is provided, only + * the index slice `[group.start, group.start + group.count)` is + * pushed — lets `Mesh.draw()` render multi-material OBJs one + * material at a time without rebuilding geometry. * @param {object} mesh - a Mesh object with vertices, uvs, indices, and texture properties * @param {number} tint - tint color in UINT32 (argb) format + * @param {{start: number, count: number}} [group] - optional index buffer slice */ - addMesh(mesh, tint) { + addMesh(mesh, tint, group) { const vertices = mesh.vertices; const uvs = mesh.uvs; const indices = mesh.indices; + // `triIdx` and `endLimit` bracket the index range to draw — + // the whole buffer by default, or just the requested group slice + const startIdx = group ? group.start : 0; + const endLimit = group ? group.start + group.count : indices.length; // upload and activate the texture const unit = this.uploadTexture(mesh.texture); @@ -72,8 +80,8 @@ export default class MeshBatcher extends MaterialBatcher { const maxIndices = this.indexBuffer.data.length; // process triangles in chunks that fit the buffer - let triIdx = 0; - while (triIdx < indices.length) { + let triIdx = startIdx; + while (triIdx < endLimit) { // figure out how many triangles fit in the current batch const vertexData = this.vertexData; const availVerts = maxVerts - vertexData.vertexCount; @@ -89,7 +97,7 @@ export default class MeshBatcher extends MaterialBatcher { continue; } - const endIdx = Math.min(triIdx + maxTris * 3, indices.length); + const endIdx = Math.min(triIdx + maxTris * 3, endLimit); // build a local vertex remap for this chunk // capture base offset before pushing any vertices diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index 81a442963..1f3748ec9 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -1217,12 +1217,22 @@ export default class WebGLRenderer extends Renderer { /** * Draw a textured triangle mesh. - * Enables hardware depth testing and backface culling for the duration of the draw, - * then restores the previous GL state. Large meshes are automatically chunked - * across multiple draw calls to fit the vertex/index buffer limits. + * Enables hardware depth testing and backface culling for the + * duration of the draw, then restores the previous GL state. Large + * meshes are automatically chunked across multiple draw calls to + * fit the vertex/index buffer limits. + * + * When called with a `group` (a `{start, count}` slice of the + * mesh's index buffer), only that slice is drawn — used by `Mesh` + * to render multi-material OBJs one material at a time. The depth + * buffer is cleared only on the first call per frame; subsequent + * group draws compose against the existing depth so the multi- + * material draws still test against each other. * @param {Mesh} mesh - a Mesh renderable or compatible object + * @param {{start: number, count: number}} [group] - optional index + * buffer slice to draw (defaults to the whole mesh) */ - drawMesh(mesh) { + drawMesh(mesh, group) { const gl = this.gl; this.setBatcher("mesh"); @@ -1236,8 +1246,15 @@ export default class WebGLRenderer extends Renderer { gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LESS); gl.depthMask(true); - gl.clearDepth(1.0); - gl.clear(gl.DEPTH_BUFFER_BIT); + // Only clear depth on the first draw of a mesh — subsequent + // per-group calls within the same Mesh.draw() compose against + // each other so far/near occlusion across materials stays + // correct. Mesh signals "first call" by passing no group OR by + // passing group with start === 0 (the first material's slice). + if (!group || group.start === 0) { + gl.clearDepth(1.0); + gl.clear(gl.DEPTH_BUFFER_BIT); + } // disable blending during opaque mesh rendering to avoid depth/blend conflicts gl.disable(gl.BLEND); @@ -1252,6 +1269,7 @@ export default class WebGLRenderer extends Renderer { this.currentBatcher.addMesh( mesh, this.currentTint.toUint32(this.getGlobalAlpha()), + group, ); // flush and restore GL state diff --git a/packages/melonjs/tests/mesh.spec.js b/packages/melonjs/tests/mesh.spec.js index d2515420c..205e510f6 100644 --- a/packages/melonjs/tests/mesh.spec.js +++ b/packages/melonjs/tests/mesh.spec.js @@ -259,6 +259,128 @@ describe("OBJ Parser", () => { expect(obj.vertexCount).toBeGreaterThan(0); expect(obj.indices.length).toBeGreaterThan(0); }); + + // ── multi-material groups (Three.js / glTF "groups" convention) ────── + + it("emits a single material-less group for OBJs with no usemtl", async () => { + const obj = await parseOBJString(` + v 0 0 0 + v 1 0 0 + v 1 1 0 + f 1 2 3 + `); + expect(Array.isArray(obj.groups)).toBe(true); + expect(obj.groups.length).toBe(1); + expect(obj.groups[0].materialName).toBe(null); + expect(obj.groups[0].start).toBe(0); + expect(obj.groups[0].count).toBe(obj.indices.length); + }); + + it("emits one group per usemtl directive, slicing the index buffer", async () => { + const obj = await parseOBJString(` + v 0 0 0 + v 1 0 0 + v 1 1 0 + v 0 1 0 + v 2 0 0 + v 2 1 0 + usemtl red + f 1 2 3 + usemtl blue + f 1 3 4 + usemtl green + f 2 5 6 + `); + expect(obj.groups.length).toBe(3); + expect(obj.groups[0].materialName).toBe("red"); + expect(obj.groups[0].start).toBe(0); + expect(obj.groups[0].count).toBe(3); + expect(obj.groups[1].materialName).toBe("blue"); + expect(obj.groups[1].start).toBe(3); + expect(obj.groups[1].count).toBe(3); + expect(obj.groups[2].materialName).toBe("green"); + expect(obj.groups[2].start).toBe(6); + expect(obj.groups[2].count).toBe(3); + // every index is covered by exactly one group + const total = obj.groups.reduce((s, g) => { + return s + g.count; + }, 0); + expect(total).toBe(obj.indices.length); + }); + + it("emits an anonymous null-material group for faces declared before any usemtl", async () => { + const obj = await parseOBJString(` + v 0 0 0 + v 1 0 0 + v 1 1 0 + v 0 1 0 + f 1 2 3 + usemtl red + f 1 3 4 + `); + // 2 groups: the anonymous pre-usemtl chunk + the explicit "red" + expect(obj.groups.length).toBe(2); + expect(obj.groups[0].materialName).toBe(null); + expect(obj.groups[0].count).toBe(3); + expect(obj.groups[1].materialName).toBe("red"); + expect(obj.groups[1].count).toBe(3); + }); + + it("handles triangulated quads inside a material group", async () => { + const obj = await parseOBJString(` + v 0 0 0 + v 1 0 0 + v 1 1 0 + v 0 1 0 + usemtl panels + f 1 2 3 4 + `); + // quad → 2 triangles → 6 indices + expect(obj.groups.length).toBe(1); + expect(obj.groups[0].materialName).toBe("panels"); + expect(obj.groups[0].count).toBe(6); + expect(obj.indices.length).toBe(6); + }); + + it("group boundaries survive CW→CCW winding correction", async () => { + // CW winding (negative volume) gets flipped; the per-triangle + // index reorder must not move triangles between groups + const obj = await parseOBJString(` + v -1 -1 -1 + v -1 1 -1 + v 1 -1 -1 + v 1 1 -1 + usemtl front + f 1 3 4 + f 1 4 2 + `); + expect(obj.groups.length).toBe(1); + expect(obj.groups[0].materialName).toBe("front"); + // count is total indices, regardless of winding flip + expect(obj.groups[0].count).toBe(obj.indices.length); + }); + + it("empty OBJ produces an empty groups array (not null)", async () => { + const obj = await parseOBJString(``); + expect(Array.isArray(obj.groups)).toBe(true); + expect(obj.groups.length).toBe(0); + }); + + it("usemtl with no following faces leaves a zero-count group", async () => { + const obj = await parseOBJString(` + v 0 0 0 + v 1 0 0 + v 1 1 0 + usemtl red + f 1 2 3 + usemtl unused + `); + expect(obj.groups.length).toBe(2); + expect(obj.groups[0].materialName).toBe("red"); + expect(obj.groups[0].count).toBe(3); + expect(obj.groups[1].materialName).toBe("unused"); + expect(obj.groups[1].count).toBe(0); + }); }); // ── Matrix3d extensions ───────────────────────────────────────────────────── From 9f9d58db5fd3997f94ee8e424c5eeaa981634802 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 07:45:19 +0800 Subject: [PATCH 02/13] feat(examples): multi-material OBJ showcase with Kenney spaceships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loads four Kenney Space Kit (CC0) spacecraft (craft_speederA / B / racer / miner) and renders them rotating in 3D in a 2x2 grid. Each spacecraft has 3-5 `usemtl` material groups (metal / metalRed / dark / metalDark) — the new groups[] OBJ parser API + Mesh.draw per-group iteration paint each panel with its correct Kd color, instead of collapsing the whole model into one flat tint. Compare with the existing mesh3d example (single texture across the whole mesh): same Mesh class, no extra wiring beyond passing the MTL name through `material:`. The multi-material code path activates automatically when the OBJ has multiple `usemtl` directives + a matching MTL is bound. LICENSE.md updated to credit Kenney (CC0 — attribution not legally required, included as a courtesy, mirroring the pool-matter pool- table assets pattern). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/examples/LICENSE.md | 11 + .../assets/multiMaterialMesh/craft_miner.mtl | 14 + .../assets/multiMaterialMesh/craft_miner.obj | 997 ++++++++++++++++++ .../assets/multiMaterialMesh/craft_racer.mtl | 14 + .../assets/multiMaterialMesh/craft_racer.obj | 762 +++++++++++++ .../multiMaterialMesh/craft_speederA.mtl | 14 + .../multiMaterialMesh/craft_speederA.obj | 828 +++++++++++++++ .../multiMaterialMesh/craft_speederB.mtl | 14 + .../multiMaterialMesh/craft_speederB.obj | 728 +++++++++++++ .../ExampleMultiMaterialMesh.tsx | 141 +++ packages/examples/src/main.tsx | 13 + 11 files changed, 3536 insertions(+) create mode 100644 packages/examples/public/assets/multiMaterialMesh/craft_miner.mtl create mode 100644 packages/examples/public/assets/multiMaterialMesh/craft_miner.obj create mode 100644 packages/examples/public/assets/multiMaterialMesh/craft_racer.mtl create mode 100644 packages/examples/public/assets/multiMaterialMesh/craft_racer.obj create mode 100644 packages/examples/public/assets/multiMaterialMesh/craft_speederA.mtl create mode 100644 packages/examples/public/assets/multiMaterialMesh/craft_speederA.obj create mode 100644 packages/examples/public/assets/multiMaterialMesh/craft_speederB.mtl create mode 100644 packages/examples/public/assets/multiMaterialMesh/craft_speederB.obj create mode 100644 packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx diff --git a/packages/examples/LICENSE.md b/packages/examples/LICENSE.md index 3729a6a78..e55b48f18 100644 --- a/packages/examples/LICENSE.md +++ b/packages/examples/LICENSE.md @@ -37,3 +37,14 @@ published by Casino RPG on OpenGameArt.org: These assets are released under **CC0 1.0 Universal (Public Domain Dedication)** — no attribution legally required, but provided here as a courtesy to the original creator. + +### `multiMaterialMesh` example + +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: + + + +Released under **CC0 1.0 Universal (Public Domain Dedication)** — no +attribution legally required, credited here as a courtesy. diff --git a/packages/examples/public/assets/multiMaterialMesh/craft_miner.mtl b/packages/examples/public/assets/multiMaterialMesh/craft_miner.mtl new file mode 100644 index 000000000..08b0a85d9 --- /dev/null +++ b/packages/examples/public/assets/multiMaterialMesh/craft_miner.mtl @@ -0,0 +1,14 @@ +# Created by Kenney (www.kenney.nl) + +newmtl dark +Kd 0.2745098 0.2980392 0.3411765 + +newmtl metalDark +Kd 0.6750623 0.7100219 0.7735849 + +newmtl metal +Kd 0.8431373 0.8705882 0.9098039 + +newmtl metalRed +Kd 1 0.6285242 0.2028302 + diff --git a/packages/examples/public/assets/multiMaterialMesh/craft_miner.obj b/packages/examples/public/assets/multiMaterialMesh/craft_miner.obj new file mode 100644 index 000000000..86c63017c --- /dev/null +++ b/packages/examples/public/assets/multiMaterialMesh/craft_miner.obj @@ -0,0 +1,997 @@ +# Created by Kenney (www.kenney.nl) + +mtllib craft_miner.mtl + +g craft_miner + +v 0.5 0.2 -1.3 +v 0.5 0.2 -1.1 +v 0.3 0.2 -1.3 +v 0.3 0.2 -1.1 +v 0.3 0.5 -1.3 +v 0.3 0.5 -1.1 +v 0.5 0.5 -1.1 +v 0.5 0.5 -1.3 +v -0.3 0.2 -1.3 +v -0.3 0.2 -1.1 +v -0.5 0.2 -1.3 +v -0.5 0.2 -1.1 +v -0.3 0.5 -1.1 +v -0.3 0.5 -1.3 +v -0.5 0.5 -1.1 +v -0.5 0.5 -1.3 +v -0.2 0.6900496 -0.1004963 +v -0.0999999 0.4784731 0.3125871 +v -0.2 0.2668965 0.3256706 +v 0.1 0.4784731 0.3125871 +v 0.2 0.6900496 -0.1004963 +v 0.2 0.2668965 0.3256706 +v 0.6 0.6 -1.2 +v 0.6 0.5 -0.9000001 +v 0.2 0.6 -1.2 +v 0.2 0.54 -1.02 +v 0.2 0.5 -0.9000001 +v 0.6 0.6 -1.3 +v 0.2 0.6 -1.3 +v 0.6 0.1 -1.3 +v 0.6 0.3 -0.9000001 +v 0.6 0.1 -0.3 +v 0.6 0.3 -0.3 +v 0.2 0.1 -0.3 +v 0.2 0.1 -1.1 +v 0.2 0.1 -1.3 +v 0.2 0.5 -1.1 +v 0.2 0.6 -0.9000001 +v -0.2 0.7 -0.7 +v 0.2 0.7 -0.7 +v -0.2 0.6 -0.9000001 +v -0.2 0.54 -1.02 +v -0.2 0.5 -1.1 +v 0.2 0.6 -0.5 +v 0.2 0.7 -0.3 +v 0.2 0.5 -0.5 +v 0.2 0.5 -0.3 +v -0.2 0.1 -1.1 +v 0.2 0 -0.7 +v -0.2 0 -0.7 +v -0.2 0.7 -0.3 +v 0.2 0 -0.3 +v -0.2 0 -0.3 +v -0.2 0.6 -1.2 +v -0.2 0.1 -1.3 +v -0.2 0.6 -1.3 +v -0.5999999 0.6 -1.2 +v -0.2 0.5 -0.9000001 +v -0.5999999 0.5 -0.9000001 +v -0.5999999 0.6 -1.3 +v -0.5999999 0.3 -0.9000001 +v -0.5999999 0.1 -1.3 +v -0.5999999 0.1 -0.3 +v -0.5999999 0.3 -0.3 +v -0.2 0.1 -0.3 +v -0.2 0.6 -0.5 +v -0.2 0.5 -0.3 +v -0.2 0.5 -0.5 +v 0.6894426 0.07888544 0.1 +v 0.65 0 0.1 +v 0.6894426 0.07888544 0.7 +v 0.65 0 0.7 +v 0.6 0.3 0.1 +v 0.5699999 0.36 0.1 +v 0.6 0.3 0.7 +v 0.5699999 0.36 0.7 +v 0.65 0.4 0.1 +v 0.6894426 0.3211145 0.1 +v 0.65 0.4 0.7 +v 0.6894426 0.3211145 0.7 +v 0.5699999 0.04 0.7 +v 0.6 0.1 0.7 +v 0.5699999 0.04 0.1 +v 0.6 0.1 0.1 +v 0.4 0.1 0.1 +v 0.4 0.1540133 0.1 +v 0.4 0.1 0.7 +v 0.4 0.1540133 0.3 +v 0.4 0.2040136 0.3 +v 0.4 0.3 0.7 +v 0.4 0.2540133 0.3 +v 0.4 0.3 0.1 +v 0.4 0.2540133 0.1 +v 0.4 0.125 1 +v 0.4 0.275 1 +v -0.4000001 0.3 0.1 +v -0.4000001 0.25 0.1 +v -0.4000001 0.3 0.7 +v -0.4000001 0.25 0.3 +v -0.4000001 0.15 0.3 +v -0.4000001 0.1 0.7 +v -0.4000001 0.1 0.1 +v -0.4000001 0.15 0.1 +v -0.4000001 0.275 1 +v -0.4000001 0.125 1 +v -0.4000001 0.2 0.3 +v -0.6500001 0 0.1 +v -0.5699999 0.04 0.1 +v -0.6894426 0.07888544 0.1 +v -0.5999999 0.1 0.1 +v -0.6894426 0.3211145 0.1 +v -0.5999999 0.3 0.1 +v -0.5699999 0.36 0.1 +v -0.6500001 0.4 0.1 +v -0.5699999 0.36 0.7 +v -0.5999999 0.3 0.7 +v -0.6894426 0.3211145 0.7 +v -0.6500001 0.4 0.7 +v -0.6500001 0 0.7 +v -0.6894426 0.07888544 0.7 +v -0.5699999 0.04 0.7 +v -0.5999999 0.1 0.7 +v 0.6 0.6 -0.9000001 +v 0.6 0.6 -0.5 +v 0.6 0.5 -0.5 +v 0.45 0.6 -0.5 +v 0.35 0.6 -0.5 +v 0.35 0.6 -0.4 +v 0.45 0.6 -0.4 +v 0.35 0.5 -0.5 +v 0.45 0.5 -0.5 +v 0.45 0.5 -0.3 +v 0.45 0.55 -0.3 +v 0.35 0.55 -0.3 +v 0.35 0.5 -0.3 +v -0.5999999 0.6 -0.9000001 +v -0.3499999 0.6 -0.5 +v -0.45 0.6 -0.5 +v -0.5999999 0.6 -0.5 +v -0.45 0.6 -0.4 +v -0.3499999 0.6 -0.4 +v -0.5999999 0.5 -0.5 +v 0.6 0.275 1 +v 0.6 0.25 1.3 +v 0.4 0.25 1.3 +v 0.6 0.125 1 +v 0.6 0.15 1.3 +v 0.4 0.15 1.3 +v 0.3 0.1540133 0.1 +v 0.3 0.2540133 0.1 +v 0.3 0.1540133 0.2 +v 0.3 0.2540133 0.2 +v 0.35 0.1540133 0.3 +v 0.35 0.2540133 0.3 +v -0.3499999 0.5 -0.5 +v -0.45 0.5 -0.5 +v -0.45 0.5 -0.3 +v -0.45 0.55 -0.3 +v -0.3499999 0.5 -0.3 +v -0.3499999 0.55 -0.3 +v -0.3 0.25 0.1 +v -0.3 0.15 0.1 +v -0.3 0.25 0.2 +v -0.3 0.15 0.2 +v -0.3499999 0.25 0.3 +v -0.3499999 0.15 0.3 +v -0.4000001 0.25 1.3 +v -0.4000001 0.15 1.3 +v -0.5999999 0.15 1.3 +v -0.5999999 0.25 1.3 +v -0.5999999 0.275 1 +v -0.5999999 0.125 1 +v 0.9 0.3 -0.9000001 +v 0.9 0.5 -0.9000001 +v 0.9 0.5 -0.7 +v 0.9 0.3 -0.7 +v 0.7 0.5 -0.3 +v 0.7 0.3 -0.3 +v 0.6 0.5 -0.3 +v 0.2 0.3 0.1 +v 0.2 0.1 0.1 +v -0.9000001 0.3 -0.9000001 +v -0.9000001 0.5 -0.9000001 +v -0.7 0.3 -0.3 +v -0.9000001 0.3 -0.7 +v -0.9000001 0.5 -0.7 +v -0.7 0.5 -0.3 +v 0.2 0.1834482 0.3256706 +v 0.2 0.05 0.1 +v -0.2 0.1834482 0.3256706 +v -0.2 0.05 0.1 +v -0.5999999 0.5 -0.3 +v -0.2 0.1 0.1 +v -0.2 0.3 0.1 + +vn 0 1 0 +vn 1 0 0 +vn 0 0 -1 +vn 0 -1 0 +vn -1 0 0 +vn -0.8155257 0.410667 0.4077628 +vn 0.8155257 0.410667 0.4077628 +vn 0 0.8900457 0.4558712 +vn 0 0.06171992 0.9980935 +vn 0 0.9486833 0.3162278 +vn 0 0.8944272 -0.4472136 +vn 0 -0.9701425 -0.2425356 +vn 0.8944272 -0.4472136 0 +vn -0.8944272 -0.4472136 0 +vn 0.8944272 0.4472136 0 +vn 0 0 1 +vn -0.4472136 -0.8944272 0 +vn -0.8944272 0.4472136 0 +vn -0.4472136 0.8944272 0 +vn 0.4472136 0.8944272 0 +vn 0.4472136 -0.8944272 0 +vn 0 0.8944272 0.4472136 +vn 0 0.9965457 0.08304549 +vn 0 -0.9965457 0.08304549 +vn -0.8944272 0 0.4472136 +vn 0.8944272 0 0.4472136 +vn 0 0.9987586 0.0498137 +vn 0 -0.8607637 0.5090048 +vn 0 -0.9922779 0.1240347 + +vt -19.68504 -51.1811 +vt -19.68504 -43.30709 +vt -11.81102 -51.1811 +vt -11.81102 -43.30709 +vt 51.1811 19.68504 +vt 51.1811 7.874016 +vt 43.30709 19.68504 +vt 43.30709 7.874016 +vt -11.81102 7.874016 +vt -19.68504 7.874016 +vt -11.81102 19.68504 +vt -19.68504 19.68504 +vt 19.68504 -43.30709 +vt 19.68504 -51.1811 +vt 11.81102 -43.30709 +vt 11.81102 -51.1811 +vt -51.1811 7.874016 +vt -51.1811 19.68504 +vt -43.30709 7.874016 +vt -43.30709 19.68504 +vt 19.68504 7.874016 +vt 11.81102 7.874016 +vt 19.68504 19.68504 +vt 11.81102 19.68504 +vt -7.06021 22.60518 +vt 9.246657 13.46949 +vt 7.946689 4.333806 +vt -9.246657 13.46949 +vt 7.06021 22.60518 +vt -7.946689 4.333806 +vt 7.874016 15.9063 +vt 3.937008 -2.365937 +vt -7.874016 15.9063 +vt -3.937008 -2.365937 +vt 7.874016 9.696349 +vt -7.874016 9.696349 +vt 3.937008 18.04205 +vt -3.937008 18.04205 +vt 23.62205 52.28963 +vt 23.62205 39.83972 +vt 7.874016 52.28963 +vt 7.874016 44.81968 +vt 7.874016 39.83972 +vt -23.62205 -51.1811 +vt -23.62205 -47.24409 +vt -7.874016 -51.1811 +vt -7.874016 -47.24409 +vt 51.1811 23.62205 +vt 51.1811 3.937008 +vt 47.24409 23.62205 +vt 35.43307 11.81102 +vt 11.81102 3.937008 +vt 35.43307 19.68504 +vt 11.81102 11.81102 +vt 23.62205 -11.81102 +vt 23.62205 -51.1811 +vt 7.874016 -11.81102 +vt 7.874016 -43.30709 +vt 7.874016 -51.1811 +vt -51.1811 3.937008 +vt -51.1811 23.62205 +vt -43.30709 3.937008 +vt -47.24409 23.62205 +vt -40.15748 21.25984 +vt -7.874016 -26.41025 +vt -7.874016 -21.1282 +vt -7.874016 -29.93162 +vt 7.874016 -12.32478 +vt -7.874016 -12.32478 +vt 7.874016 -21.1282 +vt 7.874016 -26.41025 +vt 7.874016 -29.93162 +vt 35.43307 23.62205 +vt 40.15748 21.25984 +vt 19.68504 23.62205 +vt 27.55906 27.55906 +vt 11.81102 27.55906 +vt 7.874016 3.937008 +vt -7.874016 3.937008 +vt 7.874016 19.68504 +vt -7.874016 19.68504 +vt -7.874016 26.73621 +vt -7.874016 42.96891 +vt 7.874016 26.73621 +vt 7.874016 42.96891 +vt -7.874016 -27.55906 +vt -7.874016 -11.81102 +vt 7.874016 -27.55906 +vt 43.30709 3.937008 +vt 27.55906 0 +vt 11.81102 0 +vt -7.874016 52.28963 +vt -7.874016 44.81968 +vt -23.62205 52.28963 +vt -7.874016 39.83972 +vt -23.62205 39.83972 +vt 7.874016 -47.24409 +vt 23.62205 -47.24409 +vt -35.43307 11.81102 +vt -11.81102 3.937008 +vt -11.81102 11.81102 +vt -35.43307 19.68504 +vt -27.55906 -1.591616E-13 +vt -11.81102 -1.591616E-13 +vt -7.874016 -43.30709 +vt -23.62205 -11.81102 +vt -35.43307 23.62205 +vt -27.55906 27.55906 +vt -19.68504 23.62205 +vt -11.81102 27.55906 +vt -3.937008 14.91675 +vt -3.937008 11.44444 +vt -27.55906 14.91675 +vt -27.55906 11.44444 +vt 3.937008 -4.494183E-13 +vt 3.937008 2.641025 +vt 27.55906 -4.494183E-13 +vt 27.55906 2.641025 +vt -3.937008 2.641025 +vt -3.937008 -0.831282 +vt -27.55906 2.641025 +vt -27.55906 -0.831282 +vt 25.59055 0 +vt 22.44094 1.574803 +vt 27.14341 3.105726 +vt 23.62205 3.937008 +vt 27.14341 12.64231 +vt 23.62205 11.81102 +vt 22.44094 14.17323 +vt 25.59055 15.74803 +vt 27.55906 -22.88889 +vt 3.937008 -22.88889 +vt 27.55906 -19.36752 +vt 3.937008 -19.36752 +vt -25.59055 0 +vt -27.14341 3.105726 +vt -22.44094 1.574803 +vt -23.62205 3.937008 +vt -27.14341 12.64231 +vt -23.62205 11.81102 +vt -22.44094 14.17323 +vt -25.59055 15.74803 +vt 3.937008 11.44444 +vt 3.937008 14.08547 +vt 27.55906 11.44444 +vt 27.55906 14.08547 +vt 3.937008 29.93162 +vt 27.55906 29.93162 +vt 3.937008 26.41025 +vt 27.55906 26.41025 +vt -3.937008 12.64231 +vt -3.937008 3.105726 +vt -27.55906 12.64231 +vt -27.55906 3.105726 +vt 3.937008 3.937008 +vt 3.937008 6.063517 +vt 27.55906 3.937008 +vt 11.81102 6.063517 +vt 11.81102 8.03203 +vt 27.55906 11.81102 +vt 11.81102 10.00052 +vt 3.937008 11.81102 +vt 3.937008 10.00052 +vt 39.37008 4.92126 +vt 39.37008 10.82677 +vt -3.937008 11.81102 +vt -3.937008 9.84252 +vt -27.55906 11.81102 +vt -11.81102 9.84252 +vt -11.81102 5.905512 +vt -27.55906 3.937008 +vt -3.937008 3.937008 +vt -3.937008 5.905512 +vt -39.37008 10.82677 +vt -39.37008 4.92126 +vt -11.81102 7.874016 +vt -3.937008 -4.476419E-13 +vt -27.55906 -4.476419E-13 +vt 3.937008 -0.831282 +vt 27.55906 -0.831282 +vt -25.59055 -6.694648E-30 +vt 3.937008 14.91675 +vt 27.55906 14.91675 +vt -3.937008 14.08547 +vt -27.55906 14.08547 +vt -3.937008 26.41025 +vt -27.55906 26.41025 +vt -3.937008 29.93162 +vt -27.55906 29.93162 +vt 3.937008 3.105726 +vt 3.937008 12.64231 +vt 27.55906 3.105726 +vt 27.55906 12.64231 +vt -27.55906 -19.36752 +vt -3.937008 -19.36752 +vt -27.55906 -22.88889 +vt -3.937008 -22.88889 +vt -7.874016 23.62205 +vt -23.62205 23.62205 +vt -23.62205 19.68504 +vt -23.62205 -35.43307 +vt -23.62205 -19.68504 +vt -7.874016 -35.43307 +vt -17.71654 -19.68504 +vt -13.77953 -19.68504 +vt -13.77953 -15.74803 +vt -7.874016 -19.68504 +vt -17.71654 -15.74803 +vt 13.77953 19.68504 +vt 13.77953 23.62205 +vt 7.874016 23.62205 +vt 23.62205 19.68504 +vt 17.71654 19.68504 +vt 23.62205 23.62205 +vt 17.71654 23.62205 +vt 15.74803 23.62205 +vt 11.81102 21.65354 +vt 17.71654 24.64957 +vt 17.71654 20.24786 +vt 13.77953 24.64957 +vt 13.77953 20.24786 +vt 17.71654 21.65354 +vt 13.77953 21.65354 +vt -15.74803 23.62205 +vt -11.81102 21.65354 +vt 7.874016 -35.43307 +vt 7.874016 -19.68504 +vt 23.62205 -35.43307 +vt 13.77953 -19.68504 +vt 17.71654 -19.68504 +vt 23.62205 -19.68504 +vt 17.71654 -15.74803 +vt 13.77953 -15.74803 +vt 23.62205 -38.33497 +vt 23.62205 -50.18694 +vt 15.74803 -38.33497 +vt 15.74803 -50.18694 +vt -51.1811 9.84252 +vt -51.1811 5.905512 +vt 23.62205 5.905512 +vt 15.74803 5.905512 +vt 23.62205 9.84252 +vt 15.74803 9.84252 +vt 51.1811 5.905512 +vt 51.1811 9.84252 +vt 23.62205 39.64277 +vt 15.74803 39.64277 +vt 23.62205 51.49474 +vt 15.74803 51.49474 +vt 7.874016 6.063517 +vt 7.874016 10.00052 +vt 15.74803 11.81102 +vt 15.74803 3.937008 +vt 13.77953 11.81102 +vt 12.32478 6.063517 +vt 12.32478 10.00052 +vt 16.72649 6.063517 +vt 16.72649 10.00052 +vt 13.77953 6.063517 +vt 13.77953 10.00052 +vt 15.74803 6.063517 +vt 15.74803 8.03203 +vt 15.74803 10.00052 +vt -15.74803 11.81102 +vt -13.77953 11.81102 +vt -15.74803 3.937008 +vt -13.77953 19.68504 +vt -13.77953 23.62205 +vt -17.71654 19.68504 +vt -17.71654 23.62205 +vt -13.77953 21.65354 +vt -17.71654 21.65354 +vt -13.77953 24.64957 +vt -13.77953 20.24786 +vt -17.71654 24.64957 +vt -17.71654 20.24786 +vt -7.874016 9.84252 +vt -7.874016 5.905512 +vt -12.32478 9.84252 +vt -12.32478 5.905512 +vt -16.72649 9.84252 +vt -16.72649 5.905512 +vt -15.74803 5.905512 +vt -15.74803 7.874016 +vt -13.77953 5.905512 +vt -13.77953 9.84252 +vt -15.74803 9.84252 +vt -23.62205 5.905512 +vt -23.62205 9.84252 +vt -15.74803 -38.33497 +vt -15.74803 -50.18694 +vt -23.62205 -38.33497 +vt -23.62205 -50.18694 +vt -15.74803 51.49474 +vt -15.74803 39.64277 +vt -23.62205 51.49474 +vt -23.62205 39.64277 +vt 27.55906 19.68504 +vt 40.49572 19.68504 +vt 40.49572 11.81102 +vt 22.88889 19.68504 +vt 22.88889 11.81102 +vt 35.43307 -27.55906 +vt 35.43307 -35.43307 +vt 27.55906 -11.81102 +vt -35.43307 -35.43307 +vt -35.43307 -27.55906 +vt -27.55906 -11.81102 +vt -17.71654 -11.81102 +vt -13.77953 -11.81102 +vt 23.62205 19.36752 +vt 23.62205 1.760683 +vt 17.71654 19.36752 +vt 15.74803 1.760683 +vt 13.77953 19.36752 +vt 7.874016 1.760683 +vt 7.874016 19.36752 +vt 23.62205 27.55906 +vt 15.74803 27.55906 +vt -27.55906 19.68504 +vt -40.49572 11.81102 +vt -40.49572 19.68504 +vt -22.88889 11.81102 +vt -22.88889 19.68504 +vt 7.874016 13.16918 +vt 7.874016 5.304939 +vt -7.874016 13.16918 +vt -7.874016 5.304939 +vt 7.874016 14.71266 +vt 7.874016 4.390811 +vt -7.874016 14.71266 +vt -7.874016 4.390811 +vt 3.956547 27.16731 +vt -12.82168 10.50774 +vt -3.937008 1.968504 +vt -12.82168 7.222372 +vt 7.874016 7.222372 +vt -7.874016 7.222372 +vt 7.874016 10.50774 +vt -7.874016 10.50774 +vt 7.874016 4.150769 +vt 7.874016 -11.71982 +vt -7.874016 4.150769 +vt -7.874016 -11.71982 +vt -23.62205 27.55906 +vt -15.74803 27.55906 +vt 23.62205 -26.48301 +vt 15.74803 -26.48301 +vt 23.62205 27.79081 +vt 15.74803 27.79081 +vt 3.937008 1.968504 +vt 12.82168 7.222372 +vt 12.82168 10.50774 +vt -3.956547 27.16731 +vt -7.874016 1.760683 +vt -15.74803 1.760683 +vt -7.874016 19.36752 +vt -13.77953 19.36752 +vt -17.71654 19.36752 +vt -23.62205 1.760683 +vt -23.62205 19.36752 +vt 7.874016 11.81102 +vt 17.71654 -11.81102 +vt 13.77953 -11.81102 +vt -7.874016 11.81102 +vt -15.74803 -26.48301 +vt -23.62205 -26.48301 +vt -15.74803 27.79081 +vt -23.62205 27.79081 + +usemtl dark + +f 3/3/1 2/2/1 1/1/1 +f 2/2/1 3/3/1 4/4/1 +f 6/7/2 3/6/2 5/5/2 +f 3/6/2 6/7/2 4/8/2 +f 6/11/3 2/10/3 4/9/3 +f 2/10/3 6/11/3 7/12/3 +f 6/15/4 8/14/4 7/13/4 +f 8/14/4 6/15/4 5/16/4 +f 2/19/5 8/18/5 1/17/5 +f 8/18/5 2/19/5 7/20/5 +f 11/14/1 10/15/1 9/16/1 +f 10/15/1 11/14/1 12/13/1 +f 15/2/4 14/3/4 13/4/4 +f 14/3/4 15/2/4 16/1/4 +f 15/23/3 10/22/3 12/21/3 +f 10/22/3 15/23/3 13/24/3 +f 10/19/5 14/18/5 9/17/5 +f 14/18/5 10/19/5 13/20/5 +f 15/7/2 11/6/2 16/5/2 +f 11/6/2 15/7/2 12/8/2 +f 19/27/6 18/26/6 17/25/6 +f 22/30/7 21/29/7 20/28/7 +f 17/33/8 20/32/8 21/31/8 +f 20/32/8 17/33/8 18/34/8 +f 20/37/9 19/36/9 22/35/9 +f 19/36/9 20/37/9 18/38/9 + +usemtl metalDark + +f 25/41/10 24/40/10 23/39/10 +f 24/40/10 25/41/10 26/42/10 +f 27/43/10 24/40/10 26/42/10 +f 29/46/1 23/45/1 28/44/1 +f 23/45/1 29/46/1 25/47/1 +f 23/50/2 30/49/2 28/48/2 +f 30/49/2 23/50/2 31/51/2 +f 30/49/2 31/51/2 32/52/2 +f 31/51/2 23/50/2 24/53/2 +f 32/52/2 31/51/2 33/54/2 +f 34/57/4 30/56/4 32/55/4 +f 30/56/4 34/57/4 35/58/4 +f 30/56/4 35/58/4 36/59/4 +f 35/62/5 29/61/5 36/60/5 +f 29/61/5 35/62/5 25/63/5 +f 25/63/5 35/62/5 37/20/5 +f 26/64/5 25/63/5 37/20/5 +f 37/67/11 38/66/11 26/65/11 +f 39/68/11 38/66/11 37/67/11 +f 38/66/11 39/68/11 40/69/11 +f 39/68/11 37/67/11 41/70/11 +f 41/70/11 37/67/11 42/71/11 +f 43/72/11 42/71/11 37/67/11 +f 27/53/2 26/74/2 38/73/2 +f 40/76/2 44/75/2 38/73/2 +f 44/75/2 40/76/2 45/77/2 +f 44/75/2 45/77/2 46/23/2 +f 46/23/2 45/77/2 47/24/2 +f 43/80/3 35/79/3 48/78/3 +f 35/79/3 43/80/3 37/81/3 +f 50/84/12 35/83/12 49/82/12 +f 35/83/12 50/84/12 48/85/12 +f 39/88/1 45/87/1 40/86/1 +f 45/87/1 39/88/1 51/57/1 +f 53/87/4 49/88/4 52/57/4 +f 49/88/4 53/87/4 50/86/4 +f 34/52/2 49/90/2 35/89/2 +f 49/90/2 34/52/2 52/91/2 +f 42/74/2 43/7/2 54/50/2 +f 48/89/2 54/50/2 43/7/2 +f 55/49/2 54/50/2 48/89/2 +f 54/50/2 55/49/2 56/48/2 +f 57/94/10 42/93/10 54/92/10 +f 42/93/10 57/94/10 58/95/10 +f 58/95/10 57/94/10 59/96/10 +f 60/56/1 54/97/1 56/59/1 +f 54/97/1 60/56/1 57/98/1 +f 62/60/5 61/99/5 60/61/5 +f 61/99/5 62/60/5 63/100/5 +f 60/61/5 61/99/5 57/63/5 +f 61/99/5 63/100/5 64/101/5 +f 59/102/5 57/63/5 61/99/5 +f 50/103/5 65/100/5 48/62/5 +f 65/100/5 50/103/5 53/104/5 +f 63/106/4 48/105/4 65/87/4 +f 62/44/4 48/105/4 63/106/4 +f 48/105/4 62/44/4 55/46/4 +f 58/102/5 41/107/5 42/64/5 +f 66/109/5 39/108/5 41/107/5 +f 39/108/5 66/109/5 51/110/5 +f 51/110/5 66/109/5 67/11/5 +f 67/11/5 66/109/5 68/12/5 +f 71/113/13 70/112/13 69/111/13 +f 70/112/13 71/113/13 72/114/13 +f 75/117/14 74/116/14 73/115/14 +f 74/116/14 75/117/14 76/118/14 +f 79/121/15 78/120/15 77/119/15 +f 78/120/15 79/121/15 80/122/15 +f 71/125/16 81/124/16 72/123/16 +f 81/124/16 71/125/16 82/126/16 +f 82/126/16 71/125/16 80/127/16 +f 82/126/16 80/127/16 75/128/16 +f 75/128/16 80/127/16 76/129/16 +f 76/129/16 80/127/16 79/130/16 +f 81/133/17 70/132/17 72/131/17 +f 70/132/17 81/133/17 83/134/17 +f 83/137/3 69/136/3 70/135/3 +f 69/136/3 83/137/3 84/138/3 +f 69/136/3 84/138/3 78/139/3 +f 78/139/3 84/138/3 73/140/3 +f 78/139/3 73/140/3 74/141/3 +f 78/139/3 74/141/3 77/142/3 +f 81/145/18 84/144/18 83/143/18 +f 84/144/18 81/145/18 82/146/18 +f 74/149/19 79/148/19 77/147/19 +f 79/148/19 74/149/19 76/150/19 +f 80/153/2 69/152/2 78/151/2 +f 69/152/2 80/153/2 71/154/2 +f 87/157/5 86/156/5 85/155/5 +f 86/156/5 87/157/5 88/158/5 +f 88/158/5 87/157/5 89/159/5 +f 90/160/5 89/159/5 87/157/5 +f 90/160/5 91/161/5 89/159/5 +f 92/162/5 91/161/5 90/160/5 +f 91/161/5 92/162/5 93/163/5 +f 90/160/5 87/157/5 94/164/5 +f 90/160/5 94/164/5 95/165/5 +f 98/168/2 97/167/2 96/166/2 +f 97/167/2 98/168/2 99/169/2 +f 99/169/2 98/168/2 100/170/2 +f 101/171/2 100/170/2 98/168/2 +f 102/172/2 100/170/2 101/171/2 +f 100/170/2 102/172/2 103/173/2 +f 101/171/2 98/168/2 104/174/2 +f 101/171/2 104/174/2 105/175/2 +f 100/170/2 106/176/2 99/169/2 +f 109/125/3 108/124/3 107/123/3 +f 108/124/3 109/125/3 110/126/3 +f 110/126/3 109/125/3 111/127/3 +f 110/126/3 111/127/3 112/128/3 +f 112/128/3 111/127/3 113/129/3 +f 113/129/3 111/127/3 114/130/3 +f 115/121/13 112/177/13 113/119/13 +f 112/177/13 115/121/13 116/178/13 +f 117/180/18 114/116/18 111/179/18 +f 114/116/18 117/180/18 118/118/18 +f 121/137/16 120/136/16 119/181/16 +f 120/136/16 121/137/16 122/138/16 +f 120/136/16 122/138/16 117/139/16 +f 117/139/16 122/138/16 116/140/16 +f 117/139/16 116/140/16 115/141/16 +f 117/139/16 115/141/16 118/142/16 +f 119/145/14 109/182/14 107/143/14 +f 109/182/14 119/145/14 120/183/14 +f 122/185/15 108/112/15 110/184/15 +f 108/112/15 122/185/15 121/114/15 +f 114/188/20 115/187/20 113/186/20 +f 115/187/20 114/188/20 118/189/20 +f 120/192/5 111/191/5 109/190/5 +f 111/191/5 120/192/5 117/193/5 +f 119/196/21 108/195/21 121/194/21 +f 108/195/21 119/196/21 107/197/21 + +usemtl metal + +f 36/79/3 3/9/3 30/138/3 +f 3/9/3 36/79/3 29/198/3 +f 3/9/3 29/198/3 5/11/3 +f 5/11/3 29/198/3 8/12/3 +f 1/10/3 30/138/3 3/9/3 +f 30/138/3 1/10/3 28/199/3 +f 28/199/3 1/10/3 8/12/3 +f 28/199/3 8/12/3 29/198/3 +f 38/198/3 24/200/3 27/81/3 +f 24/200/3 38/198/3 123/199/3 +f 124/75/2 24/53/2 123/73/2 +f 24/53/2 124/75/2 125/23/2 +f 38/203/1 124/202/1 123/201/1 +f 124/202/1 38/203/1 126/204/1 +f 126/204/1 38/203/1 127/205/1 +f 126/204/1 127/205/1 128/206/1 +f 127/205/1 38/203/1 44/207/1 +f 128/206/1 129/208/1 126/204/1 +f 127/210/16 46/80/16 130/209/16 +f 46/80/16 127/210/16 44/211/16 +f 124/214/16 131/213/16 125/212/16 +f 131/213/16 124/214/16 126/215/16 +f 129/216/2 131/23/2 126/75/2 +f 131/23/2 129/216/2 132/24/2 +f 132/24/2 129/216/2 133/217/2 +f 128/220/22 133/219/22 129/218/22 +f 133/219/22 128/220/22 134/221/22 +f 133/222/16 135/209/16 132/213/16 +f 135/209/16 133/222/16 134/223/16 +f 130/12/5 128/224/5 127/109/5 +f 128/224/5 130/12/5 135/11/5 +f 128/224/5 135/11/5 134/225/5 +f 62/126/3 11/21/3 55/78/3 +f 11/21/3 62/126/3 60/214/3 +f 11/21/3 60/214/3 16/23/3 +f 16/23/3 60/214/3 14/24/3 +f 9/22/3 55/78/3 11/21/3 +f 55/78/3 9/22/3 56/211/3 +f 56/211/3 9/22/3 14/24/3 +f 56/211/3 14/24/3 60/214/3 +f 136/228/1 66/227/1 41/226/1 +f 66/227/1 136/228/1 137/229/1 +f 137/229/1 136/228/1 138/230/1 +f 138/230/1 136/228/1 139/231/1 +f 140/232/1 137/229/1 138/230/1 +f 137/229/1 140/232/1 141/233/1 +f 136/214/3 58/80/3 59/212/3 +f 58/80/3 136/214/3 41/211/3 +f 59/102/5 139/109/5 136/107/5 +f 139/109/5 59/102/5 142/12/5 +f 95/236/23 144/235/23 143/234/23 +f 144/235/23 95/236/23 145/237/23 +f 144/238/2 146/175/2 143/174/2 +f 146/175/2 144/238/2 147/239/2 +f 144/242/16 148/241/16 147/240/16 +f 148/241/16 144/242/16 145/243/16 +f 148/244/5 95/165/5 94/164/5 +f 95/165/5 148/244/5 145/245/5 +f 147/248/24 94/247/24 146/246/24 +f 94/247/24 147/248/24 148/249/24 +f 151/250/5 150/163/5 149/156/5 +f 150/163/5 151/250/5 152/251/5 +f 153/254/4 86/253/4 88/252/4 +f 86/253/4 153/254/4 149/52/4 +f 149/52/4 153/254/4 151/22/4 +f 153/257/25 152/256/25 151/255/25 +f 152/256/25 153/257/25 154/258/25 +f 88/261/16 154/260/16 153/259/16 +f 154/260/16 88/261/16 89/262/16 +f 154/260/16 89/262/16 91/263/16 +f 93/266/1 154/265/1 91/264/1 +f 154/265/1 93/266/1 150/100/1 +f 154/265/1 150/100/1 152/9/1 +f 68/81/16 137/268/16 155/267/16 +f 137/268/16 68/81/16 66/198/16 +f 138/270/16 142/200/16 156/269/16 +f 142/200/16 138/270/16 139/199/16 +f 157/11/5 138/109/5 156/12/5 +f 138/109/5 157/11/5 140/224/5 +f 140/224/5 157/11/5 158/225/5 +f 160/271/16 157/269/16 159/267/16 +f 157/269/16 160/271/16 158/272/16 +f 140/275/22 160/274/22 141/273/22 +f 160/274/22 140/275/22 158/276/22 +f 141/216/2 155/23/2 137/75/2 +f 155/23/2 141/216/2 159/24/2 +f 159/24/2 141/216/2 160/217/2 +f 163/277/2 162/173/2 161/167/2 +f 162/173/2 163/277/2 164/278/2 +f 97/253/1 163/22/1 161/52/1 +f 163/22/1 97/253/1 165/254/1 +f 165/254/1 97/253/1 99/252/1 +f 165/281/26 164/280/26 163/279/26 +f 164/280/26 165/281/26 166/282/26 +f 166/285/16 106/284/16 100/283/16 +f 106/284/16 166/285/16 165/286/16 +f 106/284/16 165/286/16 99/287/16 +f 164/9/4 103/266/4 162/100/4 +f 103/266/4 164/9/4 166/265/4 +f 103/266/4 166/265/4 100/264/4 +f 167/238/2 105/175/2 104/174/2 +f 105/175/2 167/238/2 168/239/2 +f 167/287/16 169/288/16 168/283/16 +f 169/288/16 167/287/16 170/289/16 +f 171/292/23 167/291/23 104/290/23 +f 167/291/23 171/292/23 170/293/23 +f 169/244/5 171/165/5 172/164/5 +f 171/165/5 169/244/5 170/245/5 +f 169/296/24 105/295/24 168/294/24 +f 105/295/24 169/296/24 172/297/24 + +usemtl metalRed + +f 24/200/3 173/99/3 31/140/3 +f 173/99/3 24/200/3 174/102/3 +f 175/298/2 173/51/2 174/53/2 +f 173/51/2 175/298/2 176/160/2 +f 177/301/26 176/300/26 175/299/26 +f 176/300/26 177/301/26 178/302/26 +f 178/305/4 173/304/4 176/303/4 +f 173/304/4 178/305/4 31/228/4 +f 31/228/4 178/305/4 33/55/4 +f 24/201/1 175/307/1 174/306/1 +f 175/307/1 24/201/1 177/308/1 +f 177/308/1 24/201/1 179/106/1 +f 179/106/1 24/201/1 125/202/1 +f 179/106/1 125/202/1 131/204/1 +f 179/106/1 131/204/1 132/309/1 +f 130/205/1 47/87/1 135/310/1 +f 47/87/1 130/205/1 46/207/1 +f 177/298/16 33/128/16 178/160/16 +f 33/128/16 177/298/16 179/212/16 +f 132/313/22 73/312/22 179/311/22 +f 73/312/22 132/313/22 92/314/22 +f 92/314/22 132/313/22 135/315/22 +f 92/314/22 135/315/22 180/316/22 +f 180/316/22 135/315/22 47/317/22 +f 87/319/4 84/126/4 82/318/4 +f 84/126/4 87/319/4 32/55/4 +f 32/55/4 87/319/4 85/253/4 +f 32/55/4 85/253/4 34/57/4 +f 34/57/4 85/253/4 181/78/4 +f 73/166/2 33/54/2 179/24/2 +f 33/54/2 73/166/2 32/52/2 +f 32/52/2 73/166/2 84/172/2 +f 183/53/3 61/128/3 182/51/3 +f 61/128/3 183/53/3 59/212/3 +f 184/308/4 61/201/4 64/106/4 +f 61/201/4 184/308/4 182/306/4 +f 182/306/4 184/308/4 185/307/4 +f 185/168/5 183/102/5 182/99/5 +f 183/102/5 185/168/5 186/320/5 +f 184/323/25 186/322/25 185/321/25 +f 186/322/25 184/323/25 187/324/25 +f 51/327/27 21/326/27 45/325/27 +f 21/326/27 51/327/27 17/328/27 +f 190/331/28 189/330/28 188/329/28 +f 189/330/28 190/331/28 191/332/28 +f 21/333/2 47/24/2 45/77/2 +f 47/24/2 21/333/2 180/166/2 +f 180/166/2 21/333/2 22/334/2 +f 180/166/2 22/334/2 181/172/2 +f 181/172/2 22/334/2 189/335/2 +f 189/335/2 22/334/2 188/336/2 +f 52/91/2 181/172/2 189/335/2 +f 181/172/2 52/91/2 34/52/2 +f 22/339/16 190/338/16 188/337/16 +f 190/338/16 22/339/16 19/340/16 +f 191/343/29 52/342/29 189/341/29 +f 52/342/29 191/343/29 53/344/29 +f 92/266/1 75/345/1 73/138/1 +f 75/345/1 92/266/1 90/346/1 +f 143/174/2 82/171/2 75/168/2 +f 82/171/2 143/174/2 146/175/2 +f 75/347/23 95/236/23 143/234/23 +f 95/236/23 75/347/23 90/348/23 +f 94/247/24 82/349/24 146/246/24 +f 82/349/24 94/247/24 87/350/24 +f 192/200/16 184/168/16 64/140/16 +f 184/168/16 192/200/16 187/320/16 +f 191/351/5 65/100/5 53/104/5 +f 65/100/5 191/351/5 193/155/5 +f 193/155/5 191/351/5 190/352/5 +f 193/155/5 190/352/5 194/162/5 +f 19/353/5 194/162/5 190/352/5 +f 17/354/5 194/162/5 19/353/5 +f 51/110/5 194/162/5 17/354/5 +f 194/162/5 51/110/5 67/11/5 +f 122/345/4 102/266/4 101/346/4 +f 63/106/4 102/266/4 122/345/4 +f 65/87/4 102/266/4 63/106/4 +f 102/266/4 65/87/4 193/79/4 +f 63/106/4 122/345/4 110/138/4 +f 67/357/22 96/356/22 194/355/22 +f 96/356/22 67/357/22 159/358/22 +f 96/356/22 159/358/22 157/359/22 +f 96/356/22 157/359/22 112/360/22 +f 112/360/22 157/359/22 192/361/22 +f 86/261/16 181/78/16 85/253/16 +f 181/78/16 86/261/16 149/158/16 +f 149/158/16 180/362/16 181/78/16 +f 180/362/16 149/158/16 150/161/16 +f 180/362/16 150/161/16 93/263/16 +f 180/362/16 93/263/16 92/252/16 +f 183/304/1 142/231/1 59/228/1 +f 142/231/1 183/304/1 187/305/1 +f 187/305/1 183/304/1 186/303/1 +f 192/55/1 142/231/1 187/305/1 +f 157/363/1 142/231/1 192/55/1 +f 142/231/1 157/363/1 156/230/1 +f 155/229/1 67/57/1 68/227/1 +f 67/57/1 155/229/1 159/364/1 +f 193/79/16 162/170/16 102/266/16 +f 162/170/16 193/79/16 194/365/16 +f 102/266/16 162/170/16 103/283/16 +f 162/170/16 194/365/16 161/169/16 +f 96/264/16 161/169/16 194/365/16 +f 161/169/16 96/264/16 97/287/16 +f 112/126/1 98/319/1 96/253/1 +f 98/319/1 112/126/1 116/318/1 +f 116/367/23 104/290/23 98/366/23 +f 104/290/23 116/367/23 171/292/23 +f 172/297/24 101/368/24 105/295/24 +f 101/368/24 172/297/24 122/369/24 +f 110/155/5 64/101/5 63/100/5 +f 64/101/5 110/155/5 192/11/5 +f 192/11/5 110/155/5 112/162/5 +f 172/164/5 116/160/5 122/157/5 +f 116/160/5 172/164/5 171/165/5 + diff --git a/packages/examples/public/assets/multiMaterialMesh/craft_racer.mtl b/packages/examples/public/assets/multiMaterialMesh/craft_racer.mtl new file mode 100644 index 000000000..d28d4966b --- /dev/null +++ b/packages/examples/public/assets/multiMaterialMesh/craft_racer.mtl @@ -0,0 +1,14 @@ +# Created by Kenney (www.kenney.nl) + +newmtl metal +Kd 0.8431373 0.8705882 0.9098039 + +newmtl metalDark +Kd 0.6750623 0.7100219 0.7735849 + +newmtl dark +Kd 0.2745098 0.2980392 0.3411765 + +newmtl metalRed +Kd 1 0.6285242 0.2028302 + diff --git a/packages/examples/public/assets/multiMaterialMesh/craft_racer.obj b/packages/examples/public/assets/multiMaterialMesh/craft_racer.obj new file mode 100644 index 000000000..fc2766148 --- /dev/null +++ b/packages/examples/public/assets/multiMaterialMesh/craft_racer.obj @@ -0,0 +1,762 @@ +# Created by Kenney (www.kenney.nl) + +mtllib craft_racer.mtl + +g craft_racer + +v 0.45 0.1 -0.9128351 +v 0.25 0.15 -0.9128351 +v 0.2 0.1 -0.9128351 +v 0.2 0.4 -0.9128351 +v 0.25 0.35 -0.9128351 +v 0.4 0.35 -0.9128351 +v 0.4 0.15 -0.9128351 +v 0.45 0.4 -0.9128351 +v 0.45 0.5 -0.5128353 +v 0.45 0.5 -0.01283526 +v 0.2 0.5 -0.5128353 +v 0.35 0.5 -0.1128353 +v 0.2 0.5 -0.1128353 +v 0.2 0.5 -0.3128352 +v 0.35 0.5 -0.01283526 +v -0.2 0.1 -0.9128351 +v -0.4000001 0.15 -0.9128351 +v -0.45 0.1 -0.9128351 +v -0.45 0.4 -0.9128351 +v -0.4000001 0.35 -0.9128351 +v -0.25 0.35 -0.9128351 +v -0.25 0.15 -0.9128351 +v -0.2 0.4 -0.9128351 +v 0.35 0.4 -0.1128353 +v 0.2 0.4 -0.1128353 +v 0.45 0.4 -0.5128353 +v 0.45 0.4 0.08716476 +v 0.45 0.45 0.08716476 +v 0.35 0.45 0.08716476 +v 0.35 0.4 0.08716476 +v -0.45 0.4 -0.5128353 +v -0.45 0.5 -0.5128353 +v -0.45 0.4 0.08716476 +v -0.45 0.5 -0.01283526 +v -0.45 0.45 0.08716476 +v -0.2 0.4 -0.1128353 +v -0.3499999 0.4 -0.1128353 +v -0.2 0.5 -0.1128353 +v -0.3499999 0.5 -0.1128353 +v -0.3499999 0.5 -0.01283526 +v -0.3499999 0.4 0.08716476 +v -0.3499999 0.45 0.08716476 +v -0.2 0.5 -0.5128353 +v -0.2 0.5 -0.3128352 +v 0.45 0.4 -0.8128352 +v 0.2 0.4 -0.8128352 +v 0.45 0.1 -0.5128353 +v 0.2 0.1 -0.5128353 +v 0.45 0.2 -0.5128353 +v 0.6 0.2 -0.5128353 +v 0.6 0 -0.5128353 +v 0.6 0.2 -0.2128353 +v 0.6 0 -0.2128353 +v 0.2 0 -0.5128353 +v 0.2 0.7 -0.3128352 +v 0.2 0.7 0.08716476 +v -0.2 0.7 -0.3128352 +v -0.2 0.7 0.08716476 +v 0.2 0.4784731 0.08716476 +v 0.2 0.4 0.2123492 +v -0.2 0 -0.3128352 +v 0.2 0 -0.3128352 +v -0.2 0.1 -0.3128352 +v 0.2 0.1 -0.3128352 +v -0.2 0.4 -0.8128352 +v -0.45 0.4 -0.8128352 +v -0.2 0.1 -0.5128353 +v -0.45 0.1 -0.5128353 +v -0.45 0.2 -0.5128353 +v -0.5999999 0 -0.5128353 +v -0.5999999 0.2 -0.5128353 +v -0.5999999 0 -0.2128353 +v -0.5999999 0.2 -0.2128353 +v -0.2 0 -0.5128353 +v 0.04999995 0 0.5871647 +v 0.04999995 0 0.4871647 +v -0.04999995 0 0.5871647 +v -0.04999995 0 0.4871647 +v 0.2 0 0.4871647 +v -0.2 0 0.4871647 +v -0.2 0.1 0.4871647 +v -0.2 0.2277294 0.4871647 +v -0.2 0.1258101 0.6497518 +v 0.2 0.2277294 0.4871647 +v 0.2 0.1 0.4871647 +v 0.2 0.1258101 0.6497518 +v -0.2 0.4 0.2123492 +v -0.2 0.4784731 0.08716476 +v -0.04999995 0.1 0.4871647 +v 0.04999995 0.1 0.4871647 +v 0.04999995 0.1317492 0.6871647 +v 0.04999995 0.05 0.6871647 +v -0.04999995 0.1317492 0.6871647 +v -0.04999995 0.05 0.6871647 +v 0.4 0.35 -0.8128352 +v 0.25 0.35 -0.8128352 +v 0.4 0.15 -0.8128352 +v 0.25 0.15 -0.8128352 +v 0.2707107 0.1 0.1578754 +v 0.5292892 0.1 0.1578754 +v 0.2707107 0.1 0.4164541 +v 0.2707107 0.3 0.4164541 +v 0.5292892 0.3 0.1578754 +v 0.2707107 0.3 0.1578754 +v -0.25 0.15 -0.8128352 +v -0.4000001 0.15 -0.8128352 +v -0.4000001 0.35 -0.8128352 +v -0.25 0.35 -0.8128352 +v -0.2707107 0.1 0.1578754 +v -0.2707107 0.3 0.1578754 +v -0.2707107 0.1 0.4164541 +v -0.2707107 0.3 0.4164541 +v -0.5292892 0.1 0.1578754 +v -0.5292892 0.3 0.1578754 +v 0.2 0.2668965 1.012835 +v -0.2 0.2668965 1.012835 +v 0.1 0.45 0.9371647 +v -0.0999999 0.45 0.9371647 +v 0.2 0.6900496 0.2866684 +v -0.2 0.6900496 0.2866684 +v -0.2 0.1 -1.012835 +v 0.2 0.1 -1.012835 +v -0.2 0.5 -1.012835 +v -0.0999999 0.5 -1.012835 +v 0.1 0.5 -1.012835 +v -0.0999999 0.75 -1.012835 +v 0.1 0.75 -1.012835 +v 0.2 0.5 -1.012835 +v 0.6 0.4 -0.5128353 +v 0.6 0 0.08716476 +v 0.6 0.4 0.08716476 +v 0.2 0.4 0.4871647 +v -0.0999999 0.6142857 -0.6128354 +v 0.1 0.6142857 -0.6128354 +v 0.1 0.75 -0.9128351 +v -0.0999999 0.75 -0.9128351 +v -0.5999999 0.4 -0.5128353 +v -0.5999999 0 0.08716476 +v -0.5999999 0.4 0.08716476 +v -0.2 0.4 0.4871647 +v 0.2 0.1834482 1.012835 +v -0.2 0.1834482 1.012835 + +vn 0 0 -1 +vn 0 1 0 +vn 0 0 1 +vn 1 0 0 +vn 0 0.8944272 0.4472136 +vn -1 0 0 +vn 0 0.9486833 -0.3162278 +vn 0 -1 0 +vn 0 -0.8944272 0.4472136 +vn 0 0.3819364 0.9241886 +vn 0.7684958 0.5528403 0.322152 +vn 0 0.9381591 0.3462045 +vn -0.7684958 0.5528403 0.322152 +vn 0.7071068 0 0.7071068 +vn 0 0.961524 -0.2747211 +vn 0 0.9111079 0.4121679 +vn -0.7071068 0 0.7071068 +vn 0 0.9987586 0.0498137 +vn 0 -0.9876331 0.1567831 + +vt -17.71654 3.937008 +vt -9.84252 5.905512 +vt -7.874016 3.937008 +vt -7.874016 15.74803 +vt -9.84252 13.77953 +vt -15.74803 13.77953 +vt -15.74803 5.905512 +vt -17.71654 15.74803 +vt -17.71654 -20.19037 +vt -17.71654 -0.5053265 +vt -7.874016 -20.19037 +vt -13.77953 -4.442334 +vt -7.874016 -4.442334 +vt -7.874016 -12.31635 +vt -13.77953 -0.5053265 +vt 7.874016 3.937008 +vt 15.74803 5.905512 +vt 17.71654 3.937008 +vt 17.71654 15.74803 +vt 15.74803 13.77953 +vt 9.84252 13.77953 +vt 9.84252 5.905512 +vt 7.874016 15.74803 +vt 13.77953 15.74803 +vt 13.77953 19.68504 +vt 7.874016 19.68504 +vt 20.19037 19.68504 +vt 20.19037 15.74803 +vt 0.5053265 19.68504 +vt -3.431681 15.74803 +vt -3.431681 17.71654 +vt 17.71654 9.255395 +vt 17.71654 4.853686 +vt 13.77953 9.255395 +vt 13.77953 4.853686 +vt -4.442334 15.74803 +vt -4.442334 19.68504 +vt 3.431681 15.74803 +vt -0.5053265 19.68504 +vt 3.431681 17.71654 +vt 17.71654 17.71654 +vt 13.77953 17.71654 +vt -20.19037 15.74803 +vt -20.19037 19.68504 +vt -13.77953 15.74803 +vt -7.874016 19.68504 +vt -13.77953 19.68504 +vt 4.442334 19.68504 +vt 4.442334 15.74803 +vt -13.77953 17.71654 +vt -17.71654 17.71654 +vt 7.874016 -20.19037 +vt 7.874016 -12.31635 +vt 17.71654 -20.19037 +vt 7.874016 -4.442334 +vt 13.77953 -4.442334 +vt 13.77953 -0.5053265 +vt 17.71654 -0.5053265 +vt -13.77953 9.255395 +vt -13.77953 4.853686 +vt -17.71654 9.255395 +vt -17.71654 4.853686 +vt -17.71654 -25.37922 +vt -17.71654 -12.92931 +vt -7.874016 -25.37922 +vt -7.874016 -12.92931 +vt 17.71654 -35.9384 +vt 7.874016 -35.9384 +vt 35.9384 15.74803 +vt 35.9384 3.937008 +vt 32.00139 15.74803 +vt 20.19037 3.937008 +vt 20.19037 7.874016 +vt -17.71654 -35.9384 +vt -17.71654 -32.00139 +vt -7.874016 -35.9384 +vt -7.874016 -32.00139 +vt 20.19037 0 +vt 8.379342 7.874016 +vt 8.379342 0 +vt -7.874016 0 +vt -23.62205 0 +vt -23.62205 7.874016 +vt -17.71654 7.874016 +vt -7.874016 3.431681 +vt 7.874016 3.431681 +vt 12.31635 27.55906 +vt 12.31635 19.68504 +vt -3.431681 27.55906 +vt -3.431681 18.83752 +vt -8.360205 15.74803 +vt 7.874016 -3.556525E-13 +vt -7.874016 -3.556525E-13 +vt 7.874016 -32.00139 +vt 17.71654 -32.00139 +vt 7.874016 -25.37922 +vt 7.874016 -12.92931 +vt 17.71654 -25.37922 +vt 17.71654 -12.92931 +vt -35.9384 3.937008 +vt -35.9384 15.74803 +vt -20.19037 3.937008 +vt -32.00139 15.74803 +vt -20.19037 7.874016 +vt -20.19037 0 +vt -8.379342 0 +vt -8.379342 7.874016 +vt 7.874016 0 +vt 23.62205 0 +vt 23.62205 7.874016 +vt 17.71654 7.874016 +vt 1.968504 23.11672 +vt 1.968504 19.17971 +vt -1.968504 23.11672 +vt -1.968504 19.17971 +vt 7.874016 19.17971 +vt -7.874016 19.17971 +vt 19.17971 3.937008 +vt 19.17971 8.965723 +vt 25.58078 4.953154 +vt -19.17971 8.965723 +vt -19.17971 3.937008 +vt -25.58078 4.953154 +vt 8.360205 15.74803 +vt 3.431681 18.83752 +vt 3.431681 27.55906 +vt -12.31635 27.55906 +vt -12.31635 19.68504 +vt -1.968504 0 +vt -1.968504 3.937008 +vt 1.968504 0 +vt 1.968504 3.937008 +vt -19.17971 0 +vt -27.05373 5.186978 +vt -23.11672 0 +vt -27.05373 1.968504 +vt 19.17971 0 +vt 23.11672 0 +vt 27.05373 5.186978 +vt 27.05373 1.968504 +vt 1.968504 25.07793 +vt 1.968504 20.67622 +vt -1.968504 25.07793 +vt -1.968504 20.67622 +vt 1.968504 1.968504 +vt -1.968504 1.968504 +vt 1.968504 5.186978 +vt -1.968504 5.186978 +vt 15.74803 -32.00139 +vt 15.74803 -35.9384 +vt 9.84252 -32.00139 +vt 9.84252 -35.9384 +vt -15.74803 -35.9384 +vt -15.74803 -32.00139 +vt -9.84252 -35.9384 +vt -9.84252 -32.00139 +vt -35.9384 5.905512 +vt -35.9384 13.77953 +vt -32.00139 5.905512 +vt -32.00139 13.77953 +vt 35.9384 13.77953 +vt 35.9384 5.905512 +vt 32.00139 13.77953 +vt 32.00139 5.905512 +vt -10.6579 6.215566 +vt -20.83816 6.215566 +vt -10.6579 16.39583 +vt 10.6579 16.39583 +vt 20.83816 6.215566 +vt 10.6579 6.215566 +vt 20.83816 3.937008 +vt 10.6579 3.937008 +vt 20.83816 11.81102 +vt 10.6579 11.81102 +vt -6.215566 11.81102 +vt -6.215566 3.937008 +vt -16.39583 11.81102 +vt -16.39583 3.937008 +vt 6.215566 3.937008 +vt 6.215566 11.81102 +vt 16.39583 3.937008 +vt 16.39583 11.81102 +vt -10.6579 3.937008 +vt -20.83816 3.937008 +vt -10.6579 11.81102 +vt -20.83816 11.81102 +vt 7.874016 -5.518738 +vt -7.874016 -5.518738 +vt 3.937008 2.281402 +vt -3.937008 2.281402 +vt -32.50535 4.869821 +vt -7.364489 16.21137 +vt -33.7308 -3.781218 +vt 7.874016 -1.182768 +vt 3.937008 -28.48101 +vt -7.874016 -1.182768 +vt -3.937008 -28.48101 +vt 7.364489 16.21137 +vt 32.50535 4.869821 +vt 33.7308 -3.781218 +vt -7.874016 -39.8754 +vt 7.874016 -39.8754 +vt 3.937008 19.68504 +vt -3.937008 19.68504 +vt 3.937008 29.52756 +vt -3.937008 29.52756 +vt -23.62205 15.74803 +vt 23.62205 3.431681 +vt 23.62205 -8.379342 +vt 23.62205 -20.19037 +vt 14.27674 -8.073708E-15 +vt 10.33974 11.81102 +vt 14.27674 15.74803 +vt -7.994335 15.74803 +vt -4.057327 11.81102 +vt -4.057327 3.937008 +vt 10.33974 3.937008 +vt -7.994335 -8.073708E-15 +vt -7.994335 8.965723 +vt -7.994335 3.937008 +vt -3.431681 0 +vt 39.8754 3.937008 +vt 39.8754 19.68504 +vt 3.937008 -32.93326 +vt 3.937008 -16.55506 +vt 7.874016 -32.93326 +vt 7.874016 -4.271411 +vt -3.937008 -16.55506 +vt -7.874016 -4.271411 +vt -3.937008 -32.93326 +vt -7.874016 -32.93326 +vt 39.8754 29.52756 +vt 35.9384 29.52756 +vt 24.12737 24.18448 +vt -39.8754 29.52756 +vt -35.9384 29.52756 +vt -39.8754 19.68504 +vt -24.12737 24.18448 +vt 3.937008 44.91407 +vt 3.937008 31.95071 +vt -3.937008 44.91407 +vt -3.937008 31.95071 +vt -3.937008 -39.8754 +vt -3.937008 -35.9384 +vt 3.937008 -39.8754 +vt 3.937008 -35.9384 +vt -20.19037 1.136868E-13 +vt -12.31635 1.136868E-13 +vt -12.31635 3.937008 +vt 20.19037 -1.641642E-27 +vt 12.31635 3.937008 +vt 12.31635 -1.641642E-27 +vt 23.62205 15.74803 +vt -23.62205 3.431681 +vt -23.62205 -20.19037 +vt -23.62205 -8.379342 +vt -14.27674 15.74803 +vt -10.33974 3.937008 +vt -14.27674 1.005672E-14 +vt 7.994335 1.005672E-14 +vt 4.057327 3.937008 +vt 4.057327 11.81102 +vt -10.33974 11.81102 +vt 7.994335 15.74803 +vt 7.994335 3.937008 +vt 7.994335 8.965723 +vt 3.431681 0 +vt -13.77953 3.431681 +vt -7.874016 8.360205 +vt -17.71654 3.431681 +vt -39.8754 3.937008 +vt 7.874016 -2.054602 +vt 7.874016 -9.918843 +vt -7.874016 -2.054602 +vt -7.874016 -9.918843 +vt 7.874016 7.222372 +vt -7.874016 7.222372 +vt 7.874016 10.50774 +vt -7.874016 10.50774 +vt 11.28616 27.16731 +vt 19.17971 15.74803 +vt 39.8754 10.50774 +vt 39.8754 7.222372 +vt 7.874016 40.51461 +vt 7.874016 26.041 +vt -7.874016 40.51461 +vt 1.968504 27.53239 +vt 7.874016 19.55977 +vt -1.968504 27.53239 +vt -1.968504 19.55977 +vt -7.874016 19.55977 +vt -7.874016 26.041 +vt 1.968504 19.55977 +vt -11.28616 27.16731 +vt -19.17971 15.74803 +vt -39.8754 10.50774 +vt -39.8754 7.222372 +vt 17.71654 3.431681 +vt 13.77953 3.431681 +vt 7.874016 8.360205 + +usemtl metal + +f 3/3/1 2/2/1 1/1/1 +f 2/2/1 3/3/1 4/4/1 +f 2/2/1 4/4/1 5/5/1 +f 5/5/1 4/4/1 6/6/1 +f 7/7/1 1/1/1 2/2/1 +f 1/1/1 7/7/1 8/8/1 +f 8/8/1 7/7/1 6/6/1 +f 8/8/1 6/6/1 4/4/1 +f 11/11/2 10/10/2 9/9/2 +f 10/10/2 11/11/2 12/12/2 +f 12/12/2 11/11/2 13/13/2 +f 13/13/2 11/11/2 14/14/2 +f 15/15/2 10/10/2 12/12/2 +f 18/18/1 17/17/1 16/16/1 +f 17/17/1 18/18/1 19/19/1 +f 17/17/1 19/19/1 20/20/1 +f 20/20/1 19/19/1 21/21/1 +f 22/22/1 16/16/1 17/17/1 +f 16/16/1 22/22/1 23/23/1 +f 23/23/1 22/22/1 21/21/1 +f 23/23/1 21/21/1 19/19/1 +f 12/25/3 25/23/3 24/24/3 +f 25/23/3 12/25/3 13/26/3 +f 10/29/4 26/28/4 9/27/4 +f 26/28/4 10/29/4 27/30/4 +f 27/30/4 10/29/4 28/31/4 +f 15/34/5 28/33/5 10/32/5 +f 28/33/5 15/34/5 29/35/5 +f 30/38/6 12/37/6 24/36/6 +f 12/37/6 30/38/6 15/39/6 +f 15/39/6 30/38/6 29/40/6 +f 28/41/3 30/24/3 27/19/3 +f 30/24/3 28/41/3 29/42/3 +f 33/38/6 32/44/6 31/43/6 +f 32/44/6 33/38/6 34/39/6 +f 34/39/6 33/38/6 35/40/6 +f 38/46/3 37/45/3 36/4/3 +f 37/45/3 38/46/3 39/47/3 +f 40/29/4 37/49/4 39/48/4 +f 37/49/4 40/29/4 41/30/4 +f 41/30/4 40/29/4 42/31/4 +f 42/50/3 33/8/3 41/45/3 +f 33/8/3 42/50/3 35/51/3 +f 32/54/2 44/53/2 43/52/2 +f 44/53/2 32/54/2 38/55/2 +f 38/55/2 32/54/2 39/56/2 +f 39/56/2 32/54/2 40/57/2 +f 40/57/2 32/54/2 34/58/2 +f 34/61/5 42/60/5 40/59/5 +f 42/60/5 34/61/5 35/62/5 + +usemtl metalDark + +f 46/65/7 9/64/7 45/63/7 +f 9/64/7 46/65/7 11/66/7 +f 48/52/8 1/67/8 47/54/8 +f 1/67/8 48/52/8 3/68/8 +f 45/71/4 1/70/4 8/69/4 +f 1/70/4 45/71/4 47/72/4 +f 47/72/4 45/71/4 9/27/4 +f 47/72/4 9/27/4 26/28/4 +f 47/72/4 26/28/4 49/73/4 +f 4/76/2 45/75/2 8/74/2 +f 45/75/2 4/76/2 46/77/2 +f 52/79/4 51/78/4 50/73/4 +f 51/78/4 52/79/4 53/80/4 +f 48/3/1 51/82/1 54/81/1 +f 51/82/1 48/3/1 47/1/1 +f 51/82/1 47/1/1 50/83/1 +f 50/83/1 47/1/1 49/84/1 +f 57/53/2 56/85/2 55/14/2 +f 56/85/2 57/53/2 58/86/2 +f 56/89/4 14/88/4 55/87/4 +f 14/88/4 56/89/4 13/48/4 +f 13/48/4 56/89/4 25/49/4 +f 25/49/4 56/89/4 59/90/4 +f 25/49/4 59/90/4 60/91/4 +f 63/16/1 62/93/1 61/92/1 +f 62/93/1 63/16/1 64/3/1 +f 19/67/2 65/94/2 23/68/2 +f 65/94/2 19/67/2 66/95/2 +f 68/9/8 16/76/8 67/11/8 +f 16/76/8 68/9/8 18/74/8 +f 66/98/7 43/97/7 65/96/7 +f 43/97/7 66/98/7 32/99/7 +f 68/102/6 19/101/6 18/100/6 +f 19/101/6 68/102/6 66/103/6 +f 66/103/6 68/102/6 32/44/6 +f 32/44/6 68/102/6 69/104/6 +f 32/44/6 69/104/6 31/43/6 +f 72/106/6 71/104/6 70/105/6 +f 71/104/6 72/106/6 73/107/6 +f 70/109/1 68/18/1 74/108/1 +f 68/18/1 70/109/1 71/110/1 +f 68/18/1 71/110/1 69/111/1 +f 67/16/1 74/108/1 68/18/1 +f 77/114/8 76/113/8 75/112/8 +f 78/115/8 76/113/8 77/114/8 +f 61/14/8 76/113/8 78/115/8 +f 62/53/8 76/113/8 61/14/8 +f 76/113/8 62/53/8 79/116/8 +f 61/14/8 78/115/8 80/117/8 +f 83/120/6 82/119/6 81/118/6 +f 86/123/4 85/122/4 84/121/4 +f 87/124/6 38/37/6 36/36/6 +f 38/37/6 87/124/6 88/125/6 +f 58/126/6 38/37/6 88/125/6 +f 57/127/6 38/37/6 58/126/6 +f 38/37/6 57/127/6 44/128/6 +f 89/130/3 80/81/3 78/129/3 +f 80/81/3 89/130/3 81/3/3 +f 85/16/3 76/131/3 79/108/3 +f 76/131/3 85/16/3 90/132/3 +f 91/134/4 76/133/4 90/122/4 +f 76/133/4 91/134/4 75/135/4 +f 75/135/4 91/134/4 92/136/4 +f 77/138/6 89/118/6 78/137/6 +f 89/118/6 77/138/6 93/139/6 +f 93/139/6 77/138/6 94/140/6 +f 94/143/9 75/142/9 92/141/9 +f 75/142/9 94/143/9 77/144/9 +f 91/147/3 94/146/3 92/145/3 +f 94/146/3 91/147/3 93/148/3 + +usemtl dark + +f 96/151/8 6/150/8 95/149/8 +f 6/150/8 96/151/8 5/152/8 +f 2/155/2 97/154/2 7/153/2 +f 97/154/2 2/155/2 98/156/2 +f 96/5/1 97/7/1 98/2/1 +f 97/7/1 96/5/1 95/6/1 +f 97/159/6 6/158/6 7/157/6 +f 6/158/6 97/159/6 95/160/6 +f 96/163/4 2/162/4 5/161/4 +f 2/162/4 96/163/4 98/164/4 +f 101/167/2 100/166/2 99/165/2 +f 104/170/8 103/169/8 102/168/8 +f 103/173/3 99/172/3 100/171/3 +f 99/172/3 103/173/3 104/174/3 +f 102/177/4 99/176/4 104/175/4 +f 99/176/4 102/177/4 101/178/4 +f 17/150/2 105/151/2 22/152/2 +f 105/151/2 17/150/2 106/149/2 +f 107/163/4 17/162/4 20/161/4 +f 17/162/4 107/163/4 106/164/4 +f 107/20/1 105/22/1 106/17/1 +f 105/22/1 107/20/1 108/21/1 +f 107/154/8 21/155/8 108/156/8 +f 21/155/8 107/154/8 20/153/8 +f 105/159/6 21/158/6 22/157/6 +f 21/158/6 105/159/6 108/160/6 +f 111/181/6 110/180/6 109/179/6 +f 110/180/6 111/181/6 112/182/6 +f 113/169/2 111/168/2 109/170/2 +f 114/166/8 110/165/8 112/167/8 +f 110/185/3 113/184/3 109/183/3 +f 113/184/3 110/185/3 114/186/3 +f 117/189/10 116/188/10 115/187/10 +f 116/188/10 117/189/10 118/190/10 +f 115/193/11 119/192/11 117/191/11 +f 120/196/12 117/195/12 119/194/12 +f 117/195/12 120/196/12 118/197/12 +f 116/200/13 118/199/13 120/198/13 + +usemtl metalRed + +f 63/14/8 48/52/8 64/53/8 +f 48/52/8 63/14/8 3/68/8 +f 3/68/8 63/14/8 67/11/8 +f 16/76/8 3/68/8 67/11/8 +f 121/201/8 3/68/8 16/76/8 +f 3/68/8 121/201/8 122/202/8 +f 123/26/1 122/3/1 121/16/1 +f 122/3/1 123/26/1 124/203/1 +f 125/204/1 122/3/1 124/203/1 +f 126/205/1 125/204/1 124/203/1 +f 125/204/1 126/205/1 127/206/1 +f 122/3/1 125/204/1 128/46/1 +f 26/8/1 50/83/1 49/84/1 +f 50/83/1 26/8/1 129/207/1 +f 79/116/8 53/209/8 130/208/8 +f 53/209/8 79/116/8 51/210/8 +f 51/210/8 79/116/8 54/52/8 +f 54/52/8 79/116/8 62/53/8 +f 131/213/14 103/212/14 130/211/14 +f 103/212/14 131/213/14 132/214/14 +f 103/212/14 132/214/14 102/215/14 +f 102/215/14 132/214/14 101/216/14 +f 100/217/14 130/211/14 103/212/14 +f 130/211/14 100/217/14 79/218/14 +f 79/218/14 100/217/14 101/216/14 +f 79/218/14 101/216/14 132/214/14 +f 79/218/14 132/214/14 84/219/14 +f 79/218/14 84/219/14 85/220/14 +f 131/30/4 50/73/4 129/28/4 +f 50/73/4 131/30/4 52/79/4 +f 52/79/4 131/30/4 53/80/4 +f 53/80/4 131/30/4 130/221/4 +f 128/223/4 4/69/4 122/222/4 +f 4/69/4 128/223/4 55/87/4 +f 4/69/4 55/87/4 46/71/4 +f 46/71/4 55/87/4 11/27/4 +f 11/27/4 55/87/4 14/88/4 +f 3/70/4 122/222/4 4/69/4 +f 123/226/15 133/225/15 124/224/15 +f 57/227/15 133/225/15 123/226/15 +f 57/227/15 134/228/15 133/225/15 +f 55/229/15 134/228/15 57/227/15 +f 55/229/15 125/230/15 134/228/15 +f 125/230/15 55/229/15 128/231/15 +f 135/233/4 125/223/4 127/232/4 +f 125/223/4 135/233/4 134/234/4 +f 124/237/6 136/236/6 126/235/6 +f 136/236/6 124/237/6 133/238/6 +f 136/241/16 134/240/16 135/239/16 +f 134/240/16 136/241/16 133/242/16 +f 126/245/2 135/244/2 127/243/2 +f 135/244/2 126/245/2 136/246/2 +f 62/248/6 48/102/6 54/247/6 +f 48/102/6 62/248/6 64/249/6 +f 63/251/4 74/250/4 67/72/4 +f 74/250/4 63/251/4 61/252/4 +f 137/253/1 69/111/1 71/110/1 +f 69/111/1 137/253/1 31/19/1 +f 138/254/8 61/14/8 80/117/8 +f 61/14/8 138/254/8 74/11/8 +f 74/11/8 138/254/8 70/255/8 +f 70/255/8 138/254/8 72/256/8 +f 138/259/17 113/258/17 139/257/17 +f 113/258/17 138/259/17 80/260/17 +f 113/258/17 80/260/17 111/261/17 +f 111/261/17 80/260/17 112/262/17 +f 114/263/17 139/257/17 113/258/17 +f 139/257/17 114/263/17 140/264/17 +f 140/264/17 114/263/17 112/262/17 +f 140/264/17 112/262/17 80/260/17 +f 140/264/17 80/260/17 81/265/17 +f 140/264/17 81/265/17 82/266/17 +f 138/267/6 73/107/6 72/106/6 +f 139/38/6 73/107/6 138/267/6 +f 137/43/6 73/107/6 139/38/6 +f 73/107/6 137/43/6 71/104/6 +f 25/13/2 30/268/2 24/12/2 +f 30/268/2 25/13/2 60/269/2 +f 132/117/2 30/268/2 60/269/2 +f 132/117/2 27/270/2 30/268/2 +f 131/254/2 27/270/2 132/117/2 +f 131/254/2 26/9/2 27/270/2 +f 26/9/2 131/254/2 129/255/2 +f 16/100/6 123/237/6 121/271/6 +f 123/237/6 16/100/6 23/101/6 +f 123/237/6 23/101/6 57/127/6 +f 57/127/6 23/101/6 65/103/6 +f 57/127/6 65/103/6 43/44/6 +f 57/127/6 43/44/6 44/128/6 +f 58/274/18 119/273/18 56/272/18 +f 119/273/18 58/274/18 120/275/18 +f 115/278/3 142/277/3 141/276/3 +f 142/277/3 115/278/3 116/279/3 +f 87/124/6 58/126/6 88/125/6 +f 58/126/6 87/124/6 120/280/6 +f 120/280/6 87/124/6 140/281/6 +f 120/280/6 140/281/6 116/282/6 +f 116/282/6 140/281/6 82/119/6 +f 116/282/6 82/119/6 83/120/6 +f 116/282/6 83/120/6 142/283/6 +f 142/286/19 86/285/19 141/284/19 +f 86/285/19 142/286/19 91/287/19 +f 86/285/19 91/287/19 85/288/19 +f 91/287/19 142/286/19 93/289/19 +f 93/289/19 142/286/19 89/290/19 +f 89/290/19 142/286/19 81/291/19 +f 81/291/19 142/286/19 83/292/19 +f 90/293/19 85/288/19 91/287/19 +f 119/294/4 59/90/4 56/89/4 +f 59/90/4 119/294/4 60/91/4 +f 60/91/4 119/294/4 132/295/4 +f 132/295/4 119/294/4 115/296/4 +f 132/295/4 115/296/4 84/121/4 +f 84/121/4 115/296/4 86/123/4 +f 86/123/4 115/296/4 141/297/4 +f 137/210/2 33/298/2 31/54/2 +f 139/208/2 33/298/2 137/210/2 +f 139/208/2 41/299/2 33/298/2 +f 140/116/2 41/299/2 139/208/2 +f 140/116/2 36/55/2 41/299/2 +f 36/55/2 140/116/2 87/300/2 +f 41/299/2 36/55/2 37/56/2 + diff --git a/packages/examples/public/assets/multiMaterialMesh/craft_speederA.mtl b/packages/examples/public/assets/multiMaterialMesh/craft_speederA.mtl new file mode 100644 index 000000000..555969a1f --- /dev/null +++ b/packages/examples/public/assets/multiMaterialMesh/craft_speederA.mtl @@ -0,0 +1,14 @@ +# Created by Kenney (www.kenney.nl) + +newmtl metal +Kd 0.8431373 0.8705882 0.9098039 + +newmtl metalRed +Kd 1 0.6285242 0.2028302 + +newmtl dark +Kd 0.2745098 0.2980392 0.3411765 + +newmtl metalDark +Kd 0.6750623 0.7100219 0.7735849 + diff --git a/packages/examples/public/assets/multiMaterialMesh/craft_speederA.obj b/packages/examples/public/assets/multiMaterialMesh/craft_speederA.obj new file mode 100644 index 000000000..a4dfe00a5 --- /dev/null +++ b/packages/examples/public/assets/multiMaterialMesh/craft_speederA.obj @@ -0,0 +1,828 @@ +# Created by Kenney (www.kenney.nl) + +mtllib craft_speederA.mtl + +g craft_speederA + +v 1 0.2 -0.6500001 +v 1 0.2 -0.35 +v 0.5 0.2833333 -0.6500001 +v 0.4 0.3 0.04999995 +v 0.4 0.3 -0.6500001 +v 0.4 0.3 -0.15 +v 0.5 8.145571E-10 -1.05 +v 0.3 0.1 -1.05 +v 0.2 8.145571E-10 -1.05 +v 0.2 0.5 -1.05 +v 0.3 0.4 -1.05 +v 0.4 0.4 -1.05 +v 0.4 0.1 -1.05 +v 0.5 0.5 -1.05 +v 0.3 0.3 0.04999995 +v 0.2 0.3 -0.04999995 +v 0.25 0.3 0.25 +v 0.2 0.3 0.25 +v 0.3 0.3 0.15 +v -0.2 8.146143E-10 -1.05 +v -0.4000001 0.1 -1.05 +v -0.5 8.146143E-10 -1.05 +v -0.5 0.5 -1.05 +v -0.4000001 0.4 -1.05 +v -0.3 0.4 -1.05 +v -0.3 0.1 -1.05 +v -0.2 0.5 -1.05 +v -0.2 0.3 0.25 +v -0.3 0.3 0.04999995 +v -0.2 0.3 -0.04999995 +v -0.4000001 0.3 -0.15 +v -0.25 0.3 0.25 +v -0.4000001 0.3 0.04999995 +v -0.3 0.3 0.15 +v -0.4000001 0.3 -0.6500001 +v -0.5 0.2833333 -0.6500001 +v -1 0.2 -0.35 +v -1 0.2 -0.6500001 +v 0.3 0.1 0.2495037 +v 0.3 0.1 0.04999995 +v -0.3 0.1 0.2495037 +v 0.2 0.1 0.04999995 +v -0.2 0.1 0.04999995 +v -0.2 0.1 -0.35 +v -0.3 0.1 0.04999995 +v -0.5 0.1 -0.6500001 +v -0.2 0.1 -0.6500001 +v -0.4000001 0.1 0.04999995 +v -1 0.1 -0.35 +v -1 0.1 -0.6500001 +v 0.2 0.1 -0.6500001 +v 0.4 0.1 0.04999995 +v 0.5 0.1 -0.6500001 +v 1 0.1 -0.6500001 +v 1 0.1 -0.35 +v 0.2 0.1 -0.35 +v -0.2 0.3049752 1.05 +v -0.3 0.2 0.25 +v -0.2 0.2 0.25 +v -0.25 0.2 0.25 +v 0.3 0.2 0.25 +v 0.2 0.3049752 1.05 +v 0.25 0.2 0.25 +v 0.2 0.2 0.25 +v -0.3 0.2 0.15 +v -0.2 0.2099504 1.05 +v 0.2 0.2099504 1.05 +v 0.3 0.2 0.15 +v 0.255 0.4 -0.6500001 +v 0.255 0.4 -0.35 +v 0.2 0.4 -0.6500001 +v 0.2 0.4 -0.04999995 +v 0.2 0.4 -0.35 +v 0.4 0.4 -0.15 +v 0.4 0.4 -0.35 +v 0.36375 0.65 -0.75 +v 0.4 0.4 -0.6500001 +v 0.36375 0.65 -0.5999999 +v 0.29125 0.65 -0.75 +v 0.29125 0.65 -0.5999999 +v -0.04999995 0.7 0.04999995 +v -0.2 0.7 0.04999995 +v -0.04999995 0.7 -0.35 +v -0.2 0.7 -0.35 +v 0.2 0.7 -0.35 +v 0.2 0.7 0.04999995 +v 0.04999995 0.7 -0.35 +v 0.04999995 0.7 0.04999995 +v -0.2 0.4 -0.6500001 +v -0.2 0.4 -0.35 +v -0.2550001 0.4 -0.6500001 +v -0.2 0.4 -0.04999995 +v -0.2550001 0.4 -0.35 +v -0.4000001 0.4 -0.15 +v -0.4000001 0.4 -0.35 +v -0.29125 0.65 -0.5999999 +v -0.36375 0.65 -0.5999999 +v -0.4000001 0.4 -0.6500001 +v -0.36375 0.65 -0.75 +v -0.29125 0.65 -0.75 +v 0.2 0.4 1.05 +v 0.15 0.6856326 0.09789133 +v -0.1500001 0.6856326 0.09789133 +v -0.1500001 0.4143674 1.002109 +v 0.15 0.4143674 1.002109 +v -0.2 0.4 1.05 +v -0.2 0 -0.35 +v 0.2 0 -0.35 +v 4.887581E-06 0 -0.35 +v 0.3 0.4 -0.8499999 +v 0.3 0.1 -0.8499999 +v 0.4 0.1 -0.8499999 +v 0.4 0.4 -0.8499999 +v -0.4000001 0.1 -0.8499999 +v -0.3 0.1 -0.8499999 +v -0.4000001 0.4 -0.8499999 +v -0.3 0.4 -0.8499999 +v 0.04999995 0.7 0.35 +v -0.04999995 0.7 0.35 +v 0.5 0.5 -0.9499998 +v 0.2 0.5 -0.9499998 +v 0.5 0.4 -0.6500001 +v 0.2 0.4666667 -0.8499999 +v 0.5 8.145571E-10 -0.6500001 +v 0.2 8.145568E-10 -0.8499999 +v -0.2 8.145568E-10 -0.8499999 +v -0.2 0.4666667 -0.8499999 +v 0.2 0.6 -0.8499999 +v -0.2 0.6 -0.8499999 +v 1.001358E-05 0.7 -0.35 +v 0.04999995 0.8 -0.35 +v 0.04999995 0.8 -0.04999995 +v 0.04999995 0.75 0.04999995 +v -0.04999995 0.8 -0.35 +v -0.04999995 0.75 0.04999995 +v -0.04999995 0.8 -0.04999995 +v -0.2 0.5 -0.9499998 +v -0.5 8.146143E-10 -0.6500001 +v -0.5 0.5 -0.9499998 +v -0.5 0.4 -0.6500001 +v 0.2 8.145568E-10 -0.6500001 +v -0.2 8.145568E-10 -0.6500001 + +vn 0.164399 0.986394 0 +vn 0 0 -1 +vn 0 1 0 +vn -0.164399 0.986394 0 +vn 0 -1 0 +vn 0 0.9915004 -0.1301037 +vn -1 0 0 +vn 0 0 1 +vn 0 -0.9906985 0.1360752 +vn 1 0 0 +vn -0.9922773 -0.0003233984 0.1240396 +vn 0.8944272 0 0.4472135 +vn 0.9922773 -0.0003233984 0.1240396 +vn -0.8944272 0 0.4472136 +vn 0.5547002 0 0.8320503 +vn 0.9896504 0.1434993 0 +vn -0.9896504 0.1434993 0 +vn 0 -0.3713907 -0.9284768 +vn 0 0.7071068 0.7071068 +vn -0.5547002 0 0.8320503 +vn 0 0.9578263 0.2873479 +vn 0 -0.9701425 0.2425356 +vn 0 0.9159844 0.4012138 +vn 0.6529287 0.7254763 0.2176429 +vn -0.6529287 0.7254763 0.2176429 +vn 0 0.9983801 -0.05689657 +vn 0 0.9486833 0.3162278 +vn 0.4472136 0 0.8944272 +vn 0 0.9805807 -0.1961161 +vn 0 0.8944272 0.4472136 +vn -0.4472136 0 0.8944272 + +vt 25.59055 -37.53992 +vt 13.77953 -37.53992 +vt 25.59055 -17.58336 +vt -1.968504 -13.59204 +vt 25.59055 -13.59204 +vt 5.905512 -13.59204 +vt -19.68504 3.206918E-08 +vt -11.81102 3.937008 +vt -7.874016 3.206918E-08 +vt -7.874016 19.68504 +vt -11.81102 15.74803 +vt -15.74803 15.74803 +vt -15.74803 3.937008 +vt -19.68504 19.68504 +vt -15.74803 1.968504 +vt -11.81102 1.968504 +vt -15.74803 -5.905512 +vt -7.874016 -1.968504 +vt -9.84252 9.84252 +vt -7.874016 9.84252 +vt -11.81102 5.905512 +vt 7.874016 3.207143E-08 +vt 15.74803 3.937008 +vt 19.68504 3.207143E-08 +vt 19.68504 19.68504 +vt 15.74803 15.74803 +vt 11.81102 15.74803 +vt 11.81102 3.937008 +vt 7.874016 19.68504 +vt 7.874016 9.84252 +vt 11.81102 1.968504 +vt 7.874016 -1.968504 +vt 15.74803 -5.905512 +vt 9.84252 9.84252 +vt 15.74803 1.968504 +vt 11.81102 5.905512 +vt -25.59055 -13.59204 +vt -5.905512 -13.59204 +vt -25.59055 -17.58336 +vt 1.968504 -13.59204 +vt -13.77953 -37.53992 +vt -25.59055 -37.53992 +vt 11.81102 9.822981 +vt 11.81102 1.968504 +vt -11.81102 9.822981 +vt 7.874016 1.968504 +vt -7.874016 1.968504 +vt -7.874016 -13.77953 +vt -11.81102 1.968504 +vt -19.68504 -25.59055 +vt -7.874016 -25.59055 +vt -15.74803 1.968504 +vt -39.37008 -13.77953 +vt -39.37008 -25.59055 +vt 7.874016 -25.59055 +vt 15.74803 1.968504 +vt 19.68504 -25.59055 +vt 39.37008 -25.59055 +vt 39.37008 -13.77953 +vt 7.874016 -13.77953 +vt 7.874018 42.54936 +vt 11.81102 10.7833 +vt 7.874016 10.7833 +vt 9.842521 10.7833 +vt -11.81102 10.7833 +vt -7.874018 42.54936 +vt -9.842521 10.7833 +vt -7.874016 10.7833 +vt 1.968504 3.937008 +vt 5.905512 7.874016 +vt 9.822981 3.937008 +vt 9.84252 7.874016 +vt 5.905512 11.81102 +vt 1.968504 11.81102 +vt -7.874016 8.265762 +vt -7.874016 12.0069 +vt 7.874016 8.265762 +vt 7.874016 12.0069 +vt 11.81102 10.26734 +vt -11.81102 10.26734 +vt 7.874016 42.07884 +vt -7.874016 42.07884 +vt -1.968504 11.81102 +vt -5.905512 7.874016 +vt -5.905512 11.81102 +vt -9.822981 3.937008 +vt -1.968504 3.937008 +vt -9.84252 7.874016 +vt 8.282086 3.941192 +vt 8.301474 7.8782 +vt 40.04264 8.269947 +vt 40.04264 12.01108 +vt -11.81102 5.905511 +vt -11.81102 9.84252 +vt -1.668339E-07 7.874016 +vt -4.401709 7.874016 +vt -2.193064E-07 11.81102 +vt -4.401709 11.81102 +vt 7.874016 7.874016 +vt 9.84252 11.81102 +vt 7.874016 11.81102 +vt -8.282086 3.941192 +vt -40.04264 8.269947 +vt -8.301474 7.8782 +vt -40.04264 12.01108 +vt 11.81102 5.905511 +vt 11.81102 9.84252 +vt -9.84252 11.81102 +vt -7.874016 7.874016 +vt -7.874016 11.81102 +vt 6.350136E-08 7.874016 +vt 1.159738E-07 11.81102 +vt 4.401709 7.874016 +vt 4.401709 11.81102 +vt 25.59055 7.874016 +vt 25.59055 3.937008 +vt 13.77953 7.874016 +vt 13.77953 3.937008 +vt 40.40139 3.937008 +vt 12.01122 3.937008 +vt 40.40139 7.874016 +vt 12.01122 11.81102 +vt -39.37008 3.937008 +vt -39.37008 7.874016 +vt -19.68504 3.937008 +vt -19.68504 11.15486 +vt -10.03937 -25.59055 +vt -10.03937 -13.77953 +vt -7.874016 -1.968504 +vt -15.74803 -13.77953 +vt 29.52756 23.27067 +vt 25.59055 13.32521 +vt 23.62205 23.27067 +vt 13.77953 13.32521 +vt 25.59055 15.74803 +vt 25.59055 11.81102 +vt 13.77953 15.74803 +vt 5.905512 15.74803 +vt -14.32087 -29.52756 +vt -14.32087 -23.62205 +vt -11.46654 -29.52756 +vt -11.46654 -23.62205 +vt -29.52756 26.97114 +vt -23.62205 26.97114 +vt -25.59055 17.02569 +vt -13.77953 17.02569 +vt -10.03937 24.12577 +vt -15.74803 24.12577 +vt -11.46654 34.72649 +vt -14.32087 34.72649 +vt 15.74803 20.87914 +vt 10.03937 20.87914 +vt 14.32087 34.79856 +vt 11.46654 34.79856 +vt 1.968504 1.968504 +vt 1.968504 -13.77953 +vt -1.968504 -13.77953 +vt -1.968504 1.968504 +vt 10.03937 -25.59055 +vt 7.874016 -1.968504 +vt 10.03937 -13.77953 +vt 15.74803 -13.77953 +vt -10.03937 20.87914 +vt -15.74803 20.87914 +vt -11.46654 34.79856 +vt -14.32087 34.79856 +vt -25.59055 11.81102 +vt -25.59055 15.74803 +vt -13.77953 15.74803 +vt -5.905512 15.74803 +vt -29.52756 23.27067 +vt -23.62205 23.27067 +vt -25.59055 13.32521 +vt -13.77953 13.32521 +vt 29.52756 26.97114 +vt 25.59055 17.02569 +vt 23.62205 26.97114 +vt 13.77953 17.02569 +vt 11.46654 -29.52756 +vt 11.46654 -23.62205 +vt 14.32087 -29.52756 +vt 14.32087 -23.62205 +vt 15.74803 24.12577 +vt 10.03937 24.12577 +vt 14.32087 34.72649 +vt 11.46654 34.72649 +vt -25.59055 3.937008 +vt -25.59055 7.874016 +vt -13.77953 3.937008 +vt -13.77953 7.874016 +vt -40.40139 3.937008 +vt -40.40139 7.874016 +vt -12.01122 3.937008 +vt -12.01122 11.81102 +vt 39.37008 3.937008 +vt 19.68504 3.937008 +vt 39.37008 7.874016 +vt 19.68504 11.15486 +vt 13.77953 27.55906 +vt -1.968504 27.55906 +vt 1.968504 15.74803 +vt -41.33858 15.74803 +vt 7.874016 -35.07002 +vt 5.905512 4.065047 +vt 7.874016 6.033551 +vt 1.968504 6.033551 +vt -5.905512 4.065047 +vt -1.968504 6.033551 +vt -7.874016 6.033551 +vt -5.905512 -33.10151 +vt 5.905512 -33.10151 +vt -7.874016 -35.07002 +vt -13.77953 27.55906 +vt -1.968504 15.74803 +vt 1.968504 27.55906 +vt 41.33858 15.74803 +vt 15.74803 11.81102 +vt 11.81102 11.81102 +vt -11.81102 11.81102 +vt -15.74803 11.81102 +vt -13.77953 -2.273737E-13 +vt 1.968504 3.937008 +vt 13.77953 2.273737E-13 +vt -1.968504 3.937008 +vt 7.874016 2.864594 +vt 7.874016 -13.3681 +vt -7.874016 2.864594 +vt 0.0001911516 -13.3681 +vt -7.874016 -13.3681 +vt 41.33858 3.937008 +vt 33.46457 15.74803 +vt 33.46457 3.937008 +vt -15.74803 -41.33858 +vt -15.74803 -33.46457 +vt -11.81102 -41.33858 +vt -11.81102 -33.46457 +vt 15.74803 -33.46457 +vt 15.74803 -41.33858 +vt 11.81102 -33.46457 +vt 11.81102 -41.33858 +vt -41.33858 3.937008 +vt -33.46457 3.937008 +vt -33.46457 15.74803 +vt 5.905512 -29.59315 +vt -5.905512 -29.59315 +vt 1.968504 -1.564759 +vt -1.968504 -1.564759 +vt -1.788728 13.62952 +vt -35.56101 -1.887771 +vt -12.44991 14.45138 +vt 1.788728 13.62952 +vt 12.44991 14.45138 +vt 35.56101 -1.887771 +vt -5.905512 5.383578 +vt -1.968504 15.32522 +vt 5.905512 5.383578 +vt 1.968504 15.32522 +vt -19.68504 -41.33858 +vt -19.68504 -37.40157 +vt -7.874016 -41.33858 +vt -7.874016 -37.40157 +vt 19.68504 41.70721 +vt 19.68504 29.25729 +vt 7.874016 41.70721 +vt 15.74803 29.25729 +vt 10.03937 29.25729 +vt 7.874016 29.25729 +vt 7.874016 37.55724 +vt 41.33858 3.206918E-08 +vt 25.59055 3.206918E-08 +vt 41.33858 19.68504 +vt 37.40157 19.68504 +vt 25.59055 11.15486 +vt 19.68504 15.74803 +vt 16.72649 11.81102 +vt 7.923076 11.81102 +vt 16.72649 15.74803 +vt 7.923076 15.74803 +vt -41.33858 3.206917E-08 +vt -41.33858 19.68504 +vt -33.46457 3.206916E-08 +vt -37.40157 19.68504 +vt -33.46457 18.3727 +vt 7.874016 3.206916E-08 +vt -7.874016 3.206916E-08 +vt 7.874016 18.3727 +vt -7.874016 18.3727 +vt -7.874016 23.62205 +vt 7.874016 23.62205 +vt -7.874016 -28.18204 +vt -7.874016 -8.107163 +vt 7.874016 -28.18204 +vt -1.968504 -8.107163 +vt -0.0003949364 -8.107163 +vt 1.968504 -8.107163 +vt 7.874016 -8.107163 +vt 33.46457 23.62205 +vt 33.46457 18.3727 +vt 13.77953 31.49606 +vt 1.968504 31.49606 +vt -1.968504 29.52756 +vt -1.968504 31.49606 +vt -0.0003951417 27.55906 +vt 1.968504 31.49606 +vt 1.968504 11.44444 +vt -1.968504 11.44444 +vt 1.968504 15.84615 +vt -1.968504 15.84615 +vt 1.968504 29.52756 +vt -1.968504 -1.968504 +vt 1.968504 -1.968504 +vt -13.77953 31.49606 +vt -1.968504 31.49606 +vt 41.33858 3.207137E-08 +vt 33.46457 3.206911E-08 +vt -41.33858 3.207143E-08 +vt -25.59055 3.207143E-08 +vt -25.59055 11.15486 +vt -7.874016 41.70721 +vt -7.874016 37.55724 +vt -19.68504 41.70721 +vt -7.874016 29.25729 +vt -10.03937 29.25729 +vt -15.74803 29.25729 +vt -19.68504 29.25729 +vt 7.874016 -41.33858 +vt 7.874016 -37.40157 +vt 19.68504 -41.33858 +vt 19.68504 -37.40157 +vt -33.46457 23.62205 +vt -7.923076 11.81102 +vt -16.72649 11.81102 +vt -7.923076 15.74803 +vt -16.72649 15.74803 +vt -19.68504 15.74803 +vt 7.874016 15.74803 +vt -7.874016 15.74803 +vt 1.968504 11.81102 +vt -41.33858 12.0069 +vt -9.84252 7.874015 +vt 9.84252 7.874015 +vt 41.33858 12.0069 +vt -1.968504 11.81102 +vt 0.0001911516 -13.77953 +vt -7.874016 -33.46457 +vt 7.874016 -33.46457 +vt -25.59055 3.206894E-08 +vt -7.874016 3.206826E-08 +vt -19.68504 3.207053E-08 +vt -7.874016 3.937008 +vt 7.874016 3.206826E-08 +vt 7.874016 3.937008 +vt 19.68504 3.206828E-08 +vt 25.59055 3.206939E-08 + +usemtl metal + +f 3/3/1 2/2/1 1/1/1 +f 2/2/1 3/3/1 4/4/1 +f 4/4/1 3/3/1 5/5/1 +f 4/4/1 5/5/1 6/6/1 +f 9/9/2 8/8/2 7/7/2 +f 8/8/2 9/9/2 10/10/2 +f 8/8/2 10/10/2 11/11/2 +f 11/11/2 10/10/2 12/12/2 +f 13/13/2 7/7/2 8/8/2 +f 7/7/2 13/13/2 14/14/2 +f 14/14/2 13/13/2 12/12/2 +f 14/14/2 12/12/2 10/10/2 +f 6/17/3 15/16/3 4/15/3 +f 15/16/3 6/17/3 16/18/3 +f 15/16/3 16/18/3 17/19/3 +f 17/19/3 16/18/3 18/20/3 +f 17/19/3 19/21/3 15/16/3 +f 22/24/2 21/23/2 20/22/2 +f 21/23/2 22/24/2 23/25/2 +f 21/23/2 23/25/2 24/26/2 +f 24/26/2 23/25/2 25/27/2 +f 26/28/2 20/22/2 21/23/2 +f 20/22/2 26/28/2 27/29/2 +f 27/29/2 26/28/2 25/27/2 +f 27/29/2 25/27/2 23/25/2 +f 30/32/3 29/31/3 28/30/3 +f 29/31/3 30/32/3 31/33/3 +f 28/30/3 29/31/3 32/34/3 +f 29/31/3 31/33/3 33/35/3 +f 34/36/3 32/34/3 29/31/3 +f 36/39/4 31/38/4 35/37/4 +f 31/38/4 36/39/4 33/40/4 +f 33/40/4 36/39/4 37/41/4 +f 37/41/4 36/39/4 38/42/4 +f 41/45/5 40/44/5 39/43/5 +f 40/44/5 41/45/5 42/46/5 +f 42/46/5 41/45/5 43/47/5 +f 43/47/5 41/45/5 44/48/5 +f 44/48/5 41/45/5 45/49/5 +f 46/50/5 44/48/5 45/49/5 +f 44/48/5 46/50/5 47/51/5 +f 46/50/5 45/49/5 48/52/5 +f 46/50/5 48/52/5 49/53/5 +f 46/50/5 49/53/5 50/54/5 +f 51/55/5 40/44/5 42/46/5 +f 51/55/5 52/56/5 40/44/5 +f 53/57/5 52/56/5 51/55/5 +f 54/58/5 52/56/5 53/57/5 +f 52/56/5 54/58/5 55/59/5 +f 51/55/5 42/46/5 56/60/5 +f 59/63/6 58/62/6 57/61/6 +f 58/62/6 59/63/6 60/64/6 +f 63/67/6 62/66/6 61/65/6 +f 62/66/6 63/67/6 64/68/6 +f 41/71/7 65/70/7 45/69/7 +f 65/70/7 41/71/7 58/72/7 +f 34/73/7 45/69/7 65/70/7 +f 45/69/7 34/73/7 29/74/7 +f 67/77/8 57/76/8 66/75/8 +f 57/76/8 67/77/8 62/78/8 +f 67/81/9 41/80/9 39/79/9 +f 41/80/9 67/81/9 66/82/9 +f 19/85/10 68/84/10 15/83/10 +f 39/86/10 15/83/10 68/84/10 +f 15/83/10 39/86/10 40/87/10 +f 39/86/10 68/84/10 61/88/10 +f 66/91/11 58/90/11 41/89/11 +f 58/90/11 66/91/11 57/92/11 +f 63/19/3 61/94/3 68/93/3 +f 19/97/12 63/96/12 68/95/12 +f 63/96/12 19/97/12 17/98/12 +f 17/100/8 64/99/8 63/72/8 +f 64/99/8 17/100/8 18/101/8 +f 61/104/13 67/103/13 39/102/13 +f 67/103/13 61/104/13 62/105/13 +f 58/107/3 60/34/3 65/106/3 +f 59/109/8 32/108/8 60/88/8 +f 32/108/8 59/109/8 28/110/8 +f 60/113/14 34/112/14 65/111/14 +f 34/112/14 60/113/14 32/114/14 + +usemtl metalRed + +f 2/117/10 54/116/10 1/115/10 +f 54/116/10 2/117/10 55/118/10 +f 2/121/15 52/120/15 55/119/15 +f 52/120/15 2/121/15 4/122/15 +f 53/125/2 1/124/2 54/123/2 +f 1/124/2 53/125/2 3/126/2 +f 71/51/3 70/128/3 69/127/3 +f 70/128/3 71/51/3 72/129/3 +f 72/129/3 71/51/3 73/48/3 +f 74/17/3 70/128/3 72/129/3 +f 70/128/3 74/17/3 75/130/3 +f 78/133/16 77/132/16 76/131/16 +f 77/132/16 78/133/16 75/134/16 +f 75/137/10 5/136/10 77/135/10 +f 5/136/10 75/137/10 6/73/10 +f 6/73/10 75/137/10 74/138/10 +f 79/141/3 78/140/3 76/139/3 +f 78/140/3 79/141/3 80/142/3 +f 69/145/17 80/144/17 79/143/17 +f 80/144/17 69/145/17 70/146/17 +f 79/149/18 77/148/18 69/147/18 +f 77/148/18 79/149/18 76/150/18 +f 78/153/19 70/152/19 75/151/19 +f 70/152/19 78/153/19 80/154/19 +f 83/156/3 82/46/3 81/155/3 +f 82/46/3 83/156/3 84/60/3 +f 87/157/3 86/47/3 85/48/3 +f 86/47/3 87/157/3 88/158/3 +f 91/159/3 90/60/3 89/55/3 +f 90/60/3 91/159/3 92/160/3 +f 92/160/3 91/159/3 93/161/3 +f 92/160/3 93/161/3 94/33/3 +f 94/33/3 93/161/3 95/162/3 +f 96/165/19 95/164/19 93/163/19 +f 95/164/19 96/165/19 97/166/19 +f 31/85/7 98/168/7 35/167/7 +f 98/168/7 31/85/7 95/169/7 +f 95/169/7 31/85/7 94/170/7 +f 98/173/17 97/172/17 99/171/17 +f 97/172/17 98/173/17 95/174/17 +f 96/177/16 91/176/16 100/175/16 +f 91/176/16 96/177/16 93/178/16 +f 99/181/3 96/180/3 100/179/3 +f 96/180/3 99/181/3 97/182/3 +f 99/185/18 91/184/18 98/183/18 +f 91/184/18 99/185/18 100/186/18 +f 49/189/7 38/188/7 50/187/7 +f 38/188/7 49/189/7 37/190/7 +f 48/193/20 37/192/20 49/191/20 +f 37/192/20 48/193/20 33/194/20 +f 38/197/2 46/196/2 50/195/2 +f 46/196/2 38/197/2 36/198/2 +f 86/200/10 73/137/10 85/199/10 +f 73/137/10 86/200/10 72/201/10 +f 72/201/10 86/200/10 101/202/10 +f 86/205/21 102/204/21 101/203/21 +f 102/204/21 86/205/21 88/206/21 +f 102/204/21 88/206/21 103/207/21 +f 103/207/21 88/206/21 81/208/21 +f 103/207/21 81/208/21 82/209/21 +f 103/207/21 82/209/21 104/210/21 +f 105/211/21 101/203/21 102/204/21 +f 101/203/21 105/211/21 106/212/21 +f 106/212/21 105/211/21 104/210/21 +f 106/212/21 104/210/21 82/209/21 +f 92/214/7 84/213/7 90/169/7 +f 84/213/7 92/214/7 82/215/7 +f 82/215/7 92/214/7 106/216/7 +f 4/217/8 40/28/8 52/23/8 +f 40/28/8 4/217/8 15/218/8 +f 29/219/8 48/13/8 45/8/8 +f 48/13/8 29/219/8 33/220/8 +f 43/222/7 44/189/7 107/221/7 +f 42/224/10 108/223/10 56/118/10 +f 43/227/22 108/226/22 42/225/22 +f 108/226/22 43/227/22 109/228/22 +f 109/228/22 43/227/22 107/229/22 + +usemtl dark + +f 110/231/10 8/230/10 11/216/10 +f 8/230/10 110/231/10 111/232/10 +f 8/235/3 112/234/3 13/233/3 +f 112/234/3 8/235/3 111/236/3 +f 110/11/2 112/13/2 111/8/2 +f 112/13/2 110/11/2 113/12/2 +f 110/239/5 12/238/5 113/237/5 +f 12/238/5 110/239/5 11/240/5 +f 112/242/7 12/202/7 13/241/7 +f 12/202/7 112/242/7 113/243/7 +f 116/26/2 115/28/2 114/23/2 +f 115/28/2 116/26/2 117/27/2 +f 116/234/5 25/235/5 117/236/5 +f 25/235/5 116/234/5 24/233/5 +f 116/231/10 21/230/10 24/216/10 +f 21/230/10 116/231/10 114/232/10 +f 115/242/7 25/202/7 26/241/7 +f 25/202/7 115/242/7 117/243/7 +f 21/238/3 115/239/3 26/240/3 +f 115/239/3 21/238/3 114/237/3 +f 118/246/23 104/245/23 105/244/23 +f 104/245/23 118/246/23 119/247/23 +f 118/250/24 105/249/24 102/248/24 +f 104/253/25 119/252/25 103/251/25 +f 103/256/26 118/255/26 102/254/26 +f 118/255/26 103/256/26 119/257/26 + +usemtl metalDark + +f 10/260/3 120/259/3 14/258/3 +f 120/259/3 10/260/3 121/261/3 +f 121/264/27 122/263/27 120/262/27 +f 122/263/27 121/264/27 77/265/27 +f 77/265/27 121/264/27 69/266/27 +f 69/266/27 121/264/27 71/267/27 +f 71/267/27 121/264/27 123/268/27 +f 14/271/10 124/270/10 7/269/10 +f 124/270/10 14/271/10 120/272/10 +f 124/270/10 120/272/10 122/135/10 +f 124/270/10 122/135/10 53/116/10 +f 53/116/10 122/135/10 3/273/10 +f 122/274/8 5/217/8 3/198/8 +f 5/217/8 122/274/8 77/26/8 +f 74/277/28 16/276/28 6/275/28 +f 16/276/28 74/277/28 72/278/28 +f 125/281/7 10/280/7 9/279/7 +f 10/280/7 125/281/7 121/282/7 +f 121/282/7 125/281/7 123/283/7 +f 127/286/2 125/285/2 126/284/2 +f 125/285/2 127/286/2 123/287/2 +f 123/287/2 127/286/2 128/288/2 +f 128/288/2 127/286/2 129/289/2 +f 129/292/29 85/291/29 128/290/29 +f 85/291/29 129/292/29 87/293/29 +f 87/293/29 129/292/29 130/294/29 +f 130/294/29 129/292/29 83/295/29 +f 83/295/29 129/292/29 84/296/29 +f 85/199/10 123/298/10 128/297/10 +f 123/298/10 85/199/10 71/135/10 +f 71/135/10 85/199/10 73/137/10 +f 131/299/10 88/200/10 87/199/10 +f 88/200/10 131/299/10 132/300/10 +f 88/200/10 132/300/10 133/301/10 +f 130/303/2 131/302/2 87/200/2 +f 131/302/2 130/303/2 83/215/2 +f 131/302/2 83/215/2 134/304/2 +f 132/307/30 135/306/30 133/305/30 +f 135/306/30 132/307/30 136/308/30 +f 133/309/8 81/200/8 88/215/8 +f 81/200/8 133/309/8 135/301/8 +f 131/157/3 136/311/3 132/310/3 +f 136/311/3 131/157/3 134/156/3 +f 81/215/7 134/312/7 83/213/7 +f 134/312/7 81/215/7 136/313/7 +f 136/313/7 81/215/7 135/309/7 +f 27/271/10 126/315/10 20/314/10 +f 126/315/10 27/271/10 137/272/10 +f 126/315/10 137/272/10 127/298/10 +f 138/317/7 23/280/7 22/316/7 +f 23/280/7 138/317/7 139/282/7 +f 139/282/7 138/317/7 140/168/7 +f 140/168/7 138/317/7 46/187/7 +f 140/168/7 46/187/7 36/318/7 +f 139/321/27 127/320/27 137/319/27 +f 127/320/27 139/321/27 89/322/27 +f 89/322/27 139/321/27 91/323/27 +f 91/323/27 139/321/27 98/324/27 +f 98/324/27 139/321/27 140/325/27 +f 23/328/3 137/327/3 27/326/3 +f 137/327/3 23/328/3 139/329/3 +f 89/168/7 129/330/7 127/283/7 +f 129/330/7 89/168/7 84/213/7 +f 84/213/7 89/168/7 90/169/7 +f 92/333/31 31/332/31 30/331/31 +f 31/332/31 92/333/31 94/334/31 +f 35/220/8 140/335/8 36/126/8 +f 140/335/8 35/220/8 98/12/8 +f 101/336/8 57/76/8 62/78/8 +f 57/76/8 101/336/8 106/337/8 +f 101/202/10 16/338/10 72/201/10 +f 16/338/10 101/202/10 18/108/10 +f 62/339/10 18/108/10 101/202/10 +f 18/108/10 62/339/10 64/340/10 +f 57/342/7 28/100/7 59/341/7 +f 106/216/7 28/100/7 57/342/7 +f 106/216/7 30/343/7 28/100/7 +f 30/343/7 106/216/7 92/214/7 +f 109/344/5 141/55/5 108/60/5 +f 126/345/5 141/55/5 109/344/5 +f 125/346/5 141/55/5 126/345/5 +f 9/326/5 141/55/5 125/346/5 +f 7/328/5 141/55/5 9/326/5 +f 141/55/5 7/328/5 124/57/5 +f 126/345/5 109/344/5 142/51/5 +f 142/51/5 109/344/5 107/48/5 +f 126/345/5 142/51/5 138/50/5 +f 22/258/5 126/345/5 138/50/5 +f 126/345/5 22/258/5 20/260/5 +f 107/221/7 47/187/7 142/347/7 +f 47/187/7 107/221/7 44/189/7 +f 47/350/8 138/349/8 142/348/8 +f 138/349/8 47/350/8 46/125/8 +f 124/353/8 51/352/8 141/351/8 +f 51/352/8 124/353/8 53/196/8 +f 51/116/10 108/223/10 141/354/10 +f 108/223/10 51/116/10 56/118/10 + diff --git a/packages/examples/public/assets/multiMaterialMesh/craft_speederB.mtl b/packages/examples/public/assets/multiMaterialMesh/craft_speederB.mtl new file mode 100644 index 000000000..525d843af --- /dev/null +++ b/packages/examples/public/assets/multiMaterialMesh/craft_speederB.mtl @@ -0,0 +1,14 @@ +# Created by Kenney (www.kenney.nl) + +newmtl metal +Kd 0.8431373 0.8705882 0.9098039 + +newmtl metalRed +Kd 1 0.6285242 0.2028302 + +newmtl metalDark +Kd 0.6750623 0.7100219 0.7735849 + +newmtl dark +Kd 0.2745098 0.2980392 0.3411765 + diff --git a/packages/examples/public/assets/multiMaterialMesh/craft_speederB.obj b/packages/examples/public/assets/multiMaterialMesh/craft_speederB.obj new file mode 100644 index 000000000..ac19ebbb0 --- /dev/null +++ b/packages/examples/public/assets/multiMaterialMesh/craft_speederB.obj @@ -0,0 +1,728 @@ +# Created by Kenney (www.kenney.nl) + +mtllib craft_speederB.mtl + +g craft_speederB + +v 0.3 5.002621E-14 0.6871647 +v 0.2 5.827581E-14 0.4871647 +v 0.2 5.716636E-14 0.6871647 +v 0.2 5.716636E-14 -0.6128354 +v 0.6 5.752731E-14 -0.6128354 +v 1 0 -0.6128354 +v 1 0 -0.01283526 +v 0.2 6.159775E-14 0.08716476 +v 0.6 5.752731E-14 -1.012835 +v 0.3 0.1 -1.012835 +v 0.2 5.752731E-14 -1.012835 +v 0.2 0.5 -1.012835 +v 0.3 0.4 -1.012835 +v 0.5 0.4 -1.012835 +v 0.5 0.1 -1.012835 +v 0.6 0.5 -1.012835 +v 1 0.1 -0.01283526 +v 0.8 0.1 -0.3628353 +v 1 0.1 -0.6128354 +v 0.6 0.1 -0.6128354 +v 0.6 0.1 -0.3628353 +v 0.8 0.1 -0.2628353 +v 0.3 0.1 0.6871647 +v 0.6 0.1 -0.01283526 +v 0.6 0.1 -0.2628353 +v 0.2 0.1 0.3871647 +v 0.2 0.1 0.6871647 +v 0.8 0.15 -0.3628353 +v 0.8 0.15 -0.2628353 +v 0.7 0.2 -0.3628353 +v 0.7 0.2 -0.2628353 +v 0.6 0.2 -0.3628353 +v 0.6 0.2 -0.2628353 +v 0.45 0.5 -0.3128352 +v 0.45 0.5 0.08716476 +v 0.2 0.5 -0.3128352 +v 0.2 0.5 0.08716476 +v 0.2 0.4 -0.3128352 +v 0.45 0.4 -0.3128352 +v 0.45 0.4 0.08716476 +v 0.2 0.4 0.08716476 +v -0.2 5.752731E-14 -1.012835 +v -0.5 0.1 -1.012835 +v -0.5999999 5.752731E-14 -1.012835 +v -0.5999999 0.5 -1.012835 +v -0.5 0.4 -1.012835 +v -0.3 0.4 -1.012835 +v -0.3 0.1 -1.012835 +v -0.2 0.5 -1.012835 +v -0.2 5.716636E-14 0.6871647 +v -0.2 5.690445E-14 0.4871647 +v -0.3 5.002621E-14 0.6871647 +v -0.2 5.716636E-14 0.08716476 +v -0.2 5.752731E-14 -0.6128354 +v -0.5999999 5.752731E-14 -0.6128354 +v -1 0 -0.01283526 +v -1 0 -0.6128354 +v -0.2 5.873156E-14 -0.01283526 +v -0.2 0.4 0.08716476 +v -0.45 0.4 0.08716476 +v -0.2 0.5 0.08716476 +v -0.45 0.5 0.08716476 +v -0.45 0.4 -0.3128352 +v -0.2 0.4 -0.3128352 +v -0.45 0.5 -0.3128352 +v -0.2 0.5 -0.3128352 +v -0.5999999 0.1 -0.2128353 +v -0.5999999 0.1 -0.01283526 +v -0.8 0.1 -0.2128353 +v -1 0.1 -0.01283526 +v -0.3 0.1 0.6871647 +v -0.2 0.1 0.6871647 +v -0.2 0.1 0.3871647 +v -0.8 0.1 -0.3128352 +v -1 0.1 -0.6128354 +v -0.5999999 0.1 -0.3128352 +v -0.5999999 0.1 -0.6128354 +v -0.7 0.2 -0.2128353 +v -0.8 0.15 -0.2128353 +v -0.7 0.2 -0.3128352 +v -0.8 0.15 -0.3128352 +v -0.5999999 0.2 -0.3128352 +v -0.5999999 0.2 -0.2128353 +v 0.2 0.03174925 0.6871647 +v 0.6 0.4 -0.6128354 +v 0.6 0.4 -0.01283526 +v 0.5292892 0.3 0.05787539 +v 0.2 0.4 0.3871647 +v 0.2707107 0.3 0.3164541 +v 0.2707107 0.2 0.3164541 +v 0.5292892 0.2 0.05787539 +v 0.2 0.4 -0.6128354 +v -0.2 0.03174925 0.6871647 +v -0.5999999 0.4 -0.01283526 +v -0.5292892 0.2 0.05787539 +v -0.2707107 0.2 0.3164541 +v -0.2707107 0.3 0.3164541 +v -0.5292892 0.3 0.05787539 +v -0.2 0.4 0.3871647 +v -0.5999999 0.4 -0.6128354 +v -0.2 0.4 -0.6128354 +v -0.2 0.073163 0.9480448 +v 0.2 0.08344824 1.012835 +v -0.2 0.08344824 1.012835 +v 0.2 0.073163 0.9480448 +v 0.6 0.5 -0.9128351 +v 0.2 0.5 -0.9128351 +v 0.2 0.42 -0.6728354 +v 0.2 5.730172E-14 -0.7128353 +v 0.2 0.4 -0.7128353 +v -0.2 0.4 -0.7128353 +v 0.2 0.6 -0.3128352 +v -0.2 0.42 -0.6728354 +v -0.2 0.6 -0.3128352 +v -0.2 5.716636E-14 -0.7128353 +v 0.2 0.6 0.08716476 +v -0.2 0.6 0.08716476 +v -0.2 0.5 -0.9128351 +v -0.5999999 0.5 -0.9128351 +v 0.2 0.5900496 0.2866684 +v -0.2 0.5900496 0.2866684 +v 0.2 0.1668965 1.012835 +v -0.2 0.1668965 1.012835 +v 0.3 0.1 -0.8128352 +v 0.5 0.1 -0.8128352 +v 0.3 0.4 -0.8128352 +v 0.5 0.4 -0.8128352 +v 0.2707107 0.3 0.05787539 +v 0.2707107 0.2 0.05787539 +v -0.5 0.4 -0.8128352 +v -0.5 0.1 -0.8128352 +v -0.3 0.4 -0.8128352 +v -0.3 0.1 -0.8128352 +v -0.2707107 0.2 0.05787539 +v -0.2707107 0.3 0.05787539 +v -0.0999999 0.5668964 0.8128353 +v 0.1 0.5668964 0.8128353 + +vn 0 -1 0 +vn 0 0 -1 +vn 0 1 0 +vn 0.4472136 0.8944272 0 +vn 1 0 0 +vn 0 0 1 +vn -1 0 0 +vn -0.4472136 0.8944272 0 +vn 0.7071068 0 0.7071068 +vn -0.7071068 0 0.7071068 +vn 0 -0.9876331 0.1567831 +vn 0 0.9486833 0.3162278 +vn 0 0.8944272 -0.4472136 +vn 0 0.9987586 0.0498137 +vn -0.9257981 0.3266113 0.1903234 +vn 0.9257981 0.3266113 0.1903234 +vn 0 0.4472136 0.8944272 +vn 0 0.9990332 0.0439609 + +vt 11.81102 27.05373 +vt 7.874016 19.17971 +vt 7.874016 27.05373 +vt 7.874016 -24.12737 +vt 23.62205 -24.12737 +vt 39.37008 -24.12737 +vt 39.37008 -0.5053265 +vt 7.874016 3.431681 +vt -23.62205 2.264855E-12 +vt -11.81102 3.937008 +vt -7.874016 2.264855E-12 +vt -7.874016 19.68504 +vt -11.81102 15.74803 +vt -19.68504 15.74803 +vt -19.68504 3.937008 +vt -23.62205 19.68504 +vt -39.37008 -0.5053265 +vt -31.49606 -14.28485 +vt -39.37008 -24.12737 +vt -23.62205 -24.12737 +vt -23.62205 -14.28485 +vt -31.49606 -10.34785 +vt -11.81102 27.05373 +vt -23.62205 -0.5053265 +vt -23.62205 -10.34785 +vt -7.874016 15.24271 +vt -7.874016 27.05373 +vt 14.28485 -25.52991 +vt 10.34785 -25.52991 +vt 14.28485 -21.1282 +vt 10.34785 -21.1282 +vt -31.49606 3.937008 +vt -31.49606 5.905512 +vt -23.62205 3.937008 +vt -23.62205 7.874016 +vt -27.55906 7.874016 +vt 14.28485 5.905512 +vt 14.28485 3.937008 +vt 10.34785 5.905512 +vt 10.34785 3.937008 +vt -27.55906 -14.28485 +vt -27.55906 -10.34785 +vt 31.49606 3.937008 +vt 23.62205 3.937008 +vt 31.49606 5.905512 +vt 23.62205 7.874016 +vt 27.55906 7.874016 +vt -17.71654 -12.31635 +vt -17.71654 3.431681 +vt -7.874016 -12.31635 +vt -7.874016 3.431681 +vt -7.874016 15.74803 +vt -17.71654 15.74803 +vt -17.71654 19.68504 +vt 12.31635 19.68504 +vt 12.31635 15.74803 +vt -3.431681 19.68504 +vt -3.431681 15.74803 +vt 17.71654 15.74803 +vt 7.874016 15.74803 +vt 17.71654 19.68504 +vt 7.874016 19.68504 +vt 7.874016 2.264855E-12 +vt 19.68504 3.937008 +vt 23.62205 2.264855E-12 +vt 23.62205 19.68504 +vt 19.68504 15.74803 +vt 11.81102 15.74803 +vt 11.81102 3.937008 +vt -7.874016 19.17971 +vt -7.874016 -24.12737 +vt -7.874016 -0.5053265 +vt -12.31635 15.74803 +vt -12.31635 19.68504 +vt 3.431681 15.74803 +vt 3.431681 19.68504 +vt 7.874016 -12.31635 +vt 17.71654 -12.31635 +vt 17.71654 3.431681 +vt 23.62205 -8.379342 +vt 23.62205 -0.5053265 +vt 31.49606 -8.379342 +vt 7.874016 15.24271 +vt 31.49606 -12.31635 +vt 23.62205 -12.31635 +vt -8.379342 -21.1282 +vt -8.379342 -25.52991 +vt -12.31635 -21.1282 +vt -12.31635 -25.52991 +vt 27.55906 -12.31635 +vt 27.55906 -8.379342 +vt -12.31635 3.937008 +vt -12.31635 5.905512 +vt -8.379342 3.937008 +vt -8.379342 5.905512 +vt -39.37008 0 +vt -39.37008 3.937008 +vt 11.81102 1.969536E-12 +vt 7.874016 2.250644E-12 +vt 7.874016 1.249971 +vt 7.874016 3.937008 +vt 24.12737 3.937008 +vt 24.12737 2.813257E-12 +vt 0.5053265 3.937008 +vt 0.5053265 2.813257E-12 +vt 28.19617 3.937008 +vt 28.19617 1.122417E-12 +vt -10.77822 3.937008 +vt -10.77822 3.091952E-12 +vt 24.12737 15.74803 +vt 0.5053265 15.74803 +vt 14.28485 7.874016 +vt 10.34785 7.874016 +vt 17.06063 3.937008 +vt 13.12362 11.81102 +vt 17.06063 15.74803 +vt -5.21045 15.74803 +vt -1.273442 11.81102 +vt -1.273442 7.874016 +vt 13.12362 7.874016 +vt -5.21045 3.937008 +vt -28.19617 1.122417E-12 +vt -28.19617 3.937008 +vt 10.77822 3.091952E-12 +vt 10.77822 3.937008 +vt -11.81102 1.969536E-12 +vt -7.874016 2.250644E-12 +vt -7.874016 1.249971 +vt -7.874016 3.937008 +vt -24.12737 2.813257E-12 +vt -24.12737 3.937008 +vt -0.5053265 2.813257E-12 +vt -0.5053265 3.937008 +vt 39.37008 0 +vt 39.37008 3.937008 +vt -17.06063 15.74803 +vt -13.12362 7.874016 +vt -17.06063 3.937008 +vt 5.21045 3.937008 +vt 1.273442 7.874016 +vt 1.273442 11.81102 +vt -13.12362 11.81102 +vt 5.21045 15.74803 +vt -8.379342 7.874016 +vt -0.5053265 15.74803 +vt -12.31635 7.874016 +vt -24.12737 15.74803 +vt 27.05373 3.682953E-12 +vt 19.17971 3.726632E-12 +vt 27.05373 1.249971 +vt -27.05373 1.249971 +vt -19.17971 4.388796E-12 +vt -27.05373 4.399107E-12 +vt 27.05373 3.937008 +vt 37.3246 2.880433 +vt 15.24271 15.74803 +vt 15.24271 3.937008 +vt 7.874016 39.89736 +vt -7.874016 26.91513 +vt -7.874016 39.89736 +vt -7.874016 37.31461 +vt -7.874016 18.94252 +vt 7.874016 37.31461 +vt 7.874016 18.94252 +vt 7.874016 26.91513 +vt -15.24271 3.937008 +vt -27.05373 3.937008 +vt -15.24271 15.74803 +vt -37.3246 2.880433 +vt 23.62205 40.31911 +vt 23.62205 27.8692 +vt 7.874016 40.31911 +vt 7.874016 27.8692 +vt 7.874016 30.35918 +vt -23.62205 -39.8754 +vt -23.62205 -35.9384 +vt -7.874016 -39.8754 +vt -7.874016 -35.9384 +vt 39.8754 19.68504 +vt 39.8754 2.264855E-12 +vt 35.9384 19.68504 +vt 24.12737 2.264855E-12 +vt -39.8754 2.271339E-12 +vt -39.8754 19.68504 +vt -28.06438 2.262457E-12 +vt -35.9384 19.68504 +vt -28.06438 15.74803 +vt -26.48958 16.53543 +vt -7.874016 -18.05881 +vt -7.874016 -16.29813 +vt 7.874016 -18.05881 +vt -7.874016 -0.4519778 +vt 7.874016 -16.29813 +vt 7.874016 -0.4519778 +vt -7.874016 2.255973E-12 +vt 7.874016 -28.06438 +vt -7.874016 -28.06438 +vt 7.874016 -39.8754 +vt 23.62205 -39.8754 +vt 7.874016 -35.9384 +vt 23.62205 -35.9384 +vt 28.06438 15.74803 +vt 26.48958 16.53543 +vt 28.06438 2.234545E-12 +vt 39.8754 2.248756E-12 +vt -39.8754 2.264855E-12 +vt -24.12737 2.264855E-12 +vt -7.874016 40.31911 +vt -7.874016 30.35918 +vt -23.62205 40.31911 +vt -7.874016 27.8692 +vt -23.62205 27.8692 +vt 7.874016 -2.25072 +vt 7.874016 -10.11496 +vt -7.874016 -2.25072 +vt -7.874016 -10.11496 +vt 12.31635 23.62205 +vt -3.431681 23.62205 +vt -11.28616 23.2303 +vt -39.8754 6.570727 +vt -39.8754 3.285364 +vt 7.874016 3.285364 +vt -7.874016 3.285364 +vt 7.874016 6.570727 +vt -7.874016 6.570727 +vt 3.431681 23.62205 +vt 11.28616 23.2303 +vt 39.8754 6.570727 +vt 39.8754 3.285364 +vt -12.31635 23.62205 +vt -19.68504 -39.8754 +vt -19.68504 -32.00139 +vt -11.81102 -39.8754 +vt -11.81102 -32.00139 +vt 39.8754 15.74803 +vt 39.8754 3.937008 +vt 32.00139 15.74803 +vt 32.00139 3.937008 +vt 19.68504 -32.00139 +vt 19.68504 -39.8754 +vt 11.81102 -32.00139 +vt 11.81102 -39.8754 +vt -39.8754 3.937008 +vt -39.8754 15.74803 +vt -32.00139 3.937008 +vt -32.00139 15.74803 +vt -2.278558 11.81102 +vt -2.278558 7.874016 +vt -12.45882 11.81102 +vt -12.45882 7.874016 +vt -10.6579 2.278558 +vt -20.83816 2.278558 +vt -10.6579 12.45882 +vt 10.6579 12.45882 +vt 20.83816 2.278558 +vt 10.6579 2.278558 +vt 20.83816 7.874016 +vt 10.6579 7.874016 +vt 20.83816 11.81102 +vt 10.6579 11.81102 +vt -10.6579 7.874016 +vt -20.83816 7.874016 +vt -10.6579 11.81102 +vt -20.83816 11.81102 +vt 2.278558 7.874016 +vt 2.278558 11.81102 +vt 12.45882 7.874016 +vt 12.45882 11.81102 +vt 9.469408 18.69498 +vt 30.55309 17.73055 +vt 37.47303 1.068768 +vt -30.55309 17.73055 +vt -9.469408 18.69498 +vt -37.47303 1.068768 +vt 7.874016 -11.95579 +vt -7.874016 -11.95579 +vt 3.937008 5.651048 +vt -3.937008 5.651048 +vt 7.874016 -10.25402 +vt 3.937008 -30.9893 +vt -7.874016 -10.25402 +vt -3.937008 -30.9893 + +usemtl metal + +f 3/3/1 2/2/1 1/1/1 +f 4/4/1 1/1/1 2/2/1 +f 5/5/1 1/1/1 4/4/1 +f 6/6/1 1/1/1 5/5/1 +f 1/1/1 6/6/1 7/7/1 +f 4/4/1 2/2/1 8/8/1 +f 11/11/2 10/10/2 9/9/2 +f 10/10/2 11/11/2 12/12/2 +f 10/10/2 12/12/2 13/13/2 +f 13/13/2 12/12/2 14/14/2 +f 15/15/2 9/9/2 10/10/2 +f 9/9/2 15/15/2 16/16/2 +f 16/16/2 15/15/2 14/14/2 +f 16/16/2 14/14/2 12/12/2 +f 19/19/3 18/18/3 17/17/3 +f 18/18/3 19/19/3 20/20/3 +f 18/18/3 20/20/3 21/21/3 +f 22/22/3 17/17/3 18/18/3 +f 17/17/3 22/22/3 23/23/3 +f 23/23/3 22/22/3 24/24/3 +f 24/24/3 22/22/3 25/25/3 +f 23/23/3 24/24/3 26/26/3 +f 23/23/3 26/26/3 27/27/3 +f 30/30/4 29/29/4 28/28/4 +f 29/29/4 30/30/4 31/31/4 +f 21/34/2 28/33/2 18/32/2 +f 28/33/2 21/34/2 32/35/2 +f 28/33/2 32/35/2 30/36/2 +f 29/39/5 18/38/5 28/37/5 +f 18/38/5 29/39/5 22/40/5 +f 32/21/3 31/42/3 30/41/3 +f 31/42/3 32/21/3 33/25/3 +f 29/45/6 25/44/6 22/43/6 +f 25/44/6 29/45/6 33/46/6 +f 33/46/6 29/45/6 31/47/6 +f 36/50/3 35/49/3 34/48/3 +f 35/49/3 36/50/3 37/51/3 +f 36/12/2 39/53/2 38/52/2 +f 39/53/2 36/12/2 34/54/2 +f 35/57/5 39/56/5 34/55/5 +f 39/56/5 35/57/5 40/58/5 +f 35/61/6 41/60/6 40/59/6 +f 41/60/6 35/61/6 37/62/6 +f 44/65/2 43/64/2 42/63/2 +f 43/64/2 44/65/2 45/66/2 +f 43/64/2 45/66/2 46/67/2 +f 46/67/2 45/66/2 47/68/2 +f 48/69/2 42/63/2 43/64/2 +f 42/63/2 48/69/2 49/62/2 +f 49/62/2 48/69/2 47/68/2 +f 49/62/2 47/68/2 45/66/2 +f 52/23/1 51/70/1 50/27/1 +f 51/70/1 52/23/1 53/51/1 +f 53/51/1 52/23/1 54/71/1 +f 54/71/1 52/23/1 55/20/1 +f 55/20/1 52/23/1 56/17/1 +f 55/20/1 56/17/1 57/19/1 +f 54/71/1 58/72/1 53/51/1 +f 61/12/6 60/53/6 59/52/6 +f 60/53/6 61/12/6 62/54/6 +f 65/61/2 64/60/2 63/59/2 +f 64/60/2 65/61/2 66/62/2 +f 60/75/7 65/74/7 63/73/7 +f 65/74/7 60/75/7 62/76/7 +f 65/78/3 61/8/3 66/77/3 +f 61/8/3 65/78/3 62/79/3 +f 69/82/3 68/81/3 67/80/3 +f 70/7/3 68/81/3 69/82/3 +f 71/1/3 68/81/3 70/7/3 +f 72/3/3 68/81/3 71/1/3 +f 68/81/3 72/3/3 73/83/3 +f 70/7/3 69/82/3 74/84/3 +f 70/7/3 74/84/3 75/6/3 +f 76/85/3 75/6/3 74/84/3 +f 75/6/3 76/85/3 77/5/3 +f 80/88/8 79/87/8 78/86/8 +f 79/87/8 80/88/8 81/89/8 +f 80/90/3 83/80/3 82/85/3 +f 83/80/3 80/90/3 78/91/3 +f 69/94/7 81/93/7 74/92/7 +f 81/93/7 69/94/7 79/95/7 +f 67/34/6 79/33/6 69/32/6 +f 79/33/6 67/34/6 83/35/6 +f 79/33/6 83/35/6 78/36/6 +f 81/45/2 76/44/2 74/43/2 +f 76/44/2 81/45/2 82/46/2 +f 82/46/2 81/45/2 80/47/2 + +usemtl metalRed + +f 5/9/2 19/97/2 6/96/2 +f 19/97/2 5/9/2 20/34/2 +f 23/69/6 3/99/6 1/98/6 +f 3/99/6 23/69/6 84/100/6 +f 84/100/6 23/69/6 27/101/6 +f 17/104/5 6/103/5 19/102/5 +f 6/103/5 17/104/5 7/105/5 +f 23/108/9 7/107/9 17/106/9 +f 7/107/9 23/108/9 1/109/9 +f 86/111/5 20/102/5 85/110/5 +f 20/102/5 86/111/5 32/112/5 +f 32/112/5 86/111/5 33/113/5 +f 33/113/5 86/111/5 25/40/5 +f 25/40/5 86/111/5 24/104/5 +f 21/38/5 20/102/5 32/112/5 +f 86/116/9 87/115/9 24/114/9 +f 87/115/9 86/116/9 88/117/9 +f 87/115/9 88/117/9 89/118/9 +f 89/118/9 88/117/9 90/119/9 +f 91/120/9 24/114/9 87/115/9 +f 24/114/9 91/120/9 26/121/9 +f 26/121/9 91/120/9 90/119/9 +f 26/121/9 90/119/9 88/117/9 +f 92/71/3 39/48/3 85/20/3 +f 39/48/3 92/71/3 38/50/3 +f 40/49/3 85/20/3 39/48/3 +f 40/49/3 86/24/3 85/20/3 +f 86/24/3 40/49/3 88/26/3 +f 88/26/3 40/49/3 41/51/3 +f 52/124/10 70/123/10 56/122/10 +f 70/123/10 52/124/10 71/125/10 +f 50/127/6 71/10/6 52/126/6 +f 71/10/6 50/127/6 93/128/6 +f 71/10/6 93/128/6 72/129/6 +f 56/132/7 75/131/7 57/130/7 +f 75/131/7 56/132/7 70/133/7 +f 75/135/2 55/65/2 57/134/2 +f 55/65/2 75/135/2 77/44/2 +f 68/138/10 95/137/10 94/136/10 +f 95/137/10 68/138/10 73/139/10 +f 95/137/10 73/139/10 96/140/10 +f 96/140/10 73/139/10 97/141/10 +f 98/142/10 94/136/10 95/137/10 +f 94/136/10 98/142/10 99/143/10 +f 99/143/10 98/142/10 97/141/10 +f 99/143/10 97/141/10 73/139/10 +f 68/133/7 83/144/7 67/94/7 +f 94/145/7 83/144/7 68/133/7 +f 94/145/7 82/146/7 83/144/7 +f 100/147/7 82/146/7 94/145/7 +f 100/147/7 76/92/7 82/146/7 +f 76/92/7 100/147/7 77/131/7 +f 100/5/3 64/77/3 101/4/3 +f 64/77/3 100/5/3 63/78/3 +f 63/78/3 100/5/3 60/79/3 +f 94/81/3 60/79/3 100/5/3 +f 99/83/3 60/79/3 94/81/3 +f 60/79/3 99/83/3 59/8/3 +f 84/150/7 2/149/7 3/148/7 +f 50/153/5 51/152/5 93/151/5 +f 102/155/7 72/154/7 93/150/7 +f 99/156/7 72/154/7 102/155/7 +f 72/154/7 99/156/7 73/157/7 +f 51/70/1 8/8/1 2/2/1 +f 8/8/1 51/70/1 53/51/1 +f 104/160/11 93/159/11 103/158/11 +f 93/159/11 104/160/11 102/161/11 +f 51/162/11 103/158/11 93/159/11 +f 51/162/11 105/163/11 103/158/11 +f 2/164/11 105/163/11 51/162/11 +f 105/163/11 2/164/11 84/165/11 +f 88/168/5 27/167/5 26/166/5 +f 27/167/5 88/168/5 105/169/5 +f 27/167/5 105/169/5 84/151/5 + +usemtl metalDark + +f 107/172/12 85/171/12 106/170/12 +f 85/171/12 107/172/12 92/173/12 +f 92/173/12 107/172/12 108/174/12 +f 12/177/3 106/176/3 16/175/3 +f 106/176/3 12/177/3 107/178/3 +f 106/181/5 9/180/5 16/179/5 +f 9/180/5 106/181/5 5/182/5 +f 5/182/5 106/181/5 85/110/5 +f 5/182/5 85/110/5 20/102/5 +f 109/185/7 12/184/7 11/183/7 +f 12/184/7 109/185/7 107/186/7 +f 107/186/7 109/185/7 110/187/7 +f 107/186/7 110/187/7 108/188/7 +f 111/191/13 108/190/13 110/189/13 +f 108/190/13 111/191/13 112/192/13 +f 112/192/13 111/191/13 113/193/13 +f 114/194/13 112/192/13 113/193/13 +f 111/60/2 109/195/2 115/99/2 +f 109/195/2 111/60/2 110/52/2 +f 114/77/3 116/51/3 112/50/3 +f 116/51/3 114/77/3 117/8/3 +f 58/72/1 115/197/1 109/196/1 +f 115/197/1 58/72/1 54/71/1 +f 115/197/1 54/71/1 55/20/1 +f 44/175/1 115/197/1 55/20/1 +f 115/197/1 44/175/1 42/177/1 +f 109/196/1 53/51/1 58/72/1 +f 4/4/1 53/51/1 109/196/1 +f 53/51/1 4/4/1 8/8/1 +f 11/198/1 4/4/1 109/196/1 +f 9/199/1 4/4/1 11/198/1 +f 4/4/1 9/199/1 5/5/1 +f 45/199/3 118/200/3 49/198/3 +f 118/200/3 45/199/3 119/201/3 +f 113/203/5 111/202/5 118/181/5 +f 115/204/5 118/181/5 111/202/5 +f 42/205/5 118/181/5 115/204/5 +f 118/181/5 42/205/5 49/179/5 +f 55/207/7 45/184/7 44/206/7 +f 45/184/7 55/207/7 119/186/7 +f 119/186/7 55/207/7 100/147/7 +f 100/147/7 55/207/7 77/131/7 +f 119/210/12 113/209/12 118/208/12 +f 113/209/12 119/210/12 101/211/12 +f 101/211/12 119/210/12 100/212/12 +f 117/215/14 120/214/14 116/213/14 +f 120/214/14 117/215/14 121/216/14 +f 116/218/5 36/55/5 112/217/5 +f 36/55/5 116/218/5 37/57/5 +f 37/57/5 116/218/5 120/219/5 +f 37/57/5 120/219/5 41/58/5 +f 41/58/5 120/219/5 88/168/5 +f 88/168/5 120/219/5 122/220/5 +f 88/168/5 122/220/5 105/169/5 +f 105/169/5 122/220/5 103/221/5 +f 38/56/5 112/217/5 36/55/5 +f 92/110/5 112/217/5 38/56/5 +f 112/217/5 92/110/5 108/203/5 +f 122/224/6 104/223/6 103/222/6 +f 104/223/6 122/224/6 123/225/6 +f 99/156/7 61/76/7 59/75/7 +f 61/76/7 99/156/7 117/226/7 +f 117/226/7 99/156/7 121/227/7 +f 121/227/7 99/156/7 123/228/7 +f 123/228/7 99/156/7 102/155/7 +f 123/228/7 102/155/7 104/229/7 +f 114/230/7 61/76/7 117/226/7 +f 114/230/7 66/74/7 61/76/7 +f 114/230/7 64/73/7 66/74/7 +f 114/230/7 101/147/7 64/73/7 +f 101/147/7 114/230/7 113/188/7 + +usemtl dark + +f 126/13/2 125/15/2 124/10/2 +f 125/15/2 126/13/2 127/14/2 +f 10/233/3 125/232/3 15/231/3 +f 125/232/3 10/233/3 124/234/3 +f 126/237/5 10/236/5 13/235/5 +f 10/236/5 126/237/5 124/238/5 +f 126/241/1 14/240/1 127/239/1 +f 14/240/1 126/241/1 13/242/1 +f 125/245/7 14/244/7 15/243/7 +f 14/244/7 125/245/7 127/246/7 +f 89/249/5 129/248/5 128/247/5 +f 129/248/5 89/249/5 90/250/5 +f 90/253/3 91/252/3 129/251/3 +f 128/256/1 87/255/1 89/254/1 +f 87/259/6 129/258/6 91/257/6 +f 129/258/6 87/259/6 128/260/6 +f 130/237/5 43/236/5 46/235/5 +f 43/236/5 130/237/5 131/238/5 +f 130/232/1 47/233/1 132/234/1 +f 47/233/1 130/232/1 46/231/1 +f 133/245/7 47/244/7 48/243/7 +f 47/244/7 133/245/7 132/246/7 +f 130/67/2 133/69/2 131/64/2 +f 133/69/2 130/67/2 132/68/2 +f 43/240/3 133/241/3 48/242/3 +f 133/241/3 43/240/3 131/239/3 +f 135/263/6 95/262/6 134/261/6 +f 95/262/6 135/263/6 98/264/6 +f 96/267/7 135/266/7 134/265/7 +f 135/266/7 96/267/7 97/268/7 +f 98/252/1 135/251/1 97/253/1 +f 95/255/3 96/254/3 134/256/3 +f 123/271/15 136/270/15 121/269/15 +f 122/274/16 120/273/16 137/272/16 +f 137/277/17 123/276/17 122/275/17 +f 123/276/17 137/277/17 136/278/17 +f 121/281/18 137/280/18 120/279/18 +f 137/280/18 121/281/18 136/282/18 + diff --git a/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx b/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx new file mode 100644 index 000000000..30f53ec39 --- /dev/null +++ b/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx @@ -0,0 +1,141 @@ +/** + * melonJS — Multi-material OBJ mesh example. + * + * Loads four Kenney Space Kit (CC0) spacecraft, each with multiple + * `usemtl` material groups (metal / metalRed / metalDark / dark / …), + * and renders them side by side rotating in 3D. Every panel of every + * spacecraft picks up the diffuse color (`Kd`) from its bound MTL + * material, so the multi-color paint scheme of each model survives — + * the OBJ parser emits `groups: [{materialName, start, count}, …]` + * (matching the Three.js / glTF convention) and `Mesh.draw()` iterates + * those, swapping tint per draw call. + * + * Compare with the `mesh3d` example: that one binds a single texture + * across the whole mesh (checkerboard on cube / sphere / teapot). + * This one demonstrates the multi-material code path — same Mesh + * class, no extra wiring beyond passing the MTL name through + * `material:`. + * + * Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License. + * See `packages/examples/LICENSE.md` for full license + asset credits + * (Kenney Space Kit 2.0, CC0). + */ +import type { CanvasRenderer, WebGLRenderer } from "melonjs"; +import { + Application, + loader, + Mesh, + Renderable, + Vector3d, + video, +} from "melonjs"; +import { createExampleComponent } from "../utils"; + +const base = `${import.meta.env.BASE_URL}assets/multiMaterialMesh/`; + +// the four Kenney spacecraft to showcase; each .obj has its companion +// .mtl with 3-5 differently-colored materials +const CRAFTS = [ + "craft_speederA", + "craft_speederB", + "craft_racer", + "craft_miner", +]; + +const createGame = () => { + const app = new Application(1024, 768, { + parent: "screen", + renderer: video.AUTO, + scale: "auto", + }); + + app.world.backgroundColor.parseCSS("#0a0a1f"); + + // preload every craft's OBJ + matching MTL. `type: "mtl"` parses + // the materials so the Mesh constructor can look them up by name + // later via `material: `. + const assets = []; + for (const name of CRAFTS) { + assets.push({ name, type: "obj", src: `${base}${name}.obj` }); + assets.push({ name, type: "mtl", src: `${base}${name}.mtl` }); + } + + loader.preload(assets, () => { + const axisY = new Vector3d(0, 1, 0); + const axisX = new Vector3d(1, 0, 0); + + /** + * Renderable wrapper for one spinning spacecraft. Owns a single + * `Mesh` instance and drives its 3D rotation each frame. The + * Mesh sees that the OBJ has multiple material groups + we + * passed `material: name`, so it builds an internal + * `mesh.groups` array and `draw()` issues one tinted draw call + * per material region. + */ + class SpinningCraft extends Renderable { + mesh: Mesh; + + constructor(modelName: string, x: number, y: number, size: number) { + super(0, 0, 1024, 768); + this.anchorPoint.set(0, 0); + this.mesh = new Mesh(x, y, { + model: modelName, + material: modelName, // MTL name = same as OBJ for Kenney pack + width: size, + height: size, + cullBackFaces: true, + }); + } + + override update(dt: number): boolean { + // Y spin + slight X wobble — gives every panel of the + // model a chance to face camera so the per-material + // colors are all visible across the animation + this.mesh.rotate(dt * 0.0008, axisY); + this.mesh.rotate(dt * 0.0003, axisX); + return true; + } + + override draw(renderer: WebGLRenderer | CanvasRenderer): void { + this.mesh.preDraw(renderer); + this.mesh.draw(renderer); + this.mesh.postDraw(renderer); + } + } + + // 2x2 grid layout + const cellW = 1024 / 2; + const cellH = 768 / 2; + const meshSize = 280; + for (let i = 0; i < CRAFTS.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const cx = cellW * col + cellW / 2; + const cy = cellH * row + cellH / 2; + app.world.addChild(new SpinningCraft(CRAFTS[i], cx, cy, meshSize)); + } + + // labels for each craft — HTML overlay since the grid is fixed + const labelStyle = + "position:absolute;color:#e0e0e0;font-family:'Courier New',monospace;" + + "font-size:14px;font-weight:bold;text-shadow:0 0 4px #000;" + + "z-index:1000;pointer-events:none;"; + const parent = app.renderer.getCanvas().parentElement; + if (parent) { + parent.style.position = "relative"; + for (let i = 0; i < CRAFTS.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const label = document.createElement("div"); + label.textContent = CRAFTS[i].replace("craft_", ""); + // approximate engine→screen position; close enough for + // labels (renderer.getCanvas().getBoundingClientRect() + // would be exact but this is fine for a static layout) + label.style.cssText = `${labelStyle}left:${(col * 0.5 + 0.25) * 100}%;top:${(row * 0.5 + 0.45) * 100}%;transform:translate(-50%,0);`; + parent.appendChild(label); + } + } + }); +}; + +export const ExampleMultiMaterialMesh = createExampleComponent(createGame); diff --git a/packages/examples/src/main.tsx b/packages/examples/src/main.tsx index dc3dc2fea..1b1c0a4c6 100644 --- a/packages/examples/src/main.tsx +++ b/packages/examples/src/main.tsx @@ -108,6 +108,11 @@ const ExampleMesh3dMaterial = lazy(() => default: m.ExampleMesh3dMaterial, })), ); +const ExampleMultiMaterialMesh = lazy(() => + import("./examples/multiMaterialMesh/ExampleMultiMaterialMesh").then((m) => ({ + default: m.ExampleMultiMaterialMesh, + })), +); const ExamplePlatformer = lazy(() => import("./examples/platformer/ExamplePlatformer").then((m) => ({ default: m.ExamplePlatformer, @@ -337,6 +342,14 @@ const examples: { description: "3D cube pet models from Kenney with MTL material support — texture and colors auto-resolved from .mtl files.", }, + { + component: , + label: "Multi-material OBJ", + path: "multi-material-mesh", + sourceDir: "multiMaterialMesh", + description: + "Kenney spacecraft (CC0) with multi-material OBJ rendering — each `usemtl` group draws with its own diffuse color via the new groups[] API.", + }, { component: , label: "Platformer", From 9c3262f28e0e72e875285090803717a9ca30ade2 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 09:26:47 +0800 Subject: [PATCH 03/13] perf(mesh): single-draw-call multi-material via per-vertex color baking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier 2 of the multi-material work: bake each material's Kd color into a per-vertex `aColor` attribute at construction time, then render the whole mesh in ONE draw call regardless of material count. Replaces the prior per-group draw iteration (one drawElements + texture bind per material) — same correct result, ~N× less GL state churn on WebGL, and Canvas no longer needs a multi-material code path at all. OBJ parser: per-material vertex dedup. Each `usemtl` switch resets the active `vertexMap`, so vertices shared across materials get separate slots in the unified vertex buffer. Required for per-vertex color baking — a vertex shared between two materials needs to carry both colors. Mesh constructor: when isMultiMaterial, allocate `vertexColors` (Uint32Array, one packed ARGB per vertex) and fill it by walking each group's index slice, assigning the group's Kd to every vertex it references. `mesh.groups` stays exposed for inspection; the `.tint` field is now informational (post-construction mutation is a no-op since colors are already baked). Runtime mutation happens through `mesh.tint` instead — multiplies on top of every baked color via the shader's existing `aColor` path. WebGL mesh batcher: new `mulPackedARGB` helper combines each vertex's baked color with the runtime `tint` (matches the ARGB packing from `Color.toUint32`). Single-material meshes (no `vertexColors`) take the unchanged fast path — same `tint` for every vertex push. WebGL renderer: reverted `drawMesh(mesh, group?)` back to single- arg `drawMesh(mesh)`. State setup runs once, batcher handles per- vertex color from the mesh. Canvas renderer: same single-arg signature. When `vertexColors` is present, per-triangle solid-fill reads `vertexColors[v0]` (all 3 vertices of a triangle share a material color by construction) multiplied by `currentTint`. One global painter's sort — no inter- group ordering glitches like the per-group approach had. Per-material textures aren't supported on Canvas (would need per-group passes); use WebGL for that case. Canvas also gains a 1×1 solid-fill fast path for Kd-only single- material meshes (a user could load an OBJ with one material whose MTL sets only `Kd`) — previously the affine drawImage produced sub-pixel artifacts at large triangle scale, now it solid-fills with the tinted color. MTL parser: dropped the obsolete "multiple materials detected — only the first material's texture will be used per mesh" warning (no longer true after tier 2), and switched the remaining MTL warnings from the deprecation helper `warning()` (which formats as "is deprecated since version undefined, please use undefined") to plain `console.warn`. Single-material meshes: zero behavior change, zero perf change. The multi-material path is gated on `isMultiMaterial` so nothing allocates or branches for the common single-material case. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/src/loader/parsers/mtl.js | 24 +++--- packages/melonjs/src/loader/parsers/obj.js | 18 +++- packages/melonjs/src/renderable/mesh.js | 82 ++++++++++--------- .../src/video/canvas/canvas_renderer.js | 70 +++++++++++++--- .../src/video/webgl/batchers/mesh_batcher.js | 63 ++++++++++---- .../melonjs/src/video/webgl/webgl_renderer.js | 28 ++----- 6 files changed, 189 insertions(+), 96 deletions(-) diff --git a/packages/melonjs/src/loader/parsers/mtl.js b/packages/melonjs/src/loader/parsers/mtl.js index 1f9270392..7225b715d 100644 --- a/packages/melonjs/src/loader/parsers/mtl.js +++ b/packages/melonjs/src/loader/parsers/mtl.js @@ -1,4 +1,7 @@ -import { warning } from "../../lang/console.js"; +// `console.warn()` from `lang/console.js` is reserved for **deprecation** +// notices (formats as "X is deprecated since version Y, please use Z" +// — not what we want for runtime MTL parser warnings about unsupported +// keywords). Use `console.warn` directly here. import { mtlList } from "../cache.js"; import { fetchData } from "./fetchdata.js"; @@ -48,7 +51,6 @@ const UNSUPPORTED_MAPS = new Set([ function parseMTL(text, basePath) { const materials = {}; let current = null; - let materialCount = 0; const lines = text.split("\n"); for (let i = 0; i < lines.length; i++) { @@ -62,7 +64,9 @@ function parseMTL(text, basePath) { // warn on unsupported texture maps if (UNSUPPORTED_MAPS.has(keyword)) { - warning("MTL: '" + keyword + "' is not supported and will be ignored"); + console.warn( + "MTL: '" + keyword + "' is not supported and will be ignored", + ); continue; } @@ -72,20 +76,18 @@ function parseMTL(text, basePath) { !UNSUPPORTED_MAPS.has(keyword) && keyword !== "Ks" ) { - warning("MTL: unknown property '" + keyword + "' will be ignored"); + console.warn("MTL: unknown property '" + keyword + "' will be ignored"); continue; } switch (keyword) { case "newmtl": - materialCount++; - if (materialCount > 1) { - warning( - "MTL: multiple materials detected — only the first material's texture will be used per mesh", - ); - } + // (was: warn on multi-material — obsolete since Mesh now + // resolves per-material draw groups via OBJ `groups[]`, + // see `Mesh.draw`; each named material is rendered with + // its own tint + texture in its own draw call) if (!parts[1]) { - warning("MTL: newmtl missing material name, skipping"); + console.warn("MTL: newmtl missing material name, skipping"); break; } current = { diff --git a/packages/melonjs/src/loader/parsers/obj.js b/packages/melonjs/src/loader/parsers/obj.js index 53221ce5c..8c37e49a7 100644 --- a/packages/melonjs/src/loader/parsers/obj.js +++ b/packages/melonjs/src/loader/parsers/obj.js @@ -60,13 +60,21 @@ function parseOBJ(text) { const texcoords = []; // unified output arrays (built in a single pass) - const vertexMap = new Map(); const vertices = []; const uvs = []; const indices = []; let vertexCount = 0; - // helper: look up or create a unified vertex for a v/vt pair + // Per-material vertex dedup: each `usemtl` switch resets the active + // `vertexMap` so the same (v, vt) reused across different materials + // produces SEPARATE unified vertices. This is the prerequisite for + // per-vertex color baking in `Mesh` — without it, a vertex shared + // between two materials couldn't carry both colors. Pre-usemtl + // faces use the initial empty map (the "anonymous" group). + let vertexMap = new Map(); + + // helper: look up or create a unified vertex for a v/vt pair in the + // current material's dedup scope function addVertex(v, vt) { const key = v * VT_KEY_MULTIPLIER + (vt + OBJ_INDEX_OFFSET); let index = vertexMap.get(key); @@ -125,6 +133,12 @@ function parseOBJ(text) { }); } groups.push({ materialName, start: indices.length, count: 0 }); + // Reset the vertex dedup scope so vertices shared with the + // previous material get re-added as distinct unified vertices. + // Required for per-vertex color baking in `Mesh` — each + // material's vertices need their own slots in the position + // buffer to carry distinct colors. + vertexMap = new Map(); }; // parse lines and build geometry in a single pass diff --git a/packages/melonjs/src/renderable/mesh.js b/packages/melonjs/src/renderable/mesh.js index a54794517..d882db3e5 100644 --- a/packages/melonjs/src/renderable/mesh.js +++ b/packages/melonjs/src/renderable/mesh.js @@ -21,11 +21,6 @@ import Renderable from "./renderable.js"; // reusable matrix for combining projection × model in draw() const _combinedMatrix = new Matrix3d(); -// reusable color used by `draw()` to save/restore `this.tint` around -// the multi-material per-group swap. Module-scoped so we don't -// allocate every frame; safe because draw is single-threaded. -const _savedTint = new Color(255, 255, 255, 1); - // Lazily-allocated 1×1 white pixel used as the texture fallback for // flat-color (Kd-only, no `map_Kd`) MTL materials. One canvas shared // across every Mesh that needs it — no per-instance allocation. @@ -242,13 +237,17 @@ export default class Mesh extends Renderable { * Per-material submesh groups, populated when the OBJ * contains multiple `usemtl` directives AND a matching MTL * is bound via the `material` setting. Each entry slices - * the shared `indices` buffer and carries its own texture + - * tint + opacity, so `draw()` can render one material per - * draw call without touching geometry. + * the shared `indices` buffer; field shape (`start`, + * `count`, `materialName`) matches the Three.js / glTF + * "groups" convention. * - * Field shape (`start`, `count`, `materialName`) matches the - * Three.js / glTF "groups" convention so the structure is - * familiar to anyone coming from those engines. + * Under the per-vertex color baking path (tier 2), the + * `tint` / `opacity` fields here are informational — the + * actual rendered color is baked into `vertexColors` at + * construction time. Mutating `groups[i].tint` after + * construction has no visible effect; use `mesh.tint` for + * runtime color multiplication, or rebuild the Mesh with + * new material settings. * @type {Array<{materialName: string|null, start: number, * count: number, texture: TextureAtlas, tint: Color, * opacity: number}>} @@ -265,6 +264,31 @@ export default class Mesh extends Renderable { if (first.opacity < 1) { this.setOpacity(first.opacity); } + + /** + * Per-vertex color buffer (one packed Uint32 per vertex) + * populated for multi-material meshes. The mesh batcher + * reads from this when present, pushing the per-vertex + * color as the `aColor` attribute — so the whole mesh + * renders in a single draw call with each material region + * carrying its baked color. Multiplied at render time by + * the global `mesh.tint`, so runtime tint mutation still + * works as expected (flash, fade, team color, etc.). + * + * Vertices were split per-material at parse time (each + * material has its own dedup scope in the OBJ parser), so + * every vertex belongs to exactly one material group and + * carries that group's color unambiguously. + * @type {Uint32Array} + */ + this.vertexColors = new Uint32Array(this.vertexCount); + for (const g of this.groups) { + const c = g.tint.toUint32(g.opacity); + const end = g.start + g.count; + for (let i = g.start; i < end; i++) { + this.vertexColors[this.indices[i]] = c; + } + } } else if (materials) { // single-material path — pick the first MTL entry const mat = materials[Object.keys(materials)[0]]; @@ -362,38 +386,18 @@ export default class Mesh extends Renderable { /** * Draw the mesh (automatically called by melonJS). - * Projects vertices through projectionMatrix × currentTransform and - * calls `renderer.drawMesh()`. For multi-material meshes (OBJ files - * with multiple `usemtl` directives + a bound MTL), iterates the - * per-material `groups` array, swapping texture and tint per draw - * so each material region renders with its own appearance. + * Projects vertices through `projectionMatrix × currentTransform` + * and hands the mesh off to `renderer.drawMesh()`. Multi-material + * dispatch (one draw per `groups[]` entry on WebGL, one global + * painter's sort with per-triangle tint on Canvas) is the + * renderer's responsibility — each backend handles depth its own + * way (hardware Z-buffer vs CPU painter's), so the right place to + * fan out groups is inside the renderer. * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance */ draw(renderer) { this._projectVertices(this.pos.x, this.pos.y, 1000); - if (this.groups && this.groups.length > 1) { - // Save the mesh's primary tint / texture so the per-group - // swaps don't leak into reads of `this.tint` / `this.texture` - // between frames (e.g. `toCanvas`, picking, debug overlays). - // `Renderable.preDraw` already called `renderer.setTint(this.tint)`, - // locking the renderer to the first group's color — we mutate - // `renderer.currentTint` directly per group instead of going - // through `setTint` because `setTint` multiplies into alpha, - // which would compound across iterations. - const savedTexture = this.texture; - _savedTint.copy(this.tint); - for (const g of this.groups) { - this.texture = g.texture; - this.tint.copy(g.tint); - renderer.currentTint.copy(g.tint); - renderer.drawMesh(this, g); - } - this.texture = savedTexture; - this.tint.copy(_savedTint); - renderer.currentTint.copy(_savedTint); - } else { - renderer.drawMesh(this); - } + renderer.drawMesh(this); } /** diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index 2bda3068d..9663f8967 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -400,14 +400,17 @@ export default class CanvasRenderer extends Renderer { * buffer. Use the WebGL renderer for correct depth ordering on * complex meshes. * - * When `group` is provided, only the index slice - * `[group.start, group.start + group.count)` is drawn — used by - * multi-material OBJs to render one material at a time. + * Multi-material meshes (`mesh.vertexColors` present) render here + * via per-triangle solid fill — each triangle's three vertices + * share one baked material color, so we read `vertexColors[v0]` + * and use it as the triangle's fillStyle, multiplied by + * `currentTint`. The single shared texture (typically the 1×1 + * white-pixel fallback for Kd-only models) is bypassed in that + * path. Canvas can't do per-material textures; if you need that, + * use the WebGL renderer. * @param {Mesh} mesh - a Mesh renderable or compatible object - * @param {{start: number, count: number}} [group] - optional index - * buffer slice to draw (defaults to the whole mesh) */ - drawMesh(mesh, group) { + drawMesh(mesh) { if (this.getGlobalAlpha() < 1 / 255) { return; } @@ -415,8 +418,7 @@ export default class CanvasRenderer extends Renderer { const vertices = mesh.vertices; const uvs = mesh.uvs; const indices = mesh.indices; - const startIdx = group ? group.start : 0; - const endLimit = group ? group.start + group.count : indices.length; + const vertexColors = mesh.vertexColors; // apply tint if set let image = mesh.texture.getTexture(); @@ -426,8 +428,36 @@ export default class CanvasRenderer extends Renderer { } const imgW = image.width; const imgH = image.height; + // Solid-fill fast path: kicks in for either (a) per-vertex + // color meshes (multi-material) where each triangle's color + // comes from the baked `vertexColors`, or (b) Kd-only single- + // material meshes where the texture is the 1×1 white-pixel + // fallback. Both bypass Canvas's per-triangle affine drawImage + // (which produces sub-pixel artifacts mapping a 1×1 source + // onto large triangles) and fall back to `fill()`. + const solidFillKd = imgW === 1 && imgH === 1; + const solidFill = solidFillKd || vertexColors !== undefined; + // pre-extract tint as 0..1 floats for per-vertex color modulation + const tintR = tint[0]; + const tintG = tint[1]; + const tintB = tint[2]; + let solidFillStyle = null; + if (solidFillKd && !vertexColors) { + // Single-material 1×1 path — one fill color for the whole + // mesh, sampled from the pre-tinted image + if (!this._meshColorCanvas) { + this._meshColorCanvas = document.createElement("canvas"); + this._meshColorCanvas.width = 1; + this._meshColorCanvas.height = 1; + this._meshColorCtx = this._meshColorCanvas.getContext("2d"); + } + this._meshColorCtx.clearRect(0, 0, 1, 1); + this._meshColorCtx.drawImage(image, 0, 0); + const pixel = this._meshColorCtx.getImageData(0, 0, 1, 1).data; + solidFillStyle = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`; + } const cullBack = mesh.cullBackFaces === true; - const triCount = (endLimit - startIdx) / 3; + const triCount = indices.length / 3; // pre-allocate flat sort array (reuse across frames via closure) // each entry stores: [sortKey, originalIndex] @@ -438,7 +468,7 @@ export default class CanvasRenderer extends Renderer { let visCount = 0; // build sort keys for visible triangles (no object allocation) - for (let j = startIdx; j < endLimit; j += 3) { + for (let j = 0; j < indices.length; j += 3) { const i0 = indices[j]; const i1 = indices[j + 1]; const i2 = indices[j + 2]; @@ -528,7 +558,25 @@ export default class CanvasRenderer extends Renderer { y2 + (y2 > cy ? 0.5 : y2 < cy ? -0.5 : 0), ); - if (rawDet === 0) { + if (solidFill) { + // Solid-fill path. Either the texture is 1×1 (Kd-only + // single-material — `solidFillStyle` is pre-computed) or + // the mesh has per-vertex baked colors (multi-material + // — read color from `vertexColors[v0]` since all 3 + // vertices of a triangle share a material color). + context.closePath(); + if (vertexColors) { + // ARGB-packed: A R G B in 4 bytes MSB→LSB + const c = vertexColors[indices[j]]; + const cr = Math.round(((c >>> 16) & 0xff) * tintR); + const cg = Math.round(((c >>> 8) & 0xff) * tintG); + const cb = Math.round((c & 0xff) * tintB); + context.fillStyle = `rgb(${cr},${cg},${cb})`; + } else { + context.fillStyle = solidFillStyle; + } + context.fill(); + } else if (rawDet === 0) { // degenerate UV triangle — sample a solid color from the texture // (common with color-palette models where all 3 UVs map to the same point) context.closePath(); diff --git a/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js b/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js index db8c446bc..660bcb57c 100644 --- a/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js +++ b/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js @@ -6,6 +6,33 @@ import { MaterialBatcher } from "./material_batcher.js"; // reusable vector for vertex transform const _v = new Vector2d(); +/** + * Per-channel multiply two ARGB-packed Uint32 colors. Used by the + * multi-material mesh path to combine a vertex's baked material color + * (`mesh.vertexColors[i]`) with the runtime `mesh.tint` before + * pushing the result as the vertex's `aColor` attribute. + * Layout (MSB→LSB): A R G B, matching `Color.toUint32`. + * @param {number} a - first ARGB packed Uint32 + * @param {number} b - second ARGB packed Uint32 + * @returns {number} their per-channel product (normalized in 0..255) + * @ignore + */ +function mulPackedARGB(a, b) { + const aa = (a >>> 24) & 0xff; + const ar = (a >>> 16) & 0xff; + const ag = (a >>> 8) & 0xff; + const ab = a & 0xff; + const ba = (b >>> 24) & 0xff; + const br = (b >>> 16) & 0xff; + const bg = (b >>> 8) & 0xff; + const bb = b & 0xff; + const cr = ((ar * br) / 255) | 0; + const cg = ((ag * bg) / 255) | 0; + const cb = ((ab * bb) / 255) | 0; + const ca = ((aa * ba) / 255) | 0; + return ((ca << 24) | (cr << 16) | (cg << 8) | cb) >>> 0; +} + /** * A WebGL Batcher for rendering textured triangle meshes. * Uses indexed drawing to efficiently render arbitrary triangle geometry. @@ -50,22 +77,21 @@ export default class MeshBatcher extends MaterialBatcher { } /** - * Add a textured mesh to the batch. When `group` is provided, only - * the index slice `[group.start, group.start + group.count)` is - * pushed — lets `Mesh.draw()` render multi-material OBJs one - * material at a time without rebuilding geometry. + * Add a textured mesh to the batch. When the mesh has a + * `vertexColors` array (multi-material OBJ + bound MTL), each + * vertex's `aColor` attribute comes from that buffer instead of + * the shared `tint` argument — so the mesh batches in a single + * draw call with per-material colors baked into the vertex stream. + * The `tint` argument is then multiplied per-vertex in the shader, + * preserving runtime flash / fade / team-color effects. * @param {object} mesh - a Mesh object with vertices, uvs, indices, and texture properties * @param {number} tint - tint color in UINT32 (argb) format - * @param {{start: number, count: number}} [group] - optional index buffer slice */ - addMesh(mesh, tint, group) { + addMesh(mesh, tint) { const vertices = mesh.vertices; const uvs = mesh.uvs; const indices = mesh.indices; - // `triIdx` and `endLimit` bracket the index range to draw — - // the whole buffer by default, or just the requested group slice - const startIdx = group ? group.start : 0; - const endLimit = group ? group.start + group.count : indices.length; + const vertexColors = mesh.vertexColors; // upload and activate the texture const unit = this.uploadTexture(mesh.texture); @@ -80,8 +106,8 @@ export default class MeshBatcher extends MaterialBatcher { const maxIndices = this.indexBuffer.data.length; // process triangles in chunks that fit the buffer - let triIdx = startIdx; - while (triIdx < endLimit) { + let triIdx = 0; + while (triIdx < indices.length) { // figure out how many triangles fit in the current batch const vertexData = this.vertexData; const availVerts = maxVerts - vertexData.vertexCount; @@ -97,7 +123,7 @@ export default class MeshBatcher extends MaterialBatcher { continue; } - const endIdx = Math.min(triIdx + maxTris * 3, endLimit); + const endIdx = Math.min(triIdx + maxTris * 3, indices.length); // build a local vertex remap for this chunk // capture base offset before pushing any vertices @@ -126,7 +152,16 @@ export default class MeshBatcher extends MaterialBatcher { y = _v.y; } - vertexData.pushMesh(x, y, z, uvs[i2], uvs[i2 + 1], tint); + // per-vertex color when the mesh provides one + // (multi-material baked colors), modulated by the + // runtime `tint` so flash / fade / team color via + // `setTint` still works on top of the baked palette. + // Single-material meshes (no `vertexColors`) fall + // back to the shared `tint` for every vertex. + const vertColor = vertexColors + ? mulPackedARGB(vertexColors[origIdx], tint) + : tint; + vertexData.pushMesh(x, y, z, uvs[i2], uvs[i2 + 1], vertColor); } // absolute index = baseOffset + localIdx chunkIndices.push(baseOffset + localIdx); diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index 1f3748ec9..b5fdf7292 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -1222,17 +1222,15 @@ export default class WebGLRenderer extends Renderer { * meshes are automatically chunked across multiple draw calls to * fit the vertex/index buffer limits. * - * When called with a `group` (a `{start, count}` slice of the - * mesh's index buffer), only that slice is drawn — used by `Mesh` - * to render multi-material OBJs one material at a time. The depth - * buffer is cleared only on the first call per frame; subsequent - * group draws compose against the existing depth so the multi- - * material draws still test against each other. + * Multi-material meshes (OBJ files with multiple `usemtl` groups + + * a bound MTL) draw in a SINGLE call here — the per-material + * colors are baked into `mesh.vertexColors` at construction time + * and pushed through the batcher's per-vertex `aColor` attribute. + * `mesh.tint` still multiplies on top at draw time, so flash / + * fade / team-color via `setTint` work the same as single-material. * @param {Mesh} mesh - a Mesh renderable or compatible object - * @param {{start: number, count: number}} [group] - optional index - * buffer slice to draw (defaults to the whole mesh) */ - drawMesh(mesh, group) { + drawMesh(mesh) { const gl = this.gl; this.setBatcher("mesh"); @@ -1246,15 +1244,8 @@ export default class WebGLRenderer extends Renderer { gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LESS); gl.depthMask(true); - // Only clear depth on the first draw of a mesh — subsequent - // per-group calls within the same Mesh.draw() compose against - // each other so far/near occlusion across materials stays - // correct. Mesh signals "first call" by passing no group OR by - // passing group with start === 0 (the first material's slice). - if (!group || group.start === 0) { - gl.clearDepth(1.0); - gl.clear(gl.DEPTH_BUFFER_BIT); - } + gl.clearDepth(1.0); + gl.clear(gl.DEPTH_BUFFER_BIT); // disable blending during opaque mesh rendering to avoid depth/blend conflicts gl.disable(gl.BLEND); @@ -1269,7 +1260,6 @@ export default class WebGLRenderer extends Renderer { this.currentBatcher.addMesh( mesh, this.currentTint.toUint32(this.getGlobalAlpha()), - group, ); // flush and restore GL state From e434934208238bbea6b62da2a1bd7ecc5c6f1fea Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 10:23:26 +0800 Subject: [PATCH 04/13] fix(examples): force WebGL on multi-material OBJ showcase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canvas multi-material rendering is correct but does per-triangle solid-fill in JS — 10-50× slower than the GPU rasterizer for the same scene. Force WebGL at the Application constructor so users opening the example see usable frame rates. Canvas remains supported in the engine for fallback / debug / correctness purposes; just not the default for this example. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ExampleMultiMaterialMesh.tsx | 98 ++++++++++++++----- 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx b/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx index 30f53ec39..89f59e0d5 100644 --- a/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx +++ b/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx @@ -20,11 +20,13 @@ * See `packages/examples/LICENSE.md` for full license + asset credits * (Kenney Space Kit 2.0, CC0). */ +import { DebugPanelPlugin } from "@melonjs/debug-plugin"; import type { CanvasRenderer, WebGLRenderer } from "melonjs"; import { Application, loader, Mesh, + plugin, Renderable, Vector3d, video, @@ -34,30 +36,49 @@ import { createExampleComponent } from "../utils"; const base = `${import.meta.env.BASE_URL}assets/multiMaterialMesh/`; // the four Kenney spacecraft to showcase; each .obj has its companion -// .mtl with 3-5 differently-colored materials +// .mtl with 3-5 differently-colored materials. Per-ship "team color" +// multipliers are applied on top of the MTL palette so the four ships +// don't all read as the same grey-and-orange blob — the multiplication +// preserves the per-material contrast (orange wings stay brighter than +// the body, dark cockpits stay dark) while tinting the whole craft +// toward its team hue. const CRAFTS = [ - "craft_speederA", - "craft_speederB", - "craft_racer", - "craft_miner", + { name: "craft_speederA", tint: [1.0, 0.6, 0.55] }, // crimson + { name: "craft_speederB", tint: [0.55, 0.75, 1.0] }, // ice blue + { name: "craft_racer", tint: [0.55, 1.0, 0.65] }, // jade + { name: "craft_miner", tint: [1.0, 0.95, 0.55] }, // gold ]; const createGame = () => { const app = new Application(1024, 768, { parent: "screen", - renderer: video.AUTO, + // Multi-material 3D meshes require the WebGL renderer for usable + // frame rates — Canvas falls back to per-triangle solid-fill in + // JS, which is correct (per-vertex baked colors, global painter's + // sort) but is 10-50× slower than the GPU rasterizer for the + // same scene. Force WebGL here. + renderer: video.WEBGL, scale: "auto", }); app.world.backgroundColor.parseCSS("#0a0a1f"); + plugin.register(DebugPanelPlugin, "debugPanel"); // preload every craft's OBJ + matching MTL. `type: "mtl"` parses // the materials so the Mesh constructor can look them up by name // later via `material: `. const assets = []; - for (const name of CRAFTS) { - assets.push({ name, type: "obj", src: `${base}${name}.obj` }); - assets.push({ name, type: "mtl", src: `${base}${name}.mtl` }); + for (const craft of CRAFTS) { + assets.push({ + name: craft.name, + type: "obj", + src: `${base}${craft.name}.obj`, + }); + assets.push({ + name: craft.name, + type: "mtl", + src: `${base}${craft.name}.mtl`, + }); } loader.preload(assets, () => { @@ -75,7 +96,13 @@ const createGame = () => { class SpinningCraft extends Renderable { mesh: Mesh; - constructor(modelName: string, x: number, y: number, size: number) { + constructor( + modelName: string, + teamTint: number[], + x: number, + y: number, + size: number, + ) { super(0, 0, 1024, 768); this.anchorPoint.set(0, 0); this.mesh = new Mesh(x, y, { @@ -85,6 +112,22 @@ const createGame = () => { height: size, cullBackFaces: true, }); + // Apply the per-ship team color via `mesh.tint`. The + // per-material Kd values are already baked into the + // vertex stream at construction time (multi-material + // tier 2), so `mesh.tint` here is the global + // multiplier — every panel's baked color × team color. + // Each material's relative brightness is preserved + // (orange wings stay brighter than the body, dark + // cockpits stay dark) while the whole craft shifts + // toward its team hue. This is the canonical + // "vertex color × material color × runtime tint" + // pattern from real-time 3D. + this.mesh.tint.setColor( + Math.round(teamTint[0] * 255), + Math.round(teamTint[1] * 255), + Math.round(teamTint[2] * 255), + ); } override update(dt: number): boolean { @@ -103,23 +146,37 @@ const createGame = () => { } } - // 2x2 grid layout + // 2x2 grid layout. Y positions are shifted up from cell centers + // so the per-row labels (placed just above each ship) and any + // cut-off below the canvas in narrow viewports don't push the + // bottom row off-screen. const cellW = 1024 / 2; - const cellH = 768 / 2; - const meshSize = 280; + const meshSize = 240; + // engine Y for each row's mesh center — top row at ~25% of the + // 768 canvas, bottom row at ~65%. Labels go just above (~18% + // below the row top edge). + const ROW_Y = [200, 530]; for (let i = 0; i < CRAFTS.length; i++) { const col = i % 2; const row = Math.floor(i / 2); const cx = cellW * col + cellW / 2; - const cy = cellH * row + cellH / 2; - app.world.addChild(new SpinningCraft(CRAFTS[i], cx, cy, meshSize)); + const craft = CRAFTS[i]; + app.world.addChild( + new SpinningCraft(craft.name, craft.tint, cx, ROW_Y[row], meshSize), + ); } - // labels for each craft — HTML overlay since the grid is fixed + // HTML labels positioned above each craft. The canvas is scaled + // by `scale: "auto"` so we use % of the parent (which wraps the + // canvas) for both axes — keeps labels aligned regardless of the + // final display size. const labelStyle = "position:absolute;color:#e0e0e0;font-family:'Courier New',monospace;" + "font-size:14px;font-weight:bold;text-shadow:0 0 4px #000;" + - "z-index:1000;pointer-events:none;"; + "z-index:1000;pointer-events:none;transform:translate(-50%,-50%);"; + // label Y as % of the 768 canvas height, sitting just above the + // mesh center (mesh half-extent ≈ meshSize/2 → ~125 engine px) + const LABEL_Y_PCT = [(ROW_Y[0] - 130) / 768, (ROW_Y[1] - 130) / 768]; const parent = app.renderer.getCanvas().parentElement; if (parent) { parent.style.position = "relative"; @@ -127,11 +184,8 @@ const createGame = () => { const col = i % 2; const row = Math.floor(i / 2); const label = document.createElement("div"); - label.textContent = CRAFTS[i].replace("craft_", ""); - // approximate engine→screen position; close enough for - // labels (renderer.getCanvas().getBoundingClientRect() - // would be exact but this is fine for a static layout) - label.style.cssText = `${labelStyle}left:${(col * 0.5 + 0.25) * 100}%;top:${(row * 0.5 + 0.45) * 100}%;transform:translate(-50%,0);`; + label.textContent = CRAFTS[i].name.replace("craft_", ""); + label.style.cssText = `${labelStyle}left:${(col * 0.5 + 0.25) * 100}%;top:${LABEL_Y_PCT[row] * 100}%;`; parent.appendChild(label); } } From 41ca71501d832715a1204be568c2218cc9aa167c Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 10:32:47 +0800 Subject: [PATCH 05/13] chore(examples): tighten multi-material example + gallery description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restructure example into named sections (layout / entry point / craft renderable / scene helpers) with extracted constants (CANVAS_W, CANVAS_H, MESH_SIZE, ROW_Y) — same behavior, easier to read at a glance - Trim verbose inline comments to one short sentence each; remove stale references to "per draw call" / tier-1 framing - Restate the file header in tier-2 terms: per-vertex color baking + single GPU draw, with `mesh.tint` as the runtime multiplier - Gallery description shifts from asset-attribution + implementation framing to engine-feature framing: "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." Matches the tone of the neighboring 3D Mesh / 3D Material gallery cards. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ExampleMultiMaterialMesh.tsx | 294 +++++++++--------- packages/examples/src/main.tsx | 2 +- 2 files changed, 154 insertions(+), 142 deletions(-) diff --git a/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx b/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx index 89f59e0d5..a53bd8187 100644 --- a/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx +++ b/packages/examples/src/examples/multiMaterialMesh/ExampleMultiMaterialMesh.tsx @@ -1,20 +1,21 @@ /** - * melonJS — Multi-material OBJ mesh example. + * melonJS — Multi-material OBJ mesh showcase. * - * Loads four Kenney Space Kit (CC0) spacecraft, each with multiple - * `usemtl` material groups (metal / metalRed / metalDark / dark / …), - * and renders them side by side rotating in 3D. Every panel of every - * spacecraft picks up the diffuse color (`Kd`) from its bound MTL - * material, so the multi-color paint scheme of each model survives — - * the OBJ parser emits `groups: [{materialName, start, count}, …]` - * (matching the Three.js / glTF convention) and `Mesh.draw()` iterates - * those, swapping tint per draw call. + * Loads four Kenney Space Kit (CC0) spacecraft, each with 3-5 named + * MTL materials (metal / metalRed / metalDark / dark / …), and renders + * them rotating in a 2×2 grid. The OBJ parser emits a Three.js / glTF + * style `groups[]` array keyed by `materialName`; the `Mesh` + * constructor bakes each material's diffuse color (`Kd`) into a + * per-vertex color buffer so the whole mesh draws in a single GPU + * call. `mesh.tint` then multiplies on top at render time — used here + * to give each ship its own team color while keeping the per-material + * palette intact (orange wings stay bright, dark cockpits stay dark). * * Compare with the `mesh3d` example: that one binds a single texture * across the whole mesh (checkerboard on cube / sphere / teapot). - * This one demonstrates the multi-material code path — same Mesh - * class, no extra wiring beyond passing the MTL name through - * `material:`. + * This one exercises the multi-material code path — same `Mesh` API, + * the only extra wiring is preloading the matching `.mtl` and passing + * its name through `material:`. * * Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License. * See `packages/examples/LICENSE.md` for full license + asset credits @@ -33,15 +34,24 @@ import { } from "melonjs"; import { createExampleComponent } from "../utils"; -const base = `${import.meta.env.BASE_URL}assets/multiMaterialMesh/`; +// ─── layout & content ───────────────────────────────────────────── -// the four Kenney spacecraft to showcase; each .obj has its companion -// .mtl with 3-5 differently-colored materials. Per-ship "team color" -// multipliers are applied on top of the MTL palette so the four ships -// don't all read as the same grey-and-orange blob — the multiplication -// preserves the per-material contrast (orange wings stay brighter than -// the body, dark cockpits stay dark) while tinting the whole craft -// toward its team hue. +const CANVAS_W = 1024; +const CANVAS_H = 768; +const MESH_SIZE = 240; + +// engine-space Y for each row's mesh center. Top row ≈ 26% of the +// canvas, bottom row ≈ 69% — shifted up from cell centers so the +// per-row labels (drawn just above each ship) and any sub-pixel +// canvas cropping don't push the bottom row off-screen. +const ROW_Y = [200, 530]; + +const ASSET_BASE = `${import.meta.env.BASE_URL}assets/multiMaterialMesh/`; + +// The four Kenney spacecraft to showcase. Per-ship `tint` is a +// 0..1 RGB multiplier applied via `mesh.tint` after the MTL palette +// is baked into the vertex stream — multiplicative, so each +// material's contrast survives and the whole craft just shifts hue. const CRAFTS = [ { name: "craft_speederA", tint: [1.0, 0.6, 0.55] }, // crimson { name: "craft_speederB", tint: [0.55, 0.75, 1.0] }, // ice blue @@ -49,14 +59,14 @@ const CRAFTS = [ { name: "craft_miner", tint: [1.0, 0.95, 0.55] }, // gold ]; +// ─── entry point ────────────────────────────────────────────────── + const createGame = () => { - const app = new Application(1024, 768, { + const app = new Application(CANVAS_W, CANVAS_H, { parent: "screen", - // Multi-material 3D meshes require the WebGL renderer for usable - // frame rates — Canvas falls back to per-triangle solid-fill in - // JS, which is correct (per-vertex baked colors, global painter's - // sort) but is 10-50× slower than the GPU rasterizer for the - // same scene. Force WebGL here. + // Multi-material 3D meshes need the WebGL renderer for usable + // frame rates — Canvas would solid-fill per triangle in JS, + // correct but 10-50× slower than the GPU rasterizer. renderer: video.WEBGL, scale: "auto", }); @@ -64,132 +74,134 @@ const createGame = () => { app.world.backgroundColor.parseCSS("#0a0a1f"); plugin.register(DebugPanelPlugin, "debugPanel"); - // preload every craft's OBJ + matching MTL. `type: "mtl"` parses - // the materials so the Mesh constructor can look them up by name - // later via `material: `. + loader.preload(buildAssetList(), () => { + spawnCrafts(app); + spawnLabels(app); + }); +}; + +// Build the list of (.obj, .mtl) pairs for every craft. `type: "mtl"` +// runs the MTL parser, populating the material cache so the Mesh +// constructor can look entries up by name via `material:`. +function buildAssetList() { const assets = []; for (const craft of CRAFTS) { assets.push({ name: craft.name, type: "obj", - src: `${base}${craft.name}.obj`, + src: `${ASSET_BASE}${craft.name}.obj`, }); assets.push({ name: craft.name, type: "mtl", - src: `${base}${craft.name}.mtl`, + src: `${ASSET_BASE}${craft.name}.mtl`, }); } + return assets; +} - loader.preload(assets, () => { - const axisY = new Vector3d(0, 1, 0); - const axisX = new Vector3d(1, 0, 0); - - /** - * Renderable wrapper for one spinning spacecraft. Owns a single - * `Mesh` instance and drives its 3D rotation each frame. The - * Mesh sees that the OBJ has multiple material groups + we - * passed `material: name`, so it builds an internal - * `mesh.groups` array and `draw()` issues one tinted draw call - * per material region. - */ - class SpinningCraft extends Renderable { - mesh: Mesh; - - constructor( - modelName: string, - teamTint: number[], - x: number, - y: number, - size: number, - ) { - super(0, 0, 1024, 768); - this.anchorPoint.set(0, 0); - this.mesh = new Mesh(x, y, { - model: modelName, - material: modelName, // MTL name = same as OBJ for Kenney pack - width: size, - height: size, - cullBackFaces: true, - }); - // Apply the per-ship team color via `mesh.tint`. The - // per-material Kd values are already baked into the - // vertex stream at construction time (multi-material - // tier 2), so `mesh.tint` here is the global - // multiplier — every panel's baked color × team color. - // Each material's relative brightness is preserved - // (orange wings stay brighter than the body, dark - // cockpits stay dark) while the whole craft shifts - // toward its team hue. This is the canonical - // "vertex color × material color × runtime tint" - // pattern from real-time 3D. - this.mesh.tint.setColor( - Math.round(teamTint[0] * 255), - Math.round(teamTint[1] * 255), - Math.round(teamTint[2] * 255), - ); - } - - override update(dt: number): boolean { - // Y spin + slight X wobble — gives every panel of the - // model a chance to face camera so the per-material - // colors are all visible across the animation - this.mesh.rotate(dt * 0.0008, axisY); - this.mesh.rotate(dt * 0.0003, axisX); - return true; - } - - override draw(renderer: WebGLRenderer | CanvasRenderer): void { - this.mesh.preDraw(renderer); - this.mesh.draw(renderer); - this.mesh.postDraw(renderer); - } - } - - // 2x2 grid layout. Y positions are shifted up from cell centers - // so the per-row labels (placed just above each ship) and any - // cut-off below the canvas in narrow viewports don't push the - // bottom row off-screen. - const cellW = 1024 / 2; - const meshSize = 240; - // engine Y for each row's mesh center — top row at ~25% of the - // 768 canvas, bottom row at ~65%. Labels go just above (~18% - // below the row top edge). - const ROW_Y = [200, 530]; - for (let i = 0; i < CRAFTS.length; i++) { - const col = i % 2; - const row = Math.floor(i / 2); - const cx = cellW * col + cellW / 2; - const craft = CRAFTS[i]; - app.world.addChild( - new SpinningCraft(craft.name, craft.tint, cx, ROW_Y[row], meshSize), - ); - } - - // HTML labels positioned above each craft. The canvas is scaled - // by `scale: "auto"` so we use % of the parent (which wraps the - // canvas) for both axes — keeps labels aligned regardless of the - // final display size. - const labelStyle = - "position:absolute;color:#e0e0e0;font-family:'Courier New',monospace;" + - "font-size:14px;font-weight:bold;text-shadow:0 0 4px #000;" + - "z-index:1000;pointer-events:none;transform:translate(-50%,-50%);"; - // label Y as % of the 768 canvas height, sitting just above the - // mesh center (mesh half-extent ≈ meshSize/2 → ~125 engine px) - const LABEL_Y_PCT = [(ROW_Y[0] - 130) / 768, (ROW_Y[1] - 130) / 768]; - const parent = app.renderer.getCanvas().parentElement; - if (parent) { - parent.style.position = "relative"; - for (let i = 0; i < CRAFTS.length; i++) { - const col = i % 2; - const row = Math.floor(i / 2); - const label = document.createElement("div"); - label.textContent = CRAFTS[i].name.replace("craft_", ""); - label.style.cssText = `${labelStyle}left:${(col * 0.5 + 0.25) * 100}%;top:${LABEL_Y_PCT[row] * 100}%;`; - parent.appendChild(label); - } - } - }); -}; +// ─── per-craft renderable ───────────────────────────────────────── + +const AXIS_Y = new Vector3d(0, 1, 0); +const AXIS_X = new Vector3d(1, 0, 0); + +/** + * One spinning spacecraft. Owns a `Mesh` constructed from the OBJ + + * MTL pair — the Mesh detects multi-material from the OBJ's `groups` + * array and bakes per-material colors into its vertex stream. The + * team color is then applied via `mesh.tint` (multiplicative against + * the baked palette). + */ +class SpinningCraft extends Renderable { + mesh: Mesh; + + constructor( + modelName: string, + teamTint: number[], + x: number, + y: number, + size: number, + ) { + super(0, 0, CANVAS_W, CANVAS_H); + this.anchorPoint.set(0, 0); + this.mesh = new Mesh(x, y, { + model: modelName, + material: modelName, // MTL name matches OBJ name in the Kenney pack + width: size, + height: size, + cullBackFaces: true, + }); + this.mesh.tint.setColor( + Math.round(teamTint[0] * 255), + Math.round(teamTint[1] * 255), + Math.round(teamTint[2] * 255), + ); + } + + override update(dt: number): boolean { + // Y spin + slight X wobble so every face of the model rotates + // into view across the animation, exercising the per-material + // color separation from all sides. + this.mesh.rotate(dt * 0.0008, AXIS_Y); + this.mesh.rotate(dt * 0.0003, AXIS_X); + return true; + } + + override draw(renderer: WebGLRenderer | CanvasRenderer): void { + this.mesh.preDraw(renderer); + this.mesh.draw(renderer); + this.mesh.postDraw(renderer); + } +} + +// ─── scene helpers ──────────────────────────────────────────────── + +function spawnCrafts(app: Application) { + const cellW = CANVAS_W / 2; + for (let i = 0; i < CRAFTS.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const cx = cellW * col + cellW / 2; + const craft = CRAFTS[i]; + app.world.addChild( + new SpinningCraft(craft.name, craft.tint, cx, ROW_Y[row], MESH_SIZE), + ); + } +} + +/** + * HTML labels positioned just above each craft. The canvas is scaled + * by `scale: "auto"`, so positions are expressed as a percentage of + * the parent element (the canvas wrapper) — they stay aligned with + * the meshes regardless of the displayed canvas size. + */ +function spawnLabels(app: Application) { + const parent = app.renderer.getCanvas().parentElement; + if (!parent) { + return; + } + parent.style.position = "relative"; + + // label Y sits just above the mesh center (mesh half-extent ≈ MESH_SIZE/2) + const labelOffsetY = MESH_SIZE / 2 + 10; + const labelYPct = [ + (ROW_Y[0] - labelOffsetY) / CANVAS_H, + (ROW_Y[1] - labelOffsetY) / CANVAS_H, + ]; + + const baseStyle = + "position:absolute;color:#e0e0e0;font-family:'Courier New',monospace;" + + "font-size:14px;font-weight:bold;text-shadow:0 0 4px #000;" + + "z-index:1000;pointer-events:none;transform:translate(-50%,-50%);"; + + for (let i = 0; i < CRAFTS.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const label = document.createElement("div"); + label.textContent = CRAFTS[i].name.replace("craft_", ""); + label.style.cssText = `${baseStyle}left:${(col * 0.5 + 0.25) * 100}%;top:${labelYPct[row] * 100}%;`; + parent.appendChild(label); + } +} export const ExampleMultiMaterialMesh = createExampleComponent(createGame); diff --git a/packages/examples/src/main.tsx b/packages/examples/src/main.tsx index 1b1c0a4c6..ca36da0ad 100644 --- a/packages/examples/src/main.tsx +++ b/packages/examples/src/main.tsx @@ -348,7 +348,7 @@ const examples: { path: "multi-material-mesh", sourceDir: "multiMaterialMesh", description: - "Kenney spacecraft (CC0) with multi-material OBJ rendering — each `usemtl` group draws with its own diffuse color via the new groups[] API.", + "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: , From d5a1f0452055468eb84c7cfdece8a11e6e7e8898 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 10:34:35 +0800 Subject: [PATCH 06/13] docs(changelog): multi-material OBJ rendering for 19.7.0 Add the per-vertex baked color path + single-draw-call multi-material to the 19.7.0 unreleased section. Documents the OBJ parser groups[] emission, Mesh's per-vertex color baking, the renderer single-draw path (WebGL aColor / Canvas solid-fill), and runtime tinting on top. Calls out single-material backward compatibility (unchanged) and the new Multi-material OBJ showcase. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index dbe7d8b7f..afae5fcf0 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -7,6 +7,7 @@ ### Added - **`renderer.setDepth(depth)`** — new public method on the base `Renderer`. Mirrors the existing `setTint` / `setColor` state-setter pattern. Sets `renderer.currentDepth`, which the batchers read at vertex-emit time and push as the z component of each vertex. `Renderable.preDraw` now forwards `this.depth` automatically, so user code typically never needs to call `setDepth` directly. - Per-sprite depth on the GPU: all batched draw paths (`QuadBatcher`, `LitQuadBatcher`, `PrimitiveBatcher`, GPU TMX) now carry the renderable's `.depth` as the z component of their vertex stream. Default shaders consume it via `vec4(aVertex, 1.0)` in `gl_Position`. With an orthographic projection (the engine default), z has no visible effect — existing 2D apps render identically. With a perspective projection, sprites at different depths are correctly scaled and parallaxed by the projection matrix. +- **Multi-material OBJ rendering** — `Mesh` now correctly draws OBJ files with multiple `usemtl` directives + a bound MTL, instead of collapsing the whole model to a single uniform color. The OBJ parser emits `groups: Array<{materialName, start, count}>` matching the Three.js / glTF convention. The Mesh constructor bakes each material's `Kd` into a per-vertex color buffer at construction time, so the whole mesh draws in a **single GPU draw call** regardless of material count. `mesh.tint` multiplies on top at render time — flash / fade / team-color via `setTint` work exactly like single-material meshes. Per-material textures (each material has its own `map_Kd`) are supported on WebGL via the per-vertex `aColor` path; Canvas falls back to solid-fill from the baked colors. Single-material meshes are unchanged — same code path, same allocations, same behavior. New `Multi-material OBJ` example showcases the feature with four Kenney Space Kit spacecraft, each given a distinct team color via `mesh.tint`. ### Changed - **Vertex attribute layout: `aVertex` widened from `vec2` to `vec3`** across `quad-multi.vert`, `quad-multi-lit.vert`, `primitive.vert`, `orthogonal-tmxlayer.vert`. `Mesh`'s shader already used `vec3 aVertex` — all batchers are now uniform. Per-vertex stride grows by 4 bytes (24 → 28 for the quad layout). Custom shaders binding attributes by name (`gl.getAttribLocation`) — the standard pattern, and what `GLShader` enforces — keep working unchanged; the byte-offset shift of `aRegion` / `aColor` / `aTextureId` is transparent because the batcher updates its own `vertexAttribPointer` offsets. Custom shaders declaring `attribute vec2 aVertex;` continue to work — WebGL silently drops the unused z component. From 2b5fc0314929335bf6d0e7e4d8e2a54cca5d092c Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 10:40:11 +0800 Subject: [PATCH 07/13] docs(mtl): drop stale single-material-only limitation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two outdated MTL parser comments removed: - Function header claimed "Multiple materials per mesh (`usemtl`) are not supported — only the first material is used" — now false since Mesh resolves per-material colors / textures via the OBJ `groups[]` emitted by the parser. - "(was: warn on multi-material — obsolete…)" inline comment near the `case "newmtl":` block — pure archaeology referencing a removed warning; the current code is self-explanatory. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/src/loader/parsers/mtl.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/melonjs/src/loader/parsers/mtl.js b/packages/melonjs/src/loader/parsers/mtl.js index 7225b715d..c8ddb48b5 100644 --- a/packages/melonjs/src/loader/parsers/mtl.js +++ b/packages/melonjs/src/loader/parsers/mtl.js @@ -41,7 +41,6 @@ const UNSUPPORTED_MAPS = new Set([ * - Only one `map_Kd` texture per material is supported * - Specular (`Ks`, `Ns`), ambient (`Ka`), and illumination model (`illum`) are parsed but ignored * - Normal maps (`map_bump`, `bump`), specular maps (`map_Ks`), and other texture maps are not supported - * - Multiple materials per mesh (`usemtl`) are not supported — only the first material is used * * @param {string} text - raw MTL file contents * @param {string} basePath - base URL path for resolving texture references @@ -82,10 +81,6 @@ function parseMTL(text, basePath) { switch (keyword) { case "newmtl": - // (was: warn on multi-material — obsolete since Mesh now - // resolves per-material draw groups via OBJ `groups[]`, - // see `Mesh.draw`; each named material is rendered with - // its own tint + texture in its own draw call) if (!parts[1]) { console.warn("MTL: newmtl missing material name, skipping"); break; From eb96fc604a86085d41a62ddf37a99a39a3a1b0e5 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 10:55:06 +0800 Subject: [PATCH 08/13] refactor(renderer): worker-safe white-pixel helper as Renderer static MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two PR review threads pointed out that the multi-material work added bare `document.createElement("canvas")` calls in Mesh and the Canvas renderer — these throw in OffscreenCanvas / worker contexts where `document` is undefined. Introduce `Renderer.getWhitePixel()` — a lazily-created, shared 1×1 white canvas using `OffscreenCanvas` where supported and falling back to `document.createElement` otherwise. Static (not instance-bound) so it's reachable from `Mesh` construction before any active renderer exists. Mesh's `isMultiMaterial` Kd-only path now uses `Renderer.getWhitePixel()` instead of its own local `getOrCreateWhitePixel()`; CanvasRenderer's `_meshColorCanvas` scratch sampler now goes through the same OffscreenCanvas-aware allocation pattern. Kept inline (not via `CanvasRenderTarget`) — that abstraction is sized for full-blown render targets with their own GL context, way heavier than what a 1x1 fallback needs. Also kept off the `video` namespace — `video.createCanvas` is on a deprecation path; canvas allocation is renderer-side concern now. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/src/renderable/mesh.js | 20 ++--------- .../src/video/canvas/canvas_renderer.js | 10 ++++-- packages/melonjs/src/video/renderer.js | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/melonjs/src/renderable/mesh.js b/packages/melonjs/src/renderable/mesh.js index d882db3e5..efdf3bb22 100644 --- a/packages/melonjs/src/renderable/mesh.js +++ b/packages/melonjs/src/renderable/mesh.js @@ -9,6 +9,7 @@ import { normalizeVertices, projectVertices, } from "../math/vertex.ts"; +import Renderer from "./../video/renderer.js"; import { TextureAtlas } from "./../video/texture/atlas.js"; import Renderable from "./renderable.js"; @@ -21,23 +22,6 @@ import Renderable from "./renderable.js"; // reusable matrix for combining projection × model in draw() const _combinedMatrix = new Matrix3d(); -// Lazily-allocated 1×1 white pixel used as the texture fallback for -// flat-color (Kd-only, no `map_Kd`) MTL materials. One canvas shared -// across every Mesh that needs it — no per-instance allocation. -let _whitePixel = null; -function getOrCreateWhitePixel() { - if (!_whitePixel) { - const c = document.createElement("canvas"); - c.width = 1; - c.height = 1; - const ctx = c.getContext("2d"); - ctx.fillStyle = "#ffffff"; - ctx.fillRect(0, 0, 1, 1); - _whitePixel = c; - } - return _whitePixel; -} - // Resolve any acceptable texture input (TextureAtlas, image / canvas // object, or asset name) to a cached `TextureAtlas`. Throws if nothing // resolves — Mesh requires a texture binding for its GL pipeline. @@ -315,7 +299,7 @@ export default class Mesh extends Renderable { // the GPU pipeline still has something to sample — the per- // group `tint` does all the visible coloring. if (!textureSource && isMultiMaterial) { - textureSource = getOrCreateWhitePixel(); + textureSource = Renderer.getWhitePixel(); } this.texture = resolveTextureAtlas(textureSource); diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index 9663f8967..e0444c43a 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -444,9 +444,15 @@ export default class CanvasRenderer extends Renderer { let solidFillStyle = null; if (solidFillKd && !vertexColors) { // Single-material 1×1 path — one fill color for the whole - // mesh, sampled from the pre-tinted image + // mesh, sampled from the pre-tinted image. Allocate the + // scratch sampler canvas via the OffscreenCanvas-aware path + // so this works inside workers / `OffscreenCanvas` contexts + // (a bare `document.createElement` would throw there). if (!this._meshColorCanvas) { - this._meshColorCanvas = document.createElement("canvas"); + this._meshColorCanvas = + typeof globalThis.OffscreenCanvas !== "undefined" + ? new globalThis.OffscreenCanvas(1, 1) + : globalThis.document.createElement("canvas"); this._meshColorCanvas.width = 1; this._meshColorCanvas.height = 1; this._meshColorCtx = this._meshColorCanvas.getContext("2d"); diff --git a/packages/melonjs/src/video/renderer.js b/packages/melonjs/src/video/renderer.js index 5b4242d20..0b8a0021a 100644 --- a/packages/melonjs/src/video/renderer.js +++ b/packages/melonjs/src/video/renderer.js @@ -272,6 +272,35 @@ export default class Renderer { return this.renderTarget.canvas; } + /** + * Shared 1×1 fully-white canvas used as a no-op texture fallback. + * Renderers and renderables that need a "blank" texture binding + * (e.g. to satisfy a shader's sampler input when there's no real + * image — Kd-only `Mesh` materials, solid-color quad fills, etc.) + * should use this rather than allocating their own. + * + * Lazily created on first call; shared across every caller; uses + * `OffscreenCanvas` where supported (worker-safe). Static so it's + * accessible without a renderer instance (e.g. from a `Mesh` + * constructor that runs before the active renderer is set). + * @returns {HTMLCanvasElement|OffscreenCanvas} the shared 1×1 white canvas + */ + static getWhitePixel() { + if (Renderer._whitePixel === null) { + const c = + typeof globalThis.OffscreenCanvas !== "undefined" + ? new globalThis.OffscreenCanvas(1, 1) + : globalThis.document.createElement("canvas"); + c.width = 1; + c.height = 1; + const ctx = c.getContext("2d"); + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, 1, 1); + Renderer._whitePixel = c; + } + return Renderer._whitePixel; + } + /** * return a reference to the current render target corresponding Context * @returns {CanvasRenderingContext2D|WebGLRenderingContext} @@ -1012,3 +1041,8 @@ export default class Renderer { return this.renderTarget.toDataURL(type, quality); } } + +// Backing field for `Renderer.getWhitePixel()` — declared outside the +// class body so it's initialized to null at module load (static class +// fields aren't universally supported in our transpile target). +Renderer._whitePixel = null; From 400cfbf1c4da84d46eb4e66affd1cd7d9c9dd697 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 11:14:58 +0800 Subject: [PATCH 09/13] refactor(renderer): Renderer.createCanvas; deprecate video.createCanvas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promotes canvas allocation from the `video` namespace (which is on a deprecation path) to a static `Renderer.createCanvas(width, height, returnOffscreenCanvas)` — the natural home for canvas-related utilities that the renderer-side of the engine needs. `Renderer.getWhitePixel()` now routes through it; CanvasRenderer's multi-material 1×1 scratch sampler, CanvasRenderTarget, and TMXLayer all migrated to `Renderer.createCanvas` (no deprecation warnings from internal code). `video.createCanvas` lives on as a deprecated forwarder in `lang/deprecated.js` and is re-exported from `video.js` so existing `video.createCanvas(...)` call sites keep working with a runtime deprecation warning until users migrate. Standard `warning("video.createCanvas", "Renderer.createCanvas", "19.7.0")` notice, matching the existing CanvasTexture / setLineWidth patterns. Also addresses several Copilot review findings in passing: - Multi-material `Mesh`: `this.tint` no longer adopts the first group's color (avoids double-multiplying the baked vertexColors against `mesh.tint` at draw time) - Single-material `Mesh`: now picks the MTL entry named by `objGroups[0].materialName` when present, falling back to the first entry only when no `usemtl` is set (fixes wrong-material selection for OBJs whose `usemtl` targets a non-first MTL entry) - Doc fixes: `resolveGroupMaterial` JSDoc field rename (`material` → `materialName`), `Mesh.draw` comment retired the stale tier-1 "one draw per groups[]" framing, obj.js docstring consistent `materialName: null` Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/src/lang/deprecated.js | 15 ++++++ packages/melonjs/src/level/tiled/TMXLayer.js | 4 +- packages/melonjs/src/loader/parsers/obj.js | 2 +- packages/melonjs/src/renderable/mesh.js | 47 ++++++++++++------- .../src/video/canvas/canvas_renderer.js | 14 ++---- packages/melonjs/src/video/renderer.js | 44 ++++++++++++++--- .../video/rendertarget/canvasrendertarget.js | 4 +- packages/melonjs/src/video/video.js | 39 ++------------- 8 files changed, 96 insertions(+), 73 deletions(-) diff --git a/packages/melonjs/src/lang/deprecated.js b/packages/melonjs/src/lang/deprecated.js index 14952d4be..ffb9ec6c9 100644 --- a/packages/melonjs/src/lang/deprecated.js +++ b/packages/melonjs/src/lang/deprecated.js @@ -1,4 +1,5 @@ import CanvasRenderer from "../video/canvas/canvas_renderer.js"; +import Renderer from "../video/renderer.js"; import CanvasRenderTarget from "../video/rendertarget/canvasrendertarget.js"; import { Batcher } from "../video/webgl/batchers/batcher.js"; import PrimitiveBatcher from "../video/webgl/batchers/primitive_batcher.js"; @@ -10,6 +11,20 @@ import { warning } from "./console.js"; * placeholder for all deprecated classes and corresponding alias for backward compatibility */ +/** + * Create and return a new Canvas element. + * @param {number} width - width + * @param {number} height - height + * @param {boolean} [returnOffscreenCanvas=false] - will return an OffscreenCanvas if supported + * @returns {HTMLCanvasElement|OffscreenCanvas} a new Canvas element of the given size + * @deprecated since 19.7.0 — use {@link Renderer.createCanvas} instead. + * @see Renderer.createCanvas + */ +export function createCanvas(width, height, returnOffscreenCanvas = false) { + warning("video.createCanvas", "Renderer.createCanvas", "19.7.0"); + return Renderer.createCanvas(width, height, returnOffscreenCanvas); +} + /** * @deprecated since 17.1.0 * @see CanvasRenderTarget diff --git a/packages/melonjs/src/level/tiled/TMXLayer.js b/packages/melonjs/src/level/tiled/TMXLayer.js index 0a5e2f18c..47bab25fd 100644 --- a/packages/melonjs/src/level/tiled/TMXLayer.js +++ b/packages/melonjs/src/level/tiled/TMXLayer.js @@ -1,7 +1,7 @@ import { vector2dPool } from "../../math/vector2d.ts"; import Renderable from "../../renderable/renderable.js"; import CanvasRenderer from "../../video/canvas/canvas_renderer"; -import { createCanvas } from "../../video/video.js"; +import Renderer from "../../video/renderer.js"; import { TMX_CLEAR_BIT_MASK, TMX_FLIP_AD, @@ -329,7 +329,7 @@ export default class TMXLayer extends Renderable { // if pre-rendering method is in use, create an offline canvas/renderer if (this.renderMode === "prerender" && !this.canvasRenderer) { this.canvasRenderer = new CanvasRenderer({ - canvas: createCanvas(this.width, this.height), + canvas: Renderer.createCanvas(this.width, this.height), width: this.width, height: this.height, transparent: true, diff --git a/packages/melonjs/src/loader/parsers/obj.js b/packages/melonjs/src/loader/parsers/obj.js index 8c37e49a7..ca1519f25 100644 --- a/packages/melonjs/src/loader/parsers/obj.js +++ b/packages/melonjs/src/loader/parsers/obj.js @@ -38,7 +38,7 @@ const OBJ_INDEX_OFFSET = 1; * pointing to a slice of the unified `indices` buffer, so callers * (e.g. `Mesh`) can render each group with its own material without * touching the geometry. A model with no `usemtl` directives produces - * a single group with `material: null`. + * a single group with `materialName: null`. * * Parsed but ignored: `vn` (normals), `g` (groups), `s` (smooth shading), * `o` (object name). diff --git a/packages/melonjs/src/renderable/mesh.js b/packages/melonjs/src/renderable/mesh.js index efdf3bb22..6930a6b5d 100644 --- a/packages/melonjs/src/renderable/mesh.js +++ b/packages/melonjs/src/renderable/mesh.js @@ -46,7 +46,7 @@ function resolveTextureAtlas(src) { * back to the explicit `textureSource`. Returns a self-contained * record that {@link Mesh#draw} can iterate without touching the MTL * cache or re-resolving anything per frame. - * @param {{material: string|null, start: number, count: number}} group + * @param {{materialName: string|null, start: number, count: number}} group * @param {object} materials - MTL material table keyed by material name * @param {string|HTMLImageElement|TextureAtlas|undefined} textureSource * @returns {object} draw descriptor for this group @@ -239,15 +239,16 @@ export default class Mesh extends Renderable { this.groups = objGroups.map((g) => { return resolveGroupMaterial(g, materials, textureSource); }); - // the legacy `texture` / `tint` / opacity stay set from the - // FIRST group so single-material code paths (e.g. - // `toCanvas()`) still produce a sensible default - const first = this.groups[0]; - textureSource = first.texture; - this.tint.copy(first.tint); - if (first.opacity < 1) { - this.setOpacity(first.opacity); - } + // `this.tint` stays at its default (white) — every + // material's Kd is already baked into `vertexColors` below, + // so applying the first group's tint globally would double- + // multiply it onto every vertex at render time. The + // renderer-level `setTint` path on top of the baked colors + // is still available for runtime flash / fade / team color. + // Use the first group's texture as the legacy + // single-material binding so callers that read `mesh.texture` + // (e.g. `toCanvas`) still get a sensible default. + textureSource = this.groups[0].texture; /** * Per-vertex color buffer (one packed Uint32 per vertex) @@ -274,8 +275,19 @@ export default class Mesh extends Renderable { } } } else if (materials) { - // single-material path — pick the first MTL entry - const mat = materials[Object.keys(materials)[0]]; + // Single-material path. Prefer the MTL entry whose name + // matches the OBJ's `usemtl` directive — an OBJ with one + // `usemtl jet_body` referencing a `.mtl` that defines + // `jet_body` alongside other materials should pick + // `jet_body`, not whichever entry happens to be first in + // the MTL parser's output. Fall back to the first entry + // when there's no `usemtl` at all (the parser emits a + // `materialName: null` anonymous group in that case). + const namedMaterial = + objGroups !== null && objGroups[0] && objGroups[0].materialName + ? materials[objGroups[0].materialName] + : null; + const mat = namedMaterial ?? materials[Object.keys(materials)[0]]; if (mat) { if (!textureSource && mat.map_Kd) { textureSource = mat.map_Kd; @@ -371,12 +383,11 @@ export default class Mesh extends Renderable { /** * Draw the mesh (automatically called by melonJS). * Projects vertices through `projectionMatrix × currentTransform` - * and hands the mesh off to `renderer.drawMesh()`. Multi-material - * dispatch (one draw per `groups[]` entry on WebGL, one global - * painter's sort with per-triangle tint on Canvas) is the - * renderer's responsibility — each backend handles depth its own - * way (hardware Z-buffer vs CPU painter's), so the right place to - * fan out groups is inside the renderer. + * and hands the mesh off to `renderer.drawMesh()` in a single + * call. Multi-material meshes still draw in one call — each + * material's diffuse color is baked into `vertexColors` at + * construction time and pushed through the renderer's per-vertex + * `aColor` (WebGL) or per-triangle solid-fill (Canvas) path. * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance */ draw(renderer) { diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index e0444c43a..634171eb6 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -444,17 +444,11 @@ export default class CanvasRenderer extends Renderer { let solidFillStyle = null; if (solidFillKd && !vertexColors) { // Single-material 1×1 path — one fill color for the whole - // mesh, sampled from the pre-tinted image. Allocate the - // scratch sampler canvas via the OffscreenCanvas-aware path - // so this works inside workers / `OffscreenCanvas` contexts - // (a bare `document.createElement` would throw there). + // mesh, sampled from the pre-tinted image. Routes through + // `Renderer.createCanvas` for `OffscreenCanvas` / worker + // safety. if (!this._meshColorCanvas) { - this._meshColorCanvas = - typeof globalThis.OffscreenCanvas !== "undefined" - ? new globalThis.OffscreenCanvas(1, 1) - : globalThis.document.createElement("canvas"); - this._meshColorCanvas.width = 1; - this._meshColorCanvas.height = 1; + this._meshColorCanvas = Renderer.createCanvas(1, 1, true); this._meshColorCtx = this._meshColorCanvas.getContext("2d"); } this._meshColorCtx.clearRect(0, 0, 1, 1); diff --git a/packages/melonjs/src/video/renderer.js b/packages/melonjs/src/video/renderer.js index 0b8a0021a..7ac97af94 100644 --- a/packages/melonjs/src/video/renderer.js +++ b/packages/melonjs/src/video/renderer.js @@ -272,6 +272,43 @@ export default class Renderer { return this.renderTarget.canvas; } + /** + * Create and return a new Canvas element (or `OffscreenCanvas` when + * supported and `returnOffscreenCanvas` is true). Centralized + * renderer-side allocator so every scratch / fallback / render- + * target canvas in the engine routes through the same + * `OffscreenCanvas`-aware path, instead of duplicating + * `document.createElement` calls that throw in worker contexts. + * @param {number} width - canvas width in pixels + * @param {number} height - canvas height in pixels + * @param {boolean} [returnOffscreenCanvas=false] - return an + * `OffscreenCanvas` if the platform supports it + * @returns {HTMLCanvasElement|OffscreenCanvas} a new canvas of the given size + */ + static createCanvas(width, height, returnOffscreenCanvas = false) { + if (width === 0 || height === 0) { + throw new Error( + "width or height was zero, Canvas could not be initialized !", + ); + } + if ( + returnOffscreenCanvas === true && + typeof globalThis.OffscreenCanvas !== "undefined" + ) { + const c = new globalThis.OffscreenCanvas(width, height); + // stub `style` for compatibility — OffscreenCanvas is detached + // from the DOM but downstream code may read it + if (typeof c.style === "undefined") { + c.style = {}; + } + return c; + } + const c = globalThis.document.createElement("canvas"); + c.width = width; + c.height = height; + return c; + } + /** * Shared 1×1 fully-white canvas used as a no-op texture fallback. * Renderers and renderables that need a "blank" texture binding @@ -287,12 +324,7 @@ export default class Renderer { */ static getWhitePixel() { if (Renderer._whitePixel === null) { - const c = - typeof globalThis.OffscreenCanvas !== "undefined" - ? new globalThis.OffscreenCanvas(1, 1) - : globalThis.document.createElement("canvas"); - c.width = 1; - c.height = 1; + const c = Renderer.createCanvas(1, 1, true); const ctx = c.getContext("2d"); ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, 1, 1); diff --git a/packages/melonjs/src/video/rendertarget/canvasrendertarget.js b/packages/melonjs/src/video/rendertarget/canvasrendertarget.js index 7c0dd0cad..31cf3e5b5 100644 --- a/packages/melonjs/src/video/rendertarget/canvasrendertarget.js +++ b/packages/melonjs/src/video/rendertarget/canvasrendertarget.js @@ -1,6 +1,6 @@ import { clamp } from "../../math/math.ts"; import { setPrefixed } from "../../utils/agent.ts"; -import { createCanvas } from "../video.js"; +import Renderer from "../renderer.js"; import RenderTarget from "./rendertarget.ts"; /** @@ -110,7 +110,7 @@ class CanvasRenderTarget extends RenderTarget { if (typeof attributes.canvas !== "undefined") { this.canvas = attributes.canvas; } else { - this.canvas = createCanvas( + this.canvas = Renderer.createCanvas( width, height, this.attributes.offscreenCanvas, diff --git a/packages/melonjs/src/video/video.js b/packages/melonjs/src/video/video.js index b9ca5c0b7..f3ac0d3e7 100644 --- a/packages/melonjs/src/video/video.js +++ b/packages/melonjs/src/video/video.js @@ -1,7 +1,6 @@ import { game } from "../application/application.ts"; import { defaultApplicationSettings } from "../application/defaultApplicationSettings.ts"; import { initialized } from "../system/bootstrap.ts"; -import * as device from "./../system/device.js"; import { on, VIDEO_INIT } from "../system/event.ts"; /** @@ -83,39 +82,11 @@ export function init(width, height, options) { return true; } -/** - * Create and return a new Canvas element - * @memberof video - * @param {number} width - width - * @param {number} height - height - * @param {boolean} [returnOffscreenCanvas=false] - will return an OffscreenCanvas if supported - * @returns {HTMLCanvasElement|OffscreenCanvas} a new Canvas element of the given size - */ -export function createCanvas(width, height, returnOffscreenCanvas = false) { - let _canvas; - - if (width === 0 || height === 0) { - throw new Error( - "width or height was zero, Canvas could not be initialized !", - ); - } - - if (device.offscreenCanvas === true && returnOffscreenCanvas === true) { - _canvas = new globalThis.OffscreenCanvas(0, 0); - // stubbing style for compatibility, - // as OffscreenCanvas is detached from the DOM - if (typeof _canvas.style === "undefined") { - _canvas.style = {}; - } - } else { - // "else" create a "standard" canvas - _canvas = globalThis.document.createElement("canvas"); - } - _canvas.width = width; - _canvas.height = height; - - return _canvas; -} +// `createCanvas` was promoted to `Renderer.createCanvas` in 19.7.0. +// The implementation + deprecation warning live in `lang/deprecated.js`; +// we re-export here so existing `video.createCanvas(...)` callers keep +// working (with a console deprecation notice) until they migrate. +export { createCanvas } from "../lang/deprecated.js"; /** * return a reference to the parent DOM element holding the main canvas From 8814c0c7f1b41b35513e35c52f67fa7767ec4c92 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 11:30:08 +0800 Subject: [PATCH 10/13] fix(api): video.createCanvas stays in video namespace only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous version routed the deprecation forwarder through `deprecated.js`, which inadvertently exposed `createCanvas` as a top-level melonJS export via the blanket `export * from "./lang/deprecated.js"` re-export — but `createCanvas` was never a top-level export, only `video.createCanvas`. Polluting the top level for a never-top-level symbol breaks the implicit "deprecated symbols stay at the API position they had before deprecation" rule the existing entries (`CanvasTexture`, `Compositor` family) all follow. Move the implementation back to `video.js` (with the same `warning( "video.createCanvas", "Renderer.createCanvas", "19.7.0")` notice and forwarding to `Renderer.createCanvas`). `deprecated.js` no longer exports it — only `video.createCanvas` is exposed, matching the original API surface. Existing top-level deprecated classes (`CanvasTexture`, `Compositor`, `PrimitiveCompositor`, `QuadCompositor`) keep their top-level slots since those genuinely were top-level exports before being deprecated. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/src/lang/deprecated.js | 15 --------------- packages/melonjs/src/video/video.js | 21 ++++++++++++++++----- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/melonjs/src/lang/deprecated.js b/packages/melonjs/src/lang/deprecated.js index ffb9ec6c9..14952d4be 100644 --- a/packages/melonjs/src/lang/deprecated.js +++ b/packages/melonjs/src/lang/deprecated.js @@ -1,5 +1,4 @@ import CanvasRenderer from "../video/canvas/canvas_renderer.js"; -import Renderer from "../video/renderer.js"; import CanvasRenderTarget from "../video/rendertarget/canvasrendertarget.js"; import { Batcher } from "../video/webgl/batchers/batcher.js"; import PrimitiveBatcher from "../video/webgl/batchers/primitive_batcher.js"; @@ -11,20 +10,6 @@ import { warning } from "./console.js"; * placeholder for all deprecated classes and corresponding alias for backward compatibility */ -/** - * Create and return a new Canvas element. - * @param {number} width - width - * @param {number} height - height - * @param {boolean} [returnOffscreenCanvas=false] - will return an OffscreenCanvas if supported - * @returns {HTMLCanvasElement|OffscreenCanvas} a new Canvas element of the given size - * @deprecated since 19.7.0 — use {@link Renderer.createCanvas} instead. - * @see Renderer.createCanvas - */ -export function createCanvas(width, height, returnOffscreenCanvas = false) { - warning("video.createCanvas", "Renderer.createCanvas", "19.7.0"); - return Renderer.createCanvas(width, height, returnOffscreenCanvas); -} - /** * @deprecated since 17.1.0 * @see CanvasRenderTarget diff --git a/packages/melonjs/src/video/video.js b/packages/melonjs/src/video/video.js index f3ac0d3e7..5bbfa3fcc 100644 --- a/packages/melonjs/src/video/video.js +++ b/packages/melonjs/src/video/video.js @@ -1,7 +1,9 @@ import { game } from "../application/application.ts"; import { defaultApplicationSettings } from "../application/defaultApplicationSettings.ts"; +import { warning } from "../lang/console.js"; import { initialized } from "../system/bootstrap.ts"; import { on, VIDEO_INIT } from "../system/event.ts"; +import Renderer from "./renderer.js"; /** * @namespace video @@ -82,11 +84,20 @@ export function init(width, height, options) { return true; } -// `createCanvas` was promoted to `Renderer.createCanvas` in 19.7.0. -// The implementation + deprecation warning live in `lang/deprecated.js`; -// we re-export here so existing `video.createCanvas(...)` callers keep -// working (with a console deprecation notice) until they migrate. -export { createCanvas } from "../lang/deprecated.js"; +/** + * Create and return a new Canvas element. + * @memberof video + * @param {number} width - width + * @param {number} height - height + * @param {boolean} [returnOffscreenCanvas=false] - will return an OffscreenCanvas if supported + * @returns {HTMLCanvasElement|OffscreenCanvas} a new Canvas element of the given size + * @deprecated since 19.7.0 — use {@link Renderer.createCanvas} instead. + * @see Renderer.createCanvas + */ +export function createCanvas(width, height, returnOffscreenCanvas = false) { + warning("video.createCanvas", "Renderer.createCanvas", "19.7.0"); + return Renderer.createCanvas(width, height, returnOffscreenCanvas); +} /** * return a reference to the parent DOM element holding the main canvas From feabe9565be66bbbf1d43cb9269b0149dae74c2b Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 11:41:01 +0800 Subject: [PATCH 11/13] =?UTF-8?q?fix(mesh):=20round=202=20Copilot=20PR=20r?= =?UTF-8?q?eview=20=E2=80=94=20texture=20fallback,=20vertex=20dedup,=20doc?= =?UTF-8?q?=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four findings from the second Copilot review pass: 1. **Single-material texture fallback** — `resolveTextureAtlas(undefined)` threw when a single-material `Mesh` was constructed without a `texture:` AND without a `material:` (or a `material:` whose MTL had no `map_Kd`). The white-pixel fallback was gated on `isMultiMaterial`, missing this case. Drop the gate — any path that ends with `textureSource` unresolved now falls through to `Renderer.getWhitePixel()`, GPU pipeline always has a sampler binding. 2. **Per-material vertex dedup cache** — the OBJ parser's `startGroup()` reset the vertex `Map` on every `usemtl`. For OBJs that interleave materials (`usemtl red ... usemtl blue ... usemtl red`), the second "red" block couldn't reuse its earlier deduplicated vertices, producing unnecessary duplication. Switch to a per-material `Map` keyed by `materialName`; same material reappearing hits its existing cache. Same vertex still gets distinct slots across DIFFERENT materials (the prerequisite for per-vertex color baking). 3. **`mesh_batcher.addMesh` doc wording** — said the shared tint was "multiplied per-vertex in the shader". Wrong — it's CPU-side via `mulPackedARGB` before `pushMesh`. Mesh shader just does `texture * aColor`. Fixed. 4. **CHANGELOG scope claim** — claimed per-material textures (each material with its own `map_Kd`) were supported via the per-vertex `aColor` path. Misleading — `aColor` carries color, not textures. Tier 2 supports per-material COLORS only; the whole mesh shares a single texture binding. Clarified the scope. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/CHANGELOG.md | 2 +- packages/melonjs/src/loader/parsers/obj.js | 35 ++++++++++++------- packages/melonjs/src/renderable/mesh.js | 13 +++---- .../src/video/webgl/batchers/mesh_batcher.js | 6 ++-- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index afae5fcf0..e1276c2dd 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -7,7 +7,7 @@ ### Added - **`renderer.setDepth(depth)`** — new public method on the base `Renderer`. Mirrors the existing `setTint` / `setColor` state-setter pattern. Sets `renderer.currentDepth`, which the batchers read at vertex-emit time and push as the z component of each vertex. `Renderable.preDraw` now forwards `this.depth` automatically, so user code typically never needs to call `setDepth` directly. - Per-sprite depth on the GPU: all batched draw paths (`QuadBatcher`, `LitQuadBatcher`, `PrimitiveBatcher`, GPU TMX) now carry the renderable's `.depth` as the z component of their vertex stream. Default shaders consume it via `vec4(aVertex, 1.0)` in `gl_Position`. With an orthographic projection (the engine default), z has no visible effect — existing 2D apps render identically. With a perspective projection, sprites at different depths are correctly scaled and parallaxed by the projection matrix. -- **Multi-material OBJ rendering** — `Mesh` now correctly draws OBJ files with multiple `usemtl` directives + a bound MTL, instead of collapsing the whole model to a single uniform color. The OBJ parser emits `groups: Array<{materialName, start, count}>` matching the Three.js / glTF convention. The Mesh constructor bakes each material's `Kd` into a per-vertex color buffer at construction time, so the whole mesh draws in a **single GPU draw call** regardless of material count. `mesh.tint` multiplies on top at render time — flash / fade / team-color via `setTint` work exactly like single-material meshes. Per-material textures (each material has its own `map_Kd`) are supported on WebGL via the per-vertex `aColor` path; Canvas falls back to solid-fill from the baked colors. Single-material meshes are unchanged — same code path, same allocations, same behavior. New `Multi-material OBJ` example showcases the feature with four Kenney Space Kit spacecraft, each given a distinct team color via `mesh.tint`. +- **Multi-material OBJ rendering** — `Mesh` now correctly draws OBJ files with multiple `usemtl` directives + a bound MTL, instead of collapsing the whole model to a single uniform color. The OBJ parser emits `groups: Array<{materialName, start, count}>` matching the Three.js / glTF convention. The Mesh constructor bakes each material's `Kd` into a per-vertex color buffer at construction time, so the whole mesh draws in a **single GPU draw call** regardless of material count. `mesh.tint` multiplies on top at render time — flash / fade / team-color via `setTint` work exactly like single-material meshes. **Scope:** per-material diffuse colors (`Kd`) are fully supported on both WebGL and Canvas; per-material textures (each material with its own `map_Kd`) are NOT supported in this pipeline — the whole mesh shares a single texture binding (the first group's texture, or a 1×1 white pixel for Kd-only models). Single-material meshes are unchanged — same code path, same allocations, same behavior. New `Multi-material OBJ` example showcases the feature with four Kenney Space Kit spacecraft, each given a distinct team color via `mesh.tint`. ### Changed - **Vertex attribute layout: `aVertex` widened from `vec2` to `vec3`** across `quad-multi.vert`, `quad-multi-lit.vert`, `primitive.vert`, `orthogonal-tmxlayer.vert`. `Mesh`'s shader already used `vec3 aVertex` — all batchers are now uniform. Per-vertex stride grows by 4 bytes (24 → 28 for the quad layout). Custom shaders binding attributes by name (`gl.getAttribLocation`) — the standard pattern, and what `GLShader` enforces — keep working unchanged; the byte-offset shift of `aRegion` / `aColor` / `aTextureId` is transparent because the batcher updates its own `vertexAttribPointer` offsets. Custom shaders declaring `attribute vec2 aVertex;` continue to work — WebGL silently drops the unused z component. diff --git a/packages/melonjs/src/loader/parsers/obj.js b/packages/melonjs/src/loader/parsers/obj.js index ca1519f25..37166fb8e 100644 --- a/packages/melonjs/src/loader/parsers/obj.js +++ b/packages/melonjs/src/loader/parsers/obj.js @@ -65,13 +65,16 @@ function parseOBJ(text) { const indices = []; let vertexCount = 0; - // Per-material vertex dedup: each `usemtl` switch resets the active - // `vertexMap` so the same (v, vt) reused across different materials - // produces SEPARATE unified vertices. This is the prerequisite for - // per-vertex color baking in `Mesh` — without it, a vertex shared - // between two materials couldn't carry both colors. Pre-usemtl - // faces use the initial empty map (the "anonymous" group). - let vertexMap = new Map(); + // Per-material vertex dedup: each material name owns its own + // `vertexMap`, so the same (v, vt) reused across different + // materials produces SEPARATE unified vertices (needed for + // per-vertex color baking in `Mesh`), but the same material + // reappearing in a later `usemtl` block re-uses its existing + // vertex slots. Pre-usemtl faces use the `null` map (the + // "anonymous" group). + const materialMaps = new Map(); + materialMaps.set(null, new Map()); + let vertexMap = materialMaps.get(null); // helper: look up or create a unified vertex for a v/vt pair in the // current material's dedup scope @@ -133,12 +136,18 @@ function parseOBJ(text) { }); } groups.push({ materialName, start: indices.length, count: 0 }); - // Reset the vertex dedup scope so vertices shared with the - // previous material get re-added as distinct unified vertices. - // Required for per-vertex color baking in `Mesh` — each - // material's vertices need their own slots in the position - // buffer to carry distinct colors. - vertexMap = new Map(); + // Swap to this material's vertex dedup scope. Vertices shared + // across materials get separate slots (required for per-vertex + // color baking in `Mesh`), but vertices reused within the same + // material — even across non-contiguous `usemtl` blocks — hit + // the cache and don't get duplicated. Lazily allocated per + // material name on first switch. + let cached = materialMaps.get(materialName); + if (cached === undefined) { + cached = new Map(); + materialMaps.set(materialName, cached); + } + vertexMap = cached; }; // parse lines and build geometry in a single pass diff --git a/packages/melonjs/src/renderable/mesh.js b/packages/melonjs/src/renderable/mesh.js index 6930a6b5d..077a5b44e 100644 --- a/packages/melonjs/src/renderable/mesh.js +++ b/packages/melonjs/src/renderable/mesh.js @@ -305,12 +305,13 @@ export default class Mesh extends Renderable { } } - // resolve texture. For multi-material meshes that have NO - // per-material textures (Kenney-style flat-color models that - // only set `Kd`), fall back to a shared 1×1 white pixel so - // the GPU pipeline still has something to sample — the per- - // group `tint` does all the visible coloring. - if (!textureSource && isMultiMaterial) { + // resolve texture. Fall back to the shared 1×1 white pixel when + // no texture source was resolved — covers Kd-only multi-material + // models (Kenney style) AND single-material meshes constructed + // without a `texture:` or a `map_Kd`-bearing `material:` (the + // GPU pipeline still needs something to sample; tint / per- + // vertex color does the actual coloring). + if (!textureSource) { textureSource = Renderer.getWhitePixel(); } this.texture = resolveTextureAtlas(textureSource); diff --git a/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js b/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js index 660bcb57c..06a44267a 100644 --- a/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js +++ b/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js @@ -82,8 +82,10 @@ export default class MeshBatcher extends MaterialBatcher { * vertex's `aColor` attribute comes from that buffer instead of * the shared `tint` argument — so the mesh batches in a single * draw call with per-material colors baked into the vertex stream. - * The `tint` argument is then multiplied per-vertex in the shader, - * preserving runtime flash / fade / team-color effects. + * The shared `tint` is then multiplied into each vertex color + * CPU-side (via `mulPackedARGB`, before `pushMesh`), preserving + * runtime flash / fade / team-color effects — the mesh shader + * itself just does `texture * aColor`, no extra uniform. * @param {object} mesh - a Mesh object with vertices, uvs, indices, and texture properties * @param {number} tint - tint color in UINT32 (argb) format */ From 5fe6325d9aa19eafcf41dd1cd52894b13b9c44eb Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 12:31:39 +0800 Subject: [PATCH 12/13] fix(mesh): round 3 Copilot PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. **canvas_renderer**: skip `cache.tint(image, ...)` when `vertexColors` is present — the solid-fill path reads color from the baked buffer per triangle and never samples the image, so tinting the image is wasted work + allocation per frame. 2. **renderer.createCanvas**: gate the OffscreenCanvas path on `device.offscreenCanvas` rather than a bare `typeof globalThis.OffscreenCanvas !== "undefined"`. The device capability check actually instantiates `new OffscreenCanvas(0,0)` and verifies `.getContext("2d")` returns non-null inside a try/catch — covers Safari's historical "OffscreenCanvas exists but only does WebGL{1,2}" quirk. Without this, `getWhitePixel()` could end up calling `getContext("2d")` on an OffscreenCanvas that doesn't support it and crash. 3. **renderer.getWhitePixel**: guard `getContext("2d")` against null with a descriptive error, so a failed allocation surfaces clearly instead of a null-deref later. 4. **canvas_renderer solid-fill alpha**: ARGB-extract now reads the A byte too and emits `rgba(...)` — preserves per-material MTL `d` opacity baked into `vertexColors`. Previously dropped silently. 5. **canvas_renderer degenerate-UV fallback**: the `rawDet === 0` branch's `_meshColorCanvas` allocation now routes through `Renderer.createCanvas(1, 1, true)` like the `solidFillKd` branch above — was the last `document.createElement("canvas")` left in the mesh path, would throw in worker / OffscreenCanvas contexts. 6. **Mesh per-group texture storage dropped**: `resolveGroupMaterial` no longer resolves a per-group `texture` field, since tier 2's mesh shader has a single `uSampler` binding shared across the whole mesh — per-material textures aren't switched at draw time. The shared `this.texture` falls back to the first group's `map_Kd` if any group has one, otherwise to the 1×1 white pixel. Removes a misleading field on `mesh.groups[*]` that the render path never read. 7. **mtl.js header comment**: dropped — explained a non-choice ("we're using console.warn because warning() is for deprecation") that's self-evident from looking at `lang/console.js`. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/melonjs/src/loader/parsers/mtl.js | 4 -- packages/melonjs/src/renderable/mesh.js | 53 ++++++++----------- .../src/video/canvas/canvas_renderer.js | 30 +++++++---- packages/melonjs/src/video/renderer.js | 19 +++++-- 4 files changed, 59 insertions(+), 47 deletions(-) diff --git a/packages/melonjs/src/loader/parsers/mtl.js b/packages/melonjs/src/loader/parsers/mtl.js index c8ddb48b5..62439ae07 100644 --- a/packages/melonjs/src/loader/parsers/mtl.js +++ b/packages/melonjs/src/loader/parsers/mtl.js @@ -1,7 +1,3 @@ -// `console.warn()` from `lang/console.js` is reserved for **deprecation** -// notices (formats as "X is deprecated since version Y, please use Z" -// — not what we want for runtime MTL parser warnings about unsupported -// keywords). Use `console.warn` directly here. import { mtlList } from "../cache.js"; import { fetchData } from "./fetchdata.js"; diff --git a/packages/melonjs/src/renderable/mesh.js b/packages/melonjs/src/renderable/mesh.js index 077a5b44e..98dbe82dd 100644 --- a/packages/melonjs/src/renderable/mesh.js +++ b/packages/melonjs/src/renderable/mesh.js @@ -42,25 +42,21 @@ function resolveTextureAtlas(src) { /** * Resolve an OBJ material group into a draw descriptor. Builds the * group's tint from the MTL's `Kd` (defaults to white if missing) and - * picks the texture from `map_Kd` if the MTL provides one, else falls - * back to the explicit `textureSource`. Returns a self-contained - * record that {@link Mesh#draw} can iterate without touching the MTL - * cache or re-resolving anything per frame. + * its opacity from `d`. Returns a self-contained record carrying just + * the index slice + color state — per-material textures (`map_Kd`) + * are NOT modeled because the mesh shader uses a single `uSampler` + * binding shared across the whole mesh; only colors are baked + * per-vertex. * @param {{materialName: string|null, start: number, count: number}} group * @param {object} materials - MTL material table keyed by material name - * @param {string|HTMLImageElement|TextureAtlas|undefined} textureSource - * @returns {object} draw descriptor for this group + * @returns {{materialName: string|null, start: number, count: number, tint: Color, opacity: number}} draw descriptor for this group * @ignore */ -function resolveGroupMaterial(group, materials, textureSource) { +function resolveGroupMaterial(group, materials) { const mat = group.materialName ? materials[group.materialName] : null; const tint = new Color(255, 255, 255, 1); let opacity = 1; - let texture = textureSource; if (mat) { - if (mat.map_Kd) { - texture = mat.map_Kd; - } if (mat.Kd) { tint.setColor( Math.round(mat.Kd[0] * 255), @@ -76,7 +72,6 @@ function resolveGroupMaterial(group, materials, textureSource) { materialName: group.materialName, start: group.start, count: group.count, - texture, // resolved to TextureAtlas in the Mesh constructor once the cache is reachable tint, opacity, }; @@ -233,11 +228,10 @@ export default class Mesh extends Renderable { * runtime color multiplication, or rebuild the Mesh with * new material settings. * @type {Array<{materialName: string|null, start: number, - * count: number, texture: TextureAtlas, tint: Color, - * opacity: number}>} + * count: number, tint: Color, opacity: number}>} */ this.groups = objGroups.map((g) => { - return resolveGroupMaterial(g, materials, textureSource); + return resolveGroupMaterial(g, materials); }); // `this.tint` stays at its default (white) — every // material's Kd is already baked into `vertexColors` below, @@ -245,10 +239,20 @@ export default class Mesh extends Renderable { // multiply it onto every vertex at render time. The // renderer-level `setTint` path on top of the baked colors // is still available for runtime flash / fade / team color. - // Use the first group's texture as the legacy - // single-material binding so callers that read `mesh.texture` - // (e.g. `toCanvas`) still get a sensible default. - textureSource = this.groups[0].texture; + // Per-material `map_Kd` textures are not switched at draw + // time (mesh shader has a single `uSampler`); pick up the + // first material's `map_Kd` for the shared texture binding + // if any group has one, else fall through to the white- + // pixel fallback further down. + if (!textureSource) { + for (const g of objGroups) { + const mat = g.materialName ? materials[g.materialName] : null; + if (mat && mat.map_Kd) { + textureSource = mat.map_Kd; + break; + } + } + } /** * Per-vertex color buffer (one packed Uint32 per vertex) @@ -316,17 +320,6 @@ export default class Mesh extends Renderable { } this.texture = resolveTextureAtlas(textureSource); - // resolve every multi-material group's texture into a real - // `TextureAtlas` (cached per image) so `draw()` can swap - // bindings without per-frame allocation. Groups whose MTL had - // no `map_Kd` fall back to the shared `this.texture` (the 1×1 - // white pixel above for Kenney-style models). - if (isMultiMaterial) { - for (const g of this.groups) { - g.texture = g.texture ? resolveTextureAtlas(g.texture) : this.texture; - } - } - /** * Projection matrix applied automatically before the model transform in draw(). * Defaults to a perspective projection (45° FOV, camera at z=-2.5) suitable for diff --git a/packages/melonjs/src/video/canvas/canvas_renderer.js b/packages/melonjs/src/video/canvas/canvas_renderer.js index 634171eb6..4cf3d9dae 100644 --- a/packages/melonjs/src/video/canvas/canvas_renderer.js +++ b/packages/melonjs/src/video/canvas/canvas_renderer.js @@ -420,10 +420,16 @@ export default class CanvasRenderer extends Renderer { const indices = mesh.indices; const vertexColors = mesh.vertexColors; - // apply tint if set + // apply tint if set. When `vertexColors` is present the solid- + // fill path below reads color from the baked buffer per + // triangle and never samples the image, so skip the tint cache + // allocation entirely in that case. let image = mesh.texture.getTexture(); const tint = this.currentTint.toArray(); - if (tint[0] !== 1.0 || tint[1] !== 1.0 || tint[2] !== 1.0) { + if ( + !vertexColors && + (tint[0] !== 1.0 || tint[1] !== 1.0 || tint[2] !== 1.0) + ) { image = this.cache.tint(image, this.currentTint.toRGB()); } const imgW = image.width; @@ -566,24 +572,30 @@ export default class CanvasRenderer extends Renderer { // vertices of a triangle share a material color). context.closePath(); if (vertexColors) { - // ARGB-packed: A R G B in 4 bytes MSB→LSB + // ARGB-packed: A R G B in 4 bytes MSB→LSB. Carries + // the per-material opacity (MTL `d`) baked at + // construction time — emit as `rgba(...)` so it + // reaches the canvas alpha channel rather than + // silently dropping to fully opaque. const c = vertexColors[indices[j]]; const cr = Math.round(((c >>> 16) & 0xff) * tintR); const cg = Math.round(((c >>> 8) & 0xff) * tintG); const cb = Math.round((c & 0xff) * tintB); - context.fillStyle = `rgb(${cr},${cg},${cb})`; + const ca = ((c >>> 24) & 0xff) / 255; + context.fillStyle = `rgba(${cr},${cg},${cb},${ca})`; } else { context.fillStyle = solidFillStyle; } context.fill(); } else if (rawDet === 0) { - // degenerate UV triangle — sample a solid color from the texture - // (common with color-palette models where all 3 UVs map to the same point) + // degenerate UV triangle — sample a solid color from + // the texture (common with color-palette models where + // all 3 UVs map to the same point). Routes through + // `Renderer.createCanvas` for worker / `OffscreenCanvas` + // safety, matching the `solidFillKd` path above. context.closePath(); if (!this._meshColorCanvas) { - this._meshColorCanvas = document.createElement("canvas"); - this._meshColorCanvas.width = 1; - this._meshColorCanvas.height = 1; + this._meshColorCanvas = Renderer.createCanvas(1, 1, true); this._meshColorCtx = this._meshColorCanvas.getContext("2d"); } const sx = Math.min(Math.max(Math.round(u0), 0), imgW - 1); diff --git a/packages/melonjs/src/video/renderer.js b/packages/melonjs/src/video/renderer.js index 7ac97af94..8fe6c0c96 100644 --- a/packages/melonjs/src/video/renderer.js +++ b/packages/melonjs/src/video/renderer.js @@ -2,6 +2,7 @@ import Path2D from "./../geometries/path2d.js"; import { Color } from "./../math/color.ts"; import { Matrix3d } from "../math/matrix3d.ts"; import { Vector2d } from "../math/vector2d.ts"; +import * as device from "../system/device.js"; import { CANVAS_ONRESIZE, emit } from "../system/event.ts"; import { Gradient } from "./gradient.js"; import RenderState from "./renderstate.js"; @@ -291,10 +292,15 @@ export default class Renderer { "width or height was zero, Canvas could not be initialized !", ); } - if ( - returnOffscreenCanvas === true && - typeof globalThis.OffscreenCanvas !== "undefined" - ) { + // `device.offscreenCanvas` is the engine's vetted capability + // check — it actually instantiates `new OffscreenCanvas(0,0)` + // and verifies `.getContext("2d")` returns a real context, with + // a try/catch for browser quirks (Safari historically only + // implemented WebGL{1,2} contexts on OffscreenCanvas). A bare + // `typeof OffscreenCanvas !== "undefined"` would let through + // environments where construction or 2D context creation fails + // and crash later callers (`getWhitePixel`, mesh solid-fill, …). + if (returnOffscreenCanvas === true && device.offscreenCanvas === true) { const c = new globalThis.OffscreenCanvas(width, height); // stub `style` for compatibility — OffscreenCanvas is detached // from the DOM but downstream code may read it @@ -326,6 +332,11 @@ export default class Renderer { if (Renderer._whitePixel === null) { const c = Renderer.createCanvas(1, 1, true); const ctx = c.getContext("2d"); + if (ctx === null) { + throw new Error( + "Renderer.getWhitePixel: 2D context unavailable on the allocated canvas", + ); + } ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, 1, 1); Renderer._whitePixel = c; From 8f6af21a43c7074740c7b42c34e658c678ce6565 Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Tue, 26 May 2026 12:47:40 +0800 Subject: [PATCH 13/13] fix(mesh): round 4 Copilot PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. **obj parser tab tolerance** — `mtllib` / `usemtl` detection used `startsWith("mtllib ")` (single-space prefix), which fails for valid OBJ files that separate the keyword from its argument with tabs or multiple spaces. Switched to the same `split(/\s+/)` tokenization the `v/vt/f` paths use, then check `parts[0]` against the keyword. 2. **Renderer ↔ CanvasRenderTarget circular import resolved** — `CanvasRenderTarget` imported `Renderer` for `Renderer.createCanvas`, while `Renderer` already imported `CanvasRenderTarget` (used in its constructor). The cycle works today via ES module hoisting but is brittle. Extracted the canvas allocator into a new `video/canvas_factory.js` helper that both can import without referencing each other. `Renderer.createCanvas` becomes a thin re-export of the helper, so the public-facing API is unchanged. `CanvasRenderTarget` and `TMXLayer` migrated to importing the helper directly. 3. **"Single draw call" wording corrected in 5 places** — Mesh.draw doc, vertexColors-field doc, mesh_batcher.addMesh doc, webgl_renderer.drawMesh doc, and CHANGELOG line 10 all overstated the guarantee. `MeshBatcher.addMesh` still chunks very large meshes across multiple flushes to respect vertex/index buffer limits — what multi-material rendering actually guarantees is **no extra draw calls per material** vs single-material rendering, not a blanket "single draw call regardless of size". Rephrased consistently across all five sites. README bullet (root + packages/melonjs/) updated to mention multi-material support in the existing 3D mesh feature line. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- packages/melonjs/CHANGELOG.md | 2 +- packages/melonjs/src/level/tiled/TMXLayer.js | 4 +- packages/melonjs/src/loader/parsers/obj.js | 21 ++++++--- packages/melonjs/src/renderable/mesh.js | 26 ++++++----- packages/melonjs/src/video/canvas_factory.js | 45 +++++++++++++++++++ packages/melonjs/src/video/renderer.js | 29 +----------- .../video/rendertarget/canvasrendertarget.js | 4 +- .../src/video/webgl/batchers/mesh_batcher.js | 6 ++- .../melonjs/src/video/webgl/webgl_renderer.js | 12 ++--- 10 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 packages/melonjs/src/video/canvas_factory.js diff --git a/README.md b/README.md index 9e79cd0b0..d33217c95 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Graphics - Built-in effects such as tinting, masking and 2D lighting (with optional per-pixel normal-map shading on sprites for 3D-looking dynamic lights) - Standard spritesheet, single and multiple Packed Textures support - Compressed texture support (DDS, KTX, KTX2, PVR, PKM) with automatic format detection and fallback -- 3D mesh rendering with OBJ/MTL model loading, perspective projection and hardware depth testing +- 3D mesh rendering with OBJ/MTL model loading, multi-material support, perspective projection and hardware depth testing - 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 diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index e1276c2dd..6260b0c7b 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -7,7 +7,7 @@ ### Added - **`renderer.setDepth(depth)`** — new public method on the base `Renderer`. Mirrors the existing `setTint` / `setColor` state-setter pattern. Sets `renderer.currentDepth`, which the batchers read at vertex-emit time and push as the z component of each vertex. `Renderable.preDraw` now forwards `this.depth` automatically, so user code typically never needs to call `setDepth` directly. - Per-sprite depth on the GPU: all batched draw paths (`QuadBatcher`, `LitQuadBatcher`, `PrimitiveBatcher`, GPU TMX) now carry the renderable's `.depth` as the z component of their vertex stream. Default shaders consume it via `vec4(aVertex, 1.0)` in `gl_Position`. With an orthographic projection (the engine default), z has no visible effect — existing 2D apps render identically. With a perspective projection, sprites at different depths are correctly scaled and parallaxed by the projection matrix. -- **Multi-material OBJ rendering** — `Mesh` now correctly draws OBJ files with multiple `usemtl` directives + a bound MTL, instead of collapsing the whole model to a single uniform color. The OBJ parser emits `groups: Array<{materialName, start, count}>` matching the Three.js / glTF convention. The Mesh constructor bakes each material's `Kd` into a per-vertex color buffer at construction time, so the whole mesh draws in a **single GPU draw call** regardless of material count. `mesh.tint` multiplies on top at render time — flash / fade / team-color via `setTint` work exactly like single-material meshes. **Scope:** per-material diffuse colors (`Kd`) are fully supported on both WebGL and Canvas; per-material textures (each material with its own `map_Kd`) are NOT supported in this pipeline — the whole mesh shares a single texture binding (the first group's texture, or a 1×1 white pixel for Kd-only models). Single-material meshes are unchanged — same code path, same allocations, same behavior. New `Multi-material OBJ` example showcases the feature with four Kenney Space Kit spacecraft, each given a distinct team color via `mesh.tint`. +- **Multi-material OBJ rendering** — `Mesh` now correctly draws OBJ files with multiple `usemtl` directives + a bound MTL, instead of collapsing the whole model to a single uniform color. The OBJ parser emits `groups: Array<{materialName, start, count}>` matching the Three.js / glTF convention. The Mesh constructor bakes each material's `Kd` into a per-vertex color buffer at construction time, so multi-material meshes need **no extra draw calls per material** vs single-material rendering — large meshes still chunk across multiple draws to respect the WebGL batcher's vertex/index buffer limits (same behavior as today's single-material path), but adding more materials to a mesh doesn't multiply that count. `mesh.tint` multiplies on top at render time — flash / fade / team-color via `setTint` work exactly like single-material meshes. **Scope:** per-material diffuse colors (`Kd`) are fully supported on both WebGL and Canvas; per-material textures (each material with its own `map_Kd`) are NOT supported in this pipeline — the whole mesh shares a single texture binding (the first group's `map_Kd` if any, else a 1×1 white pixel for Kd-only models). Single-material meshes are unchanged — same code path, same allocations, same behavior. New `Multi-material OBJ` example showcases the feature with four Kenney Space Kit spacecraft, each given a distinct team color via `mesh.tint`. ### Changed - **Vertex attribute layout: `aVertex` widened from `vec2` to `vec3`** across `quad-multi.vert`, `quad-multi-lit.vert`, `primitive.vert`, `orthogonal-tmxlayer.vert`. `Mesh`'s shader already used `vec3 aVertex` — all batchers are now uniform. Per-vertex stride grows by 4 bytes (24 → 28 for the quad layout). Custom shaders binding attributes by name (`gl.getAttribLocation`) — the standard pattern, and what `GLShader` enforces — keep working unchanged; the byte-offset shift of `aRegion` / `aColor` / `aTextureId` is transparent because the batcher updates its own `vertexAttribPointer` offsets. Custom shaders declaring `attribute vec2 aVertex;` continue to work — WebGL silently drops the unused z component. diff --git a/packages/melonjs/src/level/tiled/TMXLayer.js b/packages/melonjs/src/level/tiled/TMXLayer.js index 47bab25fd..abfcc02d4 100644 --- a/packages/melonjs/src/level/tiled/TMXLayer.js +++ b/packages/melonjs/src/level/tiled/TMXLayer.js @@ -1,7 +1,7 @@ import { vector2dPool } from "../../math/vector2d.ts"; import Renderable from "../../renderable/renderable.js"; import CanvasRenderer from "../../video/canvas/canvas_renderer"; -import Renderer from "../../video/renderer.js"; +import { createCanvas } from "../../video/canvas_factory.js"; import { TMX_CLEAR_BIT_MASK, TMX_FLIP_AD, @@ -329,7 +329,7 @@ export default class TMXLayer extends Renderable { // if pre-rendering method is in use, create an offline canvas/renderer if (this.renderMode === "prerender" && !this.canvasRenderer) { this.canvasRenderer = new CanvasRenderer({ - canvas: Renderer.createCanvas(this.width, this.height), + canvas: createCanvas(this.width, this.height), width: this.width, height: this.height, transparent: true, diff --git a/packages/melonjs/src/loader/parsers/obj.js b/packages/melonjs/src/loader/parsers/obj.js index 37166fb8e..3cabfa31c 100644 --- a/packages/melonjs/src/loader/parsers/obj.js +++ b/packages/melonjs/src/loader/parsers/obj.js @@ -159,13 +159,20 @@ function parseOBJ(text) { } const first = line[0]; - if (first === "m" && line.startsWith("mtllib ")) { - mtllib = line.substring(7).trim(); - continue; - } - if (first === "u" && line.startsWith("usemtl ")) { - startGroup(line.substring(7).trim()); - continue; + // tokenize once for keyword + argument lookup. The v/vt/f + // paths below also split on `\s+`, so this is just hoisting + // the same parse — handles tabs and multiple-space separators + // consistently across every line type. + if (first === "m" || first === "u") { + const parts = line.split(/\s+/); + if (parts[0] === "mtllib") { + mtllib = parts.slice(1).join(" "); + continue; + } + if (parts[0] === "usemtl") { + startGroup(parts.slice(1).join(" ")); + continue; + } } if (first === VERTEX_PREFIX) { const parts = line.split(/\s+/); diff --git a/packages/melonjs/src/renderable/mesh.js b/packages/melonjs/src/renderable/mesh.js index 98dbe82dd..5bbcf5a27 100644 --- a/packages/melonjs/src/renderable/mesh.js +++ b/packages/melonjs/src/renderable/mesh.js @@ -258,11 +258,14 @@ export default class Mesh extends Renderable { * Per-vertex color buffer (one packed Uint32 per vertex) * populated for multi-material meshes. The mesh batcher * reads from this when present, pushing the per-vertex - * color as the `aColor` attribute — so the whole mesh - * renders in a single draw call with each material region - * carrying its baked color. Multiplied at render time by - * the global `mesh.tint`, so runtime tint mutation still - * works as expected (flash, fade, team color, etc.). + * color as the `aColor` attribute — so multi-material + * rendering needs no extra draw calls per material vs + * single-material rendering (the batcher still chunks + * very large meshes across multiple draws to fit its + * vertex/index buffer limits, same as the single-material + * path). Multiplied at render time by the global + * `mesh.tint`, so runtime tint mutation still works as + * expected (flash, fade, team color, etc.). * * Vertices were split per-material at parse time (each * material has its own dedup scope in the OBJ parser), so @@ -377,11 +380,14 @@ export default class Mesh extends Renderable { /** * Draw the mesh (automatically called by melonJS). * Projects vertices through `projectionMatrix × currentTransform` - * and hands the mesh off to `renderer.drawMesh()` in a single - * call. Multi-material meshes still draw in one call — each - * material's diffuse color is baked into `vertexColors` at - * construction time and pushed through the renderer's per-vertex - * `aColor` (WebGL) or per-triangle solid-fill (Canvas) path. + * and hands the mesh off to `renderer.drawMesh()`. Multi-material + * meshes need no extra `drawMesh` calls per material vs single- + * material — each material's diffuse color is baked into + * `vertexColors` at construction time and pushed through the + * renderer's per-vertex `aColor` (WebGL) or per-triangle solid- + * fill (Canvas) path. The WebGL batcher may still chunk very + * large meshes across multiple `drawElements` to fit its + * vertex/index buffer limits, same as the single-material path. * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance */ draw(renderer) { diff --git a/packages/melonjs/src/video/canvas_factory.js b/packages/melonjs/src/video/canvas_factory.js new file mode 100644 index 000000000..f0a4d4b24 --- /dev/null +++ b/packages/melonjs/src/video/canvas_factory.js @@ -0,0 +1,45 @@ +import * as device from "../system/device.js"; + +/** + * Create and return a new Canvas element (or `OffscreenCanvas` when + * supported and `returnOffscreenCanvas` is true). The single low-level + * allocator used by every renderer-side scratch / fallback / render- + * target canvas in the engine. + * + * Lives in its own module (not on `Renderer` directly) to break the + * circular dependency between `Renderer` and `CanvasRenderTarget` — + * both can import this helper without referencing each other. + * `Renderer.createCanvas` re-exposes it as a static method for the + * public-facing API; internal callers can import it directly. + * + * Gates the OffscreenCanvas path on `device.offscreenCanvas` (the + * vetted capability check — actually instantiates + verifies + * `getContext("2d")` works inside a try/catch, covering historical + * Safari quirks). + * @param {number} width - canvas width in pixels + * @param {number} height - canvas height in pixels + * @param {boolean} [returnOffscreenCanvas=false] - return an + * `OffscreenCanvas` if the platform supports it + * @returns {HTMLCanvasElement|OffscreenCanvas} a new canvas of the given size + * @ignore + */ +export function createCanvas(width, height, returnOffscreenCanvas = false) { + if (width === 0 || height === 0) { + throw new Error( + "width or height was zero, Canvas could not be initialized !", + ); + } + if (returnOffscreenCanvas === true && device.offscreenCanvas === true) { + const c = new globalThis.OffscreenCanvas(width, height); + // stub `style` for compatibility — OffscreenCanvas is detached + // from the DOM but downstream code may read it + if (typeof c.style === "undefined") { + c.style = {}; + } + return c; + } + const c = globalThis.document.createElement("canvas"); + c.width = width; + c.height = height; + return c; +} diff --git a/packages/melonjs/src/video/renderer.js b/packages/melonjs/src/video/renderer.js index 8fe6c0c96..93fa3e33d 100644 --- a/packages/melonjs/src/video/renderer.js +++ b/packages/melonjs/src/video/renderer.js @@ -2,8 +2,8 @@ import Path2D from "./../geometries/path2d.js"; import { Color } from "./../math/color.ts"; import { Matrix3d } from "../math/matrix3d.ts"; import { Vector2d } from "../math/vector2d.ts"; -import * as device from "../system/device.js"; import { CANVAS_ONRESIZE, emit } from "../system/event.ts"; +import { createCanvas } from "./canvas_factory.js"; import { Gradient } from "./gradient.js"; import RenderState from "./renderstate.js"; import CanvasRenderTarget from "./rendertarget/canvasrendertarget.js"; @@ -287,32 +287,7 @@ export default class Renderer { * @returns {HTMLCanvasElement|OffscreenCanvas} a new canvas of the given size */ static createCanvas(width, height, returnOffscreenCanvas = false) { - if (width === 0 || height === 0) { - throw new Error( - "width or height was zero, Canvas could not be initialized !", - ); - } - // `device.offscreenCanvas` is the engine's vetted capability - // check — it actually instantiates `new OffscreenCanvas(0,0)` - // and verifies `.getContext("2d")` returns a real context, with - // a try/catch for browser quirks (Safari historically only - // implemented WebGL{1,2} contexts on OffscreenCanvas). A bare - // `typeof OffscreenCanvas !== "undefined"` would let through - // environments where construction or 2D context creation fails - // and crash later callers (`getWhitePixel`, mesh solid-fill, …). - if (returnOffscreenCanvas === true && device.offscreenCanvas === true) { - const c = new globalThis.OffscreenCanvas(width, height); - // stub `style` for compatibility — OffscreenCanvas is detached - // from the DOM but downstream code may read it - if (typeof c.style === "undefined") { - c.style = {}; - } - return c; - } - const c = globalThis.document.createElement("canvas"); - c.width = width; - c.height = height; - return c; + return createCanvas(width, height, returnOffscreenCanvas); } /** diff --git a/packages/melonjs/src/video/rendertarget/canvasrendertarget.js b/packages/melonjs/src/video/rendertarget/canvasrendertarget.js index 31cf3e5b5..ba07deeea 100644 --- a/packages/melonjs/src/video/rendertarget/canvasrendertarget.js +++ b/packages/melonjs/src/video/rendertarget/canvasrendertarget.js @@ -1,6 +1,6 @@ import { clamp } from "../../math/math.ts"; import { setPrefixed } from "../../utils/agent.ts"; -import Renderer from "../renderer.js"; +import { createCanvas } from "../canvas_factory.js"; import RenderTarget from "./rendertarget.ts"; /** @@ -110,7 +110,7 @@ class CanvasRenderTarget extends RenderTarget { if (typeof attributes.canvas !== "undefined") { this.canvas = attributes.canvas; } else { - this.canvas = Renderer.createCanvas( + this.canvas = createCanvas( width, height, this.attributes.offscreenCanvas, diff --git a/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js b/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js index 06a44267a..087821d55 100644 --- a/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js +++ b/packages/melonjs/src/video/webgl/batchers/mesh_batcher.js @@ -80,8 +80,10 @@ export default class MeshBatcher extends MaterialBatcher { * Add a textured mesh to the batch. When the mesh has a * `vertexColors` array (multi-material OBJ + bound MTL), each * vertex's `aColor` attribute comes from that buffer instead of - * the shared `tint` argument — so the mesh batches in a single - * draw call with per-material colors baked into the vertex stream. + * the shared `tint` argument — so multi-material rendering needs + * no extra draw calls per material vs single-material (large + * meshes still get chunked across multiple flushes to fit the + * vertex/index buffer limits — same behavior as single-material). * The shared `tint` is then multiplied into each vertex color * CPU-side (via `mulPackedARGB`, before `pushMesh`), preserving * runtime flash / fade / team-color effects — the mesh shader diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index b5fdf7292..dd3ded432 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -1223,11 +1223,13 @@ export default class WebGLRenderer extends Renderer { * fit the vertex/index buffer limits. * * Multi-material meshes (OBJ files with multiple `usemtl` groups + - * a bound MTL) draw in a SINGLE call here — the per-material - * colors are baked into `mesh.vertexColors` at construction time - * and pushed through the batcher's per-vertex `aColor` attribute. - * `mesh.tint` still multiplies on top at draw time, so flash / - * fade / team-color via `setTint` work the same as single-material. + * a bound MTL) don't add any per-material draw calls here — the + * per-material colors are baked into `mesh.vertexColors` at + * construction time and pushed through the batcher's per-vertex + * `aColor` attribute. `mesh.tint` still multiplies on top at draw + * time, so flash / fade / team-color via `setTint` work the same + * as single-material. (The chunking-for-buffer-limits behavior + * above still applies to multi-material meshes as well.) * @param {Mesh} mesh - a Mesh renderable or compatible object */ drawMesh(mesh) {