Repository navigation
Conversation
glTF 2.1 promotes KHR_materials_emissive_strength, KHR_node_visibility, KHR_mesh_quantization and EXT_texture_webp into the core specification. Both spellings must keep working: a 2.0 asset using the extension object and a 2.1 asset using the core property must behave identically, and both must stay reachable by KHR_animation_pointer and KHR_interactivity. Since those JSON pointers are derived from the object graph by recurseAllAnimatedProperties, the two are bound to a single AnimatableProperty instance via the new linkPromotedProperty helper, so neither copy can drift. The core property wins when an asset specifies both. - material.emissiveStrength: new animated property, adopted from the extension when absent. hasEmissiveStrength is now decided in fromJson, where the original JSON is visible, instead of initGl. - node.visible: new animated property; scene.gatherNodes reads it directly. - KHR_mesh_quantization needed no change. Accessors already build typed views for the quantized component types and enableAttribute already forwards accessor.normalized, with nothing gated on the extension being declared. - EXT_texture_webp mostly worked already. Added magic-byte mime sniffing so a WebP image stored in a bufferView without a mimeType decodes, and so an unrecognised data URI prefix falls through to sniffing rather than failing. Also adds the version-gating foundation the later 2.1 phases need: asset parses version into major/minor, exposes isAtLeast(), and rejects assets whose minVersion exceeds what this renderer implements. No behaviour is gated on the version yet; core 2.1 properties are accepted on any asset. The four extensions stay in allowedExtensions so 2.0 assets that list them in extensionsRequired keep loading. The core property names are inferred: the draft 2.1 spec text does not define them yet, stating only that the syntax will be settled later.
The renderer had no unit test runner: @playwright/test was a dev dependency with no tests directory. Vitest costs nothing to adopt here because it uses Vite's resolver, so the extensionless imports throughout source/ resolve the same way they do in the viewer's bundle. Plain Node cannot resolve them. Scope is deliberately narrow. Unit tests cover parsing and container logic; anything needing a GL context stays in the viewer's Playwright suite. That keeps the suite under a second, so it can run on every edit. Tests cover the step 1 promotions (both spellings, write-through in either direction, precedence, defaults), version parsing including the spec's no-leading-zeros rule, and image magic-byte sniffing. The sniffing tests pin down that a .wav file also starts with RIFF, so WebP detection has to check the form type at offset 8 as well. Phase 0 also parses the new glTF 2.1 top-level arrays. files and externalAssets get their own objects, with the validation the schema implies: mimeType is required, and exactly one of uri or bufferView must be present. Nothing consumes them yet. json.shapes is parsed, but deliberately kept separate from extensions.KHR_implicit_shapes.shapes rather than merged into one array. KHR_physics_rigid_bodies colliders index into the extension's array, so concatenating core shapes in front of them would silently shift every collider's shape. Unifying the two is left until physics is migrated to read gltf.shapes directly.
glTF 2.1 adds GLB container version 3, which widens the length fields to 64
bits, adds a per-chunk encoding field, moves chunk alignment from 4 to 8
bytes, and no longer requires the glTF JSON to be the first chunk. Version 2
stays valid and importers must read both. The container version is
independent of the glTF version in the JSON, so asset.version is still
checked separately.
The parser is rewritten rather than extended. It counted in Uint32Array units
(glbHeaderInts = 3), which cannot express 64-bit fields, and getJsonFromChunk
ignored chunkInfo.start and recomputed the offset assuming the JSON chunk was
chunk 0 -- correct by luck in v2, wrong in v3.
Note the chunk header fields are reordered between versions: v2 is
(length, type), v3 is (type, encoding, length). Reading one with the other's
offsets yields plausible garbage rather than an obvious failure, so both
layouts are tested against byte-exact fixtures.
BigInt is confined to a single readLength method. Mixing BigInt and Number
throws at runtime, so nothing downstream ever sees one; a length past
Number.MAX_SAFE_INTEGER is rejected with a specific message because it cannot
be used as an offset.
Validation that was missing: files too short for a header, a v2 file past the
2^32-4 limit its end alignment implies, a chunk running past end of file, a
chunk layout that fails to advance (previously an infinite loop), a missing
JSON chunk, and unparseable JSON (previously an uncaught throw).
buffer.chunk is what makes multiple binary chunks usable. The glTF 2.0
fallback where buffer 0 implicitly means chunk 1 is kept, but only for the
exact layout 2.0 could produce: buffer index 0, no uri, JSON at chunk 0, and
chunk 1 is a BIN chunk. A 2.1 buffer that omits chunk by mistake should fail
visibly rather than silently binding to whatever is at chunk 1. A bound chunk
must be at least as large as byteLength, not equal, so GLB padding does not
force byteLength to be rewritten.
The appendix argument to gltfLoader.load previously duck-typed its contents.
With chunk metadata to carry that no longer stretches, so a GLB container is
passed explicitly as { glb: ... } and the dropped-file path keeps its array.
ResourceLoader now throws when extractGlbData returns undefined instead of
dereferencing it and reporting a confusing property access error.
The GLB writer lives next to the parser so a format change cannot update one
without the other, and so the sample viewer's asset generator can share it.
asset.thumbnail points at an image that previews the asset. The point of the feature is that an application can show it without loading the scene, so the interesting part is not parsing the index but reading the image on its own. ResourceLoader.loadThumbnail reads only the JSON (or the GLB JSON chunk) and the one image it names. It builds no glTF document and touches no WebGL. Data URIs are returned as-is; everything else becomes an object URL the caller owns and must revoke. A thumbnail stored in a bufferView still needs that buffer. Inside a GLB the chunk is sliced directly, but a .gltf with an external .bin has to fetch the whole buffer, which is the slow case and could only be avoided with range requests. The media type is passed through to the Blob untouched rather than validated, because the spec deliberately allows any type here and lets the application decide what it can display. During a normal load a thumbnail that no texture references is now skipped, since loadThumbnail fetches it separately and decoding it twice would be wasted work. An image used as both thumbnail and material texture is still loaded, which is why the marking records usedByTexture rather than just isThumbnail.
glTF 2.0 required TEXCOORD_n and COLOR_n to start at 0 and be consecutive, so the set index in the file and the shader slot were always the same number. glTF 2.1 relaxes that, so a primitive may carry TEXCOORD_1 and TEXCOORD_3 and nothing else. The shaders still declare a fixed number of varyings, so rather than widening them, a primitive now maps the set indices it has onto contiguous slots in ascending order. That keeps the lowest-numbered set primary, which is what the spec recommends, and leaves conventional 2.0 assets mapping to themselves. Attribute parsing is now driven by a regex over the semantic rather than a switch listing every name, so no code change is needed to accept a set index nobody has used before. Three places had to follow: - The ~22 u_*UVSet uniforms are written per draw call, so each one routes its material's texCoord through the primitive's map. A material asking for a set the primitive does not provide warns once and falls back to the first set, where it previously sampled whatever happened to be in v_texcoord_1. - Morph target semantics are remapped the same way. Without this, a displacement on TEXCOORD_3 would emit a HAS_MORPH_TARGET_TEXCOORD_3 define no shader knows about, and the displacement would be silently dropped. - Sets beyond the shader's slot budget are dropped with a warning naming them, rather than being silently ignored. Only the lowest COLOR_n is used, since the shaders carry a single vertex colour varying.
glTF 2.1 moves the draft KHR_implicit_shapes extension into a core `shapes` array, adds mesh-based shape types, and gives nodes a `boundingVolume`. The defaults in the 2.1 schema differ from the draft extension this renderer implemented, and the old values were wrong in a way physics would inherit: cylinder height was 0.5 rather than 2.0, capsule height 0.5 rather than 1.0, and every radius 0.25 rather than 0.5. Assets that omit these properties will therefore collide differently than before. plane.doubleSided is dropped because the spec defines a plane as one-sided with its normal along +Y. Two new shape types are parsed: `mesh`, which may be concave, and `convexMesh`, which is the convex hull of its mesh. Nothing computes hulls yet; that matters only once shapes are rendered or used for collision. node.boundingVolume is a shape instance with its own TRS. Per KhronosGroup/glTF#2666 that TRS applies first, taking the shape into the node's local space, and only then does the node's global transform apply. The order is only observable once a node has non-uniform scale or the volume is rotated, so it is pinned by a test rather than left to inspection. Physics now reads gltf.shapes directly instead of reaching into the extension object, which is what makes it safe to have the extension populate the core array. An asset using both spellings is rejected with a warning rather than merged, because concatenating them would shift the indices colliders use. The interactivity graph pointer is registered under both /shapes.length and the old extension path so existing graphs keep working.
Both only surface with glTF 2.1 assets, and both were invisible to the unit tests because they involve ordering and consumers outside the slot mapping. Slot assignment ran before Draco decompression, which is what populates this.attributes for a compressed primitive. A Draco mesh would therefore have had its slots computed from an empty attribute set, losing every texture coordinate and vertex colour. Moved after the decode. Tangent generation checked this.attributes.TEXCOORD_0 literally, so a 2.1 primitive whose lowest set is, say, TEXCOORD_5 would silently skip generating tangents and render unlit-looking where a normal map was expected. It now asks for whichever set occupies shader slot 0. The second bug was found by the new browser test comparing an asset using TEXCOORD_5 against the same geometry using TEXCOORD_0: they render identically only because that asset has no normal map, so the missing tangents do not affect its shading. Worth noting the equality test did not catch this one on its own, it just made the difference in behaviour visible while investigating.
Preparation for external assets, with no recursion yet. glTF 2.0 only ever
needed "join the uri onto the folder the .gltf came from", which buffer.js and
image.js each implemented for themselves. glTF 2.1 adds two wrinkles that
neither could express:
- A parent asset can alias a URI appearing inside a child, redirecting it to
a file the parent holds. Matching is on the exact literal string, with no
normalisation, and aliases are not inherited by that child's own children.
- An asset embedded in a bufferView or data URI has no location of its own,
so its relative URIs resolve against its parent's base, not its own.
FileResolver owns that policy. One instance belongs to one asset; forEmbedded
and forLocation derive a child's resolver with the right base, deliberately
dropping the parent's aliases, and share a cache so a file reached from
several places is fetched once.
This collapses four image loading paths into one. Images previously took
separate routes for bufferView, dropped file, URI and data URI, each with its
own mime type handling, and the URI path let the browser fetch and decode by
handing a URL to an <img>. Everything that is not a bufferView now resolves to
bytes and goes through setImageFromBytes, which is the path bufferView images
already used. A KTX2 image loaded by URI therefore decodes from a buffer
rather than a URL.
setMimetypeFromFilename no longer guesses JPEG for an unrecognised extension.
It falls back to the server's Content-Type if there is one, and otherwise
leaves the type undefined so the magic-byte sniffing added earlier can decide.
Guessing JPEG meant a mis-detected image failed somewhere inside the decoder
instead of at the point the type was unknown.
The environment's LUT images are loaded through their own resolver with an
empty base, since their paths are relative to the page rather than to any
glTF asset.
Recursion, cycle detection and alias resolution. Documents are loaded and cached but nothing instantiates them into the scene yet, so this changes no rendering; a referenced asset is parsed and its resources resolved, and the result hangs off externalAsset.document. Ordering matters inside a document: buffers have to land before the recursion, because a referenced asset may live in a bufferView of its parent, and images load afterwards so they can come from either. Two rules from the spec drive the design. A cycle is decided by the chain from the root to here, not by whether a file has been seen before. Two siblings referencing one child is a diamond, which is legal and must load, with the shared file parsed once. Tracking "visited" instead of "on the current path" would reject it. An asset's URIs are resolved by whichever asset declared its files entry, and by nobody else. A child therefore gets exactly the aliases on its own entry, and passes none of them further down. A child named by uri gets a base of its own; an embedded child borrows its parent's, because data in a bufferView has no location to be relative to. Bounds are imposed on depth and total document count, since the reference graph comes from the asset rather than from us.
Dropping a file into the viewer warned "Unsupported source passed to the thumbnail loader" even for an asset with no thumbnail at all. loadThumbnail handled a path, an ArrayBuffer and a Blob, but the viewer hands a dropped file over as a [path, File] pair, so reading the source failed before anything had looked for a thumbnail. Thumbnails are optional, so the failure mode was wrong in two ways. A source the loader cannot read now yields no thumbnail rather than raising: whatever is wrong with it is reported by the real load, and duplicating that on the thumbnail path is just noise. The same applies to a source that is not parseable glTF. A thumbnail the asset does declare but that fails to resolve is a genuine fault and still throws. URI resolution now goes through FileResolver instead of bare fetch, so a thumbnail referenced from a dropped asset resolves against its dropped siblings. That is also why droppedFiles is threaded through. FileResolver reads dropped files with Blob.arrayBuffer rather than FileReader. The callback form predates the promise API, and FileReader does not exist outside a browser, which kept the dropped-file path out of reach of tests.
planning/shapes_all.gltf exercises every shape type, the 2.1 defaults and a bounding volume with its own transform. None of that had coverage, and the defaults in particular changed when shapes moved out of KHR_implicit_shapes, so a debug renderer written against the old values would look plausible and be wrong. The asset lives outside the repo, so the test skips when the planning checkout is absent.
glTF 2.1 promotes shapes to the core, but nothing in the viewer drew them, so an authoring mistake in a bounding volume was invisible. This adds the geometry, a minimal pass and the diagnostics. debug_geometry.js generates indexed geometry with both triangle and line indices, so one buffer set serves the solid and the wireframe style. Sphere, cylinder, cone and capsule are one surface of revolution with different profiles. A tapered capsule joins its caps along their common tangent rather than stacking two different end caps on a cylinder, which is the shape the spec actually describes. Infinite planes get a clamped quad with a grid, and every plane gets a normal arrow, because a one-sided plane is otherwise impossible to orient by eye. Mesh and convexMesh shapes are drawn from the source mesh with load-time morph weights applied, matching what the physics engines build colliders from. Convex hulls are not computed yet, so a hull is drawn as its source mesh: that over-reports concavity but never misplaces the shape. The pass is deliberately independent of the physics engines, so shapes are visible whether or not PhysX loaded. Its fragment shader writes the tonemap flag as passthrough, so a debug colour reaches the screen as written instead of being tonemapped into something else. Geometry is cached per shape under a key built from the current parameter values rather than the dirty flags, which avoids depending on who resets them; an animated shape rebuilds itself and a static one never does. Bounding volumes that fail to enclose their node geometry are drawn in a warning colour. The comparison is world-space AABB against world-space AABB, so it is conservative: it flags volumes that are genuinely too small and stays quiet on a tight volume over a rotated mesh.
The lateral surface of a capsule is the common tangent of its two cap spheres. Tangency requires height*sin(phi) + (top - bottom) = 0, so sin(phi) = (bottom - top) / height. The sign was inverted, which put the contact ring below each sphere equator instead of above it: the smaller sphere visibly started shrinking before the lateral band reached it, and the silhouette had a crease where the two met. The previous test derived its expected angle with the same inverted sign, so it confirmed the bug rather than catching it. The replacements assert properties instead: that the profile stays convex, that the band passes outside the smaller equator, and that the larger equator is the widest circle. Each of those fails against the old sign. With the correct angle a cap can reach past its own equator, and that circle is then the widest point of the whole shape, so it is inserted into the profile exactly rather than left to wherever the sampling lands.
Each node referencing an external asset gets its own copy of the child document, so transforms, animation playheads, morph weights, poses and visibility are independent per instance. The per-array policy is written out rather than being a generic deep clone. A generic clone duplicates uploaded buffers and decoded images, which looks like it works and then costs a multiple of the memory it should. Resource arrays share their elements but get their own container, because a skin allocates its joint texture by appending to images, samplers and textures, and that append must not reach the template or a sibling instance. Meshes are copied for their morph weights but their primitives are shared: a primitive owns the uploaded vertex buffers and the generated tangents and holds nothing that differs per instance. Parent links are object references, so they are rebuilt on the clone rather than remapped. Everything else in glTF is index-based and stays valid because the arrays keep their order. Documents now carry an id. A mesh index only means something together with the document it belongs to, which the renderer will need when it groups draw calls across a tree. The loader already rejects cycles, so instantiation terminates. The budget guards the case that is not a cycle: a diamond doubling the instance count at every level.
Scene traversal now descends from a node into the document it instantiates, and the renderer resolves indices against the document that owns each node rather than against the root. A mesh or material index only means something together with its document, and a tree of external assets has many. Nodes carry their owning document, so the drawables the renderer already builds did not need a new shape: every index lookup that was node-relative reads node.ownerDocument instead. Picking, hover and skin updates now walk the gathered node list, which spans the tree, rather than the root nodes. Two things outside the renderer made the same assumption and had to follow: the statistics pass, and the camera framing. The camera was the visible failure, because scene extents walked node indices against the root document, found nothing, and framed an empty box with all the content off screen. This also fixes uploading a parent whose child is already uploaded. The default walk over members reached the child through externalAssets and called its initGl with a document where a GL context belongs, which failed deep inside primitive upload with no hint of the cause. The walk now stops at the document boundary.
An instantiated asset has no entry in the animation UI, so everything it declares runs on the shared clock. Each instance owns its animation objects, so two instances hold independent state even though they run in lockstep here. The viewer also had to learn that something is moving. It only kept redrawing while the root had animations selected, so an asset whose motion came entirely from a child rendered one frame and went still.
The hierarchy depth map was keyed by node index and walked only the root document. Both are wrong once external assets are instantiated: node 0 of a child is not node 0 of the root, so a node either borrowed a depth from an unrelated node or fell back to zero, and every instance came out the colour of the root. Keying by the node itself removes the ambiguity, and the walk now descends into instantiated assets the way the rest of the traversal does. Also records the decision on interactivity: only the root document graph runs. A graph addresses the world through JSON pointers naming one document, and the spec does not say whether an event raised by one graph should reach another or whether two instances share variables. Rather than invent semantics that may later be contradicted, graphs inside an instance are skipped and the asset is named in a warning so the author is not left guessing.
uid costs nothing to support: fromKeys already copies unknown JSON keys onto the object, so a 2.1 asset carrying uids round trips untouched. These tests exist so that stays true. Nothing further is implemented on purpose. uid does not appear in the 2.1 spec draft, and the explainer is still weighing a separate uid property against requiring name to be unique, and core against an extension. Nothing in the format references a uid either, so there is no behaviour to add. That makes the uniqueness validator in the plan premature: the constraint against name, the character set and the name fallback all belong to one of the two competing designs, and warning an author for breaking a rule that may not survive is worse than staying quiet.
This is the first thing that makes a glTF 2.1 bounding volume do something rather than merely be inspectable. Only nodes that declare a volume are considered. A node without one says nothing about where its geometry is, so it always draws: the feature uses what the asset states rather than guessing from mesh bounds. Two cases are deliberately exempt. A node drawn as part of an instanced group shares one draw call with its siblings, and a GPU-instanced node is spread over transforms its own volume does not describe; in both, one volume cannot speak for what is drawn. An infinite plane is exempt too, since shapeBounds cannot measure it and undefined has to read as "cannot cull" rather than "empty". The decision lives in culling.js as a plain function over nodes rather than in the renderer, because correct culling is invisible by definition - whatever it removes was off screen - so it can only be tested by asserting the decision directly. The renderer cannot be imported under vitest, which has no GLSL plugin and deliberately keeps GL out. Shape extents are computed analytically. Building a sphere's geometry every frame to measure it would be absurd. Toggleable because a volume too small for its node hides geometry that should be visible. That is an authoring error, not a renderer one, but it is invisible without help, which is what the debug pass and its non-enclosing warning colour are for.
glTF 2.1 lets a primitive carry any number of sets and a material address any of them, but every texture getter picked its set with the same two-way conditional, which is what capped the renderer at two. All 22 of them now go through one getTexCoord helper, so raising the limit is a matter of declaring more varyings rather than editing each getter. Four is a practical ceiling rather than a spec one; sets beyond it are still dropped with a warning, and dropping the highest rather than shifting the others keeps the sets a material does reference where it expects them. The extra varyings are declared only when the primitive provides them, so an asset using the usual one or two pays nothing. The guard is the HAS_TEXCOORD_n_VEC2 define that already existed, which reaches both shader stages, so vertex and fragment agree on what is declared without a new define to keep in sync. Morph targets still only displace the first two sets, which is what animation.glsl implements. One existing test encoded the old limit by using three sets to prove overflow. It now derives the count from maxTexCoordSlots so it keeps testing overflow rather than silently testing nothing.
The previous commit left the set count spread across the shaders. Adding a fifth set meant declaring a varying in primitive.vert, declaring it again in textures.glsl, and extending the ladder of conditionals in getTexCoord: three edits in three places to change one number, with nothing but care keeping the three in agreement. The varyings are now a single sized array, v_texcoord[TEXCOORD_ARRAY_SIZE], and MAX_TEXCOORD_SLOTS in the new attribute_limits.js is the number that sizes it. getTexCoord indexes that array rather than walking a chain of early returns, so the count is a number rather than a shader edit. The attributes cannot follow, because GLSL ES 3.00 forbids arrays of vertex inputs. They stay individually named, declared by a ladder of TEXCOORD_SET_COUNT tests, with an #error for an asset that supplies more sets than there are attributes to hold them. Generating those declarations from JS would lift the last limit, but a shader assembled by string concatenation is harder to read than four #if lines, and the guard makes the ceiling loud rather than silent. That trade is worth revisiting if the ceiling ever moves. TEXCOORD_ARRAY_SIZE and TEXCOORD_SET_COUNT are pushed as primitive defines, which is the one list reaching both the vertex and the fragment stage, so the two cannot disagree about the size of a shared interface. The array is never empty: a primitive carrying no texture coordinates still has to compile, and a material addressing a set the primitive lacks has to sample a zeroed coordinate rather than fail to link. The debug view follows the same idea. One DEBUG_UV define and a u_DebugUVSet uniform replace one define per set, so the number of sets no longer multiplies shader permutations, and GltfState now lists one channel per slot instead of the hardcoded two. The fragment shader leaves the checkerboard showing when the chosen set is one this primitive does not carry, which reads as "nothing here" rather than as black UVs. The slot budget is clamped at startup to what the GPU reports it can afford, never below the two sets a glTF 2.0 asset relies on.
index.html pulled draco_decoder_gltf.js from www.gstatic.com with a blocking script tag. When that fetch failed the page carried on without a decoder, and the two console errors it left behind - ERR_SOCKET_NOT_CONNECTED, then "DracoDecoderModule is not defined" - surfaced in whichever test happened to be running, because every test asserts a clean console. A full suite run lost a test to it this week, and the failure was reported against external assets, which had nothing to do with Draco. A dependency that can fail any test for reasons unrelated to that test is worse than a slow one. The file now sits in source/libs next to libktx and mikktspace, so rollup copies it into dist/libs and the viewer's sync:renderer-assets step puts it in public/libs. eslint already ignores source/libs, so the minified source is not linted. Only the JavaScript is vendored. The decoder does name a companion draco_decoder_gltf.wasm, but this build never requests it - a Decoder constructed from the file alone runs and answers calls, with the wasm absent and no request attempted. Shipping it would have added 192 KB that nothing fetches. That is also why this is not a change in decoding behaviour: the bytes are the ones the CDN was already serving.
animation.glsl implemented getTargetTexCoord0 and getTargetTexCoord1 and nothing further, so a morph target on any set beyond the second was parsed, uploaded into the morph texture, and then never read. The primitive already remapped the target's semantic onto a shader slot, so the define was there and correct; there was simply no code looking at it. The two near-identical getters become one getTargetTexCoord taking the slot's target offset as a parameter. The offset has to be a parameter rather than derived from the slot, because MORPH_TARGET_TEXCOORD_n_OFFSET is a macro and the preprocessor cannot index it by a variable. That is the same constraint that keeps the vertex attributes individually named, so the call sites are a ladder for the same reason the declarations above them are. Each call is guarded on TEXCOORD_ARRAY_SIZE as well as on the morph define. That second guard is not redundant: a morph target may name a set the primitive itself does not provide, in which case the semantic is passed through unmapped and the define names a slot that does not exist. Without the bound the shader would write past the end of the varying array. The test compares the displaced asset against one that bakes the same shift into its geometry, so it checks that the right set moved by the right amount rather than merely that something moved - a displacement applied to the wrong slot would pass the weaker check. It reads the coordinates back through the texture coordinate debug view rather than by texturing the quad. That is deliberate: the generated assets' inline PNG currently fails to upload, so every textured test quad renders black and a comparison through a texture fetch would pass no matter what the morph did. The debug view shows the varying itself, which is the value under test.
The debug list offered "Texture Coordinates 0" through 3, one per shader slot. Slots are an implementation detail: the renderer packs an asset's sets down into consecutive slots, so an asset whose only set is TEXCOORD_5 offered a channel called 0, and one with sets 0, 2, 4 and 6 offered 0 through 3. The author is shown numbers they never wrote, and given no way to ask for the set they did. The channels are now built from the loaded asset and named by the index the file uses. They come from the slots assigned at load rather than from the attributes, so a set dropped for exceeding the renderer's budget gets no channel: offering one would promise a view the shaders cannot produce. An asset with no texture coordinates offers none at all, where before it still listed four. Resolving a channel moved from its position in a fixed list to a lookup per primitive. Each primitive maps the chosen set index through its own slots, so two primitives that number their sets differently each show the set that was asked for rather than whichever one happens to share a slot index. u_DebugUVSet is -1 where a primitive does not carry the chosen set, and the shaders now test for that rather than for an upper bound. The old test let -1 through as a valid slot, which would have shown set 0 while claiming to be another. The naming and gathering live in their own module rather than in GltfState. GltfState cannot be imported under vitest - it pulls in the physics controller and with it a wasm module Node will not load - so keeping them apart is what lets them be unit tested at all.
main made draco initialisation awaitable and awaited it just before gltf.fromJson. That call now lives in buildDocument, one per document in the asset tree, so the await moved up into loadGltf next to the mikktspace init: it has to happen once, before any document is parsed.
Scene extents were gathered from mesh positions alone, so a glTF 2.1 asset made of nothing but shapes and bounding volumes left them inverted and infinite. The fitted camera distance and clipping planes became NaN and nothing drew, which looked like the bounding volume debug pass being broken rather than the camera pointing nowhere. Bounding volumes now supply the extents when no geometry does, and a scene with neither falls back to a unit box. They do not contribute when there is geometry: a volume states where geometry may be, not what is drawn, and an oversized one would otherwise zoom every existing asset out.
Rings were sampled with a stride anchored at the first profile point, so where the pattern landed depended on where the profile happened to start. On a sphere that drew one ring 11.25 degrees from the equator and left 22.5 degrees between every other pair, and put the lowest ring at -56.25 against +78.75 at the top. The result read as a malformed sphere rather than as a sampling artefact. Rings now grow outwards from the middle of the profile, which is symmetric by construction and places the sphere's equator without it having to be forced in as a feature ring. The cylinder's rims and the capsule's tangent rings are feature rings and are unaffected.
glTF 2.1 deprecates multiple scenes and lets a file have zero of them, so an asset may carry a node hierarchy and no scenes array at all. Nothing downstream walks nodes directly; every pass starts from a scene, so such a file loaded without error and drew nothing. A 2.1 asset without scenes now gets one covering the hierarchy's own roots, which is the whole file when nothing claims a node as its child. The same applies to an external asset reached through the files array: its roots are what it contributes, where before it contributed nothing. glTF 2.0 is untouched, since that version requires the array.
Pins both halves of the version split: a 2.1 asset without a scenes array gets one over its root nodes, keeping every root rather than just the first, while a 2.0 asset is left without one as that version requires. The same split applies to a child reached through an external asset, so a 2.0 child that declares no scene still contributes nothing.
# Conflicts: # source/ResourceLoader/resource_loader.js # source/gltf/buffer.js # source/gltf/gltf.js # source/gltf/image.js
Use getRenderedWorldTransform() for shapes too
This branch has not been deployed
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.
View branch preview