@tool extends EditorScript # Apply distance-based visibility ranges to all modular CSG pieces. # # This is the equivalent of LOD for CSG-based geometry — it hides # entire pieces when they're beyond a useful visual distance. # # Ranges (defined per asset type): # Walls: hide past 50 m # Floors: hide past 60 m # Pillars: hide past 30 m # Beams: hide past 30 m # Accent panels: hide past 20 m # Door/Window: hide past 40 m # # Usage: # Select a scene in the Godot editor, then: # Scene > Run Script (select this file) # # Or from CLI on a specific scene: # godot --script scripts/apply_visibility_ranges.gd \ # --scene res://assets/scenes/modular/kit_demo.tscn const DEBUG := false # Enable for verbose logging # Default ranges per type (begin = 0.0 means always visible up to end) const RANGES := { "wall": {"begin": 0.0, "end": 50.0, "margin": 5.0}, # fade out over 5m "floor": {"begin": 0.0, "end": 60.0, "margin": 10.0}, "pillar": {"begin": 0.0, "end": 30.0, "margin": 3.0}, "beam": {"begin": 0.0, "end": 30.0, "margin": 3.0}, "accent": {"begin": 0.0, "end": 20.0, "margin": 2.0}, "doorway": {"begin": 0.0, "end": 40.0, "margin": 5.0}, "window": {"begin": 0.0, "end": 40.0, "margin": 5.0}, "endcap": {"begin": 0.0, "end": 50.0, "margin": 5.0}, "default": {"begin": 0.0, "end": 40.0, "margin": 5.0}, } func _run(): var scene_root: Node if Engine.is_editor_hint(): scene_root = get_scene() as Node if not scene_root: printerr("No open scene in editor. Open a scene first.") 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 apply_visibility_ranges.gd --scene res://path/to/scene.tscn" + "\nOr run from the Godot editor with a scene open.") return var packed = ResourceLoader.load(scene_path) if not packed: printerr("Failed to load scene: ", scene_path) return scene_root = packed.instantiate() print("=== Visibility Range Applicator ===") print("Scene: ", scene_root.name) var counts := {} # type → number of nodes modified var total := 0 _apply_to_node(scene_root, counts, total) print("") print("Modified nodes: ", total) for type_name in counts: print(" %s: %d" % [type_name, counts[type_name]]) print("") print("=== Ranges applied ===") func _apply_to_node(node: Node, counts: Dictionary, total: int) -> void: # Only process CSG nodes (the modular pieces) if node is CSGShape3D: var range_config := _get_range_for_node(node) if range_config != null: node.visibility_range_begin = range_config.begin node.visibility_range_end = range_config.end node.visibility_range_begin_margin = range_config.margin node.visibility_range_end_margin = range_config.margin var type_name := _classify_node(node) counts[type_name] = counts.get(type_name, 0) + 1 total += 1 if DEBUG: print(" %s → %.0f–%.0f m (type: %s)" % [node.name, range_config.begin, range_config.end, type_name]) # Also apply to CSGCombiner3D parents (hold doorway/window subtractions) if node is CSGCombiner3D: var range_config := _get_range_for_combiner(node) if range_config != null: node.visibility_range_begin = range_config.begin node.visibility_range_end = range_config.end node.visibility_range_begin_margin = range_config.margin node.visibility_range_end_margin = range_config.margin var type_name := _classify_node(node) counts[type_name] = counts.get(type_name, 0) + 1 total += 1 if DEBUG: print(" %s → %.0f–%.0f m (type: %s)" % [node.name, range_config.begin, range_config.end, type_name]) # Recurse for child in node.get_children(): _apply_to_node(child, counts, total) func _classify_node(node: Node) -> String: var name_lower := node.name.to_lower() if name_lower.contains("wall"): if name_lower.contains("door"): return "doorway" if name_lower.contains("window"): return "window" if name_lower.contains("corner"): return "wall" if name_lower.contains("endcap"): return "endcap" return "wall" if name_lower.contains("floor"): return "floor" if name_lower.contains("pillar"): return "pillar" if name_lower.contains("beam"): return "beam" if name_lower.contains("accent") or name_lower.contains("blue") or name_lower.contains("red"): return "accent" if name_lower.contains("panel"): return "accent" return "default" func _get_range_for_node(node: CSGShape3D) -> Dictionary: # Detect CSGBox3D size to infer type if node is CSGBox3D: var box := node as CSGBox3D var size := box.size # Floor slabs: thin, wide (Z > X on rotated ones, check min dimension) if size.y <= 0.1 and (size.x >= 2.0 or size.z >= 2.0): return RANGES.floor.duplicate() return _get_range_by_name(node) func _get_range_for_combiner(node: CSGCombiner3D) -> Dictionary: return _get_range_by_name(node) func _get_range_by_name(node: Node) -> Dictionary: var type_name := _classify_node(node) return RANGES.get(type_name, RANGES.default).duplicate()