feat(gltf): material features Tier 2 + mesh-batcher perf + texture-filter decoupling#1506
Merged
Conversation
Materials flagged KHR_materials_unlit bake their own lighting and must not be shaded again. The parser detects the extension per material; GLTFScene / GLTFModel set `lit = sceneLit && !unlit` so an unlit prim renders fullbright even in a lit scene (no double-lighting). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…support)
New `Mesh` `textureFilter` setting ("nearest" / "linear", WebGL) applied to the
resolved texture. The glTF loader reads each material's sampler `magFilter`, so
pixel-art-textured 3D models render crisp instead of blurred by the global
antiAlias default. Same image-global caveat as `textureRepeat` (#1503).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hard alpha cutout for the WebGL mesh path: fragments whose final alpha falls below a per-mesh threshold are discarded in the mesh / lit-mesh shaders, for crisp foliage / fences / chain-link / decals with no blending or back-to-front sorting. - mesh.frag / mesh-lit.frag: new uAlphaCutoff uniform + discard guard - MeshBatcher.addMesh: set the uniform (flush-free, per-mesh) when it changes, guarded behind the shader actually declaring it so custom mesh shaders are untouched - Mesh.alphaCutoff setting (default 0 = disabled) - glTF parser materialAlphaCutoff: alphaMode MASK -> alphaCutoff (def 0.5); OPAQUE/BLEND -> 0. Wired through GLTFScene + GLTFModel - tests: parser (6) + Mesh consumer (1) + WebGL end-to-end pixel (3, proving both shaders compile with the uniform and the discard works) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-illumination for the WebGL mesh path: a color added on top of the lit/unlit result so a surface glows regardless of scene lights (neon, lava, screens, glowing eyes). - mesh.frag / mesh-lit.frag: new uEmissive vec3 uniform, added to the final color (after lighting in the lit path so it glows full strength) - MeshBatcher.addMesh: set uEmissive flush-free per mesh with a per- channel redundant-set guard, behind the shader declaring the uniform; shared zero vector when a mesh has no emission - Mesh.emissive (Float32Array(3) or undefined) + toEmissive() helper (all-zero collapses to undefined → lean path) - glTF parser materialEmissive: emissiveFactor x KHR_materials_emissive_strength; wired through GLTFScene + GLTFModel - MTL parser: Ke (Blender emission export); wired in Mesh single- material path - tests: glTF parser (5) + MTL Ke (1) + WebGL end-to-end pixel (2, incl. uniform-reset-doesn't-leak) Emissive textures (emissiveTexture / map_Ke) remain out of scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…emissive) to JSDoc Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MeshBatcher.addMesh dedup'd vertices per chunk with a Map that was clear()ed every chunk; V8 drops a Map's backing table on clear(), so re-filling it reallocated as it grew — garbage proportional to vertex count (MBs/sec of GC churn on a dense scene). Replaced with a versioned typed-array remap: a per-chunk stamp invalidates every entry in O(1), arrays grow once and are reused, so a re-drawn static mesh allocates nothing per frame. Measured on a ~158k-vertex scene: ~7x less GC garbage and ~30% faster draw (11ms -> 7.6ms, Chrome/ANGLE). Benefits all 3D mesh rendering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ata camera typing
textureFilter setting ("auto" | "nearest" | "linear", default "auto")
separates texture sampling smoothness from the polygon-edge MSAA that
antiAlias controls — so you can mix them (smooth textures + no MSAA, or
crisp pixel-art textures + MSAA edges), combinations the single boolean
couldn't express. "auto" follows antiAlias (byte-identical to before);
Mesh.textureFilter still overrides per-mesh.
- backend-neutral resolver Renderer.getDefaultTextureFilter() ("linear"/
"nearest") on the base renderer so a future WebGPU backend reuses it;
WebGL maps it to a GL enum via _glTextureFilter()
- base Renderer.setTextureFilter() records the setting (Canvas no-op);
WebGL overrides to re-filter live textures (shared _reapplyTextureFilter)
- 2D Canvas has no per-texture filtering and ignores it
- adversarial tests (11): resolver truth table, explicit filter not
clobbered by antiAlias toggle, per-mesh override precedence
Also tightens GLTFData.cameras from object[] to a typed shape with
perspective/yfov so scene.cameras[0].perspective?.yfov type-checks.
README/CHANGELOG: textureFilter + the allocation-free batcher perf note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A big procedural downtown built entirely from raw mesh data (no asset) — every lit window and street-lamp head is its own emissive geometry, so they glow individually at night while the moonlit shells stay dark. Pole-mounted streetlights, dark ground + street grid, a seamless looping Camera3d flythrough, and a vignette post-effect. Showcases Mesh.emissive and the decoupled antiAlias/textureFilter combo (MSAA edges + crisp). Also: gltf-scene example uses antiAlias:true (looks better on the low-poly diorama), shorter example descriptions, and the gltf-scene camera uses pos.set(x,y) + depth=z (the inherited pos.set is typed 2-arg; see #1510). LICENSE credits the Kenney glTF kits; night-city uses no external assets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… parse Code-review follow-ups (PR #1506): - MeshBatcher.useShader override invalidates the per-mesh uAlphaCutoff / uEmissive caches when the bound GL program changes (mirrors the base batcher's currentSamplerUnit reset). Without it, a custom mesh shader declaring those uniforms, interleaved with the built-in shader on the same batcher, could be skipped via a stale cache and silently not apply the cutoff/emissive. - materialEmissive: guard a malformed (short) emissiveFactor with || 0 so a missing component can't become NaN and reach the uEmissive uniform (NaN !== 0 would dodge the all-zero collapse). +1 adversarial test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI lint (eslint src tests) enforces arrow-body-style: always; the new malformed-emissiveFactor test used a concise arrow body. No logic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
glTF / OBJ material features (Tier 2) + 3D rendering improvements
Builds on the Tier-1 glTF/GLB loader (#1504). Started as the four day-1 material features; grew to include a mesh-batcher perf win, a texture-filtering API improvement, and a showcase example.
Material features (the original scope)
Each is a generic
Meshsetting the loaders drive — usable from raw-geometry meshes too:Mesh.textureFilter+ glTF sampler filter —"nearest"/"linear"magnification frommagFilter; pixel-art models render crisp.KHR_materials_unlit— baked-lighting materials render fullbright, not re-shaded.Mesh.alphaCutoff+ glTF alpha cutout —alphaMode: "MASK"→ hard-edgeddiscard(foliage / fences / decals), no blending or sorting.Mesh.emissive+ glTF/OBJ emissive — self-illumination fromemissiveFactor(×KHR_materials_emissive_strength) and MTLKe; glows independent of scene lights.Performance
MeshBatcher.addMeshdedup'd vertices with a per-chunkMapthatclear()ed each chunk; V8 drops the backing table onclear(), so re-filling reallocated → garbage ∝ vertex count. Replaced with a versioned typed-array remap (O(1) invalidation, arrays reused). ~7× less GC garbage and ~30% faster draw on a dense (~158k-vertex) scene; benefits all 3D mesh rendering.Texture filtering decoupled from
antiAliastextureFiltersetting ("auto"|"nearest"|"linear", default"auto") separates texture sampling smoothness from polygon-edge MSAA — so you can have smooth textures with no MSAA, or crisp textures with MSAA edges (combinations the singleantiAliasboolean couldn't express)."auto"= unchanged behavior;Mesh.textureFilteroverrides per-mesh. Backend-neutral resolver on the baseRenderer(future-proof for a WebGPU backend); Canvas has no per-texture filtering and ignores it.Types
GLTFData.camerasfromobject[]to a typed shape withperspective.yfov, soscene.cameras[0].perspective?.yfovtype-checks. (DeeperCamera3d.pos3-arg typing tracked in Types: make ObservableVector3d/Vector3d assignable to 2D so Camera3d accepts pos.set(x, y, z) #1510.)Example
Camera3dflythrough, and a vignette post-effect. ShowcasesMesh.emissiveand theantiAlias/textureFiltercombo. Also: gltf-scene example enablesantiAlias.Tests / docs
Follow-up tickets filed
#1507 (static VBO cache), #1508 (instancing), #1509 (WebGL2-only + VAOs), #1510 (ObservableVector3d↔2d assignability).
🤖 Generated with Claude Code