PBR Texture Validator — Naming, Sizes and Missing Maps
A focused view of the Texture Lab's Inspect stage: the checks an artist usually discovers only after import, reported per file with the measurement that triggered them.
A PBR validator checks a texture set before import: every map has a role, all maps are the same size, the chosen workflow's required and recommended maps are there, sizes are powers of two, grey maps really are grey and a normal map really decodes to unit vectors. Drop one material's maps: roles come from the file names (_basecolor, _normal, _roughness, _ORM …), you choose the workflow, and every issue is listed with its file and the measured number. The JSON report keeps the measurements; no file is renamed or changed.

- Channel PNGsMeasured· numpy
- Normal mapMeasured· numpy
- Packed mask / ORMMeasured· Pillow
- Engine importUNVERIFIED
What is checked, and against which numbers
Roles from names. The file name is lower-cased, separators become _, and the first matching token wins, most specific first: packed (orm, arm, rma, mra, maskmap, mask), albedo (albedo, basecolor, diffuse, color …), normal (normalgl, normaldx, normal, nrm, n), roughness, smoothness (gloss), metallic, AO (ao, occlusion, ambientocclusion), height (height, displacement, bump), emission, opacity, specular. Tokens of one or two letters (_n, _m, _r) count as low confidence. Every role is a drop-down, so you correct a wrong guess instead of renaming files.
Workflows decide what is missing. Separate maps (glTF / generic): albedo required; normal, roughness, metallic recommended. Unity HDRP: albedo and the mask map required, normal recommended. Unity URP: albedo required; normal and metallic recommended. Godot ORM and Unreal ORM: albedo required; normal and the ORM texture recommended — and when roughness and metallic are present but the ORM is not, you get "pack available" instead of a bare "missing". A roughness map satisfies smoothness and the other way round.
Measurements, not guesses. A single-channel role (roughness, metallic, AO, height, opacity) stored as RGB is "grey stored as RGB" when no texel's channels differ by more than 2, and "channels differ" (probably packed) when they do. A normal map is decoded texel by texel: a sample is off-unit when its length is more than 0.12 from 1; the map passes when at most 1 % of samples are off-unit and no blue is below 128 (a vector pointing into the surface). A map assigned another role that passes the normal-map test (≤ 5 % off-unit, ≤ 0.1 % negative blue) is flagged "looks like a normal map".
Power of two is a warning, not an error. glTF 2.0 says clients SHOULD resize non-power-of-two textures on platforms with limited support when the sampler repeats or uses mipmaps; Unity's importer has a Non Power of 2 option that scales such textures. Whether that matters depends on your target, so the validator reports it and leaves the decision to you.
- Workflow
- The set of maps an engine's material expects, including how they are packed.
- Set name
- What is left of the file name after the role token; different set names in one drop trigger "naming inconsistent".
- Off-unit sample
- A decoded normal whose length differs from 1 by more than 0.12.
- Power of two
- A size of 2ⁿ pixels: 256, 512, 1024, 2048 …
Example: a rock set checked for Godot 4 ORM
Four PNGs dropped together, workflow Godot 4 ORM. This is what the validator returns:
rock_basecolor.png 2048 × 2048 albedo
rock_normal.png 2048 × 2048 normal
rock_roughness.png 1000 × 1000 roughness
rock_metallic.png 2048 × 2048 metallic
error dimension-mismatch 2048x2048 vs 1000x1000 (rock_roughness.png)
warn missing-recommended orm
info pack-available roughness + metallic → Godot 4 ORM preset
warn not-power-of-two rock_roughness.png 1000 × 1000
set ok: no (1 error)One re-export of the roughness at 2048 × 2048 clears the error and the power-of-two warning; packing roughness and metallic (with AO if you have one) in the mask packer clears "missing orm".
What it does
- Required and recommended maps per workflow, "the parts of a packed map are already here".
- Size mismatch, non-power-of-two (optionally non-square), unclassified or inconsistent names.
- Measured facts: unexpected alpha, RGB under zero alpha, grey maps stored as RGB, single-channel maps whose RGB differ.
- JSON report in the Game Labs envelope with per-texture measurements.
How to do it
- Drop the material's maps.
- Choose the workflow.
- Read the issues.
- Download the JSON report.
Check the result
- After fixing, drop the new files and keep the same workflow: the summary shows ✓ when there are no errors and no warnings.
- Download
texture-report.jsonand readissues: each entry hasid,level, the files and the numbers (sizes,maxDeviation,ratio,minBlue). - For a normal map,
normalCheck.meanLengthshould be close to 1 andblueNonNegativetrue.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
unclassified for a file you know | The name has no known token (rock_03.png), or a token outside the English list | The role column shows Unknown | Pick the role in its drop-down; the report keeps your choice |
duplicate-role for the normal map | Both _NormalGL and _NormalDX from one download were dropped | Two files carry the role normal | Keep the one your engine expects (GL for Unity and Godot); convert with the normal map converter if you only have the other |
gray-channels-differ on a roughness or metallic file | It is really a packed texture (or a coloured preview) with a single-map name | The issue gives the largest channel spread and the share of texels that differ | Set its role to the packed map, or unpack it with the channel unpacker |
looks-like-normal on a height or bump file | A normal map saved as _bump, which the name rules read as height | It passes the unit-length and blue test | Change its role to normal |
normal-blue-negative or normal-not-unit | Not a tangent-space normal map (a height or object-space map, or a map resized as colour) | The issue shows minBlue and the off-unit share | Use the right file, or generate one from height with the normal map generator |
unexpected-alpha on a roughness, metallic or AO map | The exporter added an alpha channel the engine will not read | The issue lists the lowest alpha and the number of alpha-0 texels | Export without alpha; the colour under alpha 0 is kept by the Lab, but most engines ignore it |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| The full Texture Lab | You also need to fix what the check finds: convert normals, unpack or pack channels, bleed edges, resize, and export a batch. |
| Checking in the engine | One material and one engine: import settings and missing slots show up in the material inspector, but sizes, grey-stored-as-RGB and normal validity are not measured there. |
Outputs and how each was checked
"Measured" means the downloaded file was opened again outside the page (Pillow, numpy or an independent parser) and the property was measured. Loading these files in a game engine was not tested.
| Target | Files | Check |
|---|---|---|
| Channel PNGs | <name>-r/g/b/a.png (8-bit grey) | Measured numpyByte-identical per channel, including RGB under fully transparent texels. |
| Normal map | PNG · OpenGL (+Y) or DirectX (−Y) | Measured numpyDecoded vectors are unit length (worst deviation 0.0002); OpenGL and DirectX differ in green only. |
| Packed mask / ORM | Unity HDRP mask · URP metallic · ORM (glTF, Godot) | Measured PillowPacked bytes checked: ORM preset over AO 10 / roughness 80 / metallic 220 → (10, 80, 220, 255). |
| Engine import | Unity · Unreal · Godot | UNVERIFIEDNot imported in any engine. |
Evidence
Channel unpack byte-identical on all four channels (max difference 0), 2048 transparent texels keep their RGB; edge bleed changed 448 texels, all with alpha 0, and no alpha byte; OpenGL ↔ DirectX differ exactly in green (255 − g); converted normals decode to unit vectors.
Limits
- Colour space is not detected: no ICC profile or gamma is read; the report states which roles an engine expects as sRGB or linear and whether a PNG has an sRGB chunk.
- The OpenGL/DirectX convention of a normal map is not detected here; the Studio's Texture workspace has a detector.
- Name rules know English tokens only; files are never renamed.
- The report is generic JSON; no engine imports it.
- Exact channel bytes need PNG; JPEG/WebP are decoded by the browser and marked "not byte-exact".
- Interlaced PNG and 16-bit output are not supported; up to 33.5 million pixels on the exact path.
- A normal map's convention cannot be detected from its pixels, only converted.
- Nothing was imported into Unity, Unreal or Godot: channel orders rest on their documentation and engine source.
Versions and sources
The check report was verified on the Texture Lab (docs/TEXTURE-LAB.md): schemaVersion 1, engineTarget generic, alpha.zeroPixels = 2048 matching the fixture, exactChannels true; a JPEG input gives exactChannels: false. The numbers on this page are the thresholds in src/game/texture-set.js and src/game/texture-normal.js.
Official documentation used
Questions
Which workflows are supported?
Metallic/roughness as separate maps, and the packed layouts in the Lab's presets (Unity HDRP mask, URP metallic, Godot and Unreal ORM). There is no specular/gloss workflow.
Does it rename my files?
No. It reports inconsistent names; renaming is up to you.
Is it free, and do I need an account?
Everything on these pages is free and works without an account. There is no watermark and no export that is held back.
Are my sprites uploaded?
No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.
Does it need Aseprite or an engine plugin installed?
No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).