d631ff784a
- 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.
331 lines
12 KiB
Python
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()
|