skills/godot-3d-world-building/SKILL.md
Expert patterns for 3D level design using GridMap with MeshLibrary, CSG constructive solid geometry, WorldEnvironment setup, ProceduralSkyMaterial, and volumetric fog. Use when building 3D levels, modular tilesets, BSP-style geometry, or environmental effects. Trigger keywords: GridMap, MeshLibrary, set_cell_item, get_cell_item, map_to_local, local_to_map, CSGCombiner3D, CSGBox3D, CSGSphere3D, CSGPolygon3D, WorldEnvironment, Environment, Sky, ProceduralSkyMaterial, PanoramaSkyMaterial, fog_enabled, volumetric_fog_enabled.
npx skillsauth add thedivergentai/gd-agentic-skills godot-3d-world-buildingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert guidance for level design with GridMaps, CSG, and environmental setup.
cell_size doesn't update existing tiles, causing misalignment. Set it once at the start.MANDATORY: Read the appropriate script before implementing the corresponding pattern.
Automatic collision shape generation from meshes. Use when importing models without collision or for procedural geometry.
Runtime GridMap tile placement with batch operations and auto-navigation baking.
EditorScript to bake CSG geometry to static meshes with proper materials and collision. Use when finalizing level prototypes.
Expert technique for safe CSG baking. Awaits the end of the frame before extracting baked meshes to avoid empty data.
Level-of-detail switching based on camera distance. Manages mesh swapping and visibility for large outdoor scenes.
OccluderInstance3D configuration for manual occlusion culling. Use for indoor levels with many rooms.
# 1. Create MeshLibrary resource (editor)
# Scene → New Inherits Scene → Create Grid-aligned meshes
# Scene → Convert To → MeshLibrary...
# 2. Assign to GridMap
extends GridMap
func _ready() -> void:
mesh_library = load("res://tilesets/dungeon_library.tres")
cell_size = Vector3(2, 2, 2) # Must match library cell size
# gridmap_builder.gd
extends GridMap
# Place cell
func place_tile(grid_pos: Vector3i, tile_index: int) -> void:
set_cell_item(grid_pos, tile_index)
# Get cell
func get_tile(grid_pos: Vector3i) -> int:
return get_cell_item(grid_pos) # Returns index or INVALID_CELL_ITEM (-1)
# Remove cell
func remove_tile(grid_pos: Vector3i) -> void:
set_cell_item(grid_pos, INVALID_CELL_ITEM)
# Rotate cell (0-23, see GridMap.ROTATION_* constants)
func place_rotated(grid_pos: Vector3i, tile_index: int, orientation: int) -> void:
set_cell_item(grid_pos, tile_index, orientation)
# World position ↔ Grid coordinates
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
var camera := get_viewport().get_camera_3d()
var from := camera.project_ray_origin(event.position)
var to := from + camera.project_ray_normal(event.position) * 1000
var space := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
var result := space.intersect_ray(query)
if result:
var world_pos: Vector3 = result.position
var grid_pos := local_to_map(to_local(world_pos))
place_tile(grid_pos, 0) # Place tile at clicked position
# Grid → World
func get_cell_center(grid_pos: Vector3i) -> Vector3:
return to_global(map_to_local(grid_pos))
# tile_scene.tscn (before converting to MeshLibrary)
# Root: Node3D
# ├─ MeshInstance3D (visual)
# └─ StaticBody3D (collision)
# └─ CollisionShape3D
# CRITICAL: StaticBody3D must be sibling/child for GridMap to detect collision
# Access MeshLibrary item data
func get_tile_name(tile_index: int) -> String:
return mesh_library.get_item_name(tile_index)
# Custom metadata (stored in MeshLibrary resource)
# Use item_set_name() in editor script to organize
CSG Combiner3D
├─ CSGBox3D (Operation: Union) # Base room
├─ CSGBox3D (Operation: Subtraction) # Door cutout
└─ CSGSphere3D (Operation: Intersection) # Rounded corner
# CSGBox3D - Room primitives
var room := CSGBox3D.new()
room.size = Vector3(10, 5, 10)
# CSGCylinder3D - Pillars
var pillar := CSGCylinder3D.new()
pillar.radius = 0.5
pillar.height = 5.0
# CSGSphere3D - Domes
var dome := CSGSphere3D.new()
dome.radius = 3.0
dome.radial_segments = 16
dome.rings = 8
# CSGPolygon3D - Extruded 2D shapes
var arch := CSGPolygon3D.new()
arch.polygon = PackedVector2Array([
Vector2(-1, 0), Vector2(-1, 2), Vector2(1, 2), Vector2(1, 0)
])
arch.depth = 0.5
# ❌ BAD: Use CSG at runtime (slow)
func _ready() -> void:
var csg := CSGBox3D.new()
add_child(csg) # Recalculates mesh every frame
# ✅ GOOD: Bake to MeshInstance3D (editor only)
# Select CSG node → Mesh → Bake Mesh Instance
# Then delete CSG node
# ✅ ALSO GOOD: Use CSG for level editor, bake on export
# world_env.gd
extends WorldEnvironment
func _ready() -> void:
var env := Environment.new()
environment = env
# Procedural sky
env.background_mode = Environment.BG_SKY
var sky := Sky.new()
var sky_mat := ProceduralSkyMaterial.new()
sky_mat.sky_top_color = Color(0.4, 0.6, 1.0) # Blue
sky_mat.sky_horizon_color = Color(0.8, 0.9, 1.0) # Lighter
sky_mat.ground_bottom_color = Color(0.2, 0.2, 0.1)
sky_mat.sun_angle_max = 30.0
sky.sky_material = sky_mat
env.sky = sky
# For realistic lighting
var env := environment
env.background_mode = Environment.BG_SKY
var sky := Sky.new()
var panorama := PanoramaSkyMaterial.new()
panorama.panorama = load("res://hdri/sunset.hdr") # Equirectangular HDR image
sky.sky_material = panorama
env.sky = sky
# Sky contribution to ambient light
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
env.ambient_light_sky_contribution = 1.0
extends WorldEnvironment
func _ready() -> void:
var env := environment
env.fog_enabled = true
env.fog_mode = Environment.FOG_MODE_EXPONENTIAL
env.fog_density = 0.01 # 0.0-1.0
env.fog_light_color = Color(0.9, 0.95, 1.0) # Blueish
env.fog_light_energy = 1.0
# Distance-based fog
env.fog_enabled = true
env.fog_mode = Environment.FOG_MODE_DEPTH
env.fog_depth_begin = 50.0 # Start distance
env.fog_depth_end = 200.0 # End distance (fully opaque)
env.fog_depth_curve = 1.0 # Falloff curve
# Requires DirectionalLight3D for scattering
env.volumetric_fog_enabled = true
env.volumetric_fog_density = 0.05
env.volumetric_fog_albedo = Color(0.9, 0.9, 1.0)
env.volumetric_fog_emission = Color.BLACK
env.volumetric_fog_gi_inject = 1.0 # How much GI affects fog
# Performance settings
env.volumetric_fog_temporal_reprojection_enabled = true
env.volumetric_fog_detail_spread = 2.0
# level_streamer.gd - Load/unload GridMap chunks based on player position
extends Node3D
@export var chunk_size := 32 # Grid cells per chunk
@export var load_radius := 2 # Chunks to keep loaded
var loaded_chunks := {} # Vector2i → GridMap
func _process(delta: float) -> void:
var player_pos := get_player_position()
var player_chunk := Vector2i(
int(player_pos.x / (chunk_size * cell_size.x)),
int(player_pos.z / (chunk_size * cell_size.z))
)
# Load nearby chunks
for x in range(-load_radius, load_radius + 1):
for z in range(-load_radius, load_radius + 1):
var chunk_coord := player_chunk + Vector2i(x, z)
if chunk_coord not in loaded_chunks:
load_chunk(chunk_coord)
# Unload distant chunks
for chunk_coord in loaded_chunks.keys():
var dist := chunk_coord.distance_to(player_chunk)
if dist > load_radius:
unload_chunk(chunk_coord)
func load_chunk(coord: Vector2i) -> void:
var gridmap := GridMap.new()
gridmap.mesh_library = preload("res://library.tres")
add_child(gridmap)
loaded_chunks[coord] = gridmap
# TODO: Load chunk data from file/database
# gridmap.set_cell_item(...)
func unload_chunk(coord: Vector2i) -> void:
var gridmap: GridMap = loaded_chunks[coord]
gridmap.queue_free()
loaded_chunks.erase(coord)
# dungeon_generator.gd
extends GridMap
enum Tile { FLOOR, WALL, DOOR }
func generate_room(pos: Vector3i, size: Vector3i) -> void:
# Fill with floor
for x in range(size.x):
for z in range(size.z):
set_cell_item(pos + Vector3i(x, 0, z), Tile.FLOOR)
# Add walls
for x in range(size.x):
set_cell_item(pos + Vector3i(x, 0, 0), Tile.WALL) # North
set_cell_item(pos + Vector3i(x, 0, size.z - 1), Tile.WALL) # South
for z in range(size.z):
set_cell_item(pos + Vector3i(0, 0, z), Tile.WALL) # West
set_cell_item(pos + Vector3i(size.x - 1, 0, z), Tile.WALL) # East
func _ready() -> void:
generate_room(Vector3i(0, 0, 0), Vector3i(10, 1, 10))
# Problem: MeshLibrary items lack collision
# Solution: Ensure StaticBody3D + CollisionShape3D in source scene
# Verify in code:
var item_shapes := mesh_library.get_item_shapes(tile_index)
if item_shapes.is_empty():
push_error("Tile %d has no collision!" % tile_index)
# Problem: Z-fighting between overlapping CSG operations
# Solution: Add small offset (0.001) to prevent exact overlap
var box := CSGBox3D.new()
box.size = Vector3(10, 5, 10)
var cutout := CSGBox3D.new()
cutout.operation = CSGShape3D.OPERATION_SUBTRACTION
cutout.size = Vector3(2, 3, 2.002) # Slightly larger depth
The major drawback of MultiMesh is that individual instances cannot be frustum or occlusion culled; the entire cluster is drawn based on the bounding box of the MultiMeshInstance3D. To solve this, partition your thousands of objects into several regional MultiMeshInstance3D nodes so the engine can cull entire regions at once.
Since GridMap is optimized for visuals/collision rather than logic, use "Proxy Tiles" to mark locations for spawn points, NPCs, or triggers during level design.
class_name GridMapLogicManager extends Node3D
@export var level_grid: GridMap
@export var spawn_point_scene: PackedScene
# The ID of the invisible cube in your MeshLibrary
const SPAWN_PROXY_ID: int = 5
func _ready() -> void:
_replace_proxies_with_logic()
func _replace_proxies_with_logic() -> void:
# 1. Find all cells using the proxy tile
var proxy_cells: Array[Vector3i] = level_grid.get_used_cells_by_item(SPAWN_PROXY_ID)
for cell in proxy_cells:
# 2. Convert grid pos to world pos
var world_pos: Vector3 = level_grid.to_global(level_grid.map_to_local(cell))
# 3. Instantiate actual gameplay logic
var instance: Node3D = spawn_point_scene.instantiate()
add_child(instance)
instance.global_position = world_pos
# 4. Clear the proxy tile to save performance
level_grid.set_cell_item(cell, GridMap.INVALID_CELL_ITEM)
For massive cities, avoid rendering actual interiors. Use a Spatial shader to project the illusion of 3D depth onto a single 2D window plane.
shader_type spatial;
uniform sampler2DArray room_textures; // Cubemap-like layers
void fragment() {
// Project view vector into fake room depth
vec3 view_dir = normalize(VIEW);
// Intersection math to determine which wall/floor/ceiling pixel to sample
// Note: Use 'VIEW' and 'INV_VIEW_MATRIX' for perspective calculations
vec3 room_uv = view_dir; // Simplified placeholder
ALBEDO = texture(room_textures, room_uv).rgb;
}
To prevent frame-spikes when moving between level chunks, use ResourceLoader background threads.
class_name WorldStreamer extends Node
var load_queue: Array[String] = []
func request_chunk(path: String) -> void:
# Begin background thread request
var err = ResourceLoader.load_threaded_request(path)
if err == OK:
load_queue.append(path)
func _process(_delta: float) -> void:
for i in range(load_queue.size() - 1, -1, -1):
var path = load_queue[i]
var status = ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
# Resource ready! Instantiate and add to scene
var chunk: PackedScene = ResourceLoader.load_threaded_get(path)
add_child(chunk.instantiate())
load_queue.remove_at(i)
development
Godot Expert Auditor: Aurelius. Exhaustive never-list enforcement and architectural slap-down for Godot 4.6 projects.
development
--- name:# Godot Expert Analyst: Anara ## Visionary Architect of Godot 4.6+ Excellence > "Scale is not a feature; it is a philosophy. I don't look at what your game is today; I look at whether it can survive tomorrow." — Anara You are **Anara**, the visionary architect of Godot 4.6+ excellence. You evaluate projects not for "if they work", but for "how well they scale". Your purpose is to certify professional-grade projects and provide the blueprint for architectural transcendence. Your voice
development
Expert blueprint for AI pathfinding (tower defense, RTS, stealth) using NavigationAgent2D/3D, NavigationServer, avoidance, and dynamic navigation mesh generation. Use when implementing enemy AI, NPC movement, or obstacle avoidance. Keywords NavigationAgent2D, NavigationRegion2D, pathfinding, NavigationServer, avoidance, baking, NavigationObstacle.
development
Expert patterns for Godot audio including AudioStreamPlayer variants (2D positional, 3D spatial), AudioBus mixing architecture, dynamic effects (reverb, EQ,compression), audio pooling for performance, music transitions (crossfade, bpm-sync), and procedural audio generation. Use for music systems, sound effects, spatial audio, or audio-reactive gameplay. Trigger keywords: AudioStreamPlayer, AudioStreamPlayer2D, AudioStreamPlayer3D, AudioBus, AudioServer, AudioEffect, music_crossfade, audio_pool, positional_audio, reverb, bus_volume.