Skip to content
← Back to Blog tutorials

Why channel-packed ORM textures render wrong in Unity and Unreal

By BitSoul3D5 min read2 views

Your metallic surfaces render flat gray, roughness looks inverted, and ambient occlusion either does nothing or darkens the whole model — and the texture file looks fine in Photoshop. That's a channel-packed ORM texture, and the fix is almost always about which channel your engine expects where, not the texture itself.

Why channel-packed ORM textures render wrong in Unity and Unreal

ORM packing puts three grayscale maps into one RGB file: red for ambient occlusion, green for roughness, blue for metallic. One texture instead of three cuts VRAM roughly in half and drops your material's texture lookups from three to one — worth doing on every prop. But Unity, Unreal Engine 5, and Godot don't agree on which channel means what by default, and neither do the marketplaces and AI generators you're pulling assets from.

Why the same texture looks different in every engine

Why the same texture looks different in every engine — illustrated

Unreal's default Mask (ORM) setup reads red as AO, green as roughness, blue as metallic — the industry-standard order. Unity's Standard and URP/HDRP Lit shaders expect a Metallic Smoothness map instead: red for metallic, alpha for smoothness, not roughness, and it's a separate alpha channel, not blue. Feed Unity a raw ORM texture without conversion and metallic surfaces come out matte gray because Unity is reading your AO channel as metallic. Godot 4's ORMMaterial3D matches Unreal's R/G/B order, but only if you assign the texture as orm_texture — dropping it into the base metallic_texture slot reads a single channel and ignores the rest.

The gotcha that costs the most time: smoothness and roughness are inverted values (smoothness = 1 - roughness). A texture packed with roughness in the alpha channel will make Unity surfaces look wet and mirror-like where they should look brushed and matte, and there's no error, warning, or red text anywhere — it just looks wrong.

Close-up of a weathered metal surface showing scratches and rust variation

Fixing it in each engine

In Unreal, wire the packed texture into a Constant3Vector-less setup: connect it once, then use a BreakOutFloat3Components node (or a Mask node with R/G/B checked individually) to split it, feeding R to Ambient Occlusion, G to Roughness, and B to Metallic on your Material output. No conversion needed if the source is already R=AO/G=Rough/B=Metal.

In Unity, you can't just plug in an ORM map — URP's Lit shader has no ORM input slot. Two options: write a small conversion pass that repacks metallic into RGB and inverted roughness into alpha, or use a custom shader graph node that samples the ORM texture once and routes channels manually, skipping Unity's built-in Metallic Smoothness slot entirely.

// Unity Shader Graph custom function node: unpack ORM, invert roughness to smoothness
void UnpackORM_float(float3 orm, out float ao, out float smoothness, out float metallic)
{
    ao = orm.r;
    smoothness = 1.0 - orm.g; // Unity wants smoothness, not roughness
    metallic = orm.b;
}

Godot is the least painful once you know the slot name. Create a StandardMaterial3D, set metallic_texture_channel to match your packing (usually CHANNEL_BLUE), and assign the same texture to both ao_texture and roughness_texture with their respective channel enums set to red and green. Godot reads roughness directly — no inversion needed, unlike Unity.

EngineExpectsAORoughness/SmoothnessMetallicGotcha
Unreal Engine 5ORM (R/G/B)RedGreen (roughness)BlueWorks as-is if source is standard ORM
Unity (URP/HDRP)Metallic SmoothnessNot built inAlpha (inverted: smoothness)RedNo native ORM slot; needs a custom node or repack
Godot 4Per-channel enumsConfigurableConfigurable (roughness, not inverted)ConfigurableMust set channel enum per map, not just assign texture

Repacking when the source doesn't match

When an asset arrives with separate AO, roughness, and metallic files instead of one ORM texture — common with marketplace downloads and AI-generated models — you need to pack them yourself before any of the above applies. A channel packer node graph in Blender's Shader Editor (Separate Color nodes feeding a Combine Color node) does this without leaving your DCC:

# Blender: batch-repack three grayscale images into one ORM texture
import bpy
ao, rough, metal = [bpy.data.images[n] for n in ("ao.png", "rough.png", "metal.png")]
w, h = ao.size
orm = bpy.data.images.new("orm_packed", w, h)
orm.pixels = [v for i in range(w * h) for v in
              (ao.pixels[i*4], rough.pixels[i*4], metal.pixels[i*4], 1.0)]
orm.filepath_raw = "//orm_packed.png"
orm.file_format = "PNG"
orm.save()

Watch color space on export: AO, roughness, and metallic are all data, not color, so the packed file must be imported as Non-Color (Blender/Unreal) or with sRGB unchecked (Unity), or every value in the texture gets gamma-corrected and every material reads wrong even with channels mapped correctly.

Gotchas that survive correct channel mapping

Mipmaps blur channel boundaries at a distance, which is usually invisible for AO and roughness but visibly bleeds metallic edges on trim sheets with hard metal/non-metal borders — pad those borders by a few pixels before packing. Compression makes this worse: BC7 preserves channels independently, but BC3/DXT5 shares bits between color and alpha in a way that can introduce faint banding in the metallic channel on flat surfaces. And if you're pulling a model that already ships with a packed ORM map, check the product description before assuming R/G/B order — it varies enough between sources that guessing costs you a re-export.

A worn metal asset is the fastest way to see whether your channel mapping is right, since rust and scratches expose every roughness and metallic transition at once. The Rusted Playground ships with a standard ORM map, so it's a clean reference to diff your engine's output against — swap it into your material setup and if the rust patches read as rough/non-metallic and the bare metal reads smooth/metallic, your channels are wired correctly. For related import fixes, see how KTX2 compression support varies by engine and why Godot's glTF importer can flatten normal maps on the same kind of packed-texture assets.


This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.

Tags: texturing pbr channel-packing game-assets optimization

Skip the modeling — download it instead

A free BitSoul3D account gets you 2 GLB downloads every month for personal use plus 25 one-time AI Engine credits, no card required. PBR-textured GLB downloads with a full 3D preview before you buy, for Unreal, Unity, Godot or Blender — OBJ and 3D-printable STL come with any purchase or paid plan.

Browse 1,051 models — from $4.99 → or start free (2 downloads a month)