Skill v1.0.1
currentAutomated scan100/1001 files
version: "1.0.1" name: godot-3d-world-building description: "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."
3D World Building
Expert guidance for level design with GridMaps, CSG, and environmental setup.
NEVER Do
- NEVER forget to bake GridMap navigation — GridMaps don't auto-generate navigation meshes. Use EditorPlugin or manual NavigationRegion3D.
- NEVER use CSG for final game geometry — CSG is for prototyping. Convert to static meshes for performance (use "Bake CSG Mesh" in editor).
- NEVER scale GridMap cell size after placing tiles — Changing
cell_sizedoesn't update existing tiles, causing misalignment. Set it once at the start. - NEVER use MeshLibrary without collision shapes — Items without collision spawn visual-only geometry that players fall through.
- NEVER enable volumetric fog without DirectionalLight3D — Volumetric fog requires at least one light to scatter. No lights = no visible fog.
- NEVER animate CSG nodes during gameplay — Moving a CSG node within another forces the CPU to recalculate the boolean geometry, causing significant performance drops.
- NEVER place generic logic nodes in a GridMap — GridMap is highly optimized only for meshes, navigation, and collision. It is not a general-purpose system for placing arbitrary node structures on a grid.
- NEVER use non-manifold meshes in CSG — If you import a custom mesh for CSGMesh3D, it must be manifold (closed, no self-intersections, no interior faces, no negative volume). Non-manifold meshes will break the CSG algorithm and are completely unsupported.
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
collision_gen.gd
Automatic collision shape generation from meshes. Use when importing models without collision or for procedural geometry.
gridmap_runtime_builder.gd
Runtime GridMap tile placement with batch operations and auto-navigation baking.
csg_bake_tool.gd
EditorScript to bake CSG geometry to static meshes with proper materials and collision. Use when finalizing level prototypes.
safe_csg_baking.gd
Expert technique for safe CSG baking. Awaits the end of the frame before extracting baked meshes to avoid empty data.
lod_manager.gd
Level-of-detail switching based on camera distance. Manages mesh swapping and visibility for large outdoor scenes.
occlusion_setup.gd
OccluderInstance3D configuration for manual occlusion culling. Use for indoor levels with many rooms.
GridMap Fundamentals
Setup Workflow
# 1. Create MeshLibrary resource (editor)# Scene → New Inherits Scene → Create Grid-aligned meshes# Scene → Convert To → MeshLibrary...# 2. Assign to GridMapextends GridMapfunc _ready() -> void:mesh_library = load("res://tilesets/dungeon_library.tres")cell_size = Vector3(2, 2, 2) # Must match library cell size
Cell Manipulation
# gridmap_builder.gdextends GridMap# Place cellfunc place_tile(grid_pos: Vector3i, tile_index: int) -> void:set_cell_item(grid_pos, tile_index)# Get cellfunc get_tile(grid_pos: Vector3i) -> int:return get_cell_item(grid_pos) # Returns index or INVALID_CELL_ITEM (-1)# Remove cellfunc 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)
Coordinate Conversion
# World position ↔ Grid coordinatesfunc _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) * 1000var space := get_world_3d().direct_space_statevar query := PhysicsRayQueryParameters3D.create(from, to)var result := space.intersect_ray(query)if result:var world_pos: Vector3 = result.positionvar grid_pos := local_to_map(to_local(world_pos))place_tile(grid_pos, 0) # Place tile at clicked position# Grid → Worldfunc get_cell_center(grid_pos: Vector3i) -> Vector3:return to_global(map_to_local(grid_pos))
MeshLibrary Creation
Collision Setup
# 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
Item Metadata
# Access MeshLibrary item datafunc 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 (Constructive Solid Geometry)
Boolean Operations
CSG Combiner3D├─ CSGBox3D (Operation: Union) # Base room├─ CSGBox3D (Operation: Subtraction) # Door cutout└─ CSGSphere3D (Operation: Intersection) # Rounded corner
CSG Brush Types
# CSGBox3D - Room primitivesvar room := CSGBox3D.new()room.size = Vector3(10, 5, 10)# CSGCylinder3D - Pillarsvar pillar := CSGCylinder3D.new()pillar.radius = 0.5pillar.height = 5.0# CSGSphere3D - Domesvar dome := CSGSphere3D.new()dome.radius = 3.0dome.radial_segments = 16dome.rings = 8# CSGPolygon3D - Extruded 2D shapesvar arch := CSGPolygon3D.new()arch.polygon = PackedVector2Array([Vector2(-1, 0), Vector2(-1, 2), Vector2(1, 2), Vector2(1, 0)])arch.depth = 0.5
CSG Performance
# ❌ 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
WorldEnvironment Setup
Sky Configuration
# world_env.gdextends WorldEnvironmentfunc _ready() -> void:var env := Environment.new()environment = env# Procedural skyenv.background_mode = Environment.BG_SKYvar sky := Sky.new()var sky_mat := ProceduralSkyMaterial.new()sky_mat.sky_top_color = Color(0.4, 0.6, 1.0) # Bluesky_mat.sky_horizon_color = Color(0.8, 0.9, 1.0) # Lightersky_mat.ground_bottom_color = Color(0.2, 0.2, 0.1)sky_mat.sun_angle_max = 30.0sky.sky_material = sky_matenv.sky = sky
HDRI Skybox
# For realistic lightingvar env := environmentenv.background_mode = Environment.BG_SKYvar sky := Sky.new()var panorama := PanoramaSkyMaterial.new()panorama.panorama = load("res://hdri/sunset.hdr") # Equirectangular HDR imagesky.sky_material = panoramaenv.sky = sky# Sky contribution to ambient lightenv.ambient_light_source = Environment.AMBIENT_SOURCE_SKYenv.ambient_light_sky_contribution = 1.0
Fog & Atmosphere
Exponential Fog
extends WorldEnvironmentfunc _ready() -> void:var env := environmentenv.fog_enabled = trueenv.fog_mode = Environment.FOG_MODE_EXPONENTIALenv.fog_density = 0.01 # 0.0-1.0env.fog_light_color = Color(0.9, 0.95, 1.0) # Blueishenv.fog_light_energy = 1.0
Depth Fog
# Distance-based fogenv.fog_enabled = trueenv.fog_mode = Environment.FOG_MODE_DEPTHenv.fog_depth_begin = 50.0 # Start distanceenv.fog_depth_end = 200.0 # End distance (fully opaque)env.fog_depth_curve = 1.0 # Falloff curve
Volumetric Fog
# Requires DirectionalLight3D for scatteringenv.volumetric_fog_enabled = trueenv.volumetric_fog_density = 0.05env.volumetric_fog_albedo = Color(0.9, 0.9, 1.0)env.volumetric_fog_emission = Color.BLACKenv.volumetric_fog_gi_inject = 1.0 # How much GI affects fog# Performance settingsenv.volumetric_fog_temporal_reprojection_enabled = trueenv.volumetric_fog_detail_spread = 2.0
Level Streaming / LOD
GridMap Chunking
# level_streamer.gd - Load/unload GridMap chunks based on player positionextends Node3D@export var chunk_size := 32 # Grid cells per chunk@export var load_radius := 2 # Chunks to keep loadedvar loaded_chunks := {} # Vector2i → GridMapfunc _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 chunksfor 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 chunksfor 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)
Procedural Generation
Random Dungeon with GridMap
# dungeon_generator.gdextends GridMapenum Tile { FLOOR, WALL, DOOR }func generate_room(pos: Vector3i, size: Vector3i) -> void:# Fill with floorfor x in range(size.x):for z in range(size.z):set_cell_item(pos + Vector3i(x, 0, z), Tile.FLOOR)# Add wallsfor x in range(size.x):set_cell_item(pos + Vector3i(x, 0, 0), Tile.WALL) # Northset_cell_item(pos + Vector3i(x, 0, size.z - 1), Tile.WALL) # Southfor z in range(size.z):set_cell_item(pos + Vector3i(0, 0, z), Tile.WALL) # Westset_cell_item(pos + Vector3i(size.x - 1, 0, z), Tile.WALL) # Eastfunc _ready() -> void:generate_room(Vector3i(0, 0, 0), Vector3i(10, 1, 10))
Edge Cases
GridMap Cells Not Colliding
# 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)
CSG Mesh Flickering
# Problem: Z-fighting between overlapping CSG operations# Solution: Add small offset (0.001) to prevent exact overlapvar box := CSGBox3D.new()box.size = Vector3(10, 5, 10)var cutout := CSGBox3D.new()cutout.operation = CSGShape3D.OPERATION_SUBTRACTIONcutout.size = Vector3(2, 3, 2.002) # Slightly larger depth
Expert Techniques & Optimizations
1. Spatially Partitioning MultiMeshes
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.
Expert Pattern: GridMap-Custom-Data (Logic Proxies)
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 MeshLibraryconst SPAWN_PROXY_ID: int = 5func _ready() -> void:_replace_proxies_with_logic()func _replace_proxies_with_logic() -> void:# 1. Find all cells using the proxy tilevar 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 posvar world_pos: Vector3 = level_grid.to_global(level_grid.map_to_local(cell))# 3. Instantiate actual gameplay logicvar instance: Node3D = spawn_point_scene.instantiate()add_child(instance)instance.global_position = world_pos# 4. Clear the proxy tile to save performancelevel_grid.set_cell_item(cell, GridMap.INVALID_CELL_ITEM)
Expert Pattern: Interior-Mapping (Fake Windows)
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 layersvoid fragment() {// Project view vector into fake room depthvec3 view_dir = normalize(VIEW);// Intersection math to determine which wall/floor/ceiling pixel to sample// Note: Use 'VIEW' and 'INV_VIEW_MATRIX' for perspective calculationsvec3 room_uv = view_dir; // Simplified placeholderALBEDO = texture(room_textures, room_uv).rgb;}
Expert Pattern: World-Streaming-Queue (Stutter-Free Loading)
To prevent frame-spikes when moving between level chunks, use ResourceLoader background threads.
class_name WorldStreamer extends Nodevar load_queue: Array[String] = []func request_chunk(path: String) -> void:# Begin background thread requestvar 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 scenevar chunk: PackedScene = ResourceLoader.load_threaded_get(path)add_child(chunk.instantiate())load_queue.remove_at(i)
Reference
- Master Skill: godot-master