Skip to content
← Back to Blog tutorials

Shader LODs in Godot 4: swap materials at distance for stable FPS

By BitSoul3D4 min read124 views

Shader complexity is one of the most overlooked frame-rate killers in 3D games — and Godot 4's material system gives you the tools to fix it without rebuilding your entire pipeline. Shader LODs (Level of Detail for materials) let you swap expensive PBR shaders for lightweight alternatives when a mesh moves beyond a set camera distance. The result: consistent frame budgets on mid-range GPUs and mobile hardware, without visible quality loss in the play area.

Shader LODs in Godot 4: swap materials at distance for stable FPS

Why shader complexity tanks GPU performance at distance

Every fragment your GPU shades costs cycles, regardless of how small it appears on screen. A full PBR StandardMaterial3D with normal maps, roughness, metallic, and AO textures runs dozens of texture samples per fragment. When that material covers 400 screen pixels instead of 40,000, you're burning the same per-sample cost for almost no visual return.

On mobile GPUs — Mali, Adreno, Apple Silicon — fill rate and memory bandwidth are tightly constrained. Even mid-range desktop cards running deferred rendering in Godot 4 will accumulate overdraw across hundreds of distant props, pushing fragment shaders past their per-frame budget.

Mesh LODs reduce triangle count at distance. Shader LODs reduce shading cost. You need both. A mesh at LOD2 still runs its full material pipeline unless you swap it too.

Why shader complexity tanks GPU performance at distance — illustrated

How to set up shader LODs in Godot 4

Godot 4 doesn't have a built-in "shader LOD" system separate from mesh LODs, but GeometryInstance3D exposes visibility_range_begin and visibility_range_end — and you can assign different MaterialOverride values per LOD mesh. The cleanest approach uses multiple MeshInstance3D nodes, each with its own material complexity, toggled by distance.

Create your LOD materials

Start with three material tiers:

For the far tier, switch to unshaded and bake a representative lit colour into the albedo. At 60+ metres the lighting contribution is imperceptible and the shading cost is real.

# Create the far-distance unshaded material in code
var far_mat = StandardMaterial3D.new()
far_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
far_mat.albedo_color = Color(0.35, 0.30, 0.28)  # baked approximate lit colour
far_mesh.material_override = far_mat

Structure your scene nodes

PropRoot (Node3D)
├── MeshNear   (MeshInstance3D)  — full PBR material
├── MeshMid    (MeshInstance3D)  — stripped material
└── MeshFar    (MeshInstance3D)  — unshaded material

Assign visibility_range_begin and visibility_range_end on each MeshInstance3D:

Nodevisibility_range_beginvisibility_range_end
MeshNear022
MeshMid2062
MeshFar600 (infinite)

The 2-metre overlap creates a crossfade zone. Set visibility_range_fade_mode = VISIBILITY_RANGE_FADE_SELF on MeshNear and MeshMid to trigger dithered fading instead of a hard pop.

Triggering shader swaps via GDScript

If you're instancing many props at runtime — from BitSoul's marketplace GLB packs, for example — scripting the LOD swap is more maintainable than configuring every node by hand.

extends Node3D

@export var near_distance: float = 20.0
@export var far_distance: float = 60.0
@export var mat_near: Material
@export var mat_mid: Material
@export var mat_far: Material

@onready var mesh: MeshInstance3D = $Mesh
var _camera: Camera3D

func _ready() -> void:
    _camera = get_viewport().get_camera_3d()

func _process(_delta: float) -> void:
    if not _camera:
        return
    var dist = global_position.distance_to(_camera.global_position)
    if dist < near_distance:
        mesh.material_override = mat_near
    elif dist < far_distance:
        mesh.material_override = mat_mid
    else:
        mesh.material_override = mat_far

For large scenes with hundreds of props, don't call distance_to every frame per prop. Batch updates using a timer or a distance-check group:

# In your scene manager — check all props every 0.2 seconds
var _lod_timer: float = 0.0

func _process(delta: float) -> void:
    _lod_timer += delta
    if _lod_timer >= 0.2:
        _lod_timer = 0.0
        _update_all_shader_lods()

This reduces distance_to overhead from 60×/s to 5×/s per prop with no perceptible visual difference.

Triggering shader swaps via GDScript — illustrated

Benchmarking and validating shader LOD results

Use Godot 4's built-in profiler (Debugger → Profiler) and the RenderingServer monitor to validate your work.

Key metrics to watch:

MetricWhere to find itWhat to target
GPU frame timeMonitor → Rendering/GPU Time≤ 8 ms for 120 FPS, ≤ 16 ms for 60 FPS
Draw callsMonitor → Rendering/Draw Calls< 500 for mobile
Material changesRenderDoc / GPU profilerMinimise per-frame rebinds

Enable RenderingServer.set_debug_generate_wireframe(true) temporarily to confirm LOD switching is occurring at the correct distances. You should see material complexity step down visibly in the wireframe overlay as you increase camera distance.

Common mistake: forgetting to set visibility_range_fade_mode on overlapping LODs. Without it you get a hard one-frame pop that's immediately visible during camera movement.

For assets sourced from BitSoul's marketplace, the GLB format preserves material names, so you can match mat_near/mat_mid/mat_far by material slot index without manual reassignment.

Shader LODs are a one-time setup cost that pays off every frame your scene is running. Combine them with mesh LODs, occlusion culling, and MultiMesh for distant repeated props and you'll have a solid foundation for scalable scene performance across all target hardware.


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.


Need drop-in assets for this workflow? Grab the Architecture Modern Bundle on the BitSoul marketplace and drop them straight into your project.

Tags: architecture

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)