Skip to content

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.

Drop texture maps (PNG keeps exact channel bytes)
Runs in your browser. Files are not uploaded.
Texture Lab: a normal map generated from a CC0 texture, with the OpenGL/DirectX convention shown
The real Texture Lab with a CC0 asset (ambientCG).
Outputs
  • 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, orm

The 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

  1. Drop the maps of one material.
  2. Check the roles and the workflow.
  3. Read the issues; fix or convert maps in the other stages.
  4. Export the report or the processed textures.

Files you get

FileWhat it is for
texture-report.jsonThe 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.zipFrom 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.pngFrom the Pack stage: grey maps packed in the chosen engine layout.
rock-directx.png / rock-normal.pngFrom the Normal stage: a converted map, or a normal map generated from height.
nerulio-textures.zipFrom 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.

  1. 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 Type Default on the mask map.
  2. Unity normal maps: set Texture Type to Normal map. If the file is DirectX-style, tick Flip Green Channel in the same importer (Unity 6) or convert the file first.
  3. Unity URP Lit (Workflow Mode Metallic): assign the packed texture to Metallic Map and to Occlusion Map, and leave Smoothness › Source on Metallic Alpha. HDRP Lit: assign the mask map to Mask Map; the Metallic, Smoothness and AO Remapping sliders then appear.
  4. Godot 4: use an ORMMaterial3D for an ORM texture, or a StandardMaterial3D with separate maps. A packed texture of another layout still works in StandardMaterial3D: set roughness_texture_channel, metallic_texture_channel and ao_texture_channel to the channel that holds each map.
  5. Godot import: for a DirectX-style normal map enable Process › Normal Map Invert Y in the Import dock and click Reimport.
  6. 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

SymptomLikely causeHow to checkFix
The whole material is too glossy or too matteA data map was imported as sRGB (a value of 128 is read as about 0.216), or roughness sits where smoothness is expectedImporter: is sRGB (Color Texture) on for the roughness, metallic or mask map? Unpack the channel and compare it with the sourceTurn sRGB off for data maps; invert roughness for Unity with roughness → smoothness
Metal or AO shows up in the wrong places after packingThe 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 statsRepack with the right preset in the mask packer
Bumps look like dents, or light seems to come from belowThe normal map is in the other green-channel conventionLight 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 OcclusionURP reads occlusion from G; Nerulio's URP preset writes G = 0Channels stage: G reads min = max = 0Do 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 mipmapsFix › Mipmaps preview at 1/4 … 1/16Bleed the edge colour with texture edge bleed
dimension-mismatch errorOne map was exported at another resolutionThe issue lists every file with its sizeExport 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

OptionWhen it is the better choice
The engine's own texture importerOne 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 presetThe 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 validatorYou 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.

TargetFilesCheck
Channel PNGs<name>-r/g/b/a.png (8-bit grey)Measured numpyByte-identical per channel, including RGB under fully transparent texels.
Normal mapPNG · OpenGL (+Y) or DirectX (−Y)Measured numpyDecoded vectors are unit length (worst deviation 0.0002); OpenGL and DirectX differ in green only.
Packed mask / ORMUnity 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 importUnity · Unreal · GodotUNVERIFIEDNot 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

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).