@tool extends EditorScript # Auto-generate OccluderInstance3D nodes from CSG wall pieces. # # Scans the current scene for CSGBox3D nodes tagged as structural # (walls, floors, pillars) and creates matching OccluderInstance3D # boxes so Godot's occlusion culling system can use them. # # Usage: # 1. Open target scene in Godot editor # 2. Run: Scene > Run Script (select this file) # 3. Or run from command line: # godot --script scripts/generate_occluders.gd --scene res://path/to/scene.tscn # # Generated occluders are parented under an "Occluders" node. const OCCLUDER_PARENT_NAME := "Occluders" # Minimum CSG size (longest axis) to generate an occluder. # Very small pieces don't contribute meaningful occlusion. const MIN_OCCLUDER_SIZE := 0.5 func _run(): var scene_root: Node if Engine.is_editor_hint(): scene_root = get_scene() as Node if not scene_root: printerr("No scene is currently open in the editor.") return else: var args = OS.get_cmdline_args() var scene_path := "" for i in range(args.size()): if args[i] == "--scene" and i + 1 < args.size(): scene_path = args[i + 1] if scene_path.is_empty(): printerr("Usage: godot --script generate_occluders.gd --scene res://path/to/scene.tscn") return var packed = ResourceLoader.load(scene_path) if not packed: printerr("Failed to load scene: ", scene_path) return scene_root = packed.instantiate() print("=== Occluder Generator ===") print("Scene: ", scene_root.name if scene_root else "unknown") # Find or create the Occluders parent node var occluder_parent: Node = _find_or_create_parent(scene_root) var generated_count := 0 var skipped_count := 0 # Walk all CSGBox3D nodes in the scene _generate_from_node(scene_root, occluder_parent, generated_count, skipped_count) print("Generated: ", generated_count, " occluders") print("Skipped (too small): ", skipped_count) print("Parent: ", occluder_parent.get_path()) print("") print("IMPORTANT: Occlusion culling requires the following project settings:") print(" rendering/occlusion_culling/occlusion_rays_per_octant = 6") print(" rendering/occlusion_culling/occlusion_culler_size = 128") print("") print("=== Occluder generation complete ===") func _generate_from_node(node: Node, parent: Node, generated: int, skipped: int): # Check if this is a wall/pillar CSGBox3D (exclude subtraction/hole pieces) if node is CSGBox3D: var csg := node as CSGBox3D # Skip subtraction pieces (operation = 1 = Subtraction) # Skip pieces that are too small to occlude if csg.operation != CSGShape3D.OPERATION_SUBTRACTION: var size := csg.size var longest = max(size.x, max(size.y, size.z)) if longest >= MIN_OCCLUDER_SIZE: _create_occluder(csg, parent) generated += 1 else: skipped += 1 else: skipped += 1 # Recurse into children for child in node.get_children(): _generate_from_node(child, parent, generated, skipped) func _find_or_create_parent(root: Node) -> Node: for child in root.get_children(): if child.name == OCCLUDER_PARENT_NAME: print("Found existing Occluders node.") return child var parent := Node3D.new() parent.name = OCCLUDER_PARENT_NAME parent.set_script(null) # No script needed # Add as first child so it's visually at the top in the scene tree root.add_child(parent, true) if Engine.is_editor_hint(): parent.set_owner(root) print("Created Occluders node.") return parent func _create_occluder(csg: CSGBox3D, parent: Node) -> void: var occluder := OccluderInstance3D.new() occluder.name = "Occluder_" + csg.name # Create a box-shaped occluder matching the CSG piece var shape := OccluderShape3D.new() shape.set_as_vertices_and_indices( PackedVector3Array([]), # auto-generated from AABB PackedInt32Array([]) ) # Use a box occluder matching the CSG box dimensions var box_shape := OccluderShapeBox3D.new() box_shape.size = csg.size occluder.set_occluder_shape(box_shape) # Match the CSG node's global transform var global_transform := csg.global_transform occluder.global_transform = global_transform # Bake to static mode (not dynamic) for best performance occluder.bake_mask = 1 # Layer 1 only occluder.bake_id = 0 parent.add_child(occluder, true) if Engine.is_editor_hint(): occluder.set_owner(parent.owner) # Apply voxel resolution # Godot 4's occlusion is computed from a signed-distance field # at the configured culler size (128^3 by budget spec)