Files
tactical-shooter/client/scripts/generate_occluders.gd
T
shawn e385eae0f5 feat: performance budget profiling setup
- docs/performance-budget.md: 60fps/2GB VRAM budget with CPU, GPU,
  memory, and draw-call targets (P95<16.6ms, P99<50ms)
- scripts/profiling/: SceneTree + Node profilers, test scenes
- scripts/generate_occluders.gd: auto-generate OccluderInstance3D
  from CSG wall pieces (128^3 voxel occlusion culling)
- scripts/convert_csg_to_mesh.gd: CSG->StaticMesh baker with LOD
  slots (LOD1@15m, LOD2@30m)
- project.godot: occlusion culling + LOD settings enabled
- modular scene UV2 data from lightmapping pass
- rcon_command_handler.gd conflict resolved (kept upstream plugin cmd)
- Fixed profiler_node.gd warmup frame bug (60 frames, not 3)
- Fixed convert_csg_to_mesh.gd LOD API (add_lod + distance)
2026-07-01 19:01:03 -04:00

139 lines
4.4 KiB
GDScript

@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)