Skip to content

Build1 publisher3 min readPublished

One missing extension branch makes the only Dart VRM library throw on every VRM 1.0 file

A VRM file is a glTF 2.0 GLB, and its metadata sits in a JSON chunk under an extension key that VRM 1.0 renamed. vrm_dart reads only the old key, so 101 lines of Dart standard library now cover both versions.

The Engineer · Build desk

Illustration accompanying One missing extension branch makes the only Dart VRM library throw on every VRM 1.0 file

What happened

  • Fed a 10,917,800-byte VRM 1.0 file, vrm_dart's parse() reported ok, then the first vrmMeta access threw LateInitializationError and the thumbnail came back null.
  • VRM 1.0 moved the metadata to an extension named VRMC_vrm with renamed fields, and vrm_dart inspects only the extension named VRM.
  • On VRM 0.x the library returns a null thumbnail whenever the image is not named "Thumbnail", even though the metadata's texture field points straight at the image.
  • The replacement is 101 lines of dart:convert and dart:typed_data, and it read name, author, license and thumbnail on both format versions across five sample files.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure The exception escapes after parse() has already returned, so a try/catch wrapped around the parse call does not contain it; the throw lands at the field read, inside whatever widget asked for the model name.
  • decision Anyone still vendoring vrm_dart has to choose between adding a VRMC_vrm branch to code generated in 2021 and reading the GLB chunks directly, because the files users export now are the ones that break.
  • cost The adoption cost of the do-it-yourself route is 101 lines of standard-library code plus the tests you write for it, and you own every future format rename in that file.
  • constraint The 0.x thumbnail path can only be validated against files the author generated himself, so a team with a different exporter cannot treat this comparison as covering their inputs.

The container is why 101 lines is enough. The first 12 bytes of a VRM file are a GLB header: the magic `glTF`, version 2, and the total length, each a little-endian uint32 [2]. Every chunk after that is a 4-byte length, a 4-byte type, then the payload, and unknown types are skipped [2]. `dart:typed_data` reads those integers and `dart:convert` decodes the JSON chunk, which carries the model name, author, license, expressions, the mesh list and the index of the thumbnail image; the BIN chunk carries the thumbnail bytes [1].

The two format versions diverge most on how you reach that thumbnail. VRM 0.x names the field `texture` and the schema describes it as "Thumbnail of VRM model", and because the value is a texture index you pass through `textures[i].source` before you arrive at `images[]` [14]. VRM 1.0 names it `thumbnailImage` and describes it as "The index to the thumbnail image of the model in gltf.images" [15].

The rename is broader than the thumbnail. Under `VRMC_vrm`, `title` became `name`, `author` became `authors`, and `licenseName` became `licenseUrl`, with `name`, `authors` and `licenseUrl` all required in 1.0 [8][15]. vrm_dart has no branch for that key, so its `late` field `vrmMeta` is never assigned [8].

The 0.x failures come from generated code rather than schema drift. The library casts `images` and `bufferViews` to non-null lists, and the CC0 add-on fixture `minimal.vrm`, 4660 bytes with a single JSON chunk and no BIN chunk, has neither key [13][16]. Feeding it in produces `_TypeError: type 'Null' is not a subtype of type 'List<dynamic>' in type cast`, thrown inside `parse()` [12].

All five sample files had chunk lengths that were multiples of 4 [16]. Two of the five 0.x samples were built by the author with Blender 5.2.2 LTS and VRM add-on 4.7.1, because there is no public 0.x sample carrying a thumbnail [17]. So the 0.x texture-to-images path has been exercised against one exporter's output, on one machine, under Flutter 3.47.2 and Dart 3.13.2 [6]. If your pipeline emits unpadded chunk lengths or extra chunk types, that branch is untested here, and the offset handling is where a hand-rolled reader usually goes wrong.

gatari/vrm_dart has had no commits since 2021-10-13, roughly four years and eleven months before the test run [9][21], and that commit lands about seventeen months before VRoid Studio started exporting VRM 1.0 at v1.20.0 in March 2023 [22][11]. The upkeep argument is thinner than it looks in either direction. vrm_dart was also never published to pub, so in 2024 adopting it meant copying two files into the project, which is the same maintenance position as owning a parser, with someone else's non-null casts in it [18].

Unity has UniVRM and Unreal has VRM4U [20]. A pub.dev search for `vrm` still returns zero VRM packages [10]. The 2024 version of this post closed on not being able to reach BlendShapeGroup or the mesh list [19]; both were sitting in the JSON chunk the whole time [1].

What to watch

  • A VRM package appearing on pub.dev would put a maintained option against the 101 lines for the first time.
  • Any commit on gatari/vrm_dart adding a VRMC_vrm branch and a thumbnailImage read.
  • A public CC0 0.x sample with a thumbnail would let the texture-to-images path be checked against something other than one Blender export.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories