Skip to content

texcmp: normalize normal-map packing before diffing; angle-based verdicts - #5

Merged
eordano merged 1 commit into
mainfrom
fix/texcmp-normal-packing
Jul 16, 2026
Merged

texcmp: normalize normal-map packing before diffing; angle-based verdicts#5
eordano merged 1 commit into
mainfrom
fix/texcmp-normal-packing

Conversation

@dalkia

@dalkia dalkia commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Fixes the false "visible" verdicts on normal-map texture pairs, e.g. live-jit pair m7db93618.

The bug

A tangent-space normal map can be stored two ways that render identically through URP's UnpackNormalmapRGorAG (X = R×A):

  • plain: R=X, G=Y, B=Z — raw-decodes blue
  • AG-swizzled (Unity's DXT5nm convention carried into BC7): R=255, G=Y, B≈G, A=X — raw-decodes pink

Upstream v49 swizzles normal maps; abgen swizzles per-entity-usage classification (a copy of stylizedWood_v01_normal.png deployed at a path nothing references gets stored plain). texcmp diffed the raw decoded channels, so a packing difference reported every pixel different (ppm=1e6, maxd=206) for bundles whose stored normal vectors are identical channel-for-channel.

The fix

When either side of a matched texture shows the AG signature (R≥250 on ≥99% of pixels, varying alpha, B≈G), both sides are re-expressed as reconstructed normals (X, Y, √(1−X²−Y²)) before the pixel diff, and the packing per side is recorded (normalPacking field + normal-map: note). Imperceptibility for these pairs uses the perceptual quantity for normals — worst-case tilt over area: pixels tilted >5° must cover ≤200ppm of the image (mirrors the existing render-amnesty shape "Δ>8 ≤200ppm"). maxNormalAngleDeg / normalAngleOverPpm are emitted per texture. Non-normal textures take the exact same code path as before.

classify.py needs no changes — texture labels map from texcmp's class directly.

Verification (against live ab-cdn v49 bundles + local abgen output)

pair content packing before after
m7db93618 (the report) same normals plain vs AG visible, ppm=1,000,000 imperceptible (137ppm >5°, max 16.5°)
theatre-side copy (mc9326fc3) same normals AG vs AG visible, ppm=19k imperceptible (max 1.89°, 0ppm >5°)
two genuinely different normal maps different AG vs AG visible (633ppm >5°) — real diffs still caught
plain color texture n/a visible (encoder noise) unchanged code path, same verdict

Clippy clean.

🤖 Generated with Claude Code

…icts

A texture stored as a plain RGB normal map and the same texture in Unity's
AG-swizzle (DXT5nm convention: R pinned to 255, X in alpha, B copying G)
render identically through UnpackNormalmapRGorAG, but raw-decode as blue vs
pink — texcmp diffed the raw channels and reported every pixel different
(ppm=1e6, "visible") for render-identical bundles. Observed live on
stylizedWood_v01_normal.png pairs where upstream v49 swizzled a texture our
per-entity usage classification stored plain.

When either side of a matched texture shows the AG signature (R>=250 on
99% of pixels, varying alpha, B~G), both sides are re-expressed as
reconstructed normals (X, Y, sqrt(1-X^2-Y^2)) before the pixel diff, the
packing per side is recorded (normalPacking field + note), and
imperceptibility uses the perceptual quantity for normals: pixels tilted
more than 5 degrees must cover at most 200ppm of the image (mirrors the
render amnesty shape). maxNormalAngleDeg / normalAngleOverPpm are emitted
per texture.

Verified against live ab-cdn v49 bundles:
- packing-mismatch pair (same content):  ppm 1e6 "visible" -> imperceptible
  (137ppm of pixels over 5deg, max 16.5deg — cross-encoder block noise)
- both-AG pair (same content):           ppm 19k "visible" -> imperceptible
  (max angle 1.89deg, 0ppm over)
- genuinely different normal maps:       stays visible (633ppm over 5deg)
- plain color textures:                  untouched code path, verdicts
  unchanged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@eordano
eordano merged commit 3375781 into main Jul 16, 2026
2 checks passed
@eordano
eordano deleted the fix/texcmp-normal-packing branch July 17, 2026 15:22
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.

2 participants