Skip to content

feat(vat): --emit-uv2 flag — bake column index into source.gltf#654

Merged
fernandotonon merged 2 commits into
masterfrom
feat/vat-emit-uv2
May 21, 2026
Merged

feat(vat): --emit-uv2 flag — bake column index into source.gltf#654
fernandotonon merged 2 commits into
masterfrom
feat/vat-emit-uv2

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

Summary

Adds `qtmesh vat --emit-uv2 [N]` to inject the per-vertex bake-column index as a `TEXCOORD_` attribute (default channel 1) directly into the exported `source.gltf`. Consumers can then read UV<N> instead of running the bind-sidecar matcher at runtime — the same data that the matcher would compute lands in the glTF as a normal vertex attribute, no special-case loader code needed.

This is what makes the "open .uproject, hit Play" Unreal flow possible. Same simplification applies to the Godot demo (already shipping) and any third-party Unity / three.js / Blender consumer.

CLI

```bash

bare form: defaults to channel 1

qtmesh vat character.fbx --anim "Dance" --emit-uv2

explicit channel (1..7); channel 0 is rejected to avoid clobbering diffuse UV

qtmesh vat character.fbx --anim "Dance" --emit-uv2 2
```

Sample text output:

```
Baked OpenVAT for 'Dance' (71 frames × 5828 vertices)
texture: bakes/Dance/Dance_pos.png
sidecar: bakes/Dance/Dance-remap_info.json
bind: bakes/Dance/Dance_ogre_bind.bin (per-vertex bind-pose signature; ...)
mesh: bakes/Dance/source.gltf (vertex order matches the bake)
uv2: injected as TEXCOORD_1 — consumers can drop the runtime bind-sidecar
matcher and read (col, row) from the mesh's UV1 directly
bounds: min=(-0.700, -0.100, -0.600) max=(0.600, 2.000, 0.400)
```

JSON output gains `"uv2Channel": 1`.

Mechanics

  • `cmdVat` parses the new flag (default off, channel 1 when bare, rejects channel 0 to avoid clobbering the diffuse UV).
  • After the bake completes successfully AND the bind-sidecar permutation built a valid mapping (`sourceMeshMatchesBake`), we open `source.gltf`, append a new bufferView + accessor per primitive carrying the column indices, and wire the accessor into each primitive's attributes as `TEXCOORD_`.
  • The injection is a JSON+binary post-pass on the file Assimp just wrote. No Ogre-side or Assimp changes; safer than seeding UV2 before export (Assimp's `JoinIdenticalVertices` would shuffle it).
  • On any failure (missing .bin, primitive count mismatch, etc.) the original glTF is left untouched and the user gets a warning pointing back at the bind-sidecar matcher.

Drive-by fix

Also fixed a regression in `buildVertexPermutation`'s V handling: the post-FlipUVs export sometimes preserves and sometimes inverts V depending on Assimp's internal flag flow. Instead of guessing, the matcher now tries both V conventions and uses whichever resolves all vertices. The Ogre→glTF readback in `readGltfVertices` stops preemptively 1-V'ing.

Test plan

  • Verified on Rumba Dancing.fbx: 5828/5828 verts matched, TEXCOORD_1 written with `min=[0,0]` `max=[1023,0]` for prim 0, sequential columns per primitive.
  • `--emit-uv2 0` rejected with exit 2 + clear error
  • `--emit-uv2` without value defaults to channel 1
  • Bare `qtmesh vat` (no flag) leaves source.gltf TEXCOORD_1 absent
  • Once merged: follow-up updates the Godot demo to use TEXCOORD_1 directly and re-enables the Unreal demo

Related

Unlocks an "open .uproject, hit Play" Unreal demo (currently blocked at the UV2-bake step in #652). Follows up on #648, #649, #651.

🤖 Generated with Claude Code

Adds `qtmesh vat --emit-uv2 [N]` to inject the per-vertex bake-column
index as a TEXCOORD_<N> attribute (default channel 1) directly into
the exported source.gltf. Consumers can then read UV<N> instead of
running the bind-sidecar matcher at runtime — the same data that the
matcher would compute lands in the glTF as a normal vertex attribute,
no special-case loader code needed.

This is what makes the "open .uproject, hit Play" Unreal flow
possible. Same simplification applies to the Godot demo (already
shipping) and any third-party Unity/three.js/blender consumer.

Mechanics:

  - cmdVat parses the new flag (default off, channel 1 when bare,
    rejects channel 0 to avoid clobbering the diffuse UV).
  - After the bake completes successfully AND the bind-sidecar
    permutation built a valid mapping (`sourceMeshMatchesBake`),
    we open source.gltf, append a new bufferView + accessor per
    primitive carrying the column indices, and wire the accessor
    into each primitive's attributes as TEXCOORD_<N>.
  - The injection is a JSON+binary post-pass on the file Assimp
    just wrote. No Ogre-side or Assimp changes; safer than seeding
    UV2 before export (Assimp's JoinIdenticalVertices would shuffle
    it).
  - On any failure (missing .bin, primitive count mismatch, etc.)
    the original glTF is left untouched and the user gets a warning
    pointing back at the bind-sidecar matcher.

Also fixed a regression in `buildVertexPermutation`'s V handling:
the post-FlipUVs export sometimes preserves and sometimes inverts V
depending on Assimp's internal flag flow. Instead of guessing, the
matcher now tries both V conventions and uses whichever resolves
all vertices. The Ogre→glTF readback in readGltfVertices stops
preemptively 1-V'ing.

Verified end-to-end on Rumba Dancing.fbx — 5828/5828 verts matched,
TEXCOORD_1 written with `min=[0,0]` `max=[1023,0]` for prim 0,
sequential columns per primitive as expected.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 24 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eaceeff4-3c08-4767-af93-412b8652c34a

📥 Commits

Reviewing files that changed from the base of the PR and between 7d24c22 and a7c72dd.

📒 Files selected for processing (1)
  • src/CLIPipeline.cpp
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vat-emit-uv2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4ada8899f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/CLIPipeline.cpp Outdated
Comment on lines +5965 to +5968
if (ok && n >= 0 && n <= 7) {
channel = n;
++i;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject out-of-range --emit-uv2 channel values

When a user passes an explicit channel outside 0..7 (for example --emit-uv2 8), this branch just leaves the token unconsumed and falls back to channel 1 instead of surfacing an error. That silently violates the documented 1..7 contract and can also mis-parse the command line (e.g., 8 may later be treated as the positional file argument when none was set yet), so users can get the wrong UV channel or a confusing "file not found" failure.

Useful? React with 👍 / 👎.

Comment thread src/CLIPipeline.cpp Outdated
if (v > vMax) vMax = v;
}
// Append payload to the .bin and add a bufferView + accessor.
bf.write(payload);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check append writes before committing new accessor offsets

The return value of bf.write(payload) is ignored, so a short/failed write (e.g., disk full) still advances cursorBytes and later updates JSON metadata as if all bytes were appended. In that case the rewritten glTF can reference UV data that was never fully written, producing a corrupted source.gltf while this function still reports success.

Useful? React with 👍 / 👎.

Addresses both Codex notes on PR #654.

1. P1 — `--emit-uv2 N` no longer silently falls back to channel 1
   when N is outside the documented 1..7 range. Previously the
   non-matching numeric token was left unconsumed and could also
   be mis-parsed as the positional file argument later in the
   loop, producing a confusing "file not found" instead of a
   clear "out of range" error.

   New behaviour:
     - Distinguishes "next token is an integer literal" from
       "next token is the file path" via a digit-only check
       (allowing a leading +/-).
     - When the next token IS an integer, consumes it
       unconditionally so it can't fall through.
     - Range-checks: rejects N < 0, N > 7, and N == 0 with a
       specific message in each case.
     - Non-numeric token after `--emit-uv2` is treated as the
       bare form (channel 1) — matches the previous behaviour.

2. P2 — emitGltfUv2 now checks the return of `bf.write(payload)`
   per primitive. On a short write (disk full, FS quota, I/O
   error mid-stream) it truncates the .bin back to its original
   length and returns a structured error, so the caller doesn't
   commit JSON bufferView/accessor metadata that references data
   that was never fully written.
@fernandotonon fernandotonon merged commit cfbbbfb into master May 21, 2026
6 checks passed
@fernandotonon fernandotonon deleted the feat/vat-emit-uv2 branch May 21, 2026 02:19
fernandotonon added a commit that referenced this pull request May 21, 2026
…gltf

After PR #654 added `qtmesh vat --emit-uv2`, the bake's source.gltf
carries the per-vertex column index as TEXCOORD_1 directly. Unreal's
mesh importer reorders vertices for cache locality, but a vertex
attribute travels with its vertex through any reorder — so the
imported mesh's TexCoord[1] already points at the right column in
the position texture. No runtime UV2-baking, no bind-sidecar
matcher, no engine-version-specific Geometry Script paths.

This turns the previously-incomplete Unreal demo into a true
"open and play" setup:

  1. Open QtMeshVAT.uproject
  2. Run build_vat_demo.py from the Python console
  3. Wire BP_VATDancer's 4-node Tick (documented in README)
  4. Drop the actor into a level, hit Play.

Code changes:

  - Re-baked Content/Rumba/source.gltf + source.bin with
    `qtmesh vat --emit-uv2`. The .gltf now carries TEXCOORD_1 on
    every primitive; size grew from 642 KB to 689 KB for the UV2
    payload.
  - Replaced bake_uv2() (which returned False because it couldn't
    portably commit the UV2 write) with verify_gltf_has_uv2() —
    a fast pre-check that fails the bootstrap loudly if a user's
    bake folder predates --emit-uv2.
  - Dropped the obsolete read_ogre_bind() + matching plan and the
    struct/Geometry-Script path references.
  - Material graph comment updated: TexCoord[1] now comes straight
    from glTF's TEXCOORD_1, not a post-import EUW write.
  - README: drop "Step 4: bake UV2" entirely. New error case for
    "source.gltf is MISSING TEXCOORD_1" points the user at
    `qtmesh vat --emit-uv2`.
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant