--- name: godot-3d-materials description: "Expert patterns for Godot 3D PBR materials using StandardMaterial3D including albedo, metallic/roughness workflows, normal maps, ORM texture packing, transparency modes, and shader conversion. Use when creating realistic 3D surfaces, PBR workflows, or material optimization. Trigger keywords: StandardMaterial3D, BaseMaterial3D, albedo_texture, metallic, metallic_texture, roughness, roughness_texture, normal_texture, normal_enabled, orm_texture, transparency, alpha_scissor, alpha_hash, cull_mode, ShaderMaterial, shader parameters." --- # 3D Materials Expert guidance for PBR materials and StandardMaterial3D in Godot. ## NEVER Do - **NEVER use separate metallic/roughness/AO textures** — Use ORM packing (1 RGB texture with Occlusion/Roughness/Metallic channels) to save texture slots and memory. - **NEVER forget to enable normal_enabled** — Normal maps don't work unless you set `normal_enabled = true`. Silent failure is common. - **NEVER use TRANSPARENCY_ALPHA for cutout materials** — Use TRANSPARENCY_ALPHA_SCISSOR or TRANSPARENCY_ALPHA_HASH instead. Full alpha blending is expensive and causes sorting issues. - **NEVER set metallic = 0.5** — Materials are either metallic (1.0) or dielectric (0.0). Values between are physically incorrect except for rust/dirt transitions. - **NEVER use emission without HDR** — Emission values > 1.0 only work with HDR rendering enabled in Project Settings. - **NEVER use transparent materials for large environmental surfaces** — Transparent objects cannot rely on the Z-buffer for early fragment rejection, resulting in massive overdraw. If only a tiny part of a mesh is transparent, split the mesh into two surfaces: one opaque, one transparent. - **NEVER create hundreds of slightly varied StandardMaterial3D resources if performance is dropping** — Godot minimizes GPU state changes by automatically reusing the underlying shader for materials that share the exact same configuration flags (checkboxes). Try to group your material configurations. - **NEVER attempt to fix Z-fighting strictly by moving objects further apart** — Floating-point precision degrades over distance. To fix flickering textures, increase your Camera3D's `Near` plane property and decrease the `Far` property to compress the precision range. - **NEVER use unique Material resources per MeshInstance3D** — This breaks draw call batching. Use 'Instance Uniforms' to vary parameters while keeping a single shared material. - **NEVER mutate a shared Material `.tres` / surface material at runtime** — Call `duplicate(true)` or enable **Local To Scene**, then assign the unique instance before tweaking parameters. **MANDATORY** use [`material_fx.gd`](scripts/material_fx.gd) `ensure_unique_override()` / overlay helpers for flash/dissolve. - **NEVER use Decals on dynamic moving actors without a Cull Mask** — Bullet holes should not stick to the player's face as they walk over them. Mask out character layers. --- ## Available Scripts > **MANDATORY**: Read the appropriate script before implementing the corresponding pattern. ### Workflow router (MANDATORY / Do NOT Load) | Task | Load | Do NOT Load | |------|------|-------------| | StandardMaterial3D / ORM / transparency | `pbr_orm_packer.gd`, `transparency_sorting_fix.gd`, `material_batcher.gd`, `material_fx.gd` | `triplanar_world*.gdshader`, `vertex_wind_sway.gdshader` | | UV-less terrain / cliffs | `triplanar_world.gdshader` or `triplanar_world_projection.gdshader` + `pbr_material_builder.gd` | Wind sway unless foliage | | Foliage wind | `vertex_wind_sway.gdshader` | Triplanar unless rock/terrain also needed | | Runtime damage / dissolve | **MANDATORY** `material_fx.gd` | Editing shared imported materials | | Instance color/health / texture-array variants | **MANDATORY** `instance_uniform_batching.gdshader` | Per-mesh unique StandardMaterial3D copies; body texture-array samples | | HLOD / distant material simplify | `material_batcher.gd` (`setup_lod_materials`) | Alpha-blend distance fade (use Pixel Dither) | | Organic SSS / rim / clearcoat | `organic_material.gd`, `subsurface_scattering_setup.gd` | SSS recipes on Mobile/Compatibility | > Pure StandardMaterial3D albedo/ORM/transparency work: **Do NOT Load** triplanar, wind, or texture-array shaders. ### [material_fx.gd](scripts/material_fx.gd) **MANDATORY** for runtime FX. `ensure_unique_override()` / overlay flash / scissor dissolve — never tween shared `.tres` materials. ### [pbr_material_builder.gd](scripts/pbr_material_builder.gd) Runtime PBR material creation with ORM textures and triplanar mapping. ### [organic_material.gd](scripts/organic_material.gd) Subsurface scattering and rim lighting setup for organic surfaces (skin, leaves). Use for realistic character or vegetation materials. ### [triplanar_world.gdshader](scripts/triplanar_world.gdshader) Triplanar projection shader for terrain without UV mapping. Blends textures based on surface normals. Use for cliffs, caves, or procedural terrain. ### [pbr_orm_packer.gd](scripts/pbr_orm_packer.gd) Expert PBR resource utility. Packs Ambient Occlusion, Roughness, and Metallic into a single ORM texture to optimize VRAM and draw calls. ### [vertex_wind_sway.gdshader](scripts/vertex_wind_sway.gdshader) High-performance GPU-driven foliage animation. Uses vertex world coordinates and vertex color weight painting to simulate wind without skeletons. ### [triplanar_world_projection.gdshader](scripts/triplanar_world_projection.gdshader) UV-less environment mapping. Projects textures along X/Y/Z axes for organic blending over complex rocks and terrain. ### [subsurface_scattering_setup.gd](scripts/subsurface_scattering_setup.gd) Configuring realistic organic materials. Covers Skin Mode, Transmittance, and depth scattering settings for Forward+ rendering. ### [instance_uniform_batching.gdshader](scripts/instance_uniform_batching.gdshader) Architecture pattern for high-speed batching. Allows 10,000 meshes to share one material while maintaining unique colors or health states via instance uniforms. ### [decal_placer_expert.gd](scripts/decal_placer_expert.gd) Dynamic 3D decal system with cull masking and life-cycle management for impact effects. ### [transparency_sorting_fix.gd](scripts/transparency_sorting_fix.gd) Solving visual artifacts using Alpha Hash and Depth Prepass strategies. ### [shader_state_manager.gd](scripts/shader_state_manager.gd) Clean pattern for toggling shader-based visual states (Frozen, Burned) on multiple entities. ### [depth_precision_fix.gd](scripts/depth_precision_fix.gd) Camera-side fix for Z-fighting and texture flickering in large-scale worlds. ### [material_batcher.gd](scripts/material_batcher.gd) Global override system to ensure environmental meshes draw in optimized, state-locked batches. --- ## StandardMaterial3D Checklist (script-first) 1. Pack AO/Roughness/Metallic → **MANDATORY** [`pbr_orm_packer.gd`](scripts/pbr_orm_packer.gd); set `orm_texture` (never three separate maps). 2. Enable `normal_enabled` before assigning `normal_texture` (silent no-op otherwise). 3. Pick transparency from the matrix below — not docs-default ALPHA for cutouts. 4. Organic skin/leaves → [`organic_material.gd`](scripts/organic_material.gd) + [`subsurface_scattering_setup.gd`](scripts/subsurface_scattering_setup.gd) (Forward+ only for real SSS). 5. Runtime flash/dissolve → **MANDATORY** [`material_fx.gd`](scripts/material_fx.gd) `ensure_unique_override()` first. 6. Shared-mesh tint/variant → [`instance_uniform_batching.gdshader`](scripts/instance_uniform_batching.gdshader); never unique `.tres` per instance. 7. Distant LOD simplify / Pixel Dither fade → [`material_batcher.gd`](scripts/material_batcher.gd) `setup_lod_materials()`. 8. Check Forward+ vs Mobile feature matrix before enabling SSS/clearcoat/anisotropy. ### Forward+ vs Mobile (material features) | Feature | Forward+ | Mobile / Compatibility | WHY | |---------|----------|------------------------|-----| | Subsurface scattering / Skin Mode | Full | Limited / often unavailable | SSS needs Forward+ lighting path; fake with rim + transmittance bake on Mobile | | Clearcoat | Yes | Often stripped / approximate | Extra specular lobe cost; drop on distant LOD and Mobile | | Anisotropy | Yes | Prefer off | Flowmap + anisotropic BRDF burns mobile fragment budget | | Alpha Hash / Pixel Dither fade | Yes | Prefer Alpha Scissor or opaque dither | Hash noise + overdraw hurts tile GPUs | | Instance uniforms (batching) | Yes | Yes (prefer this) | Keeps one material; avoids unique-resource draw breaks | --- ## Transparency Modes ### Decision Matrix | Mode | Use Case | Performance | Sorting Issues | |------|----------|-------------|---------------| | ALPHA_SCISSOR | Foliage, chain-link fence | Fast | No | | ALPHA_HASH | Dithered fade, LOD transitions | Fast | Noisy | | ALPHA | Glass, water, godot-particles | Slow | Yes (render order) | ### Alpha Scissor (Cutout) ```gdscript # For leaves, grass, fences mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR mat.alpha_scissor_threshold = 0.5 # Pixels < 0.5 alpha = discarded mat.albedo_texture = load("res://leaf.png") # Must have alpha channel # Enable backface culling for performance mat.cull_mode = BaseMaterial3D.CULL_BACK ``` ### Alpha Hash (Dithered) ```gdscript # For smooth fade-outs without sorting issues mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_HASH mat.alpha_hash_scale = 1.0 # Dither pattern scale # Animate fade var tween := create_tween() tween.tween_property(mat, "albedo_color:a", 0.0, 1.0) ``` ### Alpha Blend (Full Transparency) ```gdscript # For glass, water (expensive) mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.blend_mode = BaseMaterial3D.BLEND_MODE_MIX # Disable depth writing for correct blending mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED mat.cull_mode = BaseMaterial3D.CULL_DISABLED # Show both sides ``` --- ## Texture Channel Packing **MANDATORY** [`pbr_orm_packer.gd`](scripts/pbr_orm_packer.gd) for R=AO / G=Roughness / B=Metallic. Do not inline packing recipes here. Custom channel remaps only when an imported atlas already disagrees with ORM layout. --- ## Shader Conversion ### When to Convert to ShaderMaterial - Need custom effects (dissolve, vertex displacement) - StandardMaterial3D limitations hit - Shader optimizations (remove unused features) ### Conversion Workflow ```gdscript # 1. Create StandardMaterial3D with all settings var std_mat := StandardMaterial3D.new() std_mat.albedo_color = Color.RED std_mat.metallic = 1.0 std_mat.roughness = 0.2 # 2. Convert to ShaderMaterial var shader_mat := ShaderMaterial.new() shader_mat.shader = load("res://custom_shader.gdshader") # 3. Transfer parameters manually shader_mat.set_shader_parameter("albedo", std_mat.albedo_color) shader_mat.set_shader_parameter("metallic", std_mat.metallic) shader_mat.set_shader_parameter("roughness", std_mat.roughness) ``` --- ## Material Variants (Godot 4.0+) ### Efficient Material Reuse ```gdscript # Base material (shared) var base_red_metal := StandardMaterial3D.new() base_red_metal.albedo_color = Color.RED base_red_metal.metallic = 1.0 # Variant 1: Rough var rough_variant := base_red_metal.duplicate() rough_variant.roughness = 0.8 # Variant 2: Smooth var smooth_variant := base_red_metal.duplicate() smooth_variant.roughness = 0.1 # Note: Use resource_local_to_scene for per-instance tweaks ``` --- ## Performance Optimization ### Material Batching ```gdscript # ✅ GOOD: Reuse materials across meshes const SHARED_STONE := preload("res://materials/stone.tres") func _ready() -> void: for wall in get_tree().get_nodes_in_group("stone_walls"): wall.material_override = SHARED_STONE # All walls batched in single draw call # ❌ BAD: Unique material per mesh func _ready() -> void: for wall in get_tree().get_nodes_in_group("stone_walls"): var mat := StandardMaterial3D.new() # New material! mat.albedo_color = Color(0.5, 0.5, 0.5) wall.material_override = mat # Each wall is separate draw call ``` ### Texture Atlasing ```gdscript # Combine multiple materials into one texture atlas # Then use UV offsets to select regions # material_atlas.gd extends StandardMaterial3D func set_atlas_region(tile_x: int, tile_y: int, tiles_per_row: int) -> void: var tile_size := 1.0 / tiles_per_row uv1_offset = Vector3(tile_x * tile_size, tile_y * tile_size, 0) uv1_scale = Vector3(tile_size, tile_size, 1) ``` --- ## Edge Cases ### Normal Maps Not Working ```gdscript # Problem: Forgot to enable mat.normal_enabled = true # REQUIRED # Problem: Wrong texture import settings # In Import tab: Texture → Normal Map = true ``` ### Texture Seams on Models ```gdscript # Problem: Mipmaps causing seams # Solution: Disable mipmaps for tightly-packed UVs # Import → Mipmaps → Generate = false ``` ### Material Looks Flat ```gdscript # Problem: Missing normal map or roughness variation # Solution: Add normal map + roughness texture mat.normal_enabled = true mat.normal_texture = load("res://normal.png") mat.roughness_texture = load("res://roughness.png") ``` --- ## Expert Techniques & Optimizations ### 1. LOD Transitions using Pixel Dither When utilizing Hierarchical Level of Detail (HLOD) or Visibility Ranges to fade objects out at a distance, standard alpha blending causes severe performance hits due to overlapping transparent bounds. Instead, configure the **Distance Fade** mode on your material to **Pixel Dither**. This provides a perceptually smooth fade while remaining entirely within the high-performance opaque pipeline. ### 2. Stencil Buffers (Godot 4.5+) Use the Stencil Buffer directly in `StandardMaterial3D`. This allows you to easily render outlines or X-ray effects for objects hidden behind walls without needing to write custom shaders for basic effects. ### 3. AR Shadow Overlay Shader If you are developing an AR game, you might want virtual shadows to appear on real-world camera feeds. Instead of standard blending, use Godot's built-in `shadow_to_opacity` render mode in a spatial shader. ```shader shader_type spatial; // shadow_to_opacity makes the material invisible when lit, // but opaque (dark) when it receives a shadow from another 3D object. render_mode blend_mix, depth_draw_opaque, cull_back, shadow_to_opacity; void fragment() { // The surface color is black; opacity will be driven by incoming shadows ALBEDO = vec3(0.0, 0.0, 0.0); } ``` --- ## Expert Pattern: Material-Texture-Array (Instanced Variation) **MANDATORY** [`instance_uniform_batching.gdshader`](scripts/instance_uniform_batching.gdshader) for per-instance color/health **and** texture-array index variants. Set `texture_index` via `GeometryInstance3D.set_instance_shader_parameter` — never unique materials per tree/crowd variant. --- ## Expert Pattern: Dissolve-Shader-Integration (Alpha Scissor) Use Alpha Scissor for performant dissolves (keeps shadows, avoids alpha sort). **MANDATORY** [`material_fx.gd`](scripts/material_fx.gd) `dissolve_scissor()` — it duplicates the override first. --- ## Expert Pattern: Material-LOD-System (HLOD) Mesh LOD is automatic; material shading is not. **MANDATORY** [`material_batcher.gd`](scripts/material_batcher.gd) `setup_lod_materials()` for visibility-range swaps, feature strip on distant materials, and Pixel Dither distance fade (not alpha blend). ## Expert insights (WHY — keep in body) - **ORM packing** — WHY: three separate maps triple sampler slots and break batching. One RGB = AO/Roughness/Metallic ([pbr_orm_packer.gd](scripts/pbr_orm_packer.gd)). - **Metallic 0.5** — WHY: PBR metals are 0 or 1; mid values are only for dirt/rust transitions, not default paint. - **Shared material mutation** — WHY: tweaking a shared `.tres` affects every instance. `duplicate(true)` or Local To Scene before runtime FX ([material_fx.gd](scripts/material_fx.gd)). - **Pixel Dither LOD fade** — WHY: alpha-blend distance fade keeps fragments in the transparent pipeline; dither stays opaque ([material_batcher.gd](scripts/material_batcher.gd)). ## Deep recipes (on demand) | Topic | Reference / script | |-------|-------------------| | Metal/dielectric presets / SSS / clearcoat | [pbr-workflows.md](references/pbr-workflows.md) | ## Reference > Progressive disclosure: open Official Documentation links only when researching a specific API; > load Related Skills when routing work to a peer domain — do not preload the whole lattice. ### Official Documentation - [Standard Material 3D and ORM Material 3D](https://docs.godotengine.org/en/stable/tutorials/3d/standard_material_3d.html) — Primary PBR tutorial for albedo/metallic/roughness, ORM packing, transparency modes, and feature flags on StandardMaterial3D. - [BaseMaterial3D](https://docs.godotengine.org/en/stable/classes/class_basematerial3d.html) — Shared API for transparency enums, texture channels, cull/depth draw, distance fade, and subsurface scattering controls. - [StandardMaterial3D](https://docs.godotengine.org/en/stable/classes/class_standardmaterial3d.html) — Concrete material class used throughout this skill’s builders, FX helpers, and presets. - [ORMMaterial3D](https://docs.godotengine.org/en/stable/classes/class_ormmaterial3d.html) — Dedicated ORM-packed material path when Occlusion/Roughness/Metallic already live in one RGB texture. - [Using decals](https://docs.godotengine.org/en/stable/tutorials/3d/using_decals.html) — Projector decals, cull masks, and performance limits for bullet holes and detail without unique materials. - [Importing images](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_images.html) — Correct normal-map and channel import settings so PBR maps are not treated as color data. - [Visibility ranges (HLOD)](https://docs.godotengine.org/en/stable/tutorials/3d/visibility_ranges.html) — Distance swap / fade modes that pair with simplified distant materials and Pixel Dither distance fade. - [Spatial shader](https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/spatial_shader.html) — Built-ins for triplanar, wind sway, `instance uniform`, and `shadow_to_opacity` when StandardMaterial3D is not enough. - [Your first 3D shader](https://docs.godotengine.org/en/stable/tutorials/shaders/your_first_shader/your_first_3d_shader.html) — Conversion path from StandardMaterial3D settings into a writable spatial ShaderMaterial. - [GeometryInstance3D](https://docs.godotengine.org/en/stable/classes/class_geometryinstance3d.html) — `set_instance_shader_parameter`, material overlays/overrides, and visibility-range properties for shared-material batching. - [GPU optimization](https://docs.godotengine.org/en/stable/tutorials/performance/gpu_optimization.html) — Overdraw, transparency cost, and batching guidance that motivates ORM packing and avoiding unique materials. ### Related Skills #### Prerequisites - [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — Nodes, Resources, and import basics required before authoring reusable `.tres` materials and texture sets. - [godot-resource-data-patterns](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-resource-data-patterns/SKILL.md) — Material/texture Resources, duplication vs sharing, and data-driven presets that keep draw-call batching intact. - [godot-shaders-basics](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-shaders-basics/SKILL.md) — Shading language and ShaderMaterial fundamentals before converting StandardMaterial3D or writing triplanar/instance-uniform shaders. #### Complements - [godot-3d-lighting](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-lighting/SKILL.md) — Emission energy, HDR, GI, and AreaLight3D interactions that determine how PBR and emissive materials actually read in-scene. - [godot-3d-world-building](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md) — Applying shared environment materials, decals, and triplanar projection across large level geometry. - [godot-camera-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md) — Camera3D near/far and framing choices that fix Z-fighting / depth precision issues materials alone cannot solve. - [godot-particles](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md) — Particle draw modes and alpha pipelines that must stay consistent with material transparency choices (scissor/hash vs blend). - [godot-performance-optimization](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md) — Draw-call batching, MultiMesh, and GPU budgets when scaling unique vs shared materials. - [godot-debugging-profiling](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md) — GPU/overdraw profilers to verify transparency and material-state regressions after material changes. #### Downstream / consumers - [godot-genre-open-world](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-open-world/SKILL.md) — Large worlds consume HLOD visibility ranges, dithered distance fade, and shared-material batching patterns from this skill. - [godot-procedural-generation](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-procedural-generation/SKILL.md) — Procedural meshes and terrains typically need triplanar / world-projection materials when UVs are absent or unstable. - [godot-adapt-2d-to-3d](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-adapt-2d-to-3d/SKILL.md) — Moving flat art into 3D requires PBR map setup, normal import, and transparency mode choices covered here. #### Master - [godot-master](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-master/SKILL.md) — Library router and mirrored module entry for cross-skill discovery.