
Godot 3d World Building
- 307 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-3d-world-building for development tasks
About
godot-3d-world-building: A skill for development. This provides functionality for development workflows.
- godot-3d-world-building
Godot 3d World Building by the numbers
- 307 all-time installs (skills.sh)
- +25 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,331 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-3d-world-buildingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 307 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-3d-world-building for development tasks
Files
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 GridMap
extends GridMap
func _ready() -> void:
mesh_library = load("res://tilesets/dungeon_library.tres")
cell_size = Vector3(2, 2, 2) # Must match library cell sizeCell Manipulation
# 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
# 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))---
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 collisionItem Metadata
# 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 (Constructive Solid Geometry)
Boolean Operations
CSG Combiner3D
├─ CSGBox3D (Operation: Union) # Base room
├─ CSGBox3D (Operation: Subtraction) # Door cutout
└─ CSGSphere3D (Operation: Intersection) # Rounded cornerCSG Brush Types
# 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.5CSG 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.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 = skyHDRI Skybox
# 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---
Fog & Atmosphere
Exponential Fog
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.0Depth Fog
# 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 curveVolumetric Fog
# 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 Streaming / LOD
GridMap Chunking
# 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
# 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))---
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 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---
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 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)---
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 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;
}---
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 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)Reference
- Master Skill: godot-master
# 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.
# 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).
# 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.
# 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.
# 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()
# 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.
# 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.
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.