Every extra texture you load costs VRAM, bandwidth, and draw calls. Channel packing is the single most effective technique for cutting that cost — and most developers either skip it entirely or do it wrong. This guide covers exactly how channel packing works, which channels to combine, and how to set it up in Blender, Unity, Unreal Engine 5, and Godot 4.
What Is Channel Packing and Why Does It Matter
A standard PBR material uses at least five textures: albedo, normal, metallic, roughness, and ambient occlusion. Each of these is typically a full 2K or 4K image. On a scene with 50 unique assets, that's 250 texture loads — before you add emissive, height, or detail maps.
Channel packing solves this by storing multiple greyscale maps in the separate R, G, and B (and sometimes A) channels of a single RGBA texture. Since metallic, roughness, and ambient occlusion are all single-channel greyscale maps, they can be packed into one RGB image with zero quality loss. The GPU reads them as separate channels at runtime — there's no decoding overhead.
The result: three texture fetches become one. On a 4K texture at 8 bits per channel, you save roughly 32 MB of VRAM per asset. Across a whole project, that's the difference between hitting your memory budget and blowing past it.
| Unpacked | Packed | Savings |
|---|---|---|
| Metallic (4K, 8-bit) | | 16 MB |
| Roughness (4K, 8-bit) | ORM (4K, 8-bit) | 16 MB |
| Ambient Occlusion (4K, 8-bit) | | Combined |
| Total: 48 MB | Total: 16 MB | ~66% reduction |
Which Channels to Pack Together (ORM Convention)
The industry standard is the ORM layout, used by Unreal Engine 5 by default:
- R channel — Ambient Occlusion
- G channel — Roughness
- B channel — Metallic
Unity's Mask Map (used in HDRP) uses a different convention:
- R channel — Metallic
- G channel — Ambient Occlusion
- B channel — Detail Mask
- A channel — Smoothness (inverted roughness)
Godot 4 with its ORM material node uses the Unreal convention (ORM). Always check the engine's expected channel layout before packing — mixing up R and G for metallic/roughness is the most common mistake and produces subtle, hard-to-debug shading errors.
![]()
How to Channel Pack in Blender (Node Setup)
Blender's Compositor or Shader Editor both work for this. The Compositor approach is faster for batch processing.
Compositor method:
1. Open the Compositor (switch to Compositing workspace, check *Use Nodes*).
2. Load each greyscale map as a separate Image node.
3. Use Separate RGB nodes to isolate the red channel from each image (greyscale images store their value in all three channels equally).
4. Combine into a single image using a Combine RGBA node:
- R input ← AO red channel
- G input ← Roughness red channel
- B input ← Metallic red channel
- A input ← leave at 1.0 or plug in opacity if needed
5. Connect to a File Output node. Set format to PNG (lossless) or EXR for linear data.
```python
# Blender Python — create a channel-pack compositor setup
import bpy
bpy.context.scene.use_nodes = True
tree = bpy.context.scene.node_tree
nodes = tree.nodes
links = tree.links
# Clear default nodes
for n in nodes:
nodes.remove(n)
# Load textures
def add_image_node(path, x, y):
n = nodes.new('CompositorNodeImage')
n.image = bpy.data.images.load(path)
n.location = (x, y)
return n
ao_node = add_image_node('/path/to/ao.png', -600, 300)
rough_node = add_image_node('/path/to/roughness.png', -600, 0)
metal_node = add_image_node('/path/to/metallic.png', -600, -300)
# Separate RGB (just need R channel — greyscale images)
def sep(node, x, y):
s = nodes.new('CompositorNodeSepRGBA')
s.location = (x, y)
links.new(node.outputs['Image'], s.inputs['Image'])
return s
ao_sep = sep(ao_node, -300, 300)
rough_sep = sep(rough_node, -300, 0)
metal_sep = sep(metal_node, -300, -300)
# Combine RGBA
comb = nodes.new('CompositorNodeCombRGBA')
comb.location = (0, 0)
links.new(ao_sep.outputs['R'], comb.inputs['R']) # AO -> R
links.new(rough_sep.outputs['R'], comb.inputs['G']) # Roughness -> G
links.new(metal_sep.outputs['R'], comb.inputs['B']) # Metallic -> B
# File output
out = nodes.new('CompositorNodeOutputFile')
out.base_path = '/path/to/output/'
out.file_slots[0].path = 'ORM_packed'
out.location = (300, 0)
links.new(comb.outputs['Image'], out.inputs['Image'])
print('Channel pack compositor ready — press Render to export.')
```
For Unity's Mask Map layout, swap the inputs: Metallic → R, AO → G, Detail Mask → B, Smoothness (1 minus Roughness) → A. You can invert roughness using a Math node set to *Subtract* with 1.0 as the first value.
Exporting and Using Packed Textures in Unity, Unreal Engine 5, and Godot 4
![]()
Once you have your packed texture exported, the engine setup is straightforward — but each engine has specific import settings that matter.
Unity (HDRP Lit shader)
In the Material Inspector, assign your packed texture to the Mask Map slot. Under the texture import settings, set *sRGB* to off — packed ORM data is linear, not colour-corrected. Leaving sRGB on will corrupt your roughness and metallic values.
Unreal Engine 5 (M_StandardSurface or custom material)
UE5 expects ORM layout. In your material graph, plug the packed texture into a Texture Sample node, then:
- Connect R output → Ambient Occlusion input
- Connect G output → Roughness input
- Connect B output → Metallic input
In the texture asset settings, set Compression Settings to `Masks` (not `Default Color`). This disables sRGB and applies BC4/BC5 compression instead of DXT1, preserving channel precision.
Godot 4 (ORM Material3D)
Godot 4 has a built-in `ORMMaterial3D` that directly accepts ORM-packed textures. Assign your packed image to the Occlusion/Roughness/Metallic texture slot. Set the import preset to Linear in the import panel.
Compression tip: For all engines, prefer BC5 (two-channel) for normal maps and BC7 for ORM when available. BC7 at 8bpp preserves all four channels with excellent quality — significantly better than DXT5 at the same size.
Common Mistakes and How to Avoid Them
Channel packing is simple in principle but breaks in specific ways that are hard to spot in a quick review.
Wrong channel order. Unity and Unreal use different layouts. If you pack for UE5 and use the texture in Unity's HDRP Mask Map slot, your metallic and AO will be swapped. Always label your output files with the layout (e.g. `T_Rock_ORM_UE5.png` vs `T_Rock_MaskMap_Unity.png`).
sRGB left enabled. This is the single most common bug. ORM maps must be imported as linear data. With sRGB on, the engine applies gamma correction to your roughness values — smooth surfaces look rough, rough surfaces look overblown. Always double-check import settings after adding a new texture.
Packing colour maps. Albedo is an RGB colour map and cannot be channel-packed with greyscale ORM data. Only single-channel (greyscale) maps are candidates for packing.
Using lossy compression on the source. Pack from uncompressed 16-bit source files when possible. If you pack from already-compressed JPEGs, compression artefacts get baked in and amplified, especially visible in the ambient occlusion channel.
Not verifying in-engine. Always do a before/after comparison in the engine's material preview. Sphere primitives with extreme roughness values (0, 0.5, 1.0) catch most packing errors immediately.
Channel packing is a one-time workflow investment that pays off on every asset you ship. Once your Blender compositor setup is saved as a node group, the entire process takes under a minute per asset.
Need high-quality game-ready 3D assets with pre-packed textures? Browse the BitSoul marketplace — every asset includes properly packed ORM textures, engine-specific import presets, and LOD variants. Stop wrestling with texture budgets and start shipping. Explore the BitSoul marketplace →