PBR Texture Checker — Validate a Material Set in the Browser
Drop a material's maps and the Texture Lab sorts them into roles, checks the set against the chosen workflow and reports every issue with the file and the number behind it.
A PBR material is several textures that have to agree: the same size, each map in the slot that expects it, colour maps imported as sRGB and data maps (normal, roughness, metallic, AO, height) as linear, packed channels in the order your engine reads, and a normal map in the engine's green-channel convention. Drop one material's maps into the Texture Lab: it sorts them by file name, checks the set against the workflow you choose (separate maps, Unity HDRP mask, Unity URP metallic, Godot ORM, Unreal ORM) and writes a JSON report with the number behind every issue. Its files are measured byte for byte; import into Unity, Unreal or Godot was not run.

- Channel PNGsMeasured· numpy
- Normal mapMeasured· numpy
- Packed mask / ORMMeasured· Pillow
- Engine importUNVERIFIED
What a PBR set has to get right before import
Colour or data. Base colour (albedo) and emission are pictures authored in sRGB; the engine decodes them before lighting. Roughness, metallic, ambient occlusion, height and normal maps are numbers and must reach the shader unchanged, so they are imported as linear (non-colour) data. glTF 2.0 makes this normative: base colour RGB MUST use the sRGB transfer function, the metallic and roughness values MUST be linear. In Unity the switch is the importer's sRGB (Color Texture) checkbox; Godot's shading docs say colour textures get a source_color hint and normal, roughness, metallic and height textures do not.
Packing. To save texture samples, engines read several grey maps from the channels of one RGBA image, and every engine picks its own order: Unity HDRP's Mask Map is R metallic, G AO, B detail mask, A smoothness; URP's channel-packed texture is R metallic, G occlusion, A smoothness; glTF 2.0 reads roughness from G and metallic from B, occlusion from R (so one image can serve as "ORM"); Godot's ORMMaterial3D uses the same R/G/B order, which the Godot 4.4 shader source states and the manual does not. Unity stores smoothness, which is 1 − roughness.
Normals. Unity and Godot expect OpenGL-style normal maps (green = +Y, up). A DirectX-style map differs only in its green channel; the fix is 255 − g, not a re-bake. A single map cannot be measured to tell which one it is, so the Lab converts but does not guess.
Why measure in the Lab rather than a canvas: a browser canvas stores pixels premultiplied, so every texel with alpha 0 comes back as (0, 0, 0, 0) and the packed bytes under it are gone. The Lab decodes PNG files itself (all non-interlaced colour types, 1–16 bit) and marks JPEG/WebP inputs "not byte-exact".
- sRGB / linear
- How the engine interprets the 0–255 values: sRGB values are decoded as colour (128 → about 0.216), linear values are used as they are (128 → 0.502).
- Packed (channel-packed) texture
- One RGBA image whose four channels hold four unrelated grey maps.
- ORM
- Occlusion in R, roughness in G, metallic in B — the glTF 2.0 channel order, also read by Godot's
ORMMaterial3D. - Smoothness
- Unity's inverse of roughness: smoothness = 1 − roughness (255 − value in 8 bits).
Example: an ambientCG brick set checked against three workflows
Four 1024 × 1024 PNGs from one ambientCG set. The roles come from the file names; the issues are what the Lab's validator returns (the roughness file is assumed saved as RGB with three equal channels):
File Role Import as
Bricks076C_1K-PNG_Color.png albedo sRGB
Bricks076C_1K-PNG_Roughness.png roughness linear
Bricks076C_1K-PNG_AmbientOcclusion.png ao linear
Bricks076C_1K-PNG_Displacement.png height linear
Separate maps (glTF / generic) warn missing-recommended normal, metallic
info gray-stored-as-rgb Roughness.png (R = G = B)
Unity HDRP mask map error missing-required mask map
warn missing-recommended normal
Godot 4 ORM warn missing-recommended normal, ormThe set is fine for separate maps once you add the normal map (ambientCG ships _NormalGL for Unity and Godot and _NormalDX; add only one, or the Lab warns about a duplicate role). For HDRP the mask map is an error because HDRP has no separate metallic or AO slot; there is no "pack available" hint here because the set has no metallic map — a metallic of 0 has to come from a constant channel in the mask packer.
What it does
- Roles from filenames (albedo, normal, roughness, metallic, AO, height, emission, ORM …); every role is a drop-down you can change.
- Checks: dimension mismatch, missing maps, duplicate roles, non-power-of-two, unexpected alpha, grey maps stored as RGB, packed maps.
- Normal maps validated by measurement: unit length and non-negative blue.
- Stages for channels, normals, packing, fixing (edge bleed, mipmap preview, power-of-two) and batch export.
How to do it
- Drop the maps of one material.
- Check the roles and the workflow.
- Read the issues; fix or convert maps in the other stages.
- Export the report or the processed textures.
Files you get
| File | What it is for |
|---|---|
texture-report.json | The check report: per texture its role, size, power of two, exact PNG channels, colour type, bit depth, srgbChunkPresent, colour-space guidance, alpha and normal measurements; every issue with its id, level and numbers. |
rock-channels.zip | From the Channels stage: rock-r-ao.png, rock-g-roughness.png … one 8-bit grey PNG per channel, byte-identical to the source. |
rock-mask.png | From the Pack stage: grey maps packed in the chosen engine layout. |
rock-directx.png / rock-normal.png | From the Normal stage: a converted map, or a normal map generated from height. |
nerulio-textures.zip | From Export: every file resized (optional), edge-bled (optional) and encoded, in that order. |
Use the checked set in Unity, Godot or glTF
Settings below follow the engines' documentation; Nerulio did not run these imports.
- Unity: select every data texture (roughness, metallic, AO, mask map) and turn off
sRGB (Color Texture)in the Inspector; keep it on for base colour and emission. HDRP additionally asks for Texture TypeDefaulton the mask map. - Unity normal maps: set Texture Type to
Normal map. If the file is DirectX-style, tickFlip Green Channelin the same importer (Unity 6) or convert the file first. - Unity URP Lit (Workflow Mode
Metallic): assign the packed texture to Metallic Map and to Occlusion Map, and leave Smoothness › Source onMetallic Alpha. HDRP Lit: assign the mask map to Mask Map; the Metallic, Smoothness and AO Remapping sliders then appear. - Godot 4: use an
ORMMaterial3Dfor an ORM texture, or aStandardMaterial3Dwith separate maps. A packed texture of another layout still works inStandardMaterial3D: setroughness_texture_channel,metallic_texture_channelandao_texture_channelto the channel that holds each map. - Godot import: for a DirectX-style normal map enable Process › Normal Map Invert Y in the Import dock and click Reimport.
- glTF: put roughness in G and metallic in B of the metallicRoughness texture, occlusion in R (the same image may be referenced as the occlusion texture), and keep the normal map +Y up.
Check the result
- Drop the processed files back into the Lab and choose the same workflow: the report should show 0 errors, and the Channels stage should show each channel with the min/max you expect (a constant channel reads min = max).
- In the engine, look at a surface you know: a polished metal area should give a sharp highlight and a rough stone area a broad dull one. Everything glossy or everything matte usually means inverted roughness or a data map imported as sRGB.
- Put a light above the material: raised details should be lit on their upper edge. Lit from below means the normal map is in the other convention.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| The whole material is too glossy or too matte | A data map was imported as sRGB (a value of 128 is read as about 0.216), or roughness sits where smoothness is expected | Importer: is sRGB (Color Texture) on for the roughness, metallic or mask map? Unpack the channel and compare it with the source | Turn sRGB off for data maps; invert roughness for Unity with roughness → smoothness |
| Metal or AO shows up in the wrong places after packing | The texture was packed in another engine's layout (glTF's G is roughness, HDRP's G is AO) | Open the packed file in the Channels stage with each layout preset and read the channel stats | Repack with the right preset in the mask packer |
| Bumps look like dents, or light seems to come from below | The normal map is in the other green-channel convention | Light from above: which edge of a bump is bright? | Convert with the normal map converter, or use Unity's Flip Green Channel / Godot's Normal Map Invert Y |
| URP material turns dark in its ambient light after assigning the packed texture to Occlusion | URP reads occlusion from G; Nerulio's URP preset writes G = 0 | Channels stage: G reads min = max = 0 | Do not assign it to Occlusion, or pack AO into G with a custom mapping |
| Dark or light fringe around cut-out edges (leaves, decals) | RGB under alpha 0 is averaged into the edge by filtering and mipmaps | Fix › Mipmaps preview at 1/4 … 1/16 | Bleed the edge colour with texture edge bleed |
dimension-mismatch error | One map was exported at another resolution | The issue lists every file with its size | Export all maps at one size; the Fix stage's power-of-two resize uses a quality resampler if you must resize |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| The engine's own texture importer | One or two maps whose layout you already know: you set sRGB, Texture Type and the normal-map flip there anyway, and no extra files are made. |
| Your texturing application's export preset | The maps come from a painting tool: exporting straight into the engine's layout avoids a separate packing step. Godot's docs note that Substance Painter and ArmorPaint export ORM with their Unreal Engine preset. |
| PBR texture validator | You only need the checklist (names, sizes, missing maps) and the JSON report, not the conversion stages. |
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
- The material preview is a one-light GGX approximation with flat ambient; judge lighting in the engine.
- The Unreal ORM preset follows the glTF order, which Epic does not document as Unreal's own.
- 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
Measured on the Texture Lab (docs/TEXTURE-LAB.md): channel unpack byte-identical on R, G, B and A (max difference 0) with 2048 alpha-0 texels kept; the ORM preset over AO 10 / roughness 80 / metallic 220 wrote (10, 80, 220, 255); OpenGL ↔ DirectX differed exactly in green; the check report's alpha.zeroPixels matched the fixture. Import into Unity, Unreal or Godot was not attempted: the layouts come from the documentation below, and Godot's ORM order from its 4.4 engine source, read directly.
Official documentation used
- glTF 2.0 specification (Khronos)
- Unity HDRP 17.1: Mask and detail maps
- Unity 6 URP: Assign a channel-packed texture
- Unity 6 Manual: Default texture import settings
- Godot 4.7 docs: Standard Material 3D and ORM Material 3D
- Godot 4.7 docs: BaseMaterial3D
- Godot 4.7 docs: Importing images
- Godot 4.4 source: scene/resources/material.cpp
Questions
Does it detect sRGB vs linear?
No profile or gamma is interpreted. It tells you which maps an engine expects as colour and which as linear data, and reports the PNG sRGB chunk as a plain fact.
Are the maps changed when I only check them?
No. Checking reads the pixels; nothing is written unless you export.
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).