Version
- cocos-cli
0.0.1-alpha.28 (repo checkout, build driven via the MCP server / builder-build)
- engine
4.0.0-alpha.20
- Node 25.9.0, macOS arm64
Summary
The glTF importer reads accessor data without honoring the glTF 2.0 accessor.normalized flag. Per the spec, normalized integer accessors must be decoded as f = c / 255.0 (UNSIGNED_BYTE) / f = c / 65535.0 (UNSIGNED_SHORT), and normalized-integer skin weights are explicitly permitted for WEIGHTS_n (§3.7.3.3 Skinned Mesh Attributes).
Unreal Engine's bundled glTF Exporter writes WEIGHTS_0/WEIGHTS_1 as UNSIGNED_BYTE, normalized: true (plus 8 influences as JOINTS_0+JOINTS_1). Importing such a .glb into Cocos produces a skinned mesh that renders as giant vertical spikes anchored near one point, visible immediately on scene load.
This never reproduces with Cocos's own FBX import path because FBX-glTF-conv always emits FLOAT weights (verified — it also emits two joint sets, so the influence-merge path itself is routinely exercised and fine with floats). The bug only surfaces with externally produced glTF files: UE's exporter, or any quantization tooling.
Mechanism (from reading the published compiled output, dist/core/assets/asset-handler/assets/utils/gltf-converter.js)
accessor.normalized is never consulted anywhere in the converter. Vertex attributes are read raw via the plain accessor-read path; only animation sampler outputs go through a normalize-as-float helper — and even that one converts unconditionally based on the typed array's storage type, not the flag.
- For files with two joint sets the importer then runs
PPGeometry.reduceJointInfluences() ("engine supports up to 4 joints") on the raw integer weights: it merges influence sets and renormalizes in place inside the integer-typed array (weight /= sum on a Uint8Array), truncating nearly every merged weight to 0 (or 1). The destroyed weight set then enters skinning: per-vertex weight sums are far from 1.0 → the blended joint matrices produce the spike explosion.
- Decoding
normalized accessors to float at read time would fix both: the merge already works correctly for FLOAT weights.
Mesh struct.minPosition/maxPosition and the model's runtime worldBounds stay sane — POSITION data is fine; the corruption is purely in skin weights.
Steps to reproduce
- In UE 5.6: enable the bundled glTF Exporter plugin (ships with the engine, enabled by default in 5.6). Right-click any skeletal mesh — e.g.
SKM_Quinn from the Third Person template — → Asset Actions → Export → choose .glb, keep all options at defaults.
- Verify the file (
npx @gltf-transform/cli inspect mannequin.glb): WEIGHTS_0 is UNSIGNED_BYTE, normalized: true; JOINTS_0+JOINTS_1 present.
- Import the
.glb into a Cocos project (copy into assets/, refresh), instantiate the generated .prefab sub-asset in a scene, build web-desktop with the default build config, serve, open in a browser.
- The character renders as vertical spikes. Programmatic check: dump the imported vertex bundle's weights — per-vertex sums are ≫ 1.0 (raw 0–255-derived values), where the source accessor decodes to sums of 1.0.
We can attach a repro .glb (UE-exported mannequin) on request.
Expected
Normalized integer accessors decoded to float at import per the spec; skinned UE glTF files render correctly.
Actual
Raw integer weights enter the import pipeline; the influence-merge renormalizes them in place inside the integer array, truncating weights; the mesh is unusable.
Workaround
Pre-process external GLBs before import: decode WEIGHTS_* to FLOAT and merge influences to top-4. We use a small Python script for this — happy to attach it in this thread.
This report was drafted with AI assistance and manually verified against our project before posting.
Version
0.0.1-alpha.28(repo checkout, build driven via the MCP server /builder-build)4.0.0-alpha.20Summary
The glTF importer reads accessor data without honoring the glTF 2.0
accessor.normalizedflag. Per the spec, normalized integer accessors must be decoded asf = c / 255.0(UNSIGNED_BYTE) /f = c / 65535.0(UNSIGNED_SHORT), and normalized-integer skin weights are explicitly permitted forWEIGHTS_n(§3.7.3.3 Skinned Mesh Attributes).Unreal Engine's bundled glTF Exporter writes
WEIGHTS_0/WEIGHTS_1asUNSIGNED_BYTE, normalized: true(plus 8 influences asJOINTS_0+JOINTS_1). Importing such a.glbinto Cocos produces a skinned mesh that renders as giant vertical spikes anchored near one point, visible immediately on scene load.This never reproduces with Cocos's own FBX import path because
FBX-glTF-convalways emits FLOAT weights (verified — it also emits two joint sets, so the influence-merge path itself is routinely exercised and fine with floats). The bug only surfaces with externally produced glTF files: UE's exporter, or any quantization tooling.Mechanism (from reading the published compiled output,
dist/core/assets/asset-handler/assets/utils/gltf-converter.js)accessor.normalizedis never consulted anywhere in the converter. Vertex attributes are read raw via the plain accessor-read path; only animation sampler outputs go through a normalize-as-float helper — and even that one converts unconditionally based on the typed array's storage type, not the flag.PPGeometry.reduceJointInfluences()("engine supports up to 4 joints") on the raw integer weights: it merges influence sets and renormalizes in place inside the integer-typed array (weight /= sumon a Uint8Array), truncating nearly every merged weight to 0 (or 1). The destroyed weight set then enters skinning: per-vertex weight sums are far from 1.0 → the blended joint matrices produce the spike explosion.normalizedaccessors to float at read time would fix both: the merge already works correctly for FLOAT weights.Mesh
struct.minPosition/maxPositionand the model's runtimeworldBoundsstay sane — POSITION data is fine; the corruption is purely in skin weights.Steps to reproduce
SKM_Quinnfrom the Third Person template — → Asset Actions → Export → choose.glb, keep all options at defaults.npx @gltf-transform/cli inspect mannequin.glb):WEIGHTS_0isUNSIGNED_BYTE, normalized: true;JOINTS_0+JOINTS_1present..glbinto a Cocos project (copy intoassets/, refresh), instantiate the generated.prefabsub-asset in a scene, buildweb-desktopwith the default build config, serve, open in a browser.We can attach a repro
.glb(UE-exported mannequin) on request.Expected
Normalized integer accessors decoded to float at import per the spec; skinned UE glTF files render correctly.
Actual
Raw integer weights enter the import pipeline; the influence-merge renormalizes them in place inside the integer array, truncating weights; the mesh is unusable.
Workaround
Pre-process external GLBs before import: decode
WEIGHTS_*to FLOAT and merge influences to top-4. We use a small Python script for this — happy to attach it in this thread.This report was drafted with AI assistance and manually verified against our project before posting.