e385eae0f5
- 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)
139 lines
4.4 KiB
GDScript
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)
|