← Wisp

My VRM was rejected at upload and every viewer I own said it was fine

The mesh data was correct. The declaration about the mesh data was wrong — and rendering never reads the declaration, so nothing local could see it.

I uploaded Mira.vrm to VRoid Hub and it was rejected.

Nothing was wrong on my machine. Every viewer opened it correctly. My own VRM validation passed.

The error:

POSITION Z: declared -0.391, actual -0.238

The data was correct. The declaration about the data was wrong.

glTF accessors declare their own range

In glTF, separately from the vertex data itself, each accessor declares min and max.

{
  "type": "VEC3",
  "count": 12043,
  "min": [-0.512, 0.000, -0.391],
  "max": [ 0.512, 1.621,  0.284]
}

The spec requires that declaration to match the actual data. When it doesn't, the file is invalid — even though nothing about it looks wrong.

Why you cannot catch this locally

This is the part worth internalising.

Rendering reads the data. min/max are bounds metadata; they play no part in producing pixels. So when the declaration drifts, the render is still perfectly correct.

Everything you own reports "fine", because this isn't the kind of information you can check by looking.

The only thing that catches it is a distribution target that runs a real validator. Which means the place you find out is the moment before publishing.

Where it actually broke

A script that moves vertices around. On write-back it never recomputed min/max.

Walking the chain:

ArtifactBounds
Input base filecorrect
Intermediate build productdrifted
Final outputdrifted

The input was clean, which pinned the damage to the transform step. Locating *where* it broke is what tells you the thing to fix is the script, not the file.

Fixing it must not touch the vertices

Recomputing bounds changes no vertex data at all.

node app/scripts/fix-accessor-bounds.mjs <file.vrm> --check

With --check it only inspects, exiting non-zero on drift. Without it, it repairs.

Afterwards I took the sha256 of the BIN chunk and confirmed it was byte-identical to before the repair. That's the guarantee that only the declaration moved. Skip that check and you're left unable to rule out "I broke the model while trying to fix it."

Writing the lesson down is not enough

This is the real point.

If "recompute bounds after moving vertices" only exists as a note, you will stop in the same place next time. A countermeasure that depends on remembering fails on the day you don't remember.

I wired it into verify:release-assets. A broken model now means the build refuses to start.

Then I dropped in a deliberately broken model and confirmed it actually fails. Adding a check isn't finished until you've watched it reject something.

The general shape

This trap isn't specific to glTF.

Metadata you cannot verify by looking must be recomputed on every transform, or checked by a machine.

All of them look correct while being wrong. And things that look correct go unquestioned right up until you publish.


Separately from this, I build a desktop AI agent called Wisp. That Mira.vrm is its default body. It stands on your desktop, answers when you talk to it, and runs commands when you ask — always showing you what it's about to do first.