glTF ORM Textures: Fix Roughness and Metallic Wiring

Inspect a real glTF material and packed texture to trace occlusion, roughness, metallic channels, UV bindings, and factor defaults before repainting an asset.

colinkoko5 min read

Key takeaways

  • In a packed glTF ORM texture, red supplies occlusion, green roughness, and blue metallic data when the corresponding material bindings exist.
  • WaterBottle binds both occlusion and metallic-roughness to texture 1, using UV set 0 and default factors of 1.
  • A manually rebuilt Three.js material starts with metalness 0, unlike glTF’s default metallicFactor 1; a correct texture cannot compensate for a zero multiplier.

Read the material contract before repainting the texture

For glTF’s core metallic-roughness material, green carries roughness and blue carries metallic values. An occlusion binding reads red. A single image can therefore carry all three jobs, commonly called ORM. Treat those channels as data, not as the surface’s visible color.[1][5]

If a prop becomes unexpectedly shiny or loses its metallic appearance after a handoff, first trace what the material reads. The texture may be intact while a channel, multiplier, or binding changed. This applies to generated and hand-authored assets alike; a new texture is an expensive answer to the wrong wiring problem.

Red, green, and blue conceptual material sheets connected by light trails to an unbranded metal flask.
Concept illustration of three material-data channels feeding one prop. This is not the inspected WaterBottle asset, a product screenshot, or a rendered test.

One PNG, two material bindings: the WaterBottle example

The public WaterBottle sample provides a small, inspectable case. In the pinned source, BottleMat points metallicRoughnessTexture.index and occlusionTexture.index to 1. Texture 1 points to image 1, WaterBottle_occlusionRoughnessMetallic.png. Those are zero-based array indices. The filename is a useful clue; the two bindings are the evidence that both jobs are connected.[2]

Material useTexture → imageEffective UV set
Metallic + roughness1 → 1TEXCOORD_0
Occlusion1 → 1TEXCOORD_0
Resolved material references from the inspected WaterBottle file[2][1]

Neither binding writes texCoord, so each uses the glTF default of 0. The primitive supplies TEXCOORD_0. Metallic factor, roughness factor, and occlusion strength are also omitted, giving effective defaults of 1. These defaults are resolved from the specification, rather than invented values added to the source.[2][7][8][9]

For a different asset, follow both references separately. A shared image does not require the two bindings to select the same UV set. Conversely, finding useful red-channel pixels does not establish that the material has an occlusionTexture binding. This is why a texture folder alone is not a complete material handoff.[8][1]

What the actual texture bytes tell us

The inspected PNG is 2048 × 2048 RGB, with 3,577,832 file bytes. Its Git blob hash matches the pinned repository file. The report samples stored channel bytes without color conversion, filtering, or a UV lookup. Coordinates below start at the top-left of the image; these are atlas texels, not identified points on the bottle’s rendered surface.[3]

Pixel (x, y)Stored RGBOcclusion RRoughness GMetallic B
512, 512241, 96, 2510.9450.3760.984
1024, 1024255, 100, 2481.0000.3920.973
1536, 1536255, 193, 01.0000.7570.000
Original raw-pixel inspection; normalized values are channel ÷ 255, rounded to three decimals[3][1]

The middle sample makes a channel swap concrete: its intended roughness input is about 0.392 and its metallic input about 0.973. Reversing G and B would exchange those two numbers before shading. That is a numerical wiring difference, not a photographed or measured appearance comparison. The final appearance also depends on the rest of the material and scene.

The downloadable JSON report preserves full six-decimal values, texture dimensions, hashes, bindings, and source URLs. Its companion Python script checks the exact input bytes before printing the same report. Download the two pinned source files listed in the report, then run the script with their local filenames. It needs Python and Pillow; it does not load a game engine.

The multiplier can erase a correctly connected map

Roughness and metallic factors multiply their sampled channels. Occlusion strength instead reduces the occlusion effect toward 1: effective occlusion = 1 + strength × (R − 1). At strength 0, the result is 1, meaning no occlusion effect. A simple R × strength calculation would be wrong.[7][9][1]

InputSettingResult
G = 0.6roughnessFactor = 0.50.3
B = 0.8metallicFactor = 0.250.2
R = 0.2occlusion strength = 0.50.6
Synthetic arithmetic example, separate from the WaterBottle measurements[1]

A practical trap appears when rebuilding the material manually in Three.js. A new MeshStandardMaterial starts with metalness 0 and roughness 1. Its metalnessMap uses blue and multiplies by metalness. Leaving that scalar at zero suppresses the map’s metallic contribution. This differs from glTF’s default metallicFactor of 1. Inspect the loaded material’s values before replacing it with a fresh material.[5][7]

That observation concerns a manual rebuild. It does not claim GLTFLoader ignores glTF defaults, and it is not an import test. The useful debugging question is precise: did the data change, or did the receiving material acquire different defaults?

Keep ORM out of the base-color conversion path

Three.js distinguishes color textures from non-color textures. Its material documentation assigns NoColorSpace to roughnessMap, metalnessMap, and aoMap; its color-management guide assigns sRGB to ordinary color maps such as base color and emissive textures. Do not mark every PNG as sRGB merely because an image viewer displays it in color.[5][6]

When checking an asset, keep three observations separate: the stored RGB bytes, the parameters the material derives from those bytes, and the final shaded image. This article establishes the first two for the pinned sample. A visually plausible composite PNG is not proof that any of those channels is being interpreted correctly.

Reproduce the file facts, then test the result in your scene

Use the source-linked report as a small reference case, then inspect your own asset’s material bindings and effective factors. Preserve a copy before editing. Change one suspect assignment at a time and compare in the same scene and lighting, so a material correction is not confused with a presentation change. This is a proposed debugging method, not a test performed for this article.

The WaterBottle model-associated files are listed under CC0-1.0; its license file distinguishes metadata terms and excludes logos and trademarks. The report credits Microsoft’s sample and links to the exact originals. It redistributes neither the mesh nor the texture. The cover is separate conceptual artwork.[4]

Evidence used

Sources

  1. 1.glTF 2.0 specification: official source, matching the registry revision — Khronos Group. Accessed 2026-10-03.
  2. 2.WaterBottle glTF, pinned source file — Khronos Group. Accessed 2026-10-03.
  3. 3.WaterBottle occlusion-roughness-metallic PNG, pinned source — Khronos Group. Accessed 2026-10-03.
  4. 4.WaterBottle asset license — Khronos Group. Accessed 2026-10-03.
  5. 5.MeshStandardMaterial: channels, factors, and defaults — Three.js. Accessed 2026-10-03.
  6. 6.Color management: color and non-color textures — Three.js. Accessed 2026-10-03.
  7. 7.glTF metallic-roughness schema and defaults — Khronos Group. Accessed 2026-10-03.
  8. 8.glTF texture binding and UV default — Khronos Group. Accessed 2026-10-03.
  9. 9.glTF occlusion strength and default — Khronos Group. Accessed 2026-10-03.

Plan the asset and its handoff together

Explore Goblin3D’s image-to-3D workflow, then inspect the material data in your own exported asset.

Explore image to 3D

Sources, product facts, and original evidence were checked before publication.

Share your feedback

Sign in to share feedback with the Goblin3D team.

Sign in

support@goblin3d.ai