Files
tactical-shooter/client/assets/scenes/modular/generate_scenes.py
T
shawn d631ff784a build: baked lighting pass on kit_demo test map
- Restructured kit_demo.tscn from linear demo to enclosed 5.12x5.12 room
  with walls, floor tiles, pillar, beam, doorway, window
- Added WorldEnvironment with ProceduralSky for ambient lighting
- Added DirectionalLight3D (sun key light, 45° angle, shadows enabled)
- Added OmniLight3D (warm interior fill light)
- Added ReflectionProbe for interior specular reflections
  (box projection, room-sized extents)
- Added LightmapGI with balanced quality settings
  (bounces=3, texel_scale=1.0, max_texture_size=2048, denoiser=true)
- Added tool script (kit_demo.gd) that prints bake status in editor
- Added bake_lighting.gd CLI bake script (requires GPU-enabled instance)
- Updated project.godot with reflection atlas and shadow map quality settings

Headless baking note: Godot's standard editor build requires a GPU/display
for LightmapGI.bake(). Open the scene in the Godot editor and click 'Bake
Lightmap' on the LightmapGI node to generate the baked lightmap data.
2026-07-01 00:10:19 -04:00

331 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Generate Godot 4 .tscn modular kit scenes using CSG nodes.
All dimensions in Godot meters, grid base = 2.56m (256cm).
"""
import os
SCENES_DIR = os.path.expanduser("/home/oplabs/tactical-shooter/client/assets/scenes/modular")
MAT_BASE = "res://assets/materials"
# Grid constants
U = 2.56 # 1 modular unit (256cm)
W = 0.16 # wall thickness (16cm)
F = 0.08 # floor thickness (8cm)
# Material name shortcuts
MAT_WALL = "wall_concrete_01"
MAT_WALL2 = "wall_concrete_02"
MAT_FLOOR = "floor_concrete_01"
MAT_FLOOR_TILE = "floor_tile_01"
MAT_METAL = "metal_structural_01"
MAT_BLUE = "accent_team_blue"
MAT_RED = "accent_team_red"
def tsnode(name, type_name, parent=".", props=None):
"""Build a [node] entry for .tscn format."""
lines = [f'[node name="{name}" type="{type_name}" parent="{parent}"]']
if props:
for k, v in props.items():
lines.append(f"{k} = {v}")
return "\n".join(lines)
# ── Wall pieces ──────────────────────────────────────────────────────────
def build_wall_straight():
"""Straight wall — 2.56m x 2.56m x 0.16m."""
lines = [
'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]',
'',
tsnode("WallStraight01", "CSGBox3D", ".", {
"size": f"Vector3({U}, {U}, {W})",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
]
return "\n".join(lines) + "\n"
def build_wall_corner():
"""L-corner — two CSG boxes in a CSGCombiner3D."""
lines = [
'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]',
'',
tsnode("WallCorner01", "CSGCombiner3D", ".", {"use_collision": "false"}),
tsnode("WallA", "CSGBox3D", "WallCorner01", {
"operation": "0",
"size": f"Vector3({U}, {U}, {W})",
"material": 'ExtResource("1")',
"use_collision": "true",
"transform": f"Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, {W/2})",
}),
tsnode("WallB", "CSGBox3D", "WallCorner01", {
"operation": "0",
"size": f"Vector3({W}, {U}, {U})",
"material": 'ExtResource("1")',
"use_collision": "true",
"transform": f"Transform3D(0, 0, -1, 0, 1, 0, 1, 0, 0, {W/2}, 0, 0)",
}),
]
return "\n".join(lines) + "\n"
def build_wall_doorway():
"""Wall with door opening (0.80m x 2.00m cutout)."""
dw, dh = 0.80, 2.00
off_y = dh/2 - U/2 # bottom-aligned
lines = [
'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]',
'',
tsnode("WallDoorway01", "CSGCombiner3D", ".", {"use_collision": "false"}),
tsnode("MainWall", "CSGBox3D", "WallDoorway01", {
"operation": "0",
"size": f"Vector3({U}, {U}, {W})",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
tsnode("DoorCutout", "CSGBox3D", "WallDoorway01", {
"operation": "1",
"size": f"Vector3({dw}, {dh}, {W + 0.04})",
"use_collision": "true",
"transform": f"Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, {off_y}, 0)",
}),
]
return "\n".join(lines) + "\n"
def build_wall_window():
"""Wall with window opening (1.20m x 0.80m cutout, centered slightly above mid)."""
ww, wh = 1.20, 0.80
off_y = 0.30
lines = [
'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]',
'',
tsnode("WallWindow01", "CSGCombiner3D", ".", {"use_collision": "false"}),
tsnode("MainWall", "CSGBox3D", "WallWindow01", {
"operation": "0",
"size": f"Vector3({U}, {U}, {W})",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
tsnode("WindowCutout", "CSGBox3D", "WallWindow01", {
"operation": "1",
"size": f"Vector3({ww}, {wh}, {W + 0.04})",
"use_collision": "true",
"transform": f"Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, {off_y}, 0)",
}),
]
return "\n".join(lines) + "\n"
def build_wall_endcap():
"""Narrow edge cap — 0.32m x 2.56m x 0.16m."""
lines = [
'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]',
'',
tsnode("WallEndcap01", "CSGBox3D", ".", {
"size": "Vector3(0.32, 2.56, 0.16)",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
]
return "\n".join(lines) + "\n"
# ── Floor pieces ─────────────────────────────────────────────────────────
def build_floor_tile():
"""2.56m x 0.08m x 2.56m floor slab — industrial concrete."""
lines = [
f'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_FLOOR}.tres" id="1"]',
'',
tsnode("FloorTile01", "CSGBox3D", ".", {
"size": f"Vector3({U}, {F}, {U})",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
]
return "\n".join(lines) + "\n"
def build_floor_tile_ceramic():
"""Floor slab with ceramic tile material."""
lines = [
f'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_FLOOR_TILE}.tres" id="1"]',
'',
tsnode("FloorTileCeramic01", "CSGBox3D", ".", {
"size": f"Vector3({U}, {F}, {U})",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
]
return "\n".join(lines) + "\n"
# ── Structural ───────────────────────────────────────────────────────────
def build_pillar():
"""Square pillar — 0.64m x 2.56m x 0.64m."""
lines = [
f'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL2}.tres" id="1"]',
'',
tsnode("Pillar01", "CSGBox3D", ".", {
"size": "Vector3(0.64, 2.56, 0.64)",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
]
return "\n".join(lines) + "\n"
def build_beam():
"""Ceiling beam — 3.84m x 0.32m x 0.32m metal."""
lines = [
f'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_METAL}.tres" id="1"]',
'',
tsnode("Beam01", "CSGBox3D", ".", {
"size": f"Vector3({U * 1.5}, 0.32, 0.32)",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
]
return "\n".join(lines) + "\n"
# ── Accent panels ────────────────────────────────────────────────────────
def build_accent_panel(name, mat_name, color_label):
"""A thin accent panel with team color material."""
lines = [
f'[gd_scene load_steps=2 format=3]',
f'[ext_resource type="Material" path="{MAT_BASE}/{mat_name}.tres" id="1"]',
'',
tsnode(f"{color_label}Panel01", "CSGBox3D", ".", {
"size": f"Vector3(1.28, 0.64, {W * 0.5})",
"material": 'ExtResource("1")',
"use_collision": "true",
}),
]
return "\n".join(lines) + "\n"
# ── Kit demo scene ───────────────────────────────────────────────────────
def build_kit_demo():
"""Showcase scene — places every modular piece on a 3m-spaced strip."""
# Each entry: (scene_name, ext_material_name, label_for_node)
entries = [
("wall_straight_01", MAT_WALL, "Wall01"),
("wall_corner_01", MAT_WALL, "Corner01"),
("wall_doorway_01", MAT_WALL, "Doorway01"),
("wall_window_01", MAT_WALL, "Window01"),
("wall_endcap_01", MAT_WALL, "Endcap01"),
("floor_tile_01", MAT_FLOOR, "Floor01"),
("floor_tile_ceramic_01", MAT_FLOOR_TILE, "FloorCeramic01"),
("pillar_01", MAT_WALL2, "Pillar01"),
("beam_01", MAT_METAL, "Beam01"),
("accent_blue_panel", MAT_BLUE, "BluePanel01"),
("accent_red_panel", MAT_RED, "RedPanel01"),
]
# All unique materials used
all_mats = sorted(set(e[1] for e in entries))
mat_scene_map = {} # mat_name -> {scene_name -> ext_id}
scene_map = {} # scene_name -> ext_id
ext_id_counter = 1
lines = []
# First pass: assign ext ids to scenes (PackedScene) and materials
scene_ext_ids = {}
mat_ext_ids = {}
for scene_name, mat_name, _ in entries:
if scene_name not in scene_ext_ids:
scene_ext_ids[scene_name] = ext_id_counter
ext_id_counter += 1
if mat_name not in mat_ext_ids:
mat_ext_ids[mat_name] = ext_id_counter
ext_id_counter += 1
total_ext = len(scene_ext_ids) + len(mat_ext_ids)
load_steps = total_ext + 1
lines = [f'[gd_scene load_steps={load_steps} format=3]']
# Ext resources: scenes
for scene_name, eid in scene_ext_ids.items():
lines.append(
f'[ext_resource type="PackedScene" '
f'path="res://assets/scenes/modular/{scene_name}.tscn" id="{eid}"]'
)
# Ext resources: materials (for any inline nodes)
for mat_name, eid in mat_ext_ids.items():
lines.append(
f'[ext_resource type="Material" '
f'path="{MAT_BASE}/{mat_name}.tres" id="{eid}"]'
)
lines.append("")
lines.append('[node name="KitDemo" type="Node3D"]')
lines.append("")
x = -5.0
for scene_name, mat_name, label in entries:
scene_eid = scene_ext_ids[scene_name]
lines.append(
f'[node name="{label}" type="Node3D" parent="."]'
)
lines.append(f'instance = ExtResource("{scene_eid}")')
lines.append(f'transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, {x:.1f}, 0, 0)')
lines.append("")
x += 3.0
return "\n".join(lines) + "\n"
# ── Main ─────────────────────────────────────────────────────────────────
SCENE_BUILDERS = {
"wall_straight_01": build_wall_straight,
"wall_corner_01": build_wall_corner,
"wall_doorway_01": build_wall_doorway,
"wall_window_01": build_wall_window,
"wall_endcap_01": build_wall_endcap,
"floor_tile_01": build_floor_tile,
"floor_tile_ceramic_01": build_floor_tile_ceramic,
"pillar_01": build_pillar,
"beam_01": build_beam,
"accent_blue_panel": lambda: build_accent_panel("accent_blue_panel", MAT_BLUE, "Blue"),
"accent_red_panel": lambda: build_accent_panel("accent_red_panel", MAT_RED, "Red"),
"kit_demo": build_kit_demo,
}
def main():
os.makedirs(SCENES_DIR, exist_ok=True)
print("Generating modular .tscn scenes...")
for name, builder in SCENE_BUILDERS.items():
content = builder()
path = os.path.join(SCENES_DIR, f"{name}.tscn")
with open(path, "w") as f:
f.write(content)
print(f" Created {name}.tscn ({len(content)} bytes)")
print(f"\nDone — {len(SCENE_BUILDERS)} .tscn files")
if __name__ == "__main__":
main()