godot-3d-lighting

SKILL.md

3D Lighting

Expert guidance for realistic 3D lighting with shadows and global illumination.

NEVER Do

  • NEVER use VoxelGI without setting a proper extents — Unbound VoxelGI tanks performance. Always set size to tightly fit your scene.
  • NEVER enable shadows on every light — Each shadow-casting light is expensive. Use shadows sparingly: 1-2 DirectionalLights, ~3-5 OmniLights max.
  • NEVER forget directional_shadow_mode — Default is ORTHOGONAL. For large outdoor scenes, use PARALLEL_4_SPLITS for better shadow quality at distance.
  • NEVER use LightmapGI for fully dynamic scenes — Lightmaps are baked. Moving geometry won't receive updated lighting. Use VoxelGI or SDFGI instead.
  • NEVER set omni_range too large — Light attenuation is quadratic. A range of 500 affects 785,000 sq units. Keep range as small as visually acceptable.

Available Scripts

MANDATORY: Read the appropriate script before implementing the corresponding pattern.

day_night_cycle.gd

Dynamic sun position and color based on time-of-day. Handles DirectionalLight3D rotation, color temperature, and intensity curves. Use for outdoor day/night systems.

light_probe_manager.gd

VoxelGI and SDFGI management for global illumination setup.

lighting_manager.gd

Dynamic light pooling and LOD. Manages light culling and shadow toggling based on camera distance. Use for performance optimization with many lights.

volumetric_fx.gd

Volumetric fog and god ray configuration. Runtime fog density/color adjustments and light shaft setup. Use for atmospheric effects.


DirectionalLight3D (Sun/Moon)

Shadow Cascades

# For outdoor scenes with camera moving from near to far
extends DirectionalLight3D

func _ready() -> void:
    shadow_enabled = true
    directional_shadow_mode = SHADOW_PARALLEL_4_SPLITS
    
    # Split distances (in meters from camera)
    directional_shadow_split_1 = 10.0   # First cascade: 0-10m
    directional_shadow_split_2 = 50.0   # Second: 10-50m
    directional_shadow_split_3 = 200.0  # Third: 50-200m
    # Fourth cascade: 200m - max shadow distance
    
    directional_shadow_max_distance = 500.0
    
    # Quality vs performance
    directional_shadow_blend_splits = true  # Smooth transitions

Day/Night Cycle

# sun_controller.gd
extends DirectionalLight3D

@export var time_of_day := 12.0  # 0-24 hours
@export var rotation_speed := 0.1  # Hours per second

func _process(delta: float) -> void:
    time_of_day += rotation_speed * delta
    if time_of_day >= 24.0:
        time_of_day -= 24.0
    
    # Rotate sun (0° = noon, 180° = midnight)
    var angle := (time_of_day - 12.0) * 15.0  # 15° per hour
    rotation_degrees.x = -angle
    
    # Adjust intensity
    if time_of_day < 6.0 or time_of_day > 18.0:
        light_energy = 0.0  # Night
    elif time_of_day < 7.0:
        light_energy = remap(time_of_day, 6.0, 7.0, 0.0, 1.0)  # Sunrise
    elif time_of_day > 17.0:
        light_energy = remap(time_of_day, 17.0, 18.0, 1.0, 0.0)  # Sunset
    else:
        light_energy = 1.0  # Day
    
    # Color shift
    if time_of_day < 8.0 or time_of_day > 16.0:
        light_color = Color(1.0, 0.7, 0.4)  # Orange (dawn/dusk)
    else:
        light_color = Color(1.0, 1.0, 0.9)  # Neutral white

OmniLight3D (Point Light)

Attenuation Tuning

# torch.gd
extends OmniLight3D

func _ready() -> void:
    omni_range = 10.0  # Maximum reach
    omni_attenuation = 2.0  # Falloff curve (1.0 = linear, 2.0 = quadratic/realistic)
    
    # For "magical" lights, reduce attenuation
    omni_attenuation = 0.5  # Flatter falloff, reaches farther

Flickering Effect

#  campfire.gd
extends OmniLight3D

@export var base_energy := 1.0
@export var flicker_strength := 0.3
@export var flicker_speed := 5.0

func _process(delta: float) -> void:
    var flicker := sin(Time.get_ticks_msec() * 0.001 * flicker_speed) * flicker_strength
    light_energy = base_energy + flicker

SpotLight3D (Flashlight/Headlights)

Setup

# flashlight.gd
extends SpotLight3D

func _ready() -> void:
    spot_range = 20.0
    spot_angle = 45.0  # Cone angle (degrees)
    spot_angle_attenuation = 2.0  # Edge softness
    
    shadow_enabled = true
    
    # Projector texture (optional - cookie/gobo)
    light_projector = load("res://textures/flashlight_mask.png")

Follow Camera

# player_flashlight.gd
extends SpotLight3D

@onready var camera: Camera3D = get_viewport().get_camera_3d()

func _process(delta: float) -> void:
    if camera:
        global_transform = camera.global_transform

Global Illumination: VoxelGI vs SDFGI

Decision Matrix

Feature VoxelGI SDFGI
Setup Manual bounds per room Automatic, scene-wide
Dynamic objects Fully supported Partially supported
Performance Moderate Higher cost
Use case Indoor, small-medium scenes Large outdoor scenes
Godot version 4.0+ 4.0+

VoxelGI Setup

# room_gi.gd - Place one VoxelGI per room/area
extends VoxelGI

func _ready() -> void:
    # Tightly fit the room
    size = Vector3(20, 10, 20)
    
    # Quality settings
    subdiv = VoxelGI.SUBDIV_128  # Higher = better quality, slower
    
    # Bake GI data
    bake()

SDFGI Setup

# world_environment.gd
extends WorldEnvironment

func _ready() -> void:
    var env := environment
    
    # Enable SDFGI
    env.sdfgi_enabled = true
    env.sdfgi_use_occlusion = true
    env.sdfgi_read_sky_light = true
    
    # Cascades (auto-scale based on camera)
    env.sdfgi_min_cell_size = 0.2  # Detail level
    env.sdfgi_max_distance = 200.0

LightmapGI (Baked Static Lighting)

When to Use

  • Static architecture (buildings, dungeons)
  • Mobile/low-end targets
  • No dynamic geometry

Setup

# Scene structure:
# - LightmapGI node
# - StaticBody3D meshes with GeometryInstance3D.gi_mode = STATIC

# lightmap_baker.gd
extends LightmapGI

func _ready() -> void:
    # Quality settings
    quality = LightmapGI.BAKE_QUALITY_HIGH
    bounces = 3  # Indirect light bounces
    
    # Bake (editor only, not runtime)
    # Click "Bake Lightmaps" button in editor

Environment & Sky

HDR Skybox

# world_env.gd
extends WorldEnvironment

func _ready() -> void:
    var env := environment
    
    env.background_mode = Environment.BG_SKY
    var sky := Sky.new()
    var sky_material := PanoramaSkyMaterial.new()
    sky_material.panorama = load("res://hdri/sky.hdr")
    sky.sky_material = sky_material
    env.sky = sky
    
    # Sky contribution to GI
    env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
    env.ambient_light_sky_contribution = 1.0

Volumetric Fog

extends WorldEnvironment

func _ready() -> void:
    var env := environment
    
    env.volumetric_fog_enabled = true
    env.volumetric_fog_density = 0.01
    env.volumetric_fog_albedo = Color(0.9, 0.9, 1.0)  # Blueish
    env.volumetric_fog_emission = Color.BLACK

ReflectionProbe

For localized reflections (mirrors, shiny floors):

# reflection_probe.gd
extends ReflectionProbe

func _ready() -> void:
    # Capture area
    size = Vector3(10, 5, 10)
    
    # Quality
    resolution = ReflectionProbe.RESOLUTION_512
    
    # Update mode
    update_mode = ReflectionProbe.UPDATE_ONCE  # Bake once
    # or UPDATE_ALWAYS for dynamic reflections (expensive)

Performance Optimization

Light Budgets

# Recommended limits:
# - DirectionalLight3D with shadows: 1-2
# - OmniLight3D with shadows: 3-5
# - SpotLight3D with shadows: 2-4
# - OmniLight3D without shadows: 20-30
# - SpotLight3D without shadows: 15-20

# Disable shadows on minor lights
@onready var candle_lights: Array = [$Candle1, $Candle2, $Candle3]

func _ready() -> void:
    for light in candle_lights:
        light.shadow_enabled = false  # Save performance

Per-Light Shadow Distance

# Disable shadows for distant lights
extends OmniLight3D

@export var shadow_max_distance := 50.0

func _process(delta: float) -> void:
    var camera := get_viewport().get_camera_3d()
    if camera:
        var dist := global_position.distance_to(camera.global_position)
        shadow_enabled = (dist < shadow_max_distance)

Edge Cases

Shadows Through Floors

# Problem: Thin floors let shadows through
# Solution: Increase shadow bias

extends DirectionalLight3D

func _ready() -> void:
    shadow_enabled = true
    shadow_bias = 0.1  # Increase if shadows bleed through
    shadow_normal_bias = 2.0

Light Leaking in Indoor Scenes

# Problem: VoxelGI light bleeds through walls
# Solution: Place VoxelGI nodes per-room, don't overlap

# Also: Ensure walls have proper thickness (not paper-thin)

Reference

Weekly Installs
54
GitHub Stars
35
First Seen
Feb 10, 2026
Installed on
gemini-cli54
codex53
opencode52
amp51
github-copilot51
kimi-cli51