references/gridmap-and-csg.md
# Gridmap And Csg
## GridMap Fundamentals
### Setup Workflow
```gdscript
# 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
```
### Cell Manipulation
```gdscript
# 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)
```
### Coordinate Conversion
```gdscript
# 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))
```
---
## 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
```gdscript
# 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
```
### CSG Performance
```gdscript
# ❌ 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
```
---
## 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.
```gdscript
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)
```
---
references/migration-notes.md
# Migration notes: godot-3d-world-building
Incremental upgrade for topics this skill covers. Apply **one hop**, stabilize/test, then next. Never skip hops.
If the project is **< 4.0**, follow [godot-version-migration](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-version-migration/SKILL.md) era bridges (legacy → 3→4) until 4.0, then these hops. Official 3→4: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html).
## 4.0 → 4.1
Official: [Upgrading to Godot 4.1](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.1.html)
- `MeshInstance3D.create_multiple_convex_collisions` optional `settings` — update procedural collision-from-mesh recipes.
- `Node3D.look_at` / `look_at_from_position` gain optional `use_model_front`.
## 4.1 → 4.2
Official: [Upgrading to Godot 4.2](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.2.html)
- **Mesh format** upgrade — use Project → Tools → Upgrade Mesh Surfaces; Restart & Upgrade prevents downgrade.
- ImporterMesh/MeshDataTool/SurfaceTool compression flag widths → `uint64`.
## 4.2 → 4.3
Official: [Upgrading to Godot 4.3](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.3.html)
- `Skeleton3D.add_bone` returns `int32`; `bone_pose_changed` → `skeleton_updated` — fix rigged prop attachment in levels.
- Binary serialization of scripted Objects/typed Arrays changed — re-test save/load of custom level/chunk Resources.
- `PackedByteArray` may use compact base64 storage; older editors may not open 4.3 resources with large byte arrays.
## 4.3 → 4.4
Official: [Upgrading to Godot 4.4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.4.html)
- CSG uses Manifold — **non-manifold** CSG booleans unsupported; use MeshInstance3D for quads, planes, and open meshes.
- `@export_file` stores `uid://` paths from the Inspector — level tools expecting `res://` must resolve UIDs.
- `FileAccess.store_*` methods return `bool` success — handle failures in procedural save/export helpers.
## 4.4 → 4.5
Official: [Upgrading to Godot 4.5](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.5.html)
- GLTF/BLEND/FBX naming version for non-joint nodes in skeletons — set Import dock Naming Version for kitbash assets.
- `Resource.duplicate(true)` deep-duplicates **only internal** resources; use `duplicate_deep(DEEP_DUPLICATE_ALL)` when cloning level Resource graphs.
## 4.5 → 4.6
Official: [Upgrading to Godot 4.6](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.6.html)
- `MeshInstance3D.skeleton` default empty NodePath — explicit skeleton paths required for skinned kit pieces.
- TSCN gains unique node IDs (large VCS diffs on first 4.6 save — expected for level scenes).
## 4.6 → 4.7
Official: [Upgrading to Godot 4.7](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.7.html)
- **Path3D** snap-to-colliders for placing spline points on geometry.
- **3D vertex snapping** with vertex/origin base setting (editor B key workflow).
- Prefer **AreaLight3D** for built-in rectangular area lights in blockout scenes.
- `EditorSceneFormatImporter` import constants live under `ImportFlags` enum — update custom import tooling.
references/streaming-and-procgen.md
# Streaming And Procgen
## Level Streaming / LOD
### GridMap Chunking
```gdscript
# 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)
```
---
## Procedural Generation
### Random Dungeon with GridMap
```gdscript
# 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))
```
---
## Expert Pattern: World-Streaming-Queue (Stutter-Free Loading)
To prevent frame-spikes when moving between level chunks, use `ResourceLoader` background threads.
```gdscript
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)
```
scripts/collision_gen.gd
# skills/3d-world-building/code/collision_gen.gd
extends Node
## Collision Hull Generation Protocol
# --- 1. Selection Hierarchy (Performance Order) ---
# 1. Primitive Shapes (Box, Sphere, Capsule) - LIGHTEST
# 2. Convex Hull (Simplified wrapper) - MEDIUM
# 3. Concave Mesh (Tri-mesh) - HEAVIEST (Static World Only)
func configure_collision_for_mesh(node: MeshInstance3D, type: String = "box") -> void:
match type:
"box":
node.create_multiple_convex_collisions() # Simplified convex
"convex":
node.create_convex_collision() # Pixel-perfect convex
"static_world":
node.create_trimesh_collision() # ONLY for non-moving world geometry
## EXPERT NOTE:
## NEVER use Trimesh (Concave) for moving objects. Physics will break or lag.
## For characters, always use a simple CapsuleShape3D.
## For level geometry, use 'create_multiple_convex_collisions' on import
## to balance performance and accurate physics.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/physics/collision_shapes_3d.html
# - https://docs.godotengine.org/en/stable/classes/class_meshinstance3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — StaticBody3D / shape selection for world meshes
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-materials/SKILL.md — materials on imported meshes before collision gen
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md
# =============================================================================
scripts/collision_generator.gd
# skills/3d-world-building/scripts/collision_generator.gd
extends Node
## Collision Generator (Expert Pattern)
## Helper to generate collision shapes for meshes that lack them.
## Useful for imported assets or procedural geometry.
class_name CollisionGenerator
static func create_trimesh_collision(mesh_instance: MeshInstance3D) -> StaticBody3D:
if not mesh_instance or not mesh_instance.mesh:
return null
# Check if already has child static body
for child in mesh_instance.get_children():
if child is StaticBody3D:
return child as StaticBody3D
mesh_instance.create_trimesh_collision()
return mesh_instance.get_child(mesh_instance.get_child_count() - 1) as StaticBody3D
static func create_convex_collision(mesh_instance: MeshInstance3D) -> StaticBody3D:
if not mesh_instance or not mesh_instance.mesh:
return null
mesh_instance.create_convex_collision()
return mesh_instance.get_child(mesh_instance.get_child_count() - 1) as StaticBody3D
static func create_multiple_convex_collision(mesh_instance: MeshInstance3D) -> StaticBody3D:
if not mesh_instance or not mesh_instance.mesh:
return null
mesh_instance.create_multiple_convex_collisions()
return mesh_instance.get_child(mesh_instance.get_child_count() - 1) as StaticBody3D
## EXPERT USAGE:
## Call in _ready() or EditorScript for procedurally loaded meshes.
## Use Trimesh for static scenery, Convex for dynamic objects (if needed).
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/physics/collision_shapes_3d.html
# - https://docs.godotengine.org/en/stable/classes/class_concavepolygonshape3d.html
# - https://docs.godotengine.org/en/stable/classes/class_convexpolygonshape3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — trimesh vs convex rules for static levels
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — collision cost of concave world hulls
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md
# =============================================================================
scripts/csg_bake_tool.gd
# skills/3d-world-building/code/csg_bake_tool.gd
@tool
extends Node3D
## CSG to Mesh Baking Workflow
## Tool script to automate the greybox-to-production transition.
@export var bake_now: bool = false:
set(value):
if value: bake_csg_hierarchy()
func bake_csg_hierarchy() -> void:
var csg_root = get_node_or_null("CSGRoot")
if not csg_root or not csg_root is CSGShape3D:
push_error("Please provide a CSGShape3D root named 'CSGRoot'")
return
# 1. Force update to ensure current geometry is valid
csg_root._update_shape()
# 2. Extract the baked mesh
var meshes = csg_root.get_meshes()
# Godot returns [Transform, Mesh] pairs
if meshes.size() < 2: return
var final_mesh: Mesh = meshes[1]
# 3. Create a static MeshInstance3D
var result := MeshInstance3D.new()
result.name = "BakedMesh_" + csg_root.name
result.mesh = final_mesh
# 4. Add to scene and cleanup
get_parent().add_child(result)
result.owner = get_tree().edited_scene_root
result.global_transform = csg_root.global_transform
print("CSG Baked successfully. You can now hide/delete the CSG nodes.")
## WHY BAKE?
## CSG nodes are expensive to calculate at runtime.
## Baking to MeshInstance3D allows for Occlusion Culling, Baked Lightmaps, and LODs.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/csg_tools.html
# - https://docs.godotengine.org/en/stable/classes/class_csgshape3d.html
# - https://docs.godotengine.org/en/stable/classes/class_meshinstance3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-materials/SKILL.md — materials preserved or reassigned on bake
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — StaticBody collision after CSG → mesh conversion
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md
# =============================================================================
scripts/grid_map_logic_manager.gd
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)
scripts/grid_map_manager.gd
# skills/3d-world-building/scripts/grid_map_manager.gd
extends GridMap
## Grid Map Manager (Expert Pattern)
## Handles runtime tile placement, coordinate conversion, and batch operations.
## Ensures navigation updates if using NavigationRegion3D.
class_name GridMapManager
signal tile_placed(grid_pos: Vector3i, item_id: int)
signal tile_removed(grid_pos: Vector3i)
@export var navigation_region: NavigationRegion3D
# Helper to map world position to grid center (for snapping)
func snap_to_grid(world_pos: Vector3) -> Vector3:
var grid_pos = local_to_map(to_local(world_pos))
return to_global(map_to_local(grid_pos))
func place_tile_at_world(world_pos: Vector3, item_id: int, orientation: int = 0) -> void:
var grid_pos = local_to_map(to_local(world_pos))
set_cell_item(grid_pos, item_id, orientation)
tile_placed.emit(grid_pos, item_id)
_update_navigation_deferred()
func remove_tile_at_world(world_pos: Vector3) -> void:
var grid_pos = local_to_map(to_local(world_pos))
if get_cell_item(grid_pos) != INVALID_CELL_ITEM:
set_cell_item(grid_pos, INVALID_CELL_ITEM)
tile_removed.emit(grid_pos)
_update_navigation_deferred()
func get_tile_id_at_world(world_pos: Vector3) -> int:
var grid_pos = local_to_map(to_local(world_pos))
return get_cell_item(grid_pos)
# Batch operations
func fill_area(start: Vector3i, end: Vector3i, item_id: int) -> void:
for x in range(min(start.x, end.x), max(start.x, end.x) + 1):
for y in range(min(start.y, end.y), max(start.y, end.y) + 1):
for z in range(min(start.z, end.z), max(start.z, end.z) + 1):
set_cell_item(Vector3i(x,y,z), item_id)
_update_navigation_deferred()
func _update_navigation_deferred() -> void:
if navigation_region:
# Debounce or deferred bake recommended for performance
# navigation_region.bake_navigation_mesh() (Blocking!)
# Use Thread or signal for complex maps.
pass
## EXPERT USAGE:
## Attach to GridMap node. Use snap_to_grid() for building preview.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_gridmaps.html
# - https://docs.godotengine.org/en/stable/classes/class_meshlibrary.html
# - https://docs.godotengine.org/en/stable/classes/class_navigationregion3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-navigation-pathfinding/SKILL.md — deferred nav updates after place/remove
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-scene-management/SKILL.md — owning GridMap scenes in streamed levels
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md
# =============================================================================
scripts/gridmap_runtime_builder.gd
# skills/3d-world-building/scripts/gridmap_runtime_builder.gd
extends GridMap
## GridMap Runtime Builder Expert Pattern
## Runtime tile placement with rotation validation and batch operations.
class_name GridMapRuntimeBuilder
signal cell_placed(grid_pos: Vector3i, tile_index: int)
signal cell_removed(grid_pos: Vector3i)
@export var auto_navigation_bake := false
var _batch_queue := []
var _is_batching := false
func place_cell(grid_pos: Vector3i, tile_index: int, orientation := 0) -> bool:
if tile_index < 0 or tile_index >= mesh_library.get_last_unused_item_id():
push_warning("Invalid tile index: %d" % tile_index)
return false
set_cell_item(grid_pos, tile_index, orientation)
if not _is_batching:
cell_placed.emit(grid_pos, tile_index)
if auto_navigation_bake:
_request_navigation_bake()
return true
func remove_cell(grid_pos: Vector3i) -> void:
set_cell_item(grid_pos, INVALID_CELL_ITEM)
if not _is_batching:
cell_removed.emit(grid_pos)
func begin_batch() -> void:
_is_batching = true
_batch_queue.clear()
func end_batch() -> void:
_is_batching = false
# Emit all queued signals
for data in _batch_queue:
if data.has("tile_index"):
cell_placed.emit(data.grid_pos, data.tile_index)
else:
cell_removed.emit(data.grid_pos)
_batch_queue.clear()
if auto_navigation_bake:
_request_navigation_bake()
func fill_box(from: Vector3i, to: Vector3i, tile_index: int) -> void:
begin_batch()
var min_pos := Vector3i(
mini(from.x, to.x),
mini(from.y, to.y),
mini(from.z, to.z)
)
var max_pos := Vector3i(
maxi(from.x, to.x),
maxi(from.y, to.y),
maxi(from.z, to.z)
)
for x in range(min_pos.x, max_pos.x + 1):
for y in range(min_pos.y, max_pos.y + 1):
for z in range(min_pos.z, max_pos.z + 1):
place_cell(Vector3i(x, y, z), tile_index)
end_batch()
func _request_navigation_bake() -> void:
# Requires NavigationRegion3D parent
var nav_region := get_parent() as NavigationRegion3D
if nav_region:
nav_region.bake_navigation_mesh()
## EXPERT USAGE:
## var builder := GridMapRuntimeBuilder.new()
## builder.mesh_library = load("res://library.tres")
## builder.auto_navigation_bake = true
##
## # Batch placement for performance
## builder.begin_batch()
## for i in 100:
## builder.place_cell(Vector3i(i, 0, 0), 0)
## builder.end_batch()
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_gridmaps.html
# - https://docs.godotengine.org/en/stable/classes/class_gridmap.html
# - https://docs.godotengine.org/en/stable/tutorials/navigation/navigation_introduction_3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-navigation-pathfinding/SKILL.md — rebake NavigationRegion3D after cell batches
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-procedural-generation/SKILL.md — generators driving set_cell_item at runtime
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md
# =============================================================================
scripts/lod_manager.gd
# skills/3d-world-building/code/lod_manager.gd
extends Node3D
## LOD (Level of Detail) Management Pattern
## Demonstrates setting distance-based visibility thresholds.
@export var lod_far_distance := 100.0
@export var lod_medium_distance := 50.0
func setup_mesh_lod(mesh_instance: MeshInstance3D) -> void:
# Godot 4.3+ has automatic mesh LOD generation on import.
# This script handles manual visibility/node swapping if needed.
mesh_instance.visibility_range_end = lod_far_distance
mesh_instance.visibility_range_end_margin = 10.0 # Fade margin
# Enable hysteresis to prevent flickering at the threshold
mesh_instance.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
## EXPERT NOTE:
## Always prefer the Importer's automatic LOD generation for static meshes.
## Use manual Visibility Ranges (this script) only for complex hierarchical objects
## or when swapping between a high-poly Mesh and a Billboards/Impostor.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/mesh_lod.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/visibility_ranges.html
# - https://docs.godotengine.org/en/stable/classes/class_geometryinstance3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md — distance thresholds relative to active camera
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — when visibility ranges beat full mesh LOD
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-open-world/SKILL.md — HLOD / impostors at outdoor scale
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md
# =============================================================================
scripts/occlusion_setup.gd
# skills/3d-world-building/code/occlusion_setup.gd
extends Node3D
## Occlusion Culling Expert Pattern
## Blueprints for configuring OccluderInstance3D for draw-call reduction.
func setup_room_occlusion(room_node: Node3D) -> void:
# 1. Create the OccluderInstance3D
var occluder := OccluderInstance3D.new()
occluder.name = "RoomOccluder"
# 2. Assign or generate an OccluderPolygon3D
# For simple rooms, a 'QuadOccluder3D' is most efficient.
var poly := QuadOccluder3D.new()
poly.size = Vector2(10, 5) # Match wall size
occluder.occluder = poly
room_node.add_child(occluder)
## EXPERT NOTE:
## Don't over-use complex occluders. Occlusion culling itself has a CPU cost.
## Best practice: Only occlusion-cull Large, Opaque objects (Walls, Ground, Big Rocks)
## that are guaranteed to hide many smaller objects behind them.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/occlusion_culling.html
# - https://docs.godotengine.org/en/stable/classes/class_occluderinstance3d.html
# - https://docs.godotengine.org/en/stable/classes/class_quadoccluder3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — occlusion CPU cost vs draw-call wins
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-lighting/SKILL.md — opaque occluders vs thin emissive/transparent walls
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md
# =============================================================================
scripts/safe_csg_baking.gd
extends CSGShape3D
# Safe CSG Baking
# Because CSG mesh data updates are deferred to the end of the frame,
# you must wait before extracting the baked meshes to avoid empty data.
func extract_optimized_mesh() -> void:
# Wait for the engine to finish the deferred CSG boolean calculations
await get_tree().process_frame
var optimized_mesh: ArrayMesh = bake_static_mesh()
var collision_shape: ConcavePolygonShape3D = bake_collision_shape()
# You can now assign these to a standard MeshInstance3D and StaticBody3D
# and safely queue_free() the heavy CSG node hierarchy.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/csg_tools.html
# - https://docs.godotengine.org/en/stable/classes/class_csgshape3d.html
# - https://docs.godotengine.org/en/stable/classes/class_scenetree.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — await process_frame before reading deferred CSG meshes
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — bake_collision_shape handoff to StaticBody3D
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md
# =============================================================================
scripts/world_streamer.gd
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)
SKILL.md
---
name: godot-3d-world-building
description: "Expert patterns for 3D level design using GridMap with MeshLibrary, CSG constructive solid geometry, occlusion, and runtime GridMap builders. Use when building 3D levels, modular tilesets, or BSP-style geometry. For sky/fog/Environment recipes, route to godot-3d-lighting. Trigger keywords: GridMap, MeshLibrary, set_cell_item, get_cell_item, map_to_local, local_to_map, CSGCombiner3D, CSGBox3D, CSGSphere3D, CSGPolygon3D, OccluderInstance3D, bake CSG."
---
# 3D World Building
Expert guidance for level design with GridMaps, CSG bake, and occlusion — not lighting/atmosphere authorship.
## 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_size` doesn't update existing tiles, causing misalignment. Set it once at the start.
- **NEVER ship a MeshLibrary item without verifying collision** — Call `mesh_library.get_item_shapes(tile_index)` (or inspect the source scene StaticBody3D + CollisionShape3D) before convert; empty shapes spawn visual-only geometry players fall through.
- **NEVER bake CSG before the combiner has a settled frame** — Extract meshes only after `await get_tree().process_frame` (see [safe_csg_baking.gd](scripts/safe_csg_baking.gd)); baking mid-recompute yields empty or stale ArrayMesh data. Order: finish boolean edits → wait one frame → bake → delete CSG → add collision.
- **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. Use proxy tiles + scripts for spawns/triggers.
- **NEVER use non-manifold meshes in CSG** — Custom CSGMesh3D assets must be manifold (closed, no self-intersections). Non-manifold meshes break the CSG algorithm.
---
## Available Scripts
> **MANDATORY**: Read the appropriate script before implementing the corresponding pattern.
> **Do NOT Load** lighting/sky/fog scripts or deep Environment tutorials here — route to [godot-3d-lighting](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-lighting/SKILL.md).
### [collision_gen.gd](scripts/collision_gen.gd)
Automatic collision shape generation from meshes. Use when importing models without collision or for procedural geometry.
### [gridmap_runtime_builder.gd](scripts/gridmap_runtime_builder.gd)
**Sole streaming / runtime GridMap entry** — batch tile placement, chunk-style rebuilds, and auto-navigation baking. Prefer this over ad-hoc WorldStreamer stubs.
### [csg_bake_tool.gd](scripts/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](scripts/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](scripts/lod_manager.gd)
Level-of-detail switching based on camera distance. Manages mesh swapping and visibility for large outdoor scenes.
### [occlusion_setup.gd](scripts/occlusion_setup.gd)
OccluderInstance3D configuration for manual occlusion culling. Use for indoor levels with many rooms.
### [grid_map_logic_manager.gd](scripts/grid_map_logic_manager.gd)
Proxy-tile pattern: replace invisible MeshLibrary markers with spawn/trigger scenes at `_ready`, then clear proxy cells.
### [world_streamer.gd](scripts/world_streamer.gd)
`ResourceLoader.load_threaded_request` queue — stutter-free chunk instantiation after background load completes.
---
## Golden Path (GridMap / CSG / Occlusion)
1. **MeshLibrary** — Source scene: MeshInstance3D + StaticBody3D/CollisionShape3D → Convert To MeshLibrary → verify `get_item_shapes()`.
2. **GridMap** — Set `cell_size` once, place cells, bake NavigationRegion3D. Runtime rebuilds: **MANDATORY** [gridmap_runtime_builder.gd](scripts/gridmap_runtime_builder.gd).
3. **CSG greybox** — Prototype with CSGCombiner3D → **MANDATORY** [safe_csg_baking.gd](scripts/safe_csg_baking.gd) / [csg_bake_tool.gd](scripts/csg_bake_tool.gd) → delete live CSG.
4. **Occlusion / LOD** — Indoor rooms: [occlusion_setup.gd](scripts/occlusion_setup.gd). Distance swaps: [lod_manager.gd](scripts/lod_manager.gd).
5. **Sky / fog / WorldEnvironment** — Out of scope; use peer **godot-3d-lighting** (keep only a DirectionalLight3D present if volumetric fog is enabled elsewhere).
---
## GridMap Fundamentals
### Setup (compact)
```gdscript
extends GridMap
func _ready() -> void:
mesh_library = load("res://tilesets/dungeon_library.tres")
cell_size = Vector3(2, 2, 2) # Set once; never after tiles exist
```
Cell API: `set_cell_item(pos, index[, orientation])`, `get_cell_item`, `INVALID_CELL_ITEM`, `local_to_map` / `map_to_local`. For batch/runtime placement and nav bake, load [gridmap_runtime_builder.gd](scripts/gridmap_runtime_builder.gd) — do not paste a custom chunk streamer.
### Collision verification
```gdscript
var shapes := mesh_library.get_item_shapes(tile_index)
if shapes.is_empty():
push_error("Tile %d has no collision — fix MeshLibrary source scene" % tile_index)
```
---
## CSG Bake Order
1. Finish boolean edits under `CSGCombiner3D`.
2. `await get_tree().process_frame` (WHY: CSG dirty flags settle one frame late).
3. Bake to MeshInstance3D + collision via scripts above; remove CSG from exported scenes.
4. Never animate CSG at runtime.
Brush types (Box/Cylinder/Sphere/Polygon) are editor greybox tools only — not shipping geometry.
---
## Streaming Decision
| Need | Action |
|------|--------|
| Runtime GridMap tiles / chunk rebuild + nav bake | **MANDATORY** [gridmap_runtime_builder.gd](scripts/gridmap_runtime_builder.gd) |
| Large open-world scene streaming | Peer [godot-genre-open-world](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-open-world/SKILL.md) |
| Ad-hoc WorldStreamer inline stub | **Cut** — do not reintroduce incomplete load-from-file TODOs |
---
## Expert Techniques
### Spatially Partitioning MultiMeshes
Partition dense props into regional `MultiMeshInstance3D` nodes so frustum/occlusion can cull whole clusters (single MultiMesh AABB draws everything).
### GridMap Logic Proxies
Use invisible proxy tile IDs for spawns/triggers; at `_ready`, `get_used_cells_by_item`, instantiate logic scenes, clear proxy cells. Keep logic off the GridMap itself.
### Interior-Mapping
For city-scale fake interiors, use a spatial shader on window planes — peer [godot-shaders-basics](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-shaders-basics/SKILL.md). Do not paste full shader recipes here.
### Edge Cases
- **No collision**: empty `get_item_shapes` → fix MeshLibrary source.
- **CSG z-fight**: tiny offset on subtraction brushes before bake.
## Deep recipes (on demand)
| Topic | Reference / script |
|-------|-------------------|
| GridMap / CSG bake walkthrough | [gridmap-and-csg.md](references/gridmap-and-csg.md) |
| Chunk streaming / procgen rooms | [streaming-and-procgen.md](references/streaming-and-procgen.md) |
| Proxy spawn tiles | [grid_map_logic_manager.gd](scripts/grid_map_logic_manager.gd) |
| Threaded chunk load | [world_streamer.gd](scripts/world_streamer.gd) |
## Reference
> Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
### Official Documentation
- [Using GridMaps](https://docs.godotengine.org/en/stable/tutorials/3d/using_gridmaps.html) — MeshLibrary workflow, cell placement, and when GridMap is the right modular level tool.
- [MeshLibrary](https://docs.godotengine.org/en/stable/classes/class_meshlibrary.html) — item meshes, names, and collision shapes that GridMap instances at runtime.
- [CSG tools](https://docs.godotengine.org/en/stable/tutorials/3d/csg_tools.html) — boolean prototyping with CSGCombiner3D/primitives and the bake-to-mesh handoff.
- [Environment and post-processing](https://docs.godotengine.org/en/stable/tutorials/3d/environment_and_post_processing.html) — WorldEnvironment, Sky, ProceduralSkyMaterial/PanoramaSkyMaterial, and fog modes.
- [Volumetric fog and fog volumes](https://docs.godotengine.org/en/stable/tutorials/3d/volumetric_fog.html) — scattering setup, density/albedo, and why lights are required for visible volumetric fog.
- [Occlusion culling](https://docs.godotengine.org/en/stable/tutorials/3d/occlusion_culling.html) — OccluderInstance3D placement and CPU cost tradeoffs for indoor rooms.
- [Mesh level of detail (LOD)](https://docs.godotengine.org/en/stable/tutorials/3d/mesh_lod.html) — importer auto-LOD versus manual mesh swaps for large outdoor levels.
- [Visibility ranges](https://docs.godotengine.org/en/stable/tutorials/3d/visibility_ranges.html) — GeometryInstance3D distance fade/hysteresis used by LOD managers.
- [Collision shapes (3D)](https://docs.godotengine.org/en/stable/tutorials/physics/collision_shapes_3d.html) — convex/trimesh/primitive choices for MeshLibrary items and baked CSG.
- [Navigation introduction (3D)](https://docs.godotengine.org/en/stable/tutorials/navigation/navigation_introduction_3d.html) — NavigationRegion3D baking GridMaps never auto-generate.
- [Background loading](https://docs.godotengine.org/en/stable/tutorials/io/background_loading.html) — ResourceLoader threaded chunk streaming without hitch spikes.
- [Using MultiMesh](https://docs.godotengine.org/en/stable/tutorials/performance/using_multimesh.html) — instancing dense props and why spatial MultiMesh partitions restore culling.
### Related Skills
#### Prerequisites
- [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — scene tree, resources, and import basics before MeshLibrary conversion and WorldEnvironment setup.
- [godot-physics-3d](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md) — StaticBody3D/CollisionShape3D patterns that must land in MeshLibrary source scenes or players fall through tiles.
- [godot-gdscript-mastery](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md) — typed GridMap/CSG scripting, signals, and await/process_frame patterns used in bake and runtime builders.
#### Complements
- [godot-3d-lighting](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-lighting/SKILL.md) — DirectionalLight3D and GI that volumetric fog scatters; pair env with real light setup.
- [godot-3d-materials](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-materials/SKILL.md) — StandardMaterial3D/ORM on tiles and baked CSG meshes after greybox.
- [godot-navigation-pathfinding](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-navigation-pathfinding/SKILL.md) — bake and update NavigationMesh from GridMap geometry after cell edits.
- [godot-shaders-basics](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-shaders-basics/SKILL.md) — interior-mapping and other spatial tricks for fake building interiors at city scale.
- [godot-camera-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md) — camera distance drives visibility ranges, LOD swaps, and chunk load radii.
- [godot-scene-management](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-scene-management/SKILL.md) — scene packing and threaded load queues for stutter-free world streaming.
- [godot-performance-optimization](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md) — draw-call budgets, occlusion strategy, and MultiMesh partitioning for large levels.
#### Downstream / consumers
- [godot-procedural-generation](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-procedural-generation/SKILL.md) — dungeon/terrain generators that write cells into GridMap as the placement backend.
- [godot-genre-open-world](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-open-world/SKILL.md) — chunk streaming, floating origin, and HLOD built on these world-building primitives.
- [godot-genre-sandbox](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-sandbox/SKILL.md) — player-driven building and editable voxel/grid worlds that reuse GridMap/CSG bake flows.
#### 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.