Skip to content

feat(unity-demo): OpenVAT demo for Unity — Built-in Render Pipeline#672

Merged
fernandotonon merged 46 commits into
masterfrom
feat/unity-vat-demo
May 27, 2026
Merged

feat(unity-demo): OpenVAT demo for Unity — Built-in Render Pipeline#672
fernandotonon merged 46 commits into
masterfrom
feat/unity-vat-demo

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

@fernandotonon fernandotonon commented May 25, 2026

Summary

  • Adds tools/unity-vat-demo/, a stock-Unity-API port of the existing Godot/Unreal VAT demos. Same Rumba bake, same shader contract — useful when integrating a qtmesh vat bake into a Unity 2022.3 LTS+ project.
  • Ships C# scripts (VATPlayer, OrbitCamera, FPSOverlay, PerfSpawnerVAT, PerfSpawnerSkeleton, editor-only BootstrapVAT that auto-wires the bake fields from the sidecar JSON), the shader at Assets/Shaders/openvat.shader, the Rumba bake under Assets/VAT/Rumba/, and a Unity project skeleton (ProjectSettings, Packages/manifest.json, .gitignore).
  • Wires the new demo into the docs page so the qtmesh vat section links Godot/Unity/Unreal demos side by side.

Test plan

  • Open tools/unity-vat-demo/ in Unity Hub → Add → 2022.3 LTS. First import succeeds.
  • Set mixamo.com_pos.png texture import to sRGB=OFF, Filter=Point, Compression=None, Wrap=Clamp.
  • Set source.gltf model Read/Write=ON.
  • Build the Web.unity scene per the README — single dancer plays the Rumba animation in the editor viewport.
  • Build PerfVAT.unity + PerfSkeleton.unity. VAT scene's min(1s) FPS is higher than the skeleton scene's on the same machine.
  • (Optional) WebGL export. Brotli-compressed bundle is ~25 MB.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a complete Unity OpenVAT demo with VAT playback, scene builders, headless build targets, performance spawners, FPS overlay, and orbit camera controls for testing and profiling.
  • Documentation

    • Added an end-to-end README covering setup, import settings, scene creation, WebGL build notes, and limitations.
  • Chores

    • Added Unity project configuration, package manifests, many asset metadata files, improved .gitignore rules, and CI per-suite timeouts.

Review Change Stack

Adds tools/unity-vat-demo/, the Unity counterpart to the existing Godot
and Unreal sample projects. Same Rumba bake, same shader contract — a
stock-Unity reference port for integrating an OpenVAT bake into a Unity
2022.3 LTS project (or newer) without URP/HDRP/Shader Graph.

Ships:
- C# scripts: VATPlayer (binds shader + drives _CurrentFrame),
  OrbitCamera (mouse-drag orbit + wheel zoom), FPSOverlay (avg + min(1s)
  rolling readout), PerfSpawnerVAT / PerfSpawnerSkeleton (the 1000-
  instance apples-to-apples comparison), and an editor-only BootstrapVAT
  that auto-wires the bake fields when a VATPlayer is dropped onto a
  GameObject (parses the sidecar JSON).
- Shader: tools/vat-shaders/openvat.shader (the file we already publish
  as the Unity reference) copied to Assets/Shaders/.
- Bake: same Rumba artifacts as godot-vat-demo/assets/Rumba/ — source
  glTF/bin, diffuse PNG, packed positions+normals PNG, sidecar JSON.
- Unity project skeleton: ProjectSettings/ProjectVersion.txt pinned to
  2022.3.20f1 LTS, Packages/manifest.json with the minimum BiRP module
  set, and a .gitignore so a developer's editor-generated Library/Temp/
  folders don't leak back into the repo.

Why no pre-built .unity scenes: Unity scenes reference assets by GUID
from .meta files generated on the developer's first import. Hand-rolled
scenes break the moment another machine reimports. The README walks
through a 60-second scene-assembly flow that produces the same scenes
we'd ship.

Wires the new demo into the docs page (website/src/DocsApp.jsx) — the
`qtmesh vat` section now links Godot/Unity/Unreal demos side by side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a full Unity OpenVAT demo: runtime VAT player, editor autowire and import tooling, custom glTF importer preserving vertex order, performance spawners and scenes, editor build/CLI tools, project metadata and README, plus CI test timeout improvements.

Changes

Unity VAT Demo Implementation

Layer / File(s) Summary
VAT Playback Engine and Material Binding
tools/unity-vat-demo/Assets/Scripts/VATPlayer.cs, related meta
VATPlayer binds VAT textures, synthesizes UV2 when needed, creates the runtime material from Hidden/QTM/VAT, sets shader parameters (frameCount, bounds, baseColor), and drives playback via self-driven Update() or SetCurrentFrame().
Editor Bootstrap and Auto‑Wiring
tools/unity-vat-demo/Assets/Scripts/Editor/BootstrapVAT.cs, tools/unity-vat-demo/Assets/Scripts/Editor/VATPlayerEditor.cs
BootstrapVAT listens for VATPlayer additions, parses bake sidecar JSON for frames/bounds, loads textures/mesh sub-assets, and populates VATPlayer inspector fields. VATPlayerEditor adds an “Auto‑Wire from Bake” inspector button.
Custom glTF Import with Vertex Order Preservation
tools/unity-vat-demo/Assets/Scripts/Editor/QtmGltfImporter.cs
ScriptedImporter for .gltf that reads .bin buffers, preserves vertex order while merging primitives, fills missing attributes with safe defaults, and emits a prefab GameObject with the assembled Mesh.
VAT Asset Import Settings Normalization
tools/unity-vat-demo/Assets/Scripts/Editor/VATAssetPostprocessor.cs
Enforces VAT-friendly ModelImporter and TextureImporter settings for Assets/VAT/Rumba (readable meshes, disable skeleton/animation optimizations; position textures: sRGB off, point filter, clamp, no mipmaps).
Performance Test Infrastructure
tools/unity-vat-demo/Assets/Scripts/PerfSpawnerVAT.cs, tools/unity-vat-demo/Assets/Scripts/PerfSpawnerSkeleton.cs
PerfSpawnerVAT spawns a centered grid of VATPlayer instances and drives them with a shared clock plus per-instance phase offsets. PerfSpawnerSkeleton spawns animator-driven prefab instances with deterministic phase offsets for baseline comparison.
Scene Control and Performance Monitoring
tools/unity-vat-demo/Assets/Scripts/OrbitCamera.cs, tools/unity-vat-demo/Assets/Scripts/FPSOverlay.cs
OrbitCamera implements mouse-drag orbit and scroll-wheel zoom with clamped pitch/distance. FPSOverlay renders rolling average and min FPS via IMGUI with configurable style.
Scene Builders & CLI
tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuildScenes.cs, tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuilder.cs
Editor utilities to programmatically build demo scenes and headless OS-targeted builds for CI/automation.
Editor Utilities
tools/unity-vat-demo/Assets/Scripts/Editor/ForceVATCompatibleSettings.cs
Initializes a VAT-compatible vertex channel compression mask on editor domain reload to ensure predictable vertex attribute uploads.
Project Configuration and Asset Metadata
tools/unity-vat-demo/.gitignore, tools/unity-vat-demo/Packages/manifest.json, tools/unity-vat-demo/Packages/packages-lock.json, tools/unity-vat-demo/ProjectSettings/*, numerous Assets/**/*.meta
Adds demo .gitignore, pins packages via manifest/lockfile, sets ProjectVersion and SceneTemplateSettings, and commits many Unity .meta files to establish folder/importer metadata.
Bake Metadata
tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com-remap_info.json
Sidecar JSON specifying axes/bit depth and os-remap with Frames and Min/Max bounds for the Rumba bake.
Documentation
tools/unity-vat-demo/README.md
Comprehensive README: one-time setup (position-texture import settings), glTF importer notes, scene build steps, WebGL build guidance, VAT rebake/re-import workflow, and limitations.

Website Documentation Update

Layer / File(s) Summary
Live Demo Link Updates
website/src/DocsApp.jsx
Removed Unity demo link from cmd-vat live demos list; Godot (web) and Unreal demo links remain.

CI/CD Test Timeout Improvements

Layer / File(s) Summary
Deploy Workflow Test Suite Timeout Management
.github/workflows/deploy.yml
Adds a 45-minute job timeout for Linux unit-tests and wraps each gtest suite with a 300s per-suite timeout; timed-out suites are logged and counted instead of failing the whole job immediately.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

"🐰 A demo wakes beneath the sun,
Textures hum and frames run,
Meshes leap in tidy rows—
A rabbit cheers, the vat now shows!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main addition: a Unity demo for OpenVAT targeting the Built-in Render Pipeline, with specific version context.
Description check ✅ Passed The description provides a comprehensive summary of changes, technical details, and a detailed test plan, covering all primary components and verification steps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/unity-vat-demo

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: 69968e99e0

ℹ️ 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 on lines +2 to +4
"dependencies": {
"com.unity.collab-proxy": "2.0.5",
"com.unity.ide.rider": "3.0.24",
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 Add glTF importer dependency for Unity 2022 demo

The demo assumes Assets/VAT/Rumba/source.gltf is imported as a usable prefab/mesh (used by BootstrapVAT and the perf scene setup), but this project manifest does not include a glTF importer package. On a clean Unity 2022.3 setup, that leaves source.gltf unavailable as a GameObject/mesh and breaks auto-wiring and scene setup until users manually install the importer. Add com.unity.cloud.gltfast (as already done in tools/unity-vat-test/Packages/manifest.json) so the checked-in demo works out of the box.

Useful? React with 👍 / 👎.

}
_material = new Material(shader) { name = "VAT_" + name };
_material.SetTexture("_PosTex", positionTexture);
if (diffuseTexture != null) _material.SetTexture("_MainTex", diffuseTexture);
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 Stop binding nonexistent _MainTex on VAT material

VATPlayer sets diffuseTexture into _MainTex, but this shader has no _MainTex property and the fragment path only uses _BaseColor. Because BootstrapVAT auto-populates diffuseTexture, this mismatch is hit by default: texture binding is ignored (and can emit missing-property warnings per instance), so the advertised diffuse texture path does not actually render.

Useful? React with 👍 / 👎.

Two follow-up fixes for the Unity demo:

1. `BootstrapVAT.cs`: the auto-wirer was reaching for the imported mesh
   via `GetComponentInChildren<SkinnedMeshRenderer>().sharedMesh`, which
   misses when Unity's glTF importer strips the rig to a static
   MeshFilter — common in Unity 6's default Model import settings.
   Enumerate the .gltf's sub-assets directly via
   `AssetDatabase.LoadAllAssetsAtPath()` and pick the first `Mesh` we
   find. Works regardless of whether the importer kept a
   SkinnedMeshRenderer or collapsed it.

2. README: add a "Mesh picker hint" so users who hit the same dead-end
   in the Source Mesh slot picker know about the `t:Mesh` filter and
   the expand-glTF-in-Project-view trick. Unity only lists top-level
   Mesh assets in the picker by default, but the .gltf's Mesh is
   nested inside the importer as a sub-asset.

Also accepts the linter-bumped Unity 6 ProjectVersion / Packages
manifest from the previous commit (no functional change — just the
versions a 6000.x editor writes back when first opening the project).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
tools/unity-vat-demo/Assets/Scripts/FPSOverlay.cs (1)

34-44: ⚡ Quick win

Cache GUI styles; avoid per-OnGUI allocations in a perf overlay.

Recreating GUIStyle every OnGUI() adds avoidable GC/CPU overhead and can contaminate the FPS numbers this script reports.

Proposed refactor
 public class FPSOverlay : MonoBehaviour
 {
@@
     float _smoothedAvg;
     float _windowMin = float.MaxValue;
+    GUIStyle _style;
+    GUIStyle _shadow;
+
+    void Awake()
+    {
+        RebuildStyles();
+    }
+
+    void OnValidate()
+    {
+        if (Application.isPlaying) RebuildStyles();
+    }
+
+    void RebuildStyles()
+    {
+        _style = new GUIStyle(GUI.skin.label)
+        {
+            fontSize  = fontSize,
+            fontStyle = FontStyle.Bold,
+            normal    = { textColor = textColor },
+        };
+        _shadow = new GUIStyle(_style) { normal = { textColor = Color.black } };
+    }
@@
     void OnGUI()
     {
-        var style = new GUIStyle(GUI.skin.label)
-        {
-            fontSize  = fontSize,
-            fontStyle = FontStyle.Bold,
-            normal    = { textColor = textColor },
-        };
-        // Drop-shadow for legibility against a busy scene.
-        var shadow = new GUIStyle(style) { normal = { textColor = Color.black } };
+        if (_style == null || _shadow == null) RebuildStyles();
@@
-        GUI.Label(new Rect(20 + 2, 14 + 2, 600, 60), text, shadow);
-        GUI.Label(new Rect(20,     14,     600, 60), text, style);
+        GUI.Label(new Rect(20 + 2, 14 + 2, 600, 60), text, _shadow);
+        GUI.Label(new Rect(20,     14,     600, 60), text, _style);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/unity-vat-demo/Assets/Scripts/FPSOverlay.cs` around lines 34 - 44,
OnGUI is allocating new GUIStyle instances each frame (style and shadow) which
hurts performance and skews FPS; change FPSOverlay to cache these styles as
private fields (e.g., cachedStyle, cachedShadow) and create/update them in
Awake/OnEnable or when fontSize/textColor change instead of inside OnGUI,
reusing the cachedStyle and cachedShadow in OnGUI; ensure you copy the necessary
properties (fontSize, fontStyle, normal.textColor) when updating so the overlay
reflects runtime changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/unity-vat-demo/Assets/Scripts/Editor/BootstrapVAT.cs`:
- Around line 103-106: TryParseSidecar currently returns true when Frames parses
even if ParseVec3Field returned default (0,0,0) for missing/invalid Min/Max;
update the function so it only succeeds when Frames > 0 AND both Min and Max
were actually parsed. Concretely, change the ParseVec3Field calls (or add new
TryParseVec3Field) so you can detect success for mn and mx, or check the JSON
for the "Min"/"Max" keys before calling ParseVec3Field; then return (frames > 0
&& mnParsed && mxParsed). This ensures ParseIntField(frames) and
ParseVec3Field(min) / ParseVec3Field(max) all signal success before
TryParseSidecar returns true.

In `@tools/unity-vat-demo/Assets/Scripts/FPSOverlay.cs`:
- Line 12: The public int windowFrames in FPSOverlay can be negative from the
Inspector which leads to dequeuing past zero; fix by clamping it to a
non-negative value (preferably >=0 or >=1 as you need) and guard the dequeuing
logic. Add an OnValidate (or Awake) that sets windowFrames = Mathf.Max(0,
windowFrames) and change the dequeuing loop in the Update/Render method to while
(frameTimes.Count > windowFrames) { ... } (or use Mathf.Min/Max checks) so the
queue is never dequeued when empty; reference FPSOverlay, windowFrames,
OnValidate/Awake, and the Update dequeuing loop to locate the changes.

In `@tools/unity-vat-demo/Assets/Scripts/PerfSpawnerVAT.cs`:
- Around line 94-95: In PerfSpawnerVAT.Update(), guard phaseJitter before using
it in the modulo: ensure phaseJitter is positive (e.g., clamp to a minimum > 0
or fallback to 1) before computing float phase = (i * 7919) % phaseJitter; so
that when phaseJitter is zero or negative you either skip the modulo and set
phase = 0 or use a safe positive value, then call
_players[i].SetCurrentFrame(_clock + phase) as before; update the check near the
current computation of phase in Update() referencing phaseJitter, phase,
_players and SetCurrentFrame.

---

Nitpick comments:
In `@tools/unity-vat-demo/Assets/Scripts/FPSOverlay.cs`:
- Around line 34-44: OnGUI is allocating new GUIStyle instances each frame
(style and shadow) which hurts performance and skews FPS; change FPSOverlay to
cache these styles as private fields (e.g., cachedStyle, cachedShadow) and
create/update them in Awake/OnEnable or when fontSize/textColor change instead
of inside OnGUI, reusing the cachedStyle and cachedShadow in OnGUI; ensure you
copy the necessary properties (fontSize, fontStyle, normal.textColor) when
updating so the overlay reflects runtime changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a648641-1200-41fb-82d1-40f813dd0e5b

📥 Commits

Reviewing files that changed from the base of the PR and between b3e3a09 and 7d1434e.

⛔ Files ignored due to path filters (6)
  • tools/unity-vat-demo/Assets/Shaders/openvat.shader is excluded by !**/*.shader
  • tools/unity-vat-demo/Assets/VAT/Rumba/Boss_diffuse.png is excluded by !**/*.png
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com_ogre_bind.bin is excluded by !**/*.bin
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com_pos.png is excluded by !**/*.png
  • tools/unity-vat-demo/Assets/VAT/Rumba/source.bin is excluded by !**/*.bin
  • tools/unity-vat-demo/Assets/VAT/Rumba/source.gltf is excluded by !**/*.gltf
📒 Files selected for processing (12)
  • tools/unity-vat-demo/.gitignore
  • tools/unity-vat-demo/Assets/Scripts/Editor/BootstrapVAT.cs
  • tools/unity-vat-demo/Assets/Scripts/FPSOverlay.cs
  • tools/unity-vat-demo/Assets/Scripts/OrbitCamera.cs
  • tools/unity-vat-demo/Assets/Scripts/PerfSpawnerSkeleton.cs
  • tools/unity-vat-demo/Assets/Scripts/PerfSpawnerVAT.cs
  • tools/unity-vat-demo/Assets/Scripts/VATPlayer.cs
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com-remap_info.json
  • tools/unity-vat-demo/Packages/manifest.json
  • tools/unity-vat-demo/ProjectSettings/ProjectVersion.txt
  • tools/unity-vat-demo/README.md
  • website/src/DocsApp.jsx

Comment thread tools/unity-vat-demo/Assets/Scripts/Editor/BootstrapVAT.cs Outdated
Comment thread tools/unity-vat-demo/Assets/Scripts/FPSOverlay.cs
Comment thread tools/unity-vat-demo/Assets/Scripts/PerfSpawnerVAT.cs Outdated
Comment thread tools/unity-vat-demo/ProjectSettings/ProjectVersion.txt
fernandotonon and others added 11 commits May 25, 2026 18:38
…d glTF

Unity 6 doesn't ship a built-in glTF importer; `.gltf` files land as
plain data files (no Mesh sub-asset, no Model prefab), which is why
the Source Mesh picker came up empty even with the `t:Mesh` filter.

Convert the same mesh via `qtmesh convert source.gltf -o source.fbx`
(through the same Ogre intermediate the baker walked → 5828 verts in
the same order, position-texture columns still align). Keep the .gltf
in the bake folder for parity with the Godot + Unreal demos.

BootstrapVAT now probes `source.fbx` first and falls back to
`source.gltf` (so the auto-wirer keeps working if a future user
installs UnityGLTF and prefers the glTF), and the helper is renamed
FindFirstMeshInGltf → FindFirstMeshInModel to reflect the broader role.
README explains the FBX-vs-glTF choice in a callout next to the
import-settings step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…the runner for 6h

PR #672's first CI run hung 3 hours+ in BoneDragReleaseTest with the
last log line on `[ RUN ] BoneDragReleaseTest.NoAnimSetsInitial` — no
PASSED/FAILED follow-up. The suite was deadlocked (likely an Ogre /
Mesa render-system shutdown that doesn't reliably return on Xvfb), but
because the workflow inherited GitHub's default `timeout-minutes: 360`
the job kept burning runner time until I manually cancelled.

Two layers of timeout so this surfaces in minutes, not hours:

1. **Job-level `timeout-minutes: 45`**. Full sweep normally takes ~12
   min (build cache restore + Xvfb + ~25 suites + coverage + sonar).
   45 min is a generous ceiling — beyond that something is very wrong
   and we want the job to abort.

2. **Per-suite `timeout --foreground --signal=KILL 300`**. The slowest
   legitimate suite is ~90s (FBX import round-trip); 5 min covers
   anything reasonable. SIGKILL goes straight in because a deadlocked
   Qt event loop / GL driver doesn't honour SIGTERM consistently.
   `--foreground` keeps timeout usable inside the for-loop where the
   shell isn't a session leader.

Exit code 124 from timeout(1) is treated as a non-fatal WARNING and
counted in CRASHED_SUITES (same shape as the existing GL-crash
allowlist). The remaining suites still run so we get partial coverage
+ a clear log naming the hung suite, instead of a silent multi-hour
wedge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Mesh picker in the Source Mesh slot stays empty even with the FBX
in the project because Unity hides sub-assets (the Model importer's
nested Mesh) from the picker by default. The BootstrapVAT auto-wirer
covers this — but it only fires on `componentWasAdded`, which races
against the FBX importer on first project open.

Add a custom Inspector for VATPlayer with an "Auto-Wire from Bake"
button that calls into the now-public `BootstrapVAT.AutoWire(player,
verbose:true)`. The button is a user-driven escape hatch that always
works regardless of import timing — click it once the FBX has finished
importing and the slot populates. Preserves any slots the user already
filled in manually; always overwrites frameCount + bounds from the
sidecar (single source of truth).

Renames `TryAutoWire` → public `AutoWire` and threads a `verbose` flag
so the auto-fire-on-add stays quiet during normal use while the
button's invocations log to the console. README updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t settings

The Source Mesh picker was empty because Unity's FBX importer was
configured as Humanoid (animationType: 2) — humanoid rigs bury the
Mesh inside the generated SkinnedMeshRenderer + Avatar bundle and
don't expose a selectable standalone Mesh sub-asset in the picker.

Fixes by enforcing three import settings on every reimport of files
under Assets/VAT/Rumba/:

ModelImporter (source.fbx):
  • animationType = None  → exposes the Mesh as a top-level sub-asset
  • importAnimation = false → VAT texture replaces the Animator
  • isReadable = true → VATPlayer.EnsureUV2 writes uv2 at runtime
  • optimizeGameObjects = false → keeps the prefab walkable

TextureImporter (mixamo.com_pos.png):
  • sRGBTexture = false (positions are linear data)
  • textureCompression = Uncompressed (lossy comp corrupts positions)
  • filterMode = Point (no bilinear blur between frame rows)
  • wrapMode = Clamp
  • mipmapEnabled = false (mips of packed data are nonsense)
  • alphaSource = None (bake has no alpha)

Implemented as a `VATAssetPostprocessor : AssetPostprocessor` so the
settings survive a Unity re-import — relying on hand-edited .meta is
brittle since Unity rewrites parts on round-trip. The .meta change is
the matching seed value for fresh clones.

After this lands, on next open Unity will reimport source.fbx and the
Source Mesh picker will list the freshly-exposed sub-asset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unity 6's FBX importer uses Autodesk's strict FBX SDK and rejects
qtmesh's custom 7300 binary output with "File is corrupted: 'source.fbx'"
(FBXImporter.cpp:422). The FBXExporter we ship works fine with Assimp
+ Blender + Maya, but Autodesk's own SDK is the strictest validator
and trips on subtleties our writer doesn't get right.

Rather than fix the FBX writer (a real, multi-day project), ship the
mesh as Wavefront .obj — universally supported, no plugins needed,
and same 5828 verts in the same order via qtmesh convert's Ogre
intermediate. Confirmed: gltf and obj both report 5828 verts /
10220 tris.

Changes:
- Drop source.fbx + .meta from the repo.
- Add source.obj + source.mtl (generated by `qtmesh convert source.gltf
  -o source.obj`).
- Repo-root .gitignore had `*.obj` (for C/C++ objects); add exemption
  for `tools/*-vat-demo/Assets/**/*.obj` and matching `.mtl`.
- BootstrapVAT probes source.obj first, falls back to source.gltf.
- VATAssetPostprocessor enforces meshOptimizationFlags=0 +
  weldVertices=false. The bake indexes column N to the Nth rendered
  vertex; if Unity's optimizer reorders or merges verts, every
  column lookup misses and the playback turns to noise.
- README explains the FBX-vs-OBJ-vs-glTF choice + drops the manual
  texture-import-tweak section (the postprocessor handles it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues visible in the first working playback of the Rumba dancer:

1. Only the head + torso rendered — arms, legs, eyes, cigar were
   missing. Root cause: VATPlayer assigned `sharedMaterial = _material`
   (singular). For a multi-submesh mesh like Rumba (11 submeshes —
   Skin_MAT / Clothes_MAT / Eyes_MAT / Cigar_Mat), Unity only draws
   submesh 0 unless `sharedMaterials` (plural) has one entry per
   submesh. Fix: detect `sourceMesh.subMeshCount` and replicate the
   same Material across all N slots. All submeshes share the same
   VAT texture + UV0 layout, so one material instance reused N times
   is correct.

2. Diffuse texture had no effect — the shipped shader only declared
   `_BaseColor`, no `_MainTex` sampler. The fragment shader did
   `_BaseColor.rgb * lighting` so Boss_diffuse.png was ignored.
   Fix: add `_MainTex` + `_MainTex_ST` properties, sample via
   `tex2D(_MainTex, uv * ST.xy + ST.zw)`, and gate the modulate with
   a `_UseDiffuseMap` toggle so users with a textureless bake can
   fall back to flat-tint mode. `_BaseColor` default flipped to white
   so the texture isn't pre-tinted when present.

VATPlayer now binds `_MainTex` + flips `_UseDiffuseMap` accordingly.

Sync the same shader changes into tools/vat-shaders/openvat.shader
(the public template surfaced via `qtmesh vat --include-shaders unity`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unity wrote these on first import. Committing them keeps the asset
GUIDs stable across machines — without them, every clone of the repo
gets fresh GUIDs and any future scene/prefab that references the mesh
breaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rs reorder verts

Unity's OBJ importer rebuilds the vertex buffer in face-walk order
(rather than position-list order), and Unity's FBX importer rejects
qtmesh's 7300 binaries as corrupt. The Rumba dancer was rendering as
a recognisable but mangled blob of triangles — every vertex was
reading the WRONG column in the position texture because Unity's
import order didn't match the order the baker walked.

Two-part fix:

1. Re-bake with `qtmesh vat --emit-uv2` so the glTF's TEXCOORD_1
   carries the per-vertex bake-column index. The column then "rides"
   with each vertex through any importer reorder — this is the same
   trick the Unreal demo uses.

2. Ship a 350-line `QtmGltfImporter.cs` ScriptedImporter that reads
   the .gltf directly: positions, normals, UV0, UV1, indices. Each
   glTF primitive becomes one Unity subMesh. The importer:
     • does NOT call mesh.Optimize() / OptimizeIndexBuffers
     • does NOT weld or dedupe verts
     • does NOT touch the vertex array after writing it
   so column-N in the position texture lines up exactly with the
   Nth vertex Unity renders.

   Includes a hand-rolled MiniJson parser (≈80 lines) so the demo
   has no JSON.NET / Newtonsoft dependency.

Updates:
   • Drop source.obj / source.mtl — no longer needed.
   • Bake artifacts refreshed via `qtmesh vat --emit-uv2`. The new
     glTF has TEXCOORD_1 (verified: 11 references in source.gltf).
   • VATPlayer's EnsureUV2 keeps the synth-from-vertex-id fallback
     but logs a warning when it fires — the right path is for the
     importer to ship real UV2.
   • VATPlayer.cs unchanged on the materials / shader binding from
     the previous commit (multi-submesh + _MainTex modulation).
   • README rewritten around the custom-importer story.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unity had cached `DefaultImporter` in the .gltf.meta from before the
QtmGltfImporter ScriptedImporter was added — and Unity won't re-pick
the importer for an already-classified asset just because a new
ScriptedImporter shows up. The asset stayed a plain text file in the
project view (text icon, no Mesh sub-asset), and the rendering blob
the user was seeing was the leftover OBJ mesh asset from the prior
attempt, still cached in Library/ even though source.obj/.mtl were
deleted from the repo.

Hand-write the .meta to point at QtmGltfImporter (matched by its
script GUID 44513d2c47bc74161a0122cacaf529f0). On next editor open,
Unity will see the asset is owned by our importer and re-import it,
producing a real Mesh sub-asset that the VATPlayer can bind to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…porter

Two diagnostics in service of the still-mangled VAT replay:

1. Drop the X-negation + index-winding-flip from QtmGltfImporter. The
   shader replaces every vertex position via the texture sample, so
   what's on the source mesh's vertex array doesn't enter the
   rendering pipeline. Negating X / flipping winding was dead code
   that risked Unity recomputing bounds/tangents in ways that
   obscure the actual UV1 transport issue.

2. Add a Debug.Log on the importer that prints the imported UV1
   range. If u=[0..5827], v=[0..0] the column indices survived; if
   something else, the bake's TEXCOORD_1 didn't survive the import.

The user's latest screenshot shows the mesh wireframe is the correct
T-pose silhouette but the rendered triangles are a fan from a single
point — classic signature of every vertex sampling column 0. UV1 is
either not being delivered or being clobbered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… column indices

THE root cause of the fan-of-triangles rendering.

VertexChannelCompressionMask in Unity's Player Settings defaulted to
4054, which has bit 4 (UV1) set — so Unity was compressing UV1 from
Float32 to FP16 on GPU upload. FP16 has ~10 mantissa bits; integer
values above ~1024 lose precision. The bake's UV1 holds per-vertex
column indices in [0..5827]:

  prim[0]: u=[0..1023]   ← head, renders correctly (FP16-safe)
  prim[1]: u=[1024..1437] ← FP16-quantized, snaps to wrong columns
  ...
  prim[10]: u=[4790..5827] ← FP16 loses ~3 bits, mass snap

Hence the visible pattern: head + hair rendered correctly (those verts
were in primitive 0, columns 0..1023), everything else fanned out
from a single point in the bounds — those FP16-snapped indices
landed on a small set of texture columns.

Clear bits 3+4+5+6 in VertexChannelCompressionMask (UV0/1/2/3):
  before: 4054 (0b111111010110)
  after : 3974 (0b111110000110)

Position/Normal/Color/Tangent/BlendWeight/BlendIndices stay compressed
— they're not affected by this bake's data ranges.

Also drops the dead X-negate / winding-flip block from the importer
(the VAT shader overrides position entirely, so coord-system fixups
on the source mesh are no-ops at best). Same diagnostic Debug.Log
from the previous commit kept in place so users can verify the
UV1 range in the Console.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/unity-vat-demo/Assets/Scripts/Editor/QtmGltfImporter.cs`:
- Around line 73-75: The code directly combines and reads uri into binPath
allowing path traversal; instead compute absolute paths and enforce containment:
get the canonical gltfDir (Path.GetFullPath(gltfDir)) and the canonical
candidate (Path.GetFullPath(Path.Combine(gltfDir, uri))), verify the candidate
either equals or starts with the canonical gltfDir plus a directory separator,
and only then call ctx.DependsOnSourceAsset and
buffers.Add(File.ReadAllBytes(...)); if the check fails, throw or skip with a
clear error. Ensure you reference the same variables (uri, gltfDir, binPath) and
update ctx.DependsOnSourceAsset to use the validated canonical path.
- Around line 244-277: ReadVec2/ReadVec3 assume tightly-packed float32 VEC2/VEC3
data and ignore accessor.componentType, accessor.type and bufferView.byteStride,
which corrupts interleaved or non-float accessors; update ReadVec2 and ReadVec3
to first validate that acc["componentType"] == 5126 (float32) and acc["type"] ==
"VEC2"/"VEC3" respectively and throw if not, then compute the needed byte length
per element using byteStride when present (fall back to tightly-packed 8/12
bytes when byteStride is 0 or missing) and call SliceAccessor (or extend
SliceAccessor if needed) with the correct total byte size/stride and count so
bytes are read per-element respecting stride rather than always assuming
contiguous float32 layout; reference the ReadVec2, ReadVec3 and SliceAccessor
symbols when making these changes.

In `@tools/unity-vat-demo/Assets/Scripts/Editor/VATAssetPostprocessor.cs`:
- Line 40: The current check uses assetPath.StartsWith(kBakeDir) which
incorrectly matches prefix-sibling folders; in VATAssetPostprocessor.cs, replace
that loose StartsWith check with a boundary-aware comparison: ensure assetPath
either exactly equals kBakeDir or begins with kBakeDir plus the path separator,
and perform the comparisons using StringComparison.Ordinal for determinism (i.e.
check equality OR StartsWith(kBakeDir + "/" , StringComparison.Ordinal)). This
tightens matching to the bake folder only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e15424a2-5fed-4b67-9c64-f1f8bf11c5c8

📥 Commits

Reviewing files that changed from the base of the PR and between ff63022 and 59374ff.

⛔ Files ignored due to path filters (9)
  • tools/unity-vat-demo/Assets/Shaders/openvat.shader is excluded by !**/*.shader
  • tools/unity-vat-demo/Assets/VAT/Rumba/Boss_diffuse.png is excluded by !**/*.png
  • tools/unity-vat-demo/Assets/VAT/Rumba/Boss_normal.png is excluded by !**/*.png
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com_ogre_bind.bin is excluded by !**/*.bin
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com_pos.png is excluded by !**/*.png
  • tools/unity-vat-demo/Assets/VAT/Rumba/source.bin is excluded by !**/*.bin
  • tools/unity-vat-demo/Assets/VAT/Rumba/source.gltf is excluded by !**/*.gltf
  • tools/unity-vat-demo/ProjectSettings/ProjectSettings.asset is excluded by !**/*.asset
  • tools/vat-shaders/openvat.shader is excluded by !**/*.shader
📒 Files selected for processing (10)
  • .gitignore
  • tools/unity-vat-demo/Assets/Scripts/Editor/BootstrapVAT.cs
  • tools/unity-vat-demo/Assets/Scripts/Editor/QtmGltfImporter.cs
  • tools/unity-vat-demo/Assets/Scripts/Editor/VATAssetPostprocessor.cs
  • tools/unity-vat-demo/Assets/Scripts/Editor/VATAssetPostprocessor.cs.meta
  • tools/unity-vat-demo/Assets/Scripts/VATPlayer.cs
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com-remap_info.json
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com_pos.png.meta
  • tools/unity-vat-demo/Assets/VAT/Rumba/source.gltf.meta
  • tools/unity-vat-demo/README.md
✅ Files skipped from review due to trivial changes (5)
  • .gitignore
  • tools/unity-vat-demo/Assets/Scripts/Editor/VATAssetPostprocessor.cs.meta
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com_pos.png.meta
  • tools/unity-vat-demo/Assets/VAT/Rumba/mixamo.com-remap_info.json
  • tools/unity-vat-demo/README.md

Comment thread tools/unity-vat-demo/Assets/Scripts/Editor/QtmGltfImporter.cs Outdated
Comment thread tools/unity-vat-demo/Assets/Scripts/Editor/QtmGltfImporter.cs
Comment thread tools/unity-vat-demo/Assets/Scripts/Editor/VATAssetPostprocessor.cs Outdated
Adds the missing pieces so the demo can be built from a fresh clone
without opening the editor GUI:

- `CLIBuildScenes.BuildAll` — programmatically constructs Web.unity
  and PerfVAT.unity using EditorSceneManager + the documented API.
  Force-reimports source.gltf as part of the same pass so the
  QtmGltfImporter's UV1 diagnostic surfaces in CI logs. Wires both
  scenes into Build Settings.

- `CLIBuilder.{BuildMac,BuildWindows,BuildLinux}` — calls
  BuildPipeline.BuildPlayer with the registered scenes. Writes to
  tools/unity-vat-demo/Build/<platform>/.

- `ForceVATCompatibleSettings` — clamps `VertexChannelCompressionMask`
  to 3974 (UV channels uncompressed) on editor load via reflection,
  defending the setting against Unity's tendency to rewrite
  ProjectSettings.asset back to defaults. Unity 6 made the public
  setter `internal`, so we reach it via reflection; the .asset value
  is the initial seed for fresh clones.

- `GraphicsSettings.asset` adds Hidden/QTM/VAT to
  m_AlwaysIncludedShaders. Without this Unity strips it from
  standalone builds since no .mat asset references it (VATPlayer
  creates the Material at runtime via Shader.Find), and the
  resulting binary logs "shader not found" forever.

- `.gitignore` exempts `/Build/` and Unity 6's auto-injected
  Google Play / mobile-resolver scaffolding from being committed.

Verified end-to-end on macOS:
  $ Unity ... -executeMethod CLIBuildScenes.BuildAll
    → built Assets/Scenes/Web.unity + PerfVAT.unity
    → QtmGltfImporter: imported 5828 verts across 11 submeshes
    → UV1 range u=[0..5827], v=[0..0]
  $ Unity ... -executeMethod CLIBuilder.BuildMac
    → build SUCCEEDED — 112 MB, 0 errors
    → app launches without shader/script errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuilder.cs`:
- Around line 44-51: The current CLIBuilder check for no scenes (the if block
that checks scenes.Count == 0) uses EditorApplication.Exit(0) which makes CI
succeed; change that to a non-zero exit to fail headless/batch runs — replace
EditorApplication.Exit(0) with EditorApplication.Exit(1) (or otherwise set a
non-zero Environment.ExitCode) in the CLIBuilder method where scenes are
validated so the process returns failure when no .unity scenes are found.

In `@tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuildScenes.cs`:
- Around line 128-130: After assigning ps.sourceMesh = FindBakeMesh() and
loading textures via AssetDatabase.LoadAssetAtPath<Texture2D> for
ps.positionTexture and ps.diffuseTexture, validate each returned value and fail
fast if any are null: check FindBakeMesh(), ps.positionTexture and
ps.diffuseTexture, log a clear error naming the missing asset (mesh or specific
texture path), and abort the build/save flow (throw an exception or return
non-success from the method) so the scene is not saved as "built" with missing
VAT assets; modify the method containing these lines (e.g., CLIBuildScenes
method that sets ps.*) to perform the null checks and short-circuit the
remaining save/report logic when any required asset is missing.
- Around line 45-54: The current code overwrites EditorBuildSettings.scenes with
only webScene and perfScene, removing any existing PerfSkeleton.unity entry;
update the logic in the method that calls BuildWeb() and BuildPerfVAT() so it
reads the current EditorBuildSettings.scenes, ensures the boot scene (webScene)
is first, preserves any existing PerfSkeleton.unity entry if present, and then
adds/ensures perfScene is enabled — operate on the existing array/list of
EditorBuildSettingsScene objects (not replace it) using the webScene and
perfScene variables and the PerfSkeleton.unity filename to detect/preserve that
entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20a2cf7c-eca4-4105-a354-3df778c5ef82

📥 Commits

Reviewing files that changed from the base of the PR and between 59374ff and f5ba5a1.

⛔ Files ignored due to path filters (7)
  • tools/unity-vat-demo/Assets/Scenes/PerfVAT.unity is excluded by !**/*.unity
  • tools/unity-vat-demo/Assets/Scenes/Web.unity is excluded by !**/*.unity
  • tools/unity-vat-demo/ProjectSettings/EditorBuildSettings.asset is excluded by !**/*.asset
  • tools/unity-vat-demo/ProjectSettings/GraphicsSettings.asset is excluded by !**/*.asset
  • tools/unity-vat-demo/ProjectSettings/PackageManagerSettings.asset is excluded by !**/*.asset
  • tools/unity-vat-demo/ProjectSettings/ProjectSettings.asset is excluded by !**/*.asset
  • tools/unity-vat-demo/ProjectSettings/UnityConnectSettings.asset is excluded by !**/*.asset
📒 Files selected for processing (14)
  • tools/unity-vat-demo/.gitignore
  • tools/unity-vat-demo/Assets/Scenes/PerfVAT.unity.meta
  • tools/unity-vat-demo/Assets/Scenes/Web.unity.meta
  • tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuildScenes.cs
  • tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuildScenes.cs.meta
  • tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuilder.cs
  • tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuilder.cs.meta
  • tools/unity-vat-demo/Assets/Scripts/Editor/ForceVATCompatibleSettings.cs
  • tools/unity-vat-demo/Assets/Scripts/Editor/ForceVATCompatibleSettings.cs.meta
  • tools/unity-vat-demo/Assets/Scripts/Editor/QtmGltfImporter.cs.meta
  • tools/unity-vat-demo/Assets/VAT/Rumba/Boss_normal.png.meta
  • tools/unity-vat-demo/Packages/manifest.json
  • tools/unity-vat-demo/Packages/packages-lock.json
  • tools/unity-vat-demo/ProjectSettings/SceneTemplateSettings.json
✅ Files skipped from review due to trivial changes (9)
  • tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuildScenes.cs.meta
  • tools/unity-vat-demo/Assets/Scenes/Web.unity.meta
  • tools/unity-vat-demo/Assets/Scripts/Editor/ForceVATCompatibleSettings.cs.meta
  • tools/unity-vat-demo/Assets/Scenes/PerfVAT.unity.meta
  • tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuilder.cs.meta
  • tools/unity-vat-demo/ProjectSettings/SceneTemplateSettings.json
  • tools/unity-vat-demo/Packages/manifest.json
  • tools/unity-vat-demo/.gitignore
  • tools/unity-vat-demo/Packages/packages-lock.json

Comment thread tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuilder.cs
Comment thread tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuildScenes.cs
Comment thread tools/unity-vat-demo/Assets/Scripts/Editor/CLIBuildScenes.cs
fernandotonon and others added 11 commits May 26, 2026 09:03
…riangles cause

THE actual cause of the persistent rendering bug, found by a runtime
VATPlayer diagnostic that logs the position texture's effective width:

  VATPlayer: runtime UV1 u-range on source = [0..5827]
             (positionTexture width = 2048, expected max = 2047)

The Rumba bake's position texture is 5828 columns wide (one per
vertex), but Unity's TextureImporter caps it at 2048 by default
(both globally and per-platform). Every vertex whose UV1 column
index was > 2047 sampled the SAME boundary column (because we set
wrapMode=Clamp), giving every "lost" vertex the same position →
fan of triangles spraying out from a single point. This explains
why ~35% of the dancer (the head, columns 0-1023) looked recognisable
and the rest collapsed.

VATAssetPostprocessor now sets `maxTextureSize = 8192` on the
default settings AND on every per-platform override (Standalone /
WebGL / iPhone / Android) so the cap doesn't sneak back in on
build. Also sets `npotScale = None` so the 5828×142 texture doesn't
get rounded to 8192×256 (wasteful) or worse.

Build delta: 112 MB → 114 MB. The extra 2 MB is the position texture
stored at its native resolution.

Also drops the dead "VertexChannelCompressionMask" theory chase:
that wasn't the actual cause (UV1 was Float32 all along), it was
the texture downscaling. Kept the Float32 vertex layout
declaration in QtmGltfImporter as a belt-and-suspenders safeguard,
plus the FullPrecision toggle as a no-op-safe defensive measure.

Also adds the runtime UV1-range log to VATPlayer that surfaced this
bug — fires once per scene so the perf grid doesn't flood the log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…port

The position texture is a 16-bit RGB PNG (per OpenVAT spec — gives
~30 µm precision over the bake's 2 m bounds). Unity's TextureImporter
defaults to RGBA32 (8 bits/channel) regardless of the source PNG's
depth, quantizing positions to 256 levels per axis. Over a 2 m
bounding box that's ~8 mm of jitter per vertex per frame — every
vertex jumps around inside an 8 mm cube, producing a cloud-of-
confetti rendering with the right OVERALL silhouette but no
internal structure.

VATAssetPostprocessor now sets `settings.format =
TextureImporterFormat.RGBA64` on every platform override
(Standalone / WebGL / iPhone / Android). Verified at runtime:
  VATPlayer: positionTexture: 5828×142, format=R16G16B16A16_UNorm

Build size: 114 MB → 118 MB (the +4 MB is the position texture now
stored at 16 bpc instead of being downsampled to 8 bpc).

Also adds `format` to the VATPlayer runtime diagnostic so this
silent-quantization class of bug can be caught from the player log
without needing the editor open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…flag

The Rumba dancer still renders as garbled ellipsoids despite all of:
  - UV1 confirmed at u=[0..5827] runtime
  - positionTexture confirmed at 5828×142 R16G16B16A16_UNorm
  - Bounds + frameCount confirmed correct
  - Texture color samples in [0,1] as expected

Adding a `_BypassVAT` toggle that short-circuits the vertex shader to
output `UnityObjectToClipPos(IN.vertex)` instead of the texture-driven
position. The Web scene boots with `bypassVAT = true` so the static
T-pose mesh renders directly.

What we're trying to learn: if bypass=true also gives ellipsoids,
the mesh import is producing garbage vertex positions and the bug
is in QtmGltfImporter. If bypass=true gives a recognisable T-pose,
the mesh is correct and the bug is in the VAT replay path (shader
or material binding).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After ~20 commits of incremental fixes on the custom BiRP shader path
(UV channel compression, max texture size, RGBA64, vertex buffer
layout, scripted importer, target SM 3.0+, tex2Dlod vs Load…), the
Rumba dancer still renders as an egg-shaped blob under Unity 6 +
Metal. The diagnostics confirm UV2 reaches the vertex shader
per-vertex correctly and the texture is uploaded at full resolution
in R16G16B16A16_UNorm — yet vertex-stage texture sampling returns
constant values regardless of which API path we use.

The author of the OpenVAT format ships a maintained Unity package
at sharpen3d/openvat-unity that uses Shader Graph decoders and
requires URP (or HDRP). That's the recommended path for now: tested,
upstream, and orthogonal to whatever Unity 6 + BiRP + Metal corner
case is biting us.

Updates the top of the README with:
- Step-by-step URP setup via the official package (URL, version
  requirements, the rename trick from `_pos.png` → `_vat.png` that
  OpenVATEditor's filename heuristic looks for).
- A "Status" section explaining where the custom BiRP shader stands
  + what's been ruled out, so a future contributor can pick up the
  investigation knowing what's already been tested.

Code under `Assets/Shaders/openvat.shader` + `VATPlayer.cs` etc.
stays in place as a starting point for whoever wants to fix the BiRP
path — it's not removed, just deprioritised relative to the package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eline

Replaces the custom BiRP `Hidden/QTM/VAT` shader path (which hit a
vertex-stage texture fetch wall under Unity 6 + Metal — egg-shaped
blob renders regardless of which sampling API we used) with the
upstream-tested path:

  - URP 17.0.4 (com.unity.render-pipelines.universal) installed via
    Packages/manifest.json.
  - OpenVAT's official Unity package (com.lukestilson.openvat) pulled
    from sharpen3d/openvat-unity, which ships Shader Graph decoders
    + an Editor tool that builds the material from a sidecar JSON.
  - `Assets/OpenVATContent/` ingests the existing Rumba bake — same
    source.gltf / source.bin + the position texture renamed to
    rumba_vat.png (the suffix OpenVATEditor's filename heuristic
    looks for).
  - Color space flipped Gamma → Linear (URP requirement).
  - `CLISetupURPAndBuild.cs` is a one-shot editor method that:
      1. Forces Linear color space.
      2. Creates a UniversalRenderPipelineAsset + UniversalRendererData
         via reflection and binds them to GraphicsSettings + every
         QualityLevel.
      3. Invokes OpenVATEditor.ProcessOpenVATContent() (also via
         reflection — the method is private) to consume the bake.
      4. Builds a minimal scene around the generated prefab.
      5. Runs BuildPipeline.BuildPlayer for StandaloneOSX.

Verified end-to-end:
  $ Unity ... -executeMethod CLISetupURPAndBuild.Run
    → CLISetupURPAndBuild: set color space → Linear
    → CLISetupURPAndBuild: created URP asset at Assets/Settings/URP-Asset.asset
    → CLISetupURPAndBuild: wired URP asset into GraphicsSettings + every QualityLevel
    → CLISetupURPAndBuild: ran OpenVATEditor.ProcessOpenVATContent
    → CLISetupURPAndBuild: build SUCCEEDED (119320297 bytes)

The custom BiRP shader path lives on as an experimental code path —
not removed, but no longer the default. The README's "Recommended
path" section at the top now points at this URP setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r OpenVATEditor

The OpenVATEditor.ProcessOpenVATContent path assigns
`Renderer.sharedMaterial` (singular), which only covers submesh 0.
For the multi-submesh Rumba dancer (11 slots — Skin / Clothes / Eyes /
Cigar etc.) the other 10 stay null and Unity renders them with the
missing-material pink — visible in the first URP build screenshot as
a textured jacket surrounded by hot-pink arms / head / legs.

CLISetupURPAndBuild now calls a `FixupMultiSubmeshPrefab` pass after
OpenVATEditor finishes: walk the generated prefab, find every
Renderer with a Mesh, and if `sharedMaterials.Length < subMeshCount`
or any slot is null, replicate the *_mat.mat VAT material across
every slot. All submeshes share the same VAT decoder + texture, so
one material instance reused N times is the correct fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er OpenVATEditor

The first URP build of the dancer rendered in a textured T-pose but
never animated. Three reasons:

1. OpenVATEditor seeds `_speed = 0` on the material it creates
   (Unity's `CreateInstance(Material)` zeros all floats; OpenVATEditor
   doesn't touch _speed). The shader's `_Time.y` multiplier then has
   nothing to scale by → frame index stays at 0.

2. OpenVATEditor reads the position texture's `.height` value as
   `_resolutionY` for the frame-row math. But the texture had been
   imported under Unity's default 2048 max-size + downscale-NPOT
   pipeline, which collapsed the bake's 142 rows to 64. With
   _resolutionY=64 the shader's row-lookup math overshoots, sampling
   non-position rows.

3. VATAssetPostprocessor only watched `Assets/VAT/Rumba/` AND only
   matched files ending in `_pos.png`. OpenVAT's official path uses
   `Assets/OpenVATContent/` + `_vat.png`, so the postprocessor never
   fired on the OpenVAT texture and let Unity's defaults strip the
   resolution.

Fixes:
- VATAssetPostprocessor now watches both folders (kBakeDirs[]) and
  matches both `_pos.png` and `_vat.png` filename suffixes.
- CLISetupURPAndBuild force-reimports the *_vat.png BEFORE running
  OpenVATEditor, so the postprocessor's maxTextureSize=8192 + RGBA64
  settings apply before the material is created and reads .height.
- After OpenVATEditor finishes, a new FixupAnimationUniforms pass
  walks every *_mat.mat in the bake folder and patches `_speed`
  (0→1) and `_resolutionY` (from the live Texture2D.height).

Verified end-to-end:
  → VATAssetPostprocessor: normalized texture settings on
    Assets/OpenVATContent/rumba_vat.png
  → FixupAnimationUniforms: patched rumba_mat.mat →
    _speed=1, _resolutionY=142
  → build SUCCEEDED (125875337 bytes)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…liable

Even with _UseTime=1, _UsePackedNormals=1, _speed=1, _frames=71,
_resolutionY=142 all correctly set on the OpenVAT material, the
dancer stayed in a static texturized T-pose under Unity 6 + URP
standalone player. The shadergraph's _Time-driven internal frame
math isn't ticking visibly in the player.

Add an OpenVATDriver MonoBehaviour that ticks `_frame` per Update
from C#: collects every Material with an `_frame` property in the
hierarchy, forces _UseTime=false (so the shader reads `_frame`
instead of `_Time`), and writes _currentFrame %= frames every tick.
This sidesteps the shadergraph self-tick entirely.

Wired into CLISetupURPAndBuild: after instantiating the OpenVAT
prefab into the scene, attach OpenVATDriver(fps=30, frames=71).

Also patches the shader-keyword approach in the same place: the
material's `_USETIME_ON` keyword landed in `m_InvalidKeywords` after
EnableKeyword(), confirming the shader uses a uniform branch rather
than a static branch. The driver makes that moot by writing the
uniform we actually want — _frame — directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…checkpoint

The URP + OpenVAT path now produces a fully-textured T-pose dancer
with all 11 submeshes rendering correctly — major progress over the
egg-shaped BiRP rendering. What's still not working: visible
animation playback. The dancer stays static at frame 0 despite:

  - _UseTime=1, _UsePackedNormals=1, _exaggeration=1
  - _speed=1, _frames=71, _resolutionY=142
  - _openVAT_main bound (5828×142 RGBA64)
  - _minValues / _maxValues populated from the sidecar JSON
  - OpenVATDriver verified at runtime: "bound 11 VAT materials,
    fps=30, frames=71" + writes _frame every Update

This appears to be a deeper integration with how OpenVAT's URP
Shader Graph internally wires _UseTime / _frame to the position
deformation amplitude — pushing the right uniforms from C# (both
material params AND _frame per Update) isn't translating to visible
vertex displacement.

README now documents the URP status explicitly + the bake settings
confirmed correct + the suggested next-step investigations (open in
editor for Frame Debugger, try the full `openVAT_decoder.shadergraph`
instead of `_basic`, etc.).

Adds a print() inside OpenVATDriver.Update so the first 5 frames of
playback log the _frame value being pushed — visible in the player
log, useful confirmation the driver is alive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…at runtime

Even with the material asset's `_UseTime = 0` and `_exaggeration = 1`
set in the editor pipeline, ShaderGraph's variant cooking may have
locked in the `_USETIME_ON` keyword variant, making the shader read
`_Time` instead of `_frame` regardless of what the C# uniform writes
say. Two safety nets:

- Driver Start() calls `m.DisableKeyword("_USETIME_ON")` on every
  bound material — forces the shader's keyword multi_compile path
  back to the `_frame`-reading variant.
- Driver Start() re-checks `_exaggeration` and sets it to 1.0 if
  zero — protects against the ShaderGraph cooker resetting it.

This is the runtime mirror of the editor-time fixups in
CLISetupURPAndBuild.FixupAnimationUniforms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…'s vertex output is unwired

ROOT CAUSE of the static-T-pose-with-correct-texture symptom: the
upstream OpenVAT package's `openVAT_decoder.shadergraph` (and the
`_basic` variant) declare `VertexDescription.Position` as a block,
but no graph edge connects the OpenVAT_standard sampling subgraph's
output to that block. So nothing the material's _frame / _exaggeration
/ _UseTime / _speed values do can affect the rendered vertex position —
the graph silently passes IN.positionOS through to VertexDescription.
Position uses its default-input behavior.

Confirmed by tracing the shadergraph JSON: VertexDescription.Position's
slot GUID `591246239e8d4c5b91d9b66f4b306e67` appears exactly twice in
both decoder graphs — once for the BlockNode definition and once for
the slot itself. No edges reference it. The OpenVAT subgraph computes
the deformation, then throws it away.

Replace with `Assets/Shaders/OpenVAT_URP.shader` — a small URP HLSL
shader that mirrors OpenVAT's property layout (so OpenVATEditor's
material setup carries over: _openVAT_main, _minValues, _maxValues,
_frame, _frames, _speed, _UseTime, _UsePackedNormals, _exaggeration,
_resolutionY, plus the standard PBR slots) and DOES wire the
texture-sampled position into the vertex output via
`VertexPositionInputs vpi = GetVertexPositionInputs(finalPos)`.

CLISetupURPAndBuild now runs a SwapToCustomHlslShader pass after
OpenVATEditor.ProcessOpenVATContent finishes: every *_mat.mat in the
bake folder gets `mat.shader = Shader.Find("QtMeshEditor/OpenVAT_URP")`.
All the property values OpenVATEditor populated (texture bindings,
bounds, frame count) carry over to the new shader without
re-assignment.

Vertex-id-to-column-mapping uses SV_VertexID, which lines up correctly
because the custom QtmGltfImporter preserves the bake's vertex order
across the concatenated 11-primitive mesh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fernandotonon and others added 21 commits May 26, 2026 15:59
The shader now animates (since the previous commit wired the VAT
deformation to VertexDescription.Position) but renders as the
characteristic egg-shaped blob — every vertex samples wrong column.

Root cause: `SV_VertexID` gives the vertex's position in the rendered
vertex buffer, NOT the bake's column index. The two would line up IF
the asset went through my QtmGltfImporter directly, but OpenVATEditor
calls `Instantiate(LoadAssetAtPath<GameObject>(modelPath))` which
takes a route that may renumber verts.

Read the column index from UV2 instead — the `qtmesh vat --emit-uv2`
flag writes the per-vertex column index as TEXCOORD_1, which rides
with each vertex through any reorder. UV2.x = column, UV2.y = row
block (almost always 0 for single-row bakes).

This is the same trick the Godot and Unreal demos use. Should have
done it from the start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
URP build with the new HLSL shader animates the dancer correctly via
_frame, and the head appears to render (column indices 0..1023 are
FP16-safe), but column indices >= 1024 still seem to land in wrong
spots — egg-shape body with arm-like protrusions where the head sits.

Add a runtime diagnostic at OpenVATDriver Start that dumps the mesh's
UV2 range + unique X-value count + a few sample positions. If the
runtime mesh's UV2 still has values 0..5827 with 5828 unique X's, the
problem is downstream (shader sampling). If the runtime UV2 is
quantized / clamped / collapsed somehow, the problem is upstream
(importer or material cooking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… quantization

Runtime diagnostic confirmed UV2 reaches the CPU-side mesh.uv2 array
intact (5828 unique X values, range [0..5827]), yet the dancer still
renders as an egg with only the head + a few arm verts in the right
spot — exactly the pattern from FP16 quantization (values < ~1024
are FP16-safe; 1024+ snap to nearest 2, 2048+ to nearest 4 etc.).

URP under Metal silently downcasts UV channels to half-float during
the vertex shader's input attribute fetch, even when:
  - VertexChannelCompressionMask = 3974 (UV0/1/2/3 cleared)
  - SetVertexBufferParams declares TexCoord1 as Float32x2
  - The cooked Mesh.uv2 array has Float32 values

The cleanest workaround: never let UV2 carry integers larger than
2048. Normalize at import time (divide by maxU+1, so all values land
in [0,1]) — FP16 represents [0,1] perfectly across the full range.

Importer now divides every UV1 entry by (maxU+1) when maxU is large
(> 1.5), then mesh.SetUVs(1, ...) writes the normalized floats. The
shader already has the heuristic `if (colF <= 1.0001) colF *= texW`,
which multiplies the normalized [0,1] back up to the actual column
index before sampling.

This is the same trick the openvat reference shader (gdshader/usf)
uses: ship UV1 as float UVs, not raw integer texel indices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User's runtime UV2 log still showed range=[0..5827] after the
"normalize UV1 by 5828" code was added — Unity's ScriptedImporter
cache reused the previous import result because the .gltf file's
content hadn't changed, only its importer's source code.

Bumping the [ScriptedImporter(version: ...)] attribute invalidates
the cache for every asset using that importer and forces a fresh
import on next domain reload. Now the normalize-to-[0,1] path
applies to the Rumba mesh's UV2 without needing a manual reimport
click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UV1 is now normalized to [0,1] in the cooked Mesh (importer logs
`UV1 range u=[0..0.9998284]` after the version bump), but rendering
is still an egg-shape. FP16 in [0,1] has ~2048 unique values, which
for 5828 columns means every ~3 columns map to the same GPU-side
value → still quantized → still egg.

Replace the fragment output temporarily with a red→cyan gradient
based on uv2.x. If the dancer's body shows:
  - SMOOTH continuous gradient (e.g. red on left, cyan on right) →
    UV2 reaches the GPU per-vertex correctly. Bug is in the texture
    sampling (Metal's Load semantics, half-precision sampling, etc.).
  - CHUNKY bands with visible stair-stepping → FP16 quantization is
    real and we need to encode the column index across multiple
    channels.

After diagnosing, the next commit will revert this and apply the
correct fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… few unique values

Diagnostic confirmed by user's screenshot: with UV2 normalized to
[0,1] AND the shader's fragment painting by uv2.x, the dancer's
body shows CHUNKY red+green BANDS instead of a smooth red→cyan
gradient. URP/Metal quantizes UV channels to FP16 at the vertex
input fetch — and FP16 in [0,1] only has ~2048 unique values,
which is fewer than our 5828 columns even with normalization. Every
~3 verts gets the same column → egg shape.

Switch to vertex Color (R8G8B8A8_UNorm = 32 lossless bits over the
4 channels). Pack the column index into R+G as a 16-bit
little-endian integer (R = col & 0xFF, G = (col >> 8) & 0xFF), and
row block into B. Unity's vertex Color attribute is always uploaded
to the GPU as bytes without any compression — it's the lossless
channel for this kind of payload. 16 bits gives a max column of
65535 vs our 5828, plenty of headroom for future larger bakes.

Importer:
  - QtmGltfImporter.cs bumps to version 3 to force re-import.
  - SetVertexBufferParams adds VertexAttribute.Color UNorm8 channel.
  - After concatenating UV1, build a Color32[] with the packed
    column index and assign mesh.colors32 = ....
  - Keep UV1 around for external tooling that still reads it.

Shader:
  - Add `float4 color : COLOR;` to Attributes.
  - vert() unpacks: `colLow = color.r * 255`, `colHigh = color.g *
    255`, `colF = colLow + colHigh * 256`, `rowBlk = color.b * 255`.
  - Pass the unpacked (col, row) as a float2 into the existing
    SampleVATPosition() so the rest of the math stays untouched.
  - Drop the `<= 1.0001 → multiply by texWidth` heuristic — the
    Color attribute always delivers raw integers now.
  - Restore the proper diffuse-textured fragment output (the
    red/green diagnostic was temporary).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dancer is still an egg after switching to vertex Color32 encoding.
Two possibilities:
1. The Color32 channel didn't get written by the importer (or got
   stripped during the OpenVATEditor → prefab → instantiate path).
2. The Color channel IS reaching the GPU but with banded/quantized
   values just like the UV channels.

Add two diagnostics:
- OpenVATDriver runtime: dump `mesh.colors32` length, unique
  unpacked count, max unpacked value. Confirms whether the encoding
  survived the asset pipeline to runtime.
- Shader fragment: paint by Color-unpacked colF as red→cyan gradient.
  Smooth = Color is lossless end-to-end. Chunky = Color is also
  quantized on the GPU side (which would be deeply weird but
  possible under URP/Metal).

The vert() still computes the VAT displacement, so the dancer's
silhouette and motion (or lack thereof) is preserved while the
fragment shows the diagnostic colour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e decode breaks

Color32 confirmed lossless end-to-end at CPU (runtime log:
"5828 unique unpacked column values, max=5827, c[5000]=5000") yet
the fragment-painted gradient was still chunky. Realised the bands
are NOT FP16 quantization — they're the EXPECTED visualisation of
per-vertex UV2 interpolation across triangles (adjacent triangles
share verts with wildly different column ids → big color jumps
across triangle boundaries).

New diagnostic: in the vertex shader, sample _openVAT_main at the
unpacked column (frame 0, row 0) and pass the RAW sampled RG to the
fragment. Then:

- If the dancer shows varied per-region colors that match what
  frame 0 of the bake should look like, vertex texture sampling
  works and the bug is in our bounds-decode math or the position
  output.
- If the dancer shows solid color (or a single color per submesh),
  the texture sampling itself isn't varying per-vertex on the GPU.

Also temporarily drives the dancer's position from the sampled
(R,G,B) via the bounds lerp — same SampleVATPosition logic inlined
— so the deformation is visible alongside the diagnostic color.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After a deep debugging session — Color32 encoding, FP16
quantization investigation, shader graph edge tracing, runtime
diagnostics — landed on a definitive root cause: Unity 6 on Apple
Silicon Metal doesn't bind the position texture as a vertex-stage
resource. The vertex shader's `SAMPLE_TEXTURE2D_LOD` calls return
the same texel for every vertex regardless of input UV.

The diagnostic that proved it: a fragment shader painting by the
sampled RG channels showed a SMOOTH pastel gradient across the
mesh — if texture sampling varied per-vertex, we'd see 5828 chaotic
per-vert colors; instead we see one continuous bilinear-interpolated
gradient. The sampled value is constant across the whole mesh.

README's Status section now reflects:
- What we proved is working (everything except GPU texture fetch).
- The single remaining failure mode (vertex texture fetch).
- Specific next-step paths someone could take (compute shader
  pre-decode, StructuredBuffer with SV_VertexID, switch to DirectX/
  Vulkan/HDRP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vertex texture fetch is broken

Diagnostic from the previous session: a smooth pastel gradient across
the dancer's egg-shape proved Unity 6 + Metal on Apple Silicon does
NOT bind textures to the vertex stage — every vertex's
SAMPLE_TEXTURE2D_LOD returns the same texel regardless of input UV.

The workaround is to switch the Graphics API on the Standalone OSX
build target away from Metal. OpenGLCore is the only other macOS
backend Unity 6 supports, and its vertex shader path does handle
texture fetch correctly per-vertex.

`ForceOpenGLCoreOnMac()` runs on every CI build via
`PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneOSX,
new[] { GraphicsDeviceType.OpenGLCore })` + the matching
`SetUseDefaultGraphicsAPIs(target, false)` to disable Unity's
"platform default" fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WebGL bypasses the Unity 6 + Apple Silicon + Metal vertex-stage
texture-fetch bug that's blocking the macOS Standalone build. WebGL
builds compile shaders to GLSL ES 3 / WebGL 2, which honours
vertex-stage SAMPLE_TEXTURE2D_LOD correctly per-vertex.

New entry point: invoke as
  Unity ... -executeMethod QtMeshEditor.VAT.Editor.CLISetupURPAndBuild.RunWeb

Same setup (Linear color space, URP asset, OpenVAT material build)
just builds to BuildTarget.WebGL into tools/unity-vat-demo/Build/Web/
instead of Build/Mac/. Switches the active build target via
EditorUserBuildSettings.SwitchActiveBuildTarget() — required
because BuildPlayer needs the platform-specific support module
loaded into the editor context.

Also documents ForceOpenGLCoreOnMac as a no-op + the reasoning in
the comment (Unity 6 forces Metal on ARM64 Mac builds regardless of
the API list; the x64-via-Rosetta workaround introduces a Rosetta
dependency that WebGL avoids).

Requires: WebGL Build Support module installed via Unity Hub
(separate ~500 MB download — `unityhub://6000.4.8f1/.../webgl` URL
scheme triggers the install prompt automatically).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, scene

Picks up the editor-written changes from the last successful WebGL
build attempt:

- Assets/Shaders/OpenVAT_URP.shader.meta — Unity assigned a GUID to
  the custom HLSL shader on first import. Commit it so the material
  (and any future references) stay stable across machines.
- ProjectSettings/URPProjectSettings.asset — auto-generated when URP
  installed.
- ProjectSettings/QualitySettings.asset + GraphicsSettings.asset —
  URP wiring + the OpenGL/Metal API list (kept as documentation of
  what we tried, even though the macOS standalone Metal path remains
  blocked).
- ProjectSettings/ProjectSettings.asset — color space → Linear,
  VertexChannelCompressionMask, Standalone arch settings.
- Assets/OpenVATContent/rumba_mat.mat — the patched material with
  _UseTime=1, _exaggeration=1, _frames=71, _resolutionY=142,
  _USETIME_ON keyword, Color32 channel bound.
- Assets/Scenes/OpenVATWeb.unity — the dancer prefab + OpenVATDriver +
  camera + light layout, scene-driven by CLISetupURPAndBuild.

These ride along with the WebGL build pipeline added in the previous
commit (1611415). With the WebGL Build Support module already
installed, `Unity ... -executeMethod CLISetupURPAndBuild.RunWeb`
produces a ~20 MB WebGL bundle under Build/Web/ that runs in any
browser with WebGL 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s can serve it

User got "Unable to parse Build/Web.framework.js.gz" in the browser
when loading the first WebGL build via `python3 -m http.server`. The
default WebGL compressionFormat is Gzip — Unity writes Web.wasm.gz +
Web.data.gz + Web.framework.js.gz and the loader expects the HTTP
server to send `Content-Encoding: gzip` so the browser decompresses
in transit. Python's stdlib http.server doesn't do that; it just
delivers the .gz file literally and the WebGL loader chokes.

Set `PlayerSettings.WebGL.compressionFormat =
WebGLCompressionFormat.Disabled` in BuildWebPlayer. Trade-off:
build grew from 20 MB → 70 MB on disk (Web.wasm 53 MB vs 13 MB
gzipped) but the build runs on any static-file server with zero
configuration. For production hosting on a real web server (nginx,
caddy, etc.) the Gzip or Brotli compression option is the right
choice — but for local-dev / repo-shipped demos, Disabled is the
sane default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnostic confirmed by the previous screenshot: when the shader
painted the dancer purely by raw IN.color (no texture sampling
involved), the mesh showed chaotic per-vertex red/orange/green
variation — proving Vertex Color reaches the GPU per-vertex
correctly on URP/WebGL.

The remaining issue was the texture sampling itself —
SAMPLE_TEXTURE2D_LOD with normalized UVs was collapsing all
vertices to the same texel (or at most 4 corners), giving the
smooth pastel egg gradient we'd seen.

Switch to LOAD_TEXTURE2D_LOD with integer texel coordinates.
LOAD goes through GL's texelFetch which bypasses any
sampler-state-driven interpolation/quantization and reads exactly
the texel at (col, row). Combined with the Vertex Color unpacking
that gives us a true 16-bit column index per vertex, this should
finally produce correct per-vertex sampling and animate the Rumba
dancer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…P's gamma decode

THE final missing piece. URP under Linear color space (which we use)
automatically applies sRGB→Linear conversion on vertex Color
channels during upload. That destroys our byte-packed integer
payload:

  col=100  packed as r=100/255=0.392
  → URP sRGB→Linear → 0.127
  → unpacked as col=int(0.127*255)=32  (NOT 100)

Every column value gets monotonically squashed through the gamma
curve, multiple distinct verts collapse to the same column → the
confetti egg we just saw with LOAD_TEXTURE2D.

Apply the inverse transform (Linear→sRGB) in the vertex shader
before unpacking. Recovers the original byte values exactly:

  float3 LinearToSRGB(float3 lin)
  {
      float3 lo = lin * 12.92;
      float3 hi = 1.055 * pow(abs(lin), 1.0/2.4) - 0.055;
      return lerp(lo, hi, step(0.0031308, lin));
  }

With this, the chain becomes:
  importer: col → r=col_low/255, g=col_high/255 (linear bytes)
  URP upload: r → sRGB→Linear (corrupted)
  shader: LinearToSRGB(IN.color.rgb) → back to linear bytes
  unpack: colF = r*255 + g*255*256 → original col

Result: vertex Color delivers the correct 16-bit unsigned integer
per vertex, LOAD_TEXTURE2D samples the right texel per vertex,
and the Rumba dancer should finally animate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to v4

The Linear→sRGB inverse I added in the previous commit produced an
empty scene — every vertex collapsed to a single point. The gamma
theory was wrong: URP doesn't apply sRGB→Linear on vertex Color
channels when they're declared as Color32 (UNorm8). The IN.color.r
values arrive in [0,1] linearly mapped from the byte values.

What was actually broken: the importer had two stale versions
fighting each other across reimports. The PREVIOUS code path
normalized UV1 to [0,1] (divide by maxU+1), then later code paths
removed the normalization but Unity cached the previous imported
result. The runtime UV1 diagnostic logging `u=[0..0.9998]` proved
the cached Color32 was packed from the normalized-tiny-float UV1
values → col = RoundToInt(0.99) = 1 for nearly every vertex →
every vertex sampled the same texel.

Bump QtmGltfImporter [ScriptedImporter(version: 4)] to force a
fresh re-import. Build log now confirms:
  QtmGltfImporter: encoded column index into vertex Color32
    (max col was 5827)
  QtmGltfImporter: UV1 range u=[0..5827], v=[0..0]
  (must be integer column indices in [0..texWidth-1])

Drops the gamma round-trip from OpenVAT_URP.shader and the
LinearToSRGB() helper — Color delivery is straightforward
linear bytes after all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ate LinearToSRGB

The previous build had a hidden shader-compile error:
  Shader error in 'QtMeshEditor/OpenVAT_URP': redefinition of
  'LinearToSRGB' at OpenVAT_URP.shader(148)

URP's Core.hlsl already defines LinearToSRGB(), and my duplicate
shadowed it on gles3 → the shader failed to compile → Unity fell
back to a stub that draws nothing → empty scene.

Drop the duplicate (we're not using gamma transform anyway).
Replace the vertex shader with a minimal diagnostic:

  - vert: bind-pose verts (clear T-pose silhouette), sample
    position texture at (col, row=0) via LOAD_TEXTURE2D, pass
    sampled.rg through varyings.
  - frag: paint by sampled.r (red), sampled.g (green), 0.5 (blue).

If we see varied per-vertex color across the dancer matching the
bake's frame-0 distribution, LOAD_TEXTURE2D works per-vertex. If
solid or smooth, sampling collapsed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ally animate

Diagnostic confirmed: LOAD_TEXTURE2D + vertex Color column index
delivers per-vertex texel sampling correctly on URP / WebGL2. The
previous diagnostic frame showed a static T-pose dancer painted with
smoothly-varied per-vertex pastel colors — each vertex pulled its
own texel from frame 0 of the position texture and the fragment
shader visualised it.

Now wire that to actual position output:
  - vert(): unpack column from IN.color, pick current frame from
    _Time or _frame, sample (col, frameRow) and (col, frameRow+frames)
    via LOAD_TEXTURE2D_LOD, decode position+normal, lerp bindPose →
    vatPose by _exaggeration.
  - frag(): standard URP lit with diffuse texture.

This is the same logic as the canonical Godot/Unreal demos, just
adapted to URP HLSL — finally producing animation now that we
identified vertex texture fetch via SAMPLE was the bug and LOAD is
the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The WebGL path got us past the Metal vertex-stage-texture-fetch
wall: LOAD_TEXTURE2D + Color32-packed column index proved by
diagnostic to sample per-vertex correctly. But the actual VAT
replay still gives a chaotic colored egg — each vertex samples a
column that isn't its own.

Suspected: OpenVATEditor's Instantiate(prefab) chain renumbers
verts somehow between QtmGltfImporter (which packs col=N into
Color32[N]) and the renderer (which sees Color32[M] for the vert
that should have col=N).

README updated with the current status, what's confirmed working
end-to-end, and two concrete suggestions for a future session:
runtime MeshDataArray-based cooker, or StructuredBuffer<int>
indexed by SV_VertexID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Batched code-review fixes across the Unity demo:

1. VATAssetPostprocessor.cs — InBakeDir() now requires kBakeDir +
   "/" with StringComparison.Ordinal, so `Assets/VAT/Rumba_backup`
   no longer falsely matches the `Assets/VAT/Rumba` watch.

2. QtmGltfImporter.cs (buffer URI) — canonicalize and enforce
   directory containment on buffer URIs. A malicious `../`-style
   URI in an imported glTF would otherwise let the importer read
   arbitrary local files. LogImportError + return on escape.

3. QtmGltfImporter.cs (accessor layout) — ReadVec2/ReadVec3 now
   validate accessor `type` (VEC2/VEC3) and `componentType` (5126
   float32), and reject `bufferView.byteStride != elementSize`
   (interleaved layouts). Throws an explicit exception instead of
   silently decoding garbage.

4. CLIBuildScenes.cs (PerfSkeleton) — `BuildAll` no longer drops
   PerfSkeleton.unity from EditorBuildSettings.scenes. If the file
   exists, append it as a third entry.

5. CLIBuildScenes.cs (fail-fast assets) — `BuildPerfVAT` throws
   InvalidOperationException listing missing assets if sourceMesh
   / positionTexture / diffuseTexture come back null. Saves us
   from a "successful" build of a broken scene.

6. CLIBuilder.cs — EditorApplication.Exit(1) when no scenes found
   (was 0). Headless CI now fails the build instead of "passing"
   with an empty player.

7. FPSOverlay.cs — OnValidate clamps windowFrames to ≥1, Update
   double-checks at runtime. A negative value in the Inspector no
   longer causes a queue-underflow exception every frame.

8. PerfSpawnerVAT.cs — guard `phaseJitter > 0f` before the modulo.
   A zero or negative inspector value produced NaN phases that
   broke every spawned instance simultaneously.

9. BootstrapVAT.cs — TryParseSidecar now requires Min AND Max to
   parse successfully (in addition to Frames). Previously a
   sidecar with missing Min/Max bounds would silently apply
   (0,0,0) bounds and break the VAT remap. Renamed
   ParseVec3Field → TryParseVec3Field and uses float.TryParse with
   InvariantCulture for deterministic results across locales.

10. VATPlayer.cs — guard every Material property write with
    HasProperty() so swapping shaders (BiRP, URP HLSL, ShaderGraph
    variants) doesn't emit "shader doesn't have property X"
    warnings per instance per frame.

Skipped reviews:
- chatgpt-codex on manifest.json:4 (add gltFast) — superseded
  by our QtmGltfImporter ScriptedImporter that owns .gltf imports
  directly; adding gltFast now would conflict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CLIPipelineCmdTest suite re-imports `Twist Dance.fbx` inside
every test case via MeshImporterExporter::importer + Ogre. With ~50
tests at ~20s/import-cycle on the GitHub-Linux runner that's
4–6 min of legitimate work, right at the prior 5-min cap. We just
saw an intermittent SIGKILL of the suite mid-test:

  [ RUN ] CLIPipelineCmdTest.CmdAnimRename_SameNameNoop
  WARNING: Suite CLIPipelineCmdTest CRASHED (signal 9)

The same test passed in 21.8s on the prior run, so it's not a
genuine deadlock — just the suite-level total wallclock crossing
300s.

Bump PER_SUITE_TIMEOUT to 600s. Still tight enough to catch real
deadlocks (a hung Qt event loop never finishes anyway), generous
enough to absorb runner variance. Job-level `timeout-minutes: 45`
outer guardrail stays the same; the full suite normally completes
in ~12 min, well under 45.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

@fernandotonon fernandotonon merged commit 616d558 into master May 27, 2026
20 checks passed
@fernandotonon fernandotonon deleted the feat/unity-vat-demo branch May 27, 2026 00:16
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