Every extra texture you send to the GPU costs memory bandwidth and a sampler slot. If you're still shipping separate Ambient Occlusion, Roughness, and Metallic maps as three individual files, you're leaving performance on the table. Channel packing solves this by merging all three into a single image — one ORM texture that Unreal Engine 5 and Unity both consume natively. Here's the complete workflow from Blender to engine.
What Is Channel Packing and Why It Matters
Channel packing is the practice of storing independent grayscale data in the Red, Green, and Blue channels of a single RGB image. For PBR workflows the standard layout is:
- R — Occlusion (AO)
- G — Roughness
- B — Metallic
This is the ORM convention used by Unreal Engine 5's default material functions and Unity's Lit shader. The alternative — separate R, G, and M textures — wastes two texture sample instructions per material, increases VRAM pressure, and can push you over platform sampler limits on mobile. A single 2K ORM map consumes exactly the same memory as one 2K grayscale map while encoding three channels of data.
For a project with 50 unique materials, the difference is 100 fewer texture fetches per fragment shader invocation. On mobile or VR targets this is not optional optimization — it's a requirement.
Building a Channel-Pack Node Group in Blender
![]()
Blender's Compositor is the right tool for channel packing — it operates on full-resolution image buffers without material overhead. Here's the exact setup:
- Open the Compositor workspace and enable Use Nodes.
- Add three Image input nodes: one for AO, one for Roughness, one for Metallic. Load your baked grayscale maps into each.
- Add a Combine RGBA node (`Shift+A → Converter → Combine Color`, then switch to RGBA mode).
- Connect each Image node's output to `Combine RGBA` as follows: AO → R, Roughness → G, Metallic → B. Leave A at 1.0 or connect a white constant.
- Add a Composite output node and a File Output node. Set the File Output to PNG (lossless) or 16-bit TIFF if you need precision beyond 8-bit.
```python
# Blender Python: automate ORM packing via bpy
import bpy
def pack_orm(ao_path, rough_path, metal_path, output_path):
scene = bpy.context.scene
scene.use_nodes = True
tree = scene.node_tree
nodes = tree.nodes
links = tree.links
nodes.clear()
def img_node(path, label):
n = nodes.new('CompositorNodeImage')
n.image = bpy.data.images.load(path)
n.label = label
return n
ao = img_node(ao_path, 'AO')
rough = img_node(rough_path, 'Roughness')
metal = img_node(metal_path, 'Metallic')
combine = nodes.new('CompositorNodeCombRGBA')
links.new(ao.outputs['Image'], combine.inputs['R'])
links.new(rough.outputs['Image'], combine.inputs['G'])
links.new(metal.outputs['Image'], combine.inputs['B'])
out = nodes.new('CompositorNodeOutputFile')
out.base_path = output_path
out.file_slots[0].path = 'ORM_'
out.format.file_format = 'PNG'
links.new(combine.outputs['Image'], out.inputs[0])
bpy.ops.render.render(use_viewport=False)
pack_orm('/tmp/ao.png', '/tmp/roughness.png', '/tmp/metallic.png', '/tmp/')
```
Run this from Blender's scripting workspace. The result is a single PNG with AO in Red, Roughness in Green, Metallic in Blue — ready to import into any engine.
Critical: always bake individual maps at the highest quality your pipeline allows before packing. Packing never compensates for bake errors — fix seams and artifacts in the individual maps first.
Exporting ORM Maps from Blender
Before packing, ensure your baked maps are consistent:
| Map | Color Space | Expected Range | Common Errors |
|---|---|---|---|
| AO | Non-Color | 0.0–1.0 | Using sRGB gamma — darkens incorrectly |
| Roughness | Non-Color | 0.0–1.0 | Inverted (smooth=1, rough=0) for some engines |
| Metallic | Non-Color | 0.0 or 1.0 | Mid-gray values cause engine validation warnings |
Set all bake image color spaces to Non-Color in Blender's Image Editor before baking. This prevents sRGB gamma from corrupting your linear data.
For the packed ORM output, export as PNG with 8-bit depth for most assets. Use 16-bit only for assets where AO or roughness precision is visually critical (character skin, hero props). 16-bit doubles file size with minimal visual gain on most geometry.
Importing ORM Textures into Unreal Engine 5
![]()
UE5's M_ORM material function handles packed textures natively. Here's the import checklist:
Texture import settings (Content Browser):
- Set sRGB to false — the ORM texture is linear data, not color
- Set Compression Settings to Masks (this selects BC4/BC5 compression appropriate for non-color data)
- MipGen Settings: DefaultMips works correctly; do not use NormalMap settings
Material setup:
1. In the Material Editor, add a `Texture Sample` node and plug your ORM texture in
2. Route outputs: `R → AmbientOcclusion`, `G → Roughness`, `B → Metallic`
3. Or use UE5's built-in `M_ORM` material function from the Engine Content folder — it expects exactly this layout
For reuse across many assets, build a Master Material with a single ORM TextureParameter so you can create Material Instances without touching shader logic. See BitSoul's marketplace for GLB assets that already ship ORM-packed textures ready for this workflow.
Importing ORM Textures into Unity URP
Unity's Lit shader stores its packed map differently — it uses the Mask Map with this layout:
- R — Metallic
- G — Ambient Occlusion
- B — Detail Mask
- A — Smoothness (inverted Roughness)
This is not the same as UE5's ORM order. You have two options:
Option A — Repack for Unity layout in Blender Compositor:
Swap your combine node connections: Metallic → R, AO → G, (white) → B, and for the Alpha channel connect Roughness through a Invert node (Math → Subtract: 1 − Roughness) to get Smoothness.
Option B — Use Blender's multi-output File node:
Bake a separate ORM (UE5 layout) and a separate Mask Map (Unity layout) from the same source data in one Compositor pass using two File Output nodes.
In Unity, set the Mask Map texture's sRGB (Color Texture) checkbox to off. Import as Default compression — BC7 on desktop, ASTC on mobile.
Smoothness from Roughness inversion in Unity (HLSL reference):
```hlsl
float smoothness = 1.0 - roughnessSample;
```
This single line is the most common source of the "everything looks shiny" or "everything looks matte" bug when porting between engines.
Channel Packing Checklist
Before publishing an asset with ORM textures, verify each point:
- [ ] All source maps baked with Non-Color color space in Blender
- [ ] No mid-gray metallic values on non-metal surfaces (0 or 1 only)
- [ ] AO map checked for seams at UV island borders
- [ ] Roughness range checked: no pure-black (0.0) on non-mirror surfaces
- [ ] ORM exported as PNG with sRGB unchecked at import in both engines
- [ ] UE5: Compression set to Masks, not Default Color
- [ ] Unity: Mask Map layout confirmed (R=Metallic, G=AO, A=Smoothness)
- [ ] Test render in engine — check AO darkening, metallic reflection, roughness variation
Channel packing is a one-time workflow cost that pays off every time an asset hits the GPU. Set it up once in a reusable Blender Compositor node group or Python script, and every asset you ship will meet console and mobile texture budget requirements without rework.
Ready to grab professionally prepared GLB assets with pre-packed ORM maps? Browse the BitSoul marketplace for game-ready 3D models optimized for Unreal Engine 5, Unity URP, and Godot 4.