glTF importer never consults the accessor `normalized` flag — UBYTE-normalized skin weights (e.g. from UE5's glTF Exporter) are destroyed during import, skinned mesh explodes into vertical spikes
@Changdw is already working on this.
Since Jun 15, 2026.
- Dominant language
- TypeScript
- Stars
- 430
- Forks
- 154
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 61
Description
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.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.- 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 /= 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. - Decoding
normalizedaccessors 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_Quinnfrom 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_0isUNSIGNED_BYTE, normalized: true;JOINTS_0+JOINTS_1present. - Import the
.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. - 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.