Godot 4's physically based rendering pipeline is genuinely excellent — but most tutorials stop at dragging an albedo texture into `StandardMaterial3D` and calling it done. That leaves serious performance and quality on the table. The real unlock is understanding ORM textures: a single packed image that encodes Occlusion, Roughness, and Metallic data into three separate channels. One texture slot. Three properties. Zero wasted samples.
This guide walks through the complete ORM workflow: what each channel does, how to author ORM maps in Blender and Substance Painter, how to wire them correctly inside Godot 4, and which common mistakes will silently destroy your material quality.
What Are ORM Textures and Why They Matter
ORM stands for Occlusion (R channel), Roughness (G channel), Metallic (B channel). Instead of sampling three separate grayscale textures at runtime, you pack all three values into the RGB channels of a single texture. The GPU fetches one texture instead of three — reducing memory bandwidth and texture cache misses simultaneously.
Godot 4's `StandardMaterial3D` has native support for this format through the ORM texture input under the *Occlusion*, *Roughness*, and *Metallic* combined workflow. When you assign a texture to the `orm_texture` property and enable `texture_channel` flags correctly, Godot reads R for occlusion, G for roughness, and B for metallic automatically.
![]()
Here is what each channel controls:
| Channel | Property | 0 Value | 1 Value |
|---------|----------|---------|--------|
| R (Red) | Ambient Occlusion | No occlusion (full light) | Full occlusion (shadowed) |
| G (Green) | Roughness | Mirror-smooth | Fully diffuse |
| B (Blue) | Metallic | Dielectric (plastic/stone) | Conductor (metal) |
The green channel is most visually impactful — roughness controls how tightly the specular highlight focuses. A roughness of 0.0 gives you a perfect mirror. At 1.0 you get completely diffuse scattering with no visible specular. Most real-world surfaces live between 0.3 and 0.8.
Why not separate textures? Three 1024x1024 grayscale textures at 8-bit consume 3 MB uncompressed and three texture unit binds per draw call. One 1024x1024 RGB texture consumes 3 MB — but only one bind. On mobile GPUs with limited texture units, this distinction can mean the difference between a shader that compiles and one that doesn't.
Understanding StandardMaterial3D Properties
Godot 4's `StandardMaterial3D` exposes PBR parameters across four property groups: Albedo, Metallic, Roughness, and Emission. For ORM workflows you are working primarily in the Metallic and Roughness groups.
Key properties to configure:
- `metallic` — scalar multiplier (0.0-1.0). Leave at 1.0 when using an ORM map; the texture drives variation.
- `metallic_specular` — controls the F0 reflectance for dielectric surfaces. Default 0.5 matches the 4% reflectance of most non-metals. Only change this for specialized materials like skin (0.35) or water (0.2).
- `roughness` — scalar multiplier. Set to 1.0 when using an ORM map.
- `ao_light_affect` — controls how strongly ambient occlusion darkens direct lighting. Keep this at 0.0 for physically correct results; AO should only attenuate ambient/indirect light.
- `ao_on_uv2` — routes AO texture lookup to UV2 channel. Useful if you baked a separate lightmap AO.
When setting up your ORM texture:
1. Under Metallic, set `Texture Channel` to Blue.
2. Under Roughness, set `Texture Channel` to Green.
3. Under Ambient Occlusion, enable it and set `Texture Channel` to Red.
4. Assign your ORM image to all three texture slots — Godot deduplicates the GPU resource automatically.
```gdscript
# Assign ORM map in GDScript
var mat = StandardMaterial3D.new()
var orm_tex = load("res://textures/my_asset_orm.png")
mat.ao_enabled = true
mat.ao_texture = orm_tex
mat.ao_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED
mat.roughness_texture = orm_tex
mat.roughness_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_GREEN
mat.metallic_texture = orm_tex
mat.metallic_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_BLUE
# Scalar multipliers should be 1.0 to let texture drive values
mat.metallic = 1.0
mat.roughness = 1.0
```
Creating ORM Maps in Blender and Substance Painter
The authoring step is where most developers make mistakes. Both tools output the right data, but neither outputs a pre-packed ORM file by default — you have to configure the export correctly.
![]()
Blender (via Cycles baking)
Blender bakes occlusion, roughness, and metallic as separate images. To pack them:
- Bake Ambient Occlusion to save as `ao.png` (grayscale).
- Bake Roughness from your material's roughness output to save as `rough.png`.
- Plug your metallic node into the Emission bake output to save as `metal.png`.
Then pack in Blender's Compositor using the Combine RGBA node:
- R = ao.png
- G = rough.png
- B = metal.png
- A = 1.0 or opacity mask
Save the result as `asset_orm.png`. Use 8-bit PNG unless you need 16-bit precision on roughness (rare).
Substance Painter
Substance Painter exports ORM natively via the Godot export preset. Go to File > Export Textures, select the Godot preset, and you will see an `ORM` channel in the output list. The preset packs R=AO, G=Roughness, B=Metallic — exactly what Godot 4 expects. No manual compositing needed.
```
# Substance Painter Godot export checklist
[x] Preset: Godot Engine (or manual)
[x] ORM output: R=AO, G=Roughness, B=Metallic
[x] Albedo output: sRGB color space
[x] Normal output: OpenGL convention (NOT DirectX)
[x] Resolution: power-of-two (512, 1024, 2048)
[x] Format: PNG (lossless for import into Godot)
```
Important: Substance Painter uses DirectX normal maps by default. Godot 4 expects OpenGL convention (Y-axis flipped). Either flip Y in the export preset or enable Flip Normal Map Y on your Godot material — never both.
Importing and Configuring ORM Materials in Godot 4
Once your ORM PNG is in your project, Godot's importer needs the correct settings:
- Select the ORM texture in the FileSystem panel.
- In Import settings, set Compress Mode to VRAM Compressed — Godot will compress to BC5/BC7 on desktop and ETC2 on mobile.
- Do not enable sRGB for ORM textures — occlusion, roughness, and metallic data is linear. Importing as sRGB applies a gamma curve that destroys material accuracy.
- Keep mipmaps enabled for all 3D surface materials.
After import, create a new `StandardMaterial3D`, assign the ORM texture to all three PBR channels as shown in the GDScript above, and connect your albedo and normal maps in their respective slots. The complete material stack for a game-ready asset:
- `albedo_texture` -> `asset_albedo.png` (sRGB)
- `normal_texture` -> `asset_normal.png` (linear, OpenGL)
- `orm_texture` (x3 slots) -> `asset_orm.png` (linear)
- `emission_texture` (optional) -> `asset_emissive.png` (sRGB)
Four texture files. All PBR properties covered. This is the asset format expected by the BitSoul marketplace for compatible GLB game-ready packs.
Performance Tips and Common Pitfalls
Mip-map bleeding on ORM boundaries. When roughness transitions sharply across a UV seam, lower mip levels blend those values and create a visible band at distance. Pad your UV islands by at least 4 texels at 1024 resolution (8 texels at 2048) to eliminate this.
Metallic fringing on edges. If your metallic mask has anti-aliased edges, partially metallic pixels produce physically impossible results. Keep metallic maps binary — fully 0 or fully 1 — except for intentional worn-metal transitions.
AO too strong on direct light. Setting `ao_light_affect` above 0.0 makes crevices look unnaturally dark in direct sunlight. Keep it at 0.0 and let the shadow system handle direct occlusion.
Wrong specular on non-metals. The default `metallic_specular` of 0.5 (4% F0 reflectance) is correct for most plastics, stone, and wood. If your dielectric material looks too dull or too shiny, tweak this value rather than faking it with metallic values above 0.0.
```gdscript
# Quick validation: confirm channel assignments
func validate_orm_material(mat: StandardMaterial3D) -> void:
assert(mat.ao_texture == mat.roughness_texture, "ORM: AO and Roughness must share texture")
assert(mat.roughness_texture == mat.metallic_texture, "ORM: All three must share texture")
assert(mat.ao_texture_channel == BaseMaterial3D.TEXTURE_CHANNEL_RED, "ORM: AO must read Red")
assert(mat.roughness_texture_channel == BaseMaterial3D.TEXTURE_CHANNEL_GREEN, "ORM: Roughness must read Green")
assert(mat.metallic_texture_channel == BaseMaterial3D.TEXTURE_CHANNEL_BLUE, "ORM: Metallic must read Blue")
print("Material validated: ORM channels correctly configured.")
```
ORM textures pay dividends at every scale — whether you are shipping a mobile game with aggressive memory budgets or a desktop title targeting 4K. One bind, three properties, physically accurate results. Once it clicks, you will never go back to separate channel textures.
Ready to test your ORM-ready assets in a real game engine? Browse game-ready 3D models with pre-packed ORM maps at the BitSoul marketplace — every asset is vetted for correct channel packing and Godot 4 compatibility out of the box.