Skip to content

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 #630

@johngorat

Description

@johngorat

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

  1. 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.
  2. Verify the file (npx @gltf-transform/cli inspect mannequin.glb): WEIGHTS_0 is UNSIGNED_BYTE, normalized: true; JOINTS_0+JOINTS_1 present.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions