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.
This commit is contained in:
2026-07-01 00:10:19 -04:00
parent 001ba3952e
commit d631ff784a
57 changed files with 1717 additions and 8 deletions
@@ -0,0 +1,17 @@
[gd_resource type="StandardMaterial3D" load_steps=5 format=3]
[ext_resource type="Texture2D" path="res://assets/textures/accent_team_blue/basecolor.png" id="1"]
[ext_resource type="Texture2D" path="res://assets/textures/accent_team_blue/normal.png" id="2"]
[ext_resource type="Texture2D" path="res://assets/textures/accent_team_blue/roughness.png" id="3"]
[ext_resource type="Texture2D" path="res://assets/textures/accent_team_blue/metallic.png" id="4"]
[resource]
albedo_texture = ExtResource("1")
normal_enabled = true
normal_texture = ExtResource("2")
normal_scale = 0.3
roughness = 0.35
roughness_texture = ExtResource("3")
metallic = 0.5
metallic_texture = ExtResource("4")
texture_filter = 1
uv1_scale = Vector3(1.0, 1.0, 1.0)
@@ -0,0 +1,17 @@
[gd_resource type="StandardMaterial3D" load_steps=5 format=3]
[ext_resource type="Texture2D" path="res://assets/textures/accent_team_red/basecolor.png" id="1"]
[ext_resource type="Texture2D" path="res://assets/textures/accent_team_red/normal.png" id="2"]
[ext_resource type="Texture2D" path="res://assets/textures/accent_team_red/roughness.png" id="3"]
[ext_resource type="Texture2D" path="res://assets/textures/accent_team_red/metallic.png" id="4"]
[resource]
albedo_texture = ExtResource("1")
normal_enabled = true
normal_texture = ExtResource("2")
normal_scale = 0.3
roughness = 0.35
roughness_texture = ExtResource("3")
metallic = 0.5
metallic_texture = ExtResource("4")
texture_filter = 1
uv1_scale = Vector3(1.0, 1.0, 1.0)
@@ -0,0 +1,17 @@
[gd_resource type="StandardMaterial3D" load_steps=5 format=3]
[ext_resource type="Texture2D" path="res://assets/textures/floor_concrete_01/basecolor.png" id="1"]
[ext_resource type="Texture2D" path="res://assets/textures/floor_concrete_01/normal.png" id="2"]
[ext_resource type="Texture2D" path="res://assets/textures/floor_concrete_01/roughness.png" id="3"]
[ext_resource type="Texture2D" path="res://assets/textures/floor_concrete_01/metallic.png" id="4"]
[resource]
albedo_texture = ExtResource("1")
normal_enabled = true
normal_texture = ExtResource("2")
normal_scale = 0.8
roughness = 0.9
roughness_texture = ExtResource("3")
metallic = 0.0
metallic_texture = ExtResource("4")
texture_filter = 1
uv1_scale = Vector3(2.0, 2.0, 1.0)
@@ -0,0 +1,17 @@
[gd_resource type="StandardMaterial3D" load_steps=5 format=3]
[ext_resource type="Texture2D" path="res://assets/textures/floor_tile_01/basecolor.png" id="1"]
[ext_resource type="Texture2D" path="res://assets/textures/floor_tile_01/normal.png" id="2"]
[ext_resource type="Texture2D" path="res://assets/textures/floor_tile_01/roughness.png" id="3"]
[ext_resource type="Texture2D" path="res://assets/textures/floor_tile_01/metallic.png" id="4"]
[resource]
albedo_texture = ExtResource("1")
normal_enabled = true
normal_texture = ExtResource("2")
normal_scale = 1.0
roughness = 0.6
roughness_texture = ExtResource("3")
metallic = 0.0
metallic_texture = ExtResource("4")
texture_filter = 1
uv1_scale = Vector3(2.0, 2.0, 1.0)
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
Generate Godot 4 .tres material resource files for the modular art kit.
Godot 4 text resource format:
[ext_resource type="Texture2D" path="res://..." id="1"]
[resource]
property = value
"""
import os
MATERIALS_DIR = os.path.expanduser("/home/oplabs/tactical-shooter/client/assets/materials")
TEX_BASE = "res://assets/textures"
def make_tres(name, props):
textures = props.get("textures", {})
load_steps = 1 + len(textures)
lines = [
f'[gd_resource type="StandardMaterial3D" load_steps={load_steps} format=3]',
]
ext_id = 1
ext_map = {}
for slot, rel_path in textures.items():
eid = str(ext_id)
path = f"{TEX_BASE}/{name}/{rel_path}"
lines.append(f'[ext_resource type="Texture2D" path="{path}" id="{eid}"]')
ext_map[slot] = eid
ext_id += 1
lines.append("")
lines.append("[resource]")
# Albedo
if "albedo" in ext_map:
lines.append(f'albedo_texture = ExtResource("{ext_map["albedo"]}")')
# Normal map
if "normal" in ext_map:
lines.append("normal_enabled = true")
lines.append(f'normal_texture = ExtResource("{ext_map["normal"]}")')
if "normal_scale" in props:
lines.append(f"normal_scale = {props['normal_scale']}")
# Roughness
lines.append(f"roughness = {props.get('roughness', 0.5)}")
if "roughness" in ext_map:
lines.append(f'roughness_texture = ExtResource("{ext_map["roughness"]}")')
# Metallic
lines.append(f"metallic = {props.get('metallic', 0.0)}")
if "metallic" in ext_map:
lines.append(f'metallic_texture = ExtResource("{ext_map["metallic"]}")')
# Texture filter (1 = linear)
if "texture_filter" in props:
lines.append(f"texture_filter = {props['texture_filter']}")
# UV tiling
if "uv_scale" in props:
s = props["uv_scale"]
lines.append(f'uv1_scale = Vector3({s[0]}, {s[1]}, {s[2]})')
content = "\n".join(lines) + "\n"
path = os.path.join(MATERIALS_DIR, f"{name}.tres")
with open(path, "w") as f:
f.write(content)
print(f" Created {name}.tres ({len(textures)} textures, {load_steps} steps)")
def generate():
os.makedirs(MATERIALS_DIR, exist_ok=True)
print("Generating material .tres files...")
# 1 - Wall concrete 01: primary wall, clean light concrete
make_tres("wall_concrete_01", {
"textures": {"albedo": "basecolor.png", "normal": "normal.png",
"roughness": "roughness.png", "metallic": "metallic.png"},
"roughness": 0.85, "metallic": 0.0, "normal_scale": 0.5,
"texture_filter": 1,
"uv_scale": (1.0, 1.0, 1.0),
})
# 2 - Wall concrete 02: darker accent concrete
make_tres("wall_concrete_02", {
"textures": {"albedo": "basecolor.png", "normal": "normal.png",
"roughness": "roughness.png", "metallic": "metallic.png"},
"roughness": 0.75, "metallic": 0.0, "normal_scale": 0.4,
"texture_filter": 1,
"uv_scale": (1.0, 1.0, 1.0),
})
# 3 - Floor tile 01: ceramic tile interior
make_tres("floor_tile_01", {
"textures": {"albedo": "basecolor.png", "normal": "normal.png",
"roughness": "roughness.png", "metallic": "metallic.png"},
"roughness": 0.6, "metallic": 0.0, "normal_scale": 1.0,
"texture_filter": 1,
"uv_scale": (2.0, 2.0, 1.0), # repeat to show tile grid
})
# 4 - Floor concrete 01: industrial floor
make_tres("floor_concrete_01", {
"textures": {"albedo": "basecolor.png", "normal": "normal.png",
"roughness": "roughness.png", "metallic": "metallic.png"},
"roughness": 0.9, "metallic": 0.0, "normal_scale": 0.8,
"texture_filter": 1,
"uv_scale": (2.0, 2.0, 1.0),
})
# 5 - Metal structural 01: structural beams
make_tres("metal_structural_01", {
"textures": {"albedo": "basecolor.png", "normal": "normal.png",
"roughness": "roughness.png", "metallic": "metallic.png"},
"roughness": 0.3, "metallic": 1.0, "normal_scale": 0.3,
"texture_filter": 1,
"uv_scale": (1.0, 1.0, 1.0),
})
# 6 - Accent team blue: CT-side colored panels
make_tres("accent_team_blue", {
"textures": {"albedo": "basecolor.png", "normal": "normal.png",
"roughness": "roughness.png", "metallic": "metallic.png"},
"roughness": 0.35, "metallic": 0.5, "normal_scale": 0.3,
"texture_filter": 1,
"uv_scale": (1.0, 1.0, 1.0),
})
# 7 - Accent team red: T-side colored panels
make_tres("accent_team_red", {
"textures": {"albedo": "basecolor.png", "normal": "normal.png",
"roughness": "roughness.png", "metallic": "metallic.png"},
"roughness": 0.35, "metallic": 0.5, "normal_scale": 0.3,
"texture_filter": 1,
"uv_scale": (1.0, 1.0, 1.0),
})
print("\nDone — 7 material .tres files created.")
if __name__ == "__main__":
generate()
@@ -0,0 +1,17 @@
[gd_resource type="StandardMaterial3D" load_steps=5 format=3]
[ext_resource type="Texture2D" path="res://assets/textures/metal_structural_01/basecolor.png" id="1"]
[ext_resource type="Texture2D" path="res://assets/textures/metal_structural_01/normal.png" id="2"]
[ext_resource type="Texture2D" path="res://assets/textures/metal_structural_01/roughness.png" id="3"]
[ext_resource type="Texture2D" path="res://assets/textures/metal_structural_01/metallic.png" id="4"]
[resource]
albedo_texture = ExtResource("1")
normal_enabled = true
normal_texture = ExtResource("2")
normal_scale = 0.3
roughness = 0.3
roughness_texture = ExtResource("3")
metallic = 1.0
metallic_texture = ExtResource("4")
texture_filter = 1
uv1_scale = Vector3(1.0, 1.0, 1.0)
@@ -0,0 +1,17 @@
[gd_resource type="StandardMaterial3D" load_steps=5 format=3]
[ext_resource type="Texture2D" path="res://assets/textures/wall_concrete_01/basecolor.png" id="1"]
[ext_resource type="Texture2D" path="res://assets/textures/wall_concrete_01/normal.png" id="2"]
[ext_resource type="Texture2D" path="res://assets/textures/wall_concrete_01/roughness.png" id="3"]
[ext_resource type="Texture2D" path="res://assets/textures/wall_concrete_01/metallic.png" id="4"]
[resource]
albedo_texture = ExtResource("1")
normal_enabled = true
normal_texture = ExtResource("2")
normal_scale = 0.5
roughness = 0.85
roughness_texture = ExtResource("3")
metallic = 0.0
metallic_texture = ExtResource("4")
texture_filter = 1
uv1_scale = Vector3(1.0, 1.0, 1.0)
@@ -0,0 +1,17 @@
[gd_resource type="StandardMaterial3D" load_steps=5 format=3]
[ext_resource type="Texture2D" path="res://assets/textures/wall_concrete_02/basecolor.png" id="1"]
[ext_resource type="Texture2D" path="res://assets/textures/wall_concrete_02/normal.png" id="2"]
[ext_resource type="Texture2D" path="res://assets/textures/wall_concrete_02/roughness.png" id="3"]
[ext_resource type="Texture2D" path="res://assets/textures/wall_concrete_02/metallic.png" id="4"]
[resource]
albedo_texture = ExtResource("1")
normal_enabled = true
normal_texture = ExtResource("2")
normal_scale = 0.4
roughness = 0.75
roughness_texture = ExtResource("3")
metallic = 0.0
metallic_texture = ExtResource("4")
texture_filter = 1
uv1_scale = Vector3(1.0, 1.0, 1.0)
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/accent_team_blue.tres" id="1"]
[node name="BluePanel01" type="CSGBox3D" parent="."]
size = Vector3(1.28, 0.64, 0.08)
material = ExtResource("1")
use_collision = true
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/accent_team_red.tres" id="1"]
[node name="RedPanel01" type="CSGBox3D" parent="."]
size = Vector3(1.28, 0.64, 0.08)
material = ExtResource("1")
use_collision = true
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/metal_structural_01.tres" id="1"]
[node name="Beam01" type="CSGBox3D" parent="."]
size = Vector3(3.84, 0.32, 0.32)
material = ExtResource("1")
use_collision = true
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/floor_concrete_01.tres" id="1"]
[node name="FloorTile01" type="CSGBox3D" parent="."]
size = Vector3(2.56, 0.08, 2.56)
material = ExtResource("1")
use_collision = true
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/floor_tile_01.tres" id="1"]
[node name="FloorTileCeramic01" type="CSGBox3D" parent="."]
size = Vector3(2.56, 0.08, 2.56)
material = ExtResource("1")
use_collision = true
@@ -0,0 +1,330 @@
#!/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()
+48
View File
@@ -0,0 +1,48 @@
extends Node3D
# Auto-bakes the LightmapGI when opened in the Godot editor.
# Only runs in editor mode — does nothing in exported builds.
#
# Manual bake:
# Open the scene in the Godot editor, select LightmapGI node,
# click "Bake Lightmap" in the inspector at the top of the node's properties.
#
# This script provides feedback in the editor Output panel.
@tool
func _ready():
if not Engine.is_editor_hint():
# Not in editor — nothing to do for runtime
return
# Check if we have a LightmapGI node
var lightmap = _find_lightmap_gi()
if not lightmap:
push_error("KitDemo: No LightmapGI node found in scene!")
return
# Check if already baked
if lightmap.light_data != null:
print("KitDemo: Lightmap already baked. To re-bake:")
print(" Select LightmapGI node → 'Bake Lightmap' button in Inspector")
return
# Not baked — offer to bake
print("KitDemo: LightmapGI found but not baked.")
print(" To bake: Select LightmapGI → click 'Bake Lightmap' at top of Inspector")
print(" Or right-click LightmapGI node → 'Bake Lightmap'")
print("")
print(" LightmapGI settings:")
print(" Bounces: ", lightmap.bounces)
print(" Texel Scale: ", lightmap.texel_scale)
print(" Texel Subdiv: ", lightmap.texel_subdiv)
print(" Max Texture Size: ", lightmap.max_texture_size)
print(" Interior: ", lightmap.interior)
func _find_lightmap_gi() -> LightmapGI:
for child in get_children():
if child is LightmapGI:
return child
return null
+188
View File
@@ -0,0 +1,188 @@
[gd_scene load_steps=23 format=3]
; === External Resources (modular kit pieces) ===
[ext_resource type="PackedScene" path="res://assets/scenes/modular/wall_straight_01.tscn" id="1"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/wall_corner_01.tscn" id="3"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/wall_doorway_01.tscn" id="4"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/wall_window_01.tscn" id="5"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/wall_endcap_01.tscn" id="6"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/floor_tile_01.tscn" id="7"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/floor_tile_ceramic_01.tscn" id="9"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/pillar_01.tscn" id="11"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/beam_01.tscn" id="13"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/accent_blue_panel.tscn" id="15"]
[ext_resource type="PackedScene" path="res://assets/scenes/modular/accent_red_panel.tscn" id="17"]
; === External Resources (materials) ===
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="2"]
[ext_resource type="Material" path="res://assets/materials/floor_concrete_01.tres" id="8"]
[ext_resource type="Material" path="res://assets/materials/floor_tile_01.tres" id="10"]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_02.tres" id="12"]
[ext_resource type="Material" path="res://assets/materials/metal_structural_01.tres" id="14"]
[ext_resource type="Material" path="res://assets/materials/accent_team_blue.tres" id="16"]
[ext_resource type="Material" path="res://assets/materials/accent_team_red.tres" id="18"]
; === Script ===
[ext_resource type="Script" path="res://assets/scenes/modular/kit_demo.gd" id="19"]
; === Subresources (Sky, Environment for WorldEnvironment) ===
[sub_resource type="ProceduralSkyMaterial" id="SkyMat"]
sky_top_color = Color(0.45, 0.55, 0.75)
sky_horizon_color = Color(0.65, 0.6, 0.8)
sky_bottom_color = Color(0.35, 0.35, 0.4)
ground_bottom_color = Color(0.15, 0.15, 0.15)
sky_energy_multiplier = 0.5
sky_exposure = 1.0
[sub_resource type="Sky" id="Sky"]
sky_material = SubResource("SkyMat")
[sub_resource type="Environment" id="Env"]
background_mode = 2
background_sky = SubResource("Sky")
background_sky_custom_fov = 0.0
tonemap_mode = 0
glow_enabled = false
ambient_light_color = Color(0.25, 0.25, 0.3)
ambient_light_energy = 0.4
ambient_light_sky_contribution = 0.5
; === Scene Root ===
[node name="KitDemo" type="Node3D"]
script = ExtResource("19")
; ============ FLOOR ============
; 2x2 grid of floor tiles (5.12 x 5.12)
[node name="FloorNW" type="Node3D" parent="."]
instance = ExtResource("7")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.28, 0, -1.28)
[node name="FloorNE" type="Node3D" parent="."]
instance = ExtResource("7")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.28, 0, -1.28)
[node name="FloorSW" type="Node3D" parent="."]
instance = ExtResource("9")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.28, 0, 1.28)
[node name="FloorSE" type="Node3D" parent="."]
instance = ExtResource("7")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.28, 0, 1.28)
; ============ WALLS ============
; --- South Wall (Z=2.56, along X from -2.56 to +2.56) ---
; Straight segment left of doorway
[node name="SouthWall" type="Node3D" parent="."]
instance = ExtResource("1")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.28, 0, 2.56)
; Doorway segment right
[node name="SouthDoorway" type="Node3D" parent="."]
instance = ExtResource("4")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.28, 0, 2.56)
; --- North Wall (Z=-2.56, along X from -2.56 to +2.56) ---
; Window segment left
[node name="NorthWindow" type="Node3D" parent="."]
instance = ExtResource("5")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.28, 0, -2.56)
; Straight segment right
[node name="NorthWall" type="Node3D" parent="."]
instance = ExtResource("1")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.28, 0, -2.56)
; --- West Wall (X=-2.56, along Z from -2.56 to +2.56) ---
; Rotated 90° Y so width runs along Z instead of X
; Basis: local X → global -Z, local Z → global X
[node name="WestLower" type="Node3D" parent="."]
instance = ExtResource("1")
transform = Transform3D(0, 0, -1, 0, 1, 0, 1, 0, 0, -2.56, 0, -1.28)
[node name="WestUpper" type="Node3D" parent="."]
instance = ExtResource("1")
transform = Transform3D(0, 0, -1, 0, 1, 0, 1, 0, 0, -2.56, 0, 1.28)
; --- East Wall (X=2.56, along Z from -2.56 to +2.56) ---
; Rotated 90° Y so width runs along Z, thickness along -X
[node name="EastLower" type="Node3D" parent="."]
instance = ExtResource("1")
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 2.56, 0, -1.28)
[node name="EastUpper" type="Node3D" parent="."]
instance = ExtResource("1")
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 2.56, 0, 1.28)
; ============ INTERIOR ============
; Pillar at room center
[node name="Pillar" type="Node3D" parent="."]
instance = ExtResource("11")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
; Beam overhead spanning the room
[node name="Beam" type="Node3D" parent="."]
instance = ExtResource("13")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.40, 0)
; Accent red panel on south wall straight segment
[node name="AccentRed" type="Node3D" parent="."]
instance = ExtResource("17")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.28, 1.28, 2.52)
; Accent blue panel on east wall upper segment
[node name="AccentBlue" type="Node3D" parent="."]
instance = ExtResource("15")
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 2.52, 1.28, 0)
; ============ LIGHTING ============
; --- WorldEnvironment: skybox + ambient ---
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Env")
; --- DirectionalLight: sun/moon key light from high angle ---
; Tilted ~45° down, rotated ~30° Y for directional angle
[node name="SunLight" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.866, 0, -0.5, -0.354, 0.707, -0.612, 0.354, 0.707, 0.612, 0, 5, 0)
light_energy = 1.2
light_indirect_energy = 0.8
light_color = Color(1.0, 0.95, 0.9)
shadow_enabled = true
light_bake_mode = 2
directional_shadow_max_distance = 30.0
directional_shadow_split_1 = 0.1
directional_shadow_split_2 = 0.3
directional_shadow_split_3 = 0.6
directional_shadow_blend_splits = true
; --- OmniLight: warm interior fill ---
[node name="FillLight" type="OmniLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.5, 2.4, 1.5)
light_energy = 0.4
light_indirect_energy = 0.5
light_color = Color(1.0, 0.85, 0.7)
light_bake_mode = 2
omni_range = 6.0
omni_attenuation = 0.8
shadow_enabled = false
; --- ReflectionProbe for interior specular reflections ---
[node name="ReflectionProbe" type="ReflectionProbe" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.28, 0)
box_projection = true
interior = true
extents = Vector3(2.8, 1.4, 2.8)
intensity = 1.0
max_distance = 10.0
; --- LightmapGI: baked global illumination ---
[node name="LightmapGI" type="LightmapGI" parent="."]
bounces = 3
bounce_indirect_energy = 1.0
texel_scale = 1.0
texel_subdiv = 4
max_texture_size = 2048
use_denoiser = true
denoiser_strength = 0.0
interior = true
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_02.tres" id="1"]
[node name="Pillar01" type="CSGBox3D" parent="."]
size = Vector3(0.64, 2.56, 0.64)
material = ExtResource("1")
use_collision = true
@@ -0,0 +1,17 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
[node name="WallCorner01" type="CSGCombiner3D" parent="."]
use_collision = false
[node name="WallA" type="CSGBox3D" parent="WallCorner01"]
operation = 0
size = Vector3(2.56, 2.56, 0.16)
material = ExtResource("1")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.08)
[node name="WallB" type="CSGBox3D" parent="WallCorner01"]
operation = 0
size = Vector3(0.16, 2.56, 2.56)
material = ExtResource("1")
use_collision = true
transform = Transform3D(0, 0, -1, 0, 1, 0, 1, 0, 0, 0.08, 0, 0)
@@ -0,0 +1,15 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
[node name="WallDoorway01" type="CSGCombiner3D" parent="."]
use_collision = false
[node name="MainWall" type="CSGBox3D" parent="WallDoorway01"]
operation = 0
size = Vector3(2.56, 2.56, 0.16)
material = ExtResource("1")
use_collision = true
[node name="DoorCutout" type="CSGBox3D" parent="WallDoorway01"]
operation = 1
size = Vector3(0.8, 2.0, 0.2)
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.28, 0)
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
[node name="WallEndcap01" type="CSGBox3D" parent="."]
size = Vector3(0.32, 2.56, 0.16)
material = ExtResource("1")
use_collision = true
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
[node name="WallStraight01" type="CSGBox3D" parent="."]
size = Vector3(2.56, 2.56, 0.16)
material = ExtResource("1")
use_collision = true
@@ -0,0 +1,15 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
[node name="WallWindow01" type="CSGCombiner3D" parent="."]
use_collision = false
[node name="MainWall" type="CSGBox3D" parent="WallWindow01"]
operation = 0
size = Vector3(2.56, 2.56, 0.16)
material = ExtResource("1")
use_collision = true
[node name="WindowCutout" type="CSGBox3D" parent="WallWindow01"]
operation = 1
size = Vector3(1.2, 0.8, 0.2)
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0)
Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+493
View File
@@ -0,0 +1,493 @@
#!/usr/bin/env python3
"""
Generate procedural 1K PBR texture maps for the Tactical Shooter modular art kit.
Produces: basecolor, normal, roughness, metallic — all at 1024x1024.
Usage: python3 generate_textures.py
"""
import math
import os
import random
from PIL import Image, ImageFilter, ImageChops, ImageDraw
TEXTURE_DIR = os.path.expanduser("/home/oplabs/tactical-shooter/client/assets/textures")
SIZE = 1024
random.seed(42)
def save_pbr(base_dir, name, basecolor, normal, roughness, metallic):
"""Save all PBR maps for one material."""
d = os.path.join(base_dir, name)
os.makedirs(d, exist_ok=True)
basecolor.save(os.path.join(d, "basecolor.png"))
normal.save(os.path.join(d, "normal.png"))
roughness.save(os.path.join(d, "roughness.png"))
metallic.save(os.path.join(d, "metallic.png"))
print(f" Generated {name}/ — 4 maps saved")
# ── Utility helpers ──────────────────────────────────────────────────────
def gaussian_noise(size, scale=1.0, seed=None):
"""Generate a grayscale image with gaussian noise."""
if seed is not None:
random.seed(seed)
img = Image.new("L", (size, size))
pix = img.load()
for y in range(size):
for x in range(size):
v = int(random.gauss(128, 64 * scale))
pix[x, y] = max(0, min(255, v))
return img
def tile_blur(img, radius):
"""Blur with tileable boundary by mirroring edges before blur."""
w, h = img.size
big = Image.new(img.mode, (w * 3, h * 3))
for dx in range(3):
for dy in range(3):
if dx == 1 and dy == 1:
big.paste(img, (w, h))
else:
# Mirror the tile
mirror = img.transpose(Image.FLIP_LEFT_RIGHT) if dx % 2 == 0 else img
if dy % 2 == 0:
mirror = mirror.transpose(Image.FLIP_TOP_BOTTOM)
big.paste(mirror, (w * dx, h * dy))
big = big.filter(ImageFilter.GaussianBlur(radius))
return big.crop((w, h, w * 2, h * 2))
def normal_from_height(height_img, strength=2.0):
"""Generate an RGB normal map from a grayscale height map using Sobel."""
w, h = height_img.size
# Get pixel data
hpix = height_img.load()
normal = Image.new("RGB", (w, h))
npix = normal.load()
for y in range(h):
for x in range(w):
# Sample height with wrapping for tileability
sx1 = hpix[(x - 1) % w, y]
sx2 = hpix[(x + 1) % w, y]
sy1 = hpix[x, (y - 1) % h]
sy2 = hpix[x, (y + 1) % h]
dx = (sx2 - sx1) / 255.0
dy = (sy2 - sy1) / 255.0
dz = 1.0 / strength
length = math.sqrt(dx * dx + dy * dy + dz * dz)
nx = dx / length
ny = dy / length
nz = dz / length
# Map from [-1,1] to [0,255]
npix[x, y] = (
int((nx * 0.5 + 0.5) * 255),
int((ny * 0.5 + 0.5) * 255),
int((nz * 0.5 + 0.5) * 255),
)
return normal
def make_cellular(size, cell_size=32, seed=None):
"""Generate a cellular/Voronoi-like pattern as grayscale."""
if seed is not None:
random.seed(seed)
img = Image.new("L", (size, size))
pix = img.load()
# Generate cell centers with random heights
cells = {}
for cx in range(0, size + cell_size, cell_size):
for cy in range(0, size + cell_size, cell_size):
ox = random.randint(0, cell_size - 1)
oy = random.randint(0, cell_size - 1)
h = random.randint(40, 200)
cells[(cx + ox, cy + oy)] = h
for y in range(size):
for x in range(size):
min_dist = float("inf")
best_h = 128
for (cx, cy), h in cells.items():
d = (cx - x) ** 2 + (cy - y) ** 2
if d < min_dist:
min_dist = d
best_h = h
pix[x, y] = best_h
return img
def concrete_base(size, base_rgb=(180, 175, 168), noise_scale=1.0):
"""Generate concrete-like basecolor with aggregate variation."""
# Start with light noise
noise = gaussian_noise(size, scale=0.15, seed=42)
noise = tile_blur(noise, 4)
# Add larger variation
big = gaussian_noise(size, scale=0.08, seed=43)
big = tile_blur(big, 24)
# Add some darker speckles (aggregate)
speckle = Image.new("L", (size, size), 0)
spix = speckle.load()
for _ in range(800):
x = random.randint(0, size - 1)
y = random.randint(0, size - 1)
spix[x, y] = random.randint(30, 80)
speckle = tile_blur(speckle, 1.5)
# Combine
combined = Image.new("L", (size, size))
cpix = combined.load()
npix = noise.load()
bpix = big.load()
skpix = speckle.load()
for y in range(size):
for x in range(size):
v = npix[x, y] + bpix[x, y] - 128 + skpix[x, y] - 30
cpix[x, y] = max(0, min(255, v))
# Tint and apply
result = Image.new("RGB", (size, size))
rpix = result.load()
cp = combined.load()
for y in range(size):
for x in range(size):
f = cp[x, y] / 255.0
rpix[x, y] = (
max(0, min(255, int(base_rgb[0] * f / 180))),
max(0, min(255, int(base_rgb[1] * f / 175))),
max(0, min(255, int(base_rgb[2] * f / 168))),
)
return result
# ── Material definitions ─────────────────────────────────────────────────
def generate_wall_concrete_01():
"""Light clean concrete — primary wall material."""
size = SIZE
# Basecolor
bc = concrete_base(size, base_rgb=(200, 195, 185))
bc = bc.filter(ImageFilter.SMOOTH)
# Height map for normal (slight texture)
height = gaussian_noise(size, scale=0.1, seed=100)
height = tile_blur(height, 3)
normal = normal_from_height(height, strength=1.5)
# Roughness — concrete is fairly rough
rough = gaussian_noise(size, scale=0.08, seed=200)
rough = tile_blur(rough, 6)
# Brighten a bit (rougher = lighter in roughness map)
rpix = rough.load()
for y in range(size):
for x in range(size):
rpix[x, y] = min(255, rpix[x, y] + 40)
# Metallic — none
metallic = Image.new("L", (size, size), 0)
save_pbr(TEXTURE_DIR, "wall_concrete_01", bc, normal, rough, metallic)
def generate_wall_concrete_02():
"""Darker, smoother concrete — accent walls."""
size = SIZE
bc = concrete_base(size, base_rgb=(145, 140, 135))
bc = bc.filter(ImageFilter.SMOOTH_MORE)
height = gaussian_noise(size, scale=0.06, seed=101)
height = tile_blur(height, 5)
normal = normal_from_height(height, strength=1.2)
rough = gaussian_noise(size, scale=0.06, seed=201)
rough = tile_blur(rough, 8)
rpix = rough.load()
for y in range(size):
for x in range(size):
rpix[x, y] = min(255, rpix[x, y] + 30)
metallic = Image.new("L", (size, size), 0)
save_pbr(TEXTURE_DIR, "wall_concrete_02", bc, normal, rough, metallic)
def generate_floor_tile_01():
"""Checkerboard tile floor — interior spaces."""
size = SIZE
tile_size = 64 # 64px tiles at 1024 = 16 tiles across
bc = Image.new("RGB", (size, size))
bpix = bc.load()
for y in range(size):
for x in range(size):
tx = x // tile_size
ty = y // tile_size
if (tx + ty) % 2 == 0:
# Light tile
v = 200 + random.randint(-10, 10)
bpix[x, y] = (v, v - 5, v - 10)
else:
# Dark tile
v = 160 + random.randint(-8, 8)
bpix[x, y] = (v, v - 3, v - 5)
# Add grout lines
draw = ImageDraw.Draw(bc)
for i in range(0, size + 1, tile_size):
draw.line([(i, 0), (i, size)], fill=(70, 65, 60), width=2)
draw.line([(0, i), (size, i)], fill=(70, 65, 60), width=2)
# Height map for normal
height = Image.new("L", (size, size), 128)
hpix = height.load()
for y in range(size):
for x in range(size):
tx = x // tile_size
ty = y // tile_size
gx = x % tile_size
gy = y % tile_size
# Slight bevel around edges (tile center higher)
d = min(gx, tile_size - gx, gy, tile_size - gy)
if d < 3:
hpix[x, y] = 100 # Grout is lower
else:
hpix[x, y] = 140 + (d % 4) * 5
# Apply bevel and tile variation
hpix = height.load()
for y in range(size):
for x in range(size):
tx = x // tile_size
ty = y // tile_size
gx = x % tile_size
gy = y % tile_size
d_edge = min(gx, tile_size - gx, gy, tile_size - gy)
if d_edge < 2:
hpix[x, y] = 90
elif d_edge < 5:
hpix[x, y] = 100 + d_edge * 8
else:
# Gentle tile surface variation
hpix[x, y] = 140 + ((tx * 13 + ty * 7 + x + y) % 8) * 3
normal = normal_from_height(height, strength=3.0)
# Roughness — tiles are smooth
rough = Image.new("L", (size, size), 80)
rpix = rough.load()
for y in range(size):
for x in range(size):
tx = x // tile_size
ty = y // tile_size
gx = x % tile_size
gy = y % tile_size
d_edge = min(gx, tile_size - gx, gy, tile_size - gy)
if d_edge < 2:
rpix[x, y] = 200 # Grout is rough
else:
rpix[x, y] = 60 + ((tx * 17 + ty * 11) % 10) * 4
metallic = Image.new("L", (size, size), 0)
save_pbr(TEXTURE_DIR, "floor_tile_01", bc, normal, rough, metallic)
def generate_floor_concrete_01():
"""Industrial dark concrete floor — warehouse/industrial areas."""
size = SIZE
# Base
bc = concrete_base(size, base_rgb=(130, 128, 123))
bc = bc.filter(ImageFilter.SMOOTH)
# Add surface scratches/wear marks
draw = ImageDraw.Draw(bc)
for _ in range(50):
x1 = random.randint(0, size)
y1 = random.randint(0, size)
length = random.randint(10, 100)
angle = random.uniform(0, math.pi * 2)
x2 = x1 + int(math.cos(angle) * length)
y2 = y1 + int(math.sin(angle) * length)
c = random.randint(100, 140)
draw.line([(x1, y1), (x2, y2)], fill=(c, c - 3, c - 5), width=1)
height = gaussian_noise(size, scale=0.15, seed=102)
height = tile_blur(height, 3)
hpix = height.load()
# Add groove lines (expansion joints)
for y in range(size):
for x in range(size):
gx = x % 256
gy = y % 256
if min(gx, 256 - gx) < 2 or min(gy, 256 - gy) < 2:
hpix[x, y] = max(0, hpix[x, y] - 40)
normal = normal_from_height(height, strength=2.5)
rough = Image.new("L", (size, size), 160)
rpix = rough.load()
for y in range(size):
for x in range(size):
gx = x % 256
gy = y % 256
if min(gx, 256 - gx) < 2 or min(gy, 256 - gy) < 2:
rpix[x, y] = 220 # Grooves are rougher
else:
rpix[x, y] = 150 + (hash((x // 8, y // 8)) % 20)
metallic = Image.new("L", (size, size), 0)
save_pbr(TEXTURE_DIR, "floor_concrete_01", bc, normal, rough, metallic)
def generate_metal_structural_01():
"""Structural metal — beams, supports, vents."""
size = SIZE
# Base — grey metallic
bc = Image.new("RGB", (size, size))
bpix = bc.load()
for y in range(size):
for x in range(size):
v = 160 + int(random.gauss(0, 4))
bpix[x, y] = (max(0, min(255, v)), max(0, min(255, v - 2)), max(0, min(255, v - 4)))
# Add horizontal brush marks
draw = ImageDraw.Draw(bc)
for _ in range(200):
bx = random.randint(0, size - 1)
by = random.randint(0, size - 1)
c = random.randint(-6, 6)
draw.line([(bx, by), (bx + random.randint(10, 60), by)], fill=(bpix[bx, by][0] + c,) * 3, width=1)
# Height map for normal
height = gaussian_noise(size, scale=0.05, seed=103)
height = tile_blur(height, 2)
hpix = height.load()
for y in range(size):
for x in range(size):
hpix[x, y] = max(0, min(255, hpix[x, y] - 10)) # Flatter than concrete
normal = normal_from_height(height, strength=1.0) # Subtle
# Roughness — smooth metal
rough = Image.new("L", (size, size), 40)
rpix = rough.load()
for y in range(size):
for x in range(size):
hp = hpix[x, y]
rpix[x, y] = 30 + (hp - 128) // 8
# Metallic — yes!
metallic = Image.new("L", (size, size), 255)
save_pbr(TEXTURE_DIR, "metal_structural_01", bc, normal, rough, metallic)
def generate_accent_team_blue():
"""Blue team accent — CT side colored panels."""
size = SIZE
# Deep blue
bc = Image.new("RGB", (size, size))
bpix = bc.load()
for y in range(size):
for x in range(size):
noise = random.randint(-8, 8)
bpix[x, y] = (
max(0, min(255, 40 + noise)),
max(0, min(255, 80 + noise)),
max(0, min(255, 200 + noise)),
)
bc = bc.filter(ImageFilter.SMOOTH)
# Slight brushed pattern
draw = ImageDraw.Draw(bc)
for y in range(0, size, 4):
v = random.randint(-4, 4)
draw.line([(0, y), (size, y)], fill=(40 + v, 80 + v, 200 + v), width=1)
height = Image.new("L", (size, size), 128)
hpix = height.load()
for y in range(size):
for x in range(size):
hpix[x, y] = 128 + int(random.gauss(0, 3))
hpix = height.load()
for y in range(0, size, 4):
for x in range(size):
hpix[x, y] = 110 # Slight panel line
normal = normal_from_height(height, strength=0.8)
rough = Image.new("L", (size, size), 50)
rpix = rough.load()
for y in range(size):
for x in range(size):
rpix[x, y] = 40 + (hash((x // 16, y // 16)) % 15)
# Slightly metallic (painted metal)
metallic = Image.new("L", (size, size), 128)
save_pbr(TEXTURE_DIR, "accent_team_blue", bc, normal, rough, metallic)
def generate_accent_team_red():
"""Red/orange team accent — T side colored panels."""
size = SIZE
bc = Image.new("RGB", (size, size))
bpix = bc.load()
for y in range(size):
for x in range(size):
noise = random.randint(-8, 8)
bpix[x, y] = (
max(0, min(255, 200 + noise)),
max(0, min(255, 60 + noise)),
max(0, min(255, 30 + noise)),
)
bc = bc.filter(ImageFilter.SMOOTH)
draw = ImageDraw.Draw(bc)
for y in range(0, size, 4):
v = random.randint(-4, 4)
draw.line([(0, y), (size, y)], fill=(200 + v, 60 + v, 30 + v), width=1)
height = Image.new("L", (size, size), 128)
hpix = height.load()
for y in range(size):
for x in range(size):
hpix[x, y] = 128 + int(random.gauss(0, 3))
for y in range(0, size, 4):
for x in range(size):
hpix[x, y] = 110
normal = normal_from_height(height, strength=0.8)
rough = Image.new("L", (size, size), 50)
rpix = rough.load()
for y in range(size):
for x in range(size):
rpix[x, y] = 40 + (hash((x // 16, y // 16)) % 15)
metallic = Image.new("L", (size, size), 128)
save_pbr(TEXTURE_DIR, "accent_team_red", bc, normal, rough, metallic)
# ── Main ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
os.makedirs(TEXTURE_DIR, exist_ok=True)
print("Generating PBR textures (1K)…")
print(" [1/7] wall_concrete_01")
generate_wall_concrete_01()
print(" [2/7] wall_concrete_02")
generate_wall_concrete_02()
print(" [3/7] floor_tile_01")
generate_floor_tile_01()
print(" [4/7] floor_concrete_01")
generate_floor_concrete_01()
print(" [5/7] metal_structural_01")
generate_metal_structural_01()
print(" [6/7] accent_team_blue")
generate_accent_team_blue()
print(" [7/7] accent_team_red")
generate_accent_team_red()
print("\nDone — 7 materials × 4 maps = 28 texture files generated at 1K.")
Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+70
View File
@@ -0,0 +1,70 @@
extends Node
func _ready():
print("=== LightmapGI Baked Lighting Baker ===")
print("Opening scene: res://assets/scenes/modular/kit_demo.tscn")
# Load the scene
var scene = ResourceLoader.load("res://assets/scenes/modular/kit_demo.tscn")
if not scene:
print("ERROR: Failed to load scene!")
get_tree().quit(1)
return
var instance = scene.instantiate()
get_tree().root.add_child(instance)
# Find the LightmapGI node
var lightmap = instance.find_child("LightmapGI", true, false)
if not lightmap:
print("ERROR: No LightmapGI node found in scene!")
get_tree().quit(1)
return
if not lightmap is LightmapGI:
print("ERROR: Found node 'LightmapGI' is not a LightmapGI type!")
get_tree().quit(1)
return
print("LightmapGI found. Configuration:")
print(" Bounces: ", lightmap.bounces)
print(" Texel Scale: ", lightmap.texel_scale)
print(" Texel Subdiv: ", lightmap.texel_subdiv)
print(" Max Texture Size: ", lightmap.max_texture_size)
print(" Interior: ", lightmap.interior)
# Start baking
print("")
print("=== Starting LightmapGI bake ===")
print("(This may take a while...)")
# Wait one frame for scene to fully initialize
await get_tree().process_frame
var result = lightmap.bake(instance, null)
if result != OK:
print("ERROR: Lightmap bake failed with error code: ", result)
get_tree().quit(1)
return
print("")
print("=== Lightmap bake successful! ===")
# Save the scene with baked data
print("Saving scene with baked lightmap data...")
var packed = PackedScene.new()
packed.pack(instance)
var save_result = ResourceSaver.save(packed, "res://assets/scenes/modular/kit_demo.tscn")
if save_result != OK:
print("ERROR: Failed to save scene! Error code: ", save_result)
get_tree().quit(1)
return
print("Scene saved successfully!")
print("Baked lighting data is now embedded in kit_demo.tscn")
print("(The LightmapGI node's light_data property contains the baked data)")
print("")
print("=== Done ===")
get_tree().quit(0)
+6
View File
@@ -0,0 +1,6 @@
extends Node
func _ready():
print("Hello from Godot headless script!")
print("Testing: ", Engine.get_version_info())
get_tree().quit(0)
+29
View File
@@ -0,0 +1,29 @@
extends Node
func _ready():
var materials = [
"res://assets/materials/wall_concrete_01.tres",
"res://assets/materials/wall_concrete_02.tres",
"res://assets/materials/floor_tile_01.tres",
"res://assets/materials/floor_concrete_01.tres",
"res://assets/materials/metal_structural_01.tres",
"res://assets/materials/accent_team_blue.tres",
"res://assets/materials/accent_team_red.tres",
]
var all_ok = true
for mat_path in materials:
var res = ResourceLoader.exists(mat_path)
if res:
var mat = load(mat_path)
print("OK: " + mat_path + " -> " + str(mat.resource_name))
else:
print("FAIL: " + mat_path + " not found")
all_ok = false
if all_ok:
print("\nAll materials loaded successfully.")
get_tree().quit(0)
else:
print("\nSome materials failed to load.")
get_tree().quit(1)
+1
View File
@@ -0,0 +1 @@
uid://dpbnyepcip8vb
+170
View File
@@ -0,0 +1,170 @@
# Tactical Shooter — Art Style Guide
> **Phase 3 deliverable · Modular wall/floor kit, PBR materials at 1K**
> Valorant-direction competitive FPS art direction · Godot 4 Forward+ renderer
---
## 1. Visual Direction
### Target: Valorant-Tier, Not AAA
- **Clean silhouettes** — geometry reads instantly at combat ranges. Avoid noisy trim/paneling.
- **Team-color zones** — CT (blue) and T (red/orange) territories are visually distinct at a glance. Color is used for gameplay read, not decorative variety.
- **Low-fi surface detail** — we suggest roughness and normal variation rather than carving it into geometry. 1K textures carry the detail budget.
- **No photorealism** — stylised realism with clear material categories: concrete, metal, tile, painted surfaces. Each category is immediately identifiable by its roughness/specular signature.
### What This Game Looks Like
| Aspect | Choice | Rationale |
|--------|--------|-----------|
| Lighting | Baked lightmaps + reflection probes | No real-time GI cost |
| Shadows | Baked for static, single cascade for dynamic | 60fps target on GTX 1050 |
| Reflections | Reflection probes at junctions | No SSR/SSAO cost |
| Post-processing | Tonemap only (ACES), no bloom, no DoF | Competitive clarity |
| Fog | Minimal distance fog | Don't hide silhouette readability |
| Textures | 1K PBR (1024 max), no 2K+ | VRAM budget: 2GB texture pool |
---
## 2. Modular Grid & Dimensions
The kit is built on a **2.56m (256cm) base grid** — close to 8ft imperial, powers-of-two friendly for texture tiling.
### Standard Dimensions (meters)
| Piece | Width | Height | Depth | Notes |
|-------|-------|--------|-------|-------|
| Wall straight | 2.56 | 2.56 | 0.16 | Full wall panel |
| Wall corner (L) | 2.56 × 2 leg | 2.56 | 0.16 | 90° outside corner |
| Wall doorway | 2.56 | 2.56 | 0.16 | 0.80m × 2.00m door cutout |
| Wall window | 2.56 | 2.56 | 0.16 | 1.20m × 0.80m window cutout |
| Wall endcap | 0.32 | 2.56 | 0.16 | Edge trim / termination |
| Floor slab | 2.56 | 0.08 | 2.56 | Thin slab |
| Pillar (square) | 0.64 | 2.56 | 0.64 | Structural column |
| Ceiling beam | 3.84 | 0.32 | 0.32 | Metal beam, 1.5U length |
| Accent panel | 1.28 | 0.64 | 0.08 | Team-color wall accent |
### Grid Alignment
- All pieces snap to a 1.28m (half-unit) grid in the map editor.
- Wall thickness is always 0.16m. Floors are 0.08m.
- Doorway bottom is flush with floor level. Window center is 1.58m from floor (eye level).
- Pillars occupy 0.64m × 0.64m on the floor grid (¼ of a full tile).
---
## 3. Material Library
### Material Standards
Every material is a **StandardMaterial3D** with PBR textures at **1024×1024 (1K)**:
| Texture Map | Format | Notes |
|-------------|--------|-------|
| Base Color | sRGB PNG | Diffuse/albedo with subtle colour variation |
| Normal | Linear PNG | From height map via Sobel, strength scaled per material |
| Roughness | Linear PNG (grayscale) | White = rough (255), Black = smooth (0) |
| Metallic | Linear PNG (grayscale) | White = metal (255), Black = dielectric (0) |
Texture filter: **Linear** (no mipmaps for modular kit; mipmap cost is unnecessary at 1K on a 2.56m tile with the camera ≤40m from most surfaces).
### Material Palette (7 materials)
| # | Name | Roughness | Metallic | Normal Scale | UV Tiling | Use Case |
|---|------|-----------|----------|--------------|-----------|----------|
| 1 | `wall_concrete_01` | 0.85 | 0.0 | 0.5 | 1.0 | Primary wall — light grey concrete |
| 2 | `wall_concrete_02` | 0.75 | 0.0 | 0.4 | 1.0 | Accent wall — darker grey |
| 3 | `floor_tile_01` | 0.60 | 0.0 | 1.0 | 2.0 | Interior — ceramic checkered tile |
| 4 | `floor_concrete_01` | 0.90 | 0.0 | 0.8 | 2.0 | Industrial — dark warehouse floor |
| 5 | `metal_structural_01` | 0.30 | 1.0 | 0.3 | 1.0 | Beams, vents, supports |
| 6 | `accent_team_blue` | 0.35 | 0.5 | 0.3 | 1.0 | CT territory — blue painted panels |
| 7 | `accent_team_red` | 0.35 | 0.5 | 0.3 | 1.0 | T territory — red/orange painted panels |
### Team Color Usage
- **CT (Blue)**: RGB ~(40, 80, 200) — deep blue, low saturation. Applied to accent panels, not primary walls.
- **T (Red/Orange)**: RGB ~(200, 60, 30) — burnt red-orange. Applied to accent panels, not primary walls.
- Neutral zones (uncontested) use `wall_concrete_01/02` and `floor_concrete_01`.
- Spike plant zones / bomb sites get team-colour floor accents.
---
## 4. Technical Specifications
### Texture Budget
| Category | Count | Resolution | VRAM (approx) |
|----------|-------|------------|---------------|
| Wall materials (2) | 8 maps | 1K | ~21 MB |
| Floor materials (2) | 8 maps | 1K | ~21 MB |
| Metal (1) | 4 maps | 1K | ~10 MB |
| Team accents (2) | 8 maps | 1K | ~21 MB |
| **Total (7 materials)** | **28 maps** | **1K** | **~75 MB** |
VRAM headroom for lighting data, shadow maps, and models: ~1.9 GB of the 2 GB budget.
### Performance Targets (per the visuals architecture)
| Metric | Target |
|--------|--------|
| Frame time | 16ms (60fps) |
| GPU budget | 8ms |
| CPU budget | 6ms (6ms headroom for gameplay) |
| Draw calls | 15002000/frame |
| LOD0 tris/wall | 5002000 |
### LOD Strategy (for future mesh pass)
When modular CSG pieces are replaced with custom meshes:
| LOD | Distance | Triangle Budget | Notes |
|-----|----------|-----------------|-------|
| LOD0 | 015m | 100% | Full detail |
| LOD1 | 1540m | 50% | Remove panel gaps, merge edges |
| LOD2 | 4080m | 25% | Planar collapse, decimate |
| LOD3 | 80m+ | Cull | Not visible at competitive sightlines |
---
## 5. Lighting Guidelines (for t_p3_lighting)
- **LightmapGI** on all static geometry. Texel density: 48 per unit on walls, 816 on focal surfaces.
- **Reflection probes** at corridor junctions, open plaza areas, chokepoints. Probe box size: 4U (10.24m) for corridors, 8U+ for open areas.
- **No SDFGI**, no real-time GI. 1-2 dynamic directional lights maximum (sun + maybe a single fill).
- Lightmap UV2: all modular CSG pieces export lightmap UVs automatically.
---
## 6. Mapping Guidelines (for map makers)
- **Start every map** by placing a `KitDemo` scene to see the palette.
- Snap walls to the 1.28m half-grid. Full grid is 2.56m.
- Doorway walls are exactly 2.56m wide × 2.56m tall — stack a wall piece on top if headroom is needed.
- Use `wall_concrete_02` for interior partitions, `wall_concrete_01` for exterior-facing walls.
- Cover all floor areas with `floor_tile_01` (interiors) or `floor_concrete_01` (exteriors/warehouses).
- Team accent panels are decorative only — they do not affect gameplay collision or hitbox.
---
## 7. Export & Build Pipeline
- Client project lives in `client/` with its own `project.godot`.
- Modular kit scenes are under `client/assets/scenes/modular/`.
- Materials under `client/assets/materials/`.
- Textures under `client/assets/textures/<material_name>/`.
- Textures are generated by `client/assets/textures/generate_textures.py`.
- Materials are generated by `client/assets/materials/generate_materials.py`.
- Scenes are generated by `client/assets/scenes/modular/generate_scenes.py`.
### Adding a New Material
1. Add a generator function in `generate_textures.py`
2. Run it to produce 1K PBR maps
3. Add a `.tres` entry in `generate_materials.py`
4. Run it to produce the material resource
5. Optionally create a scene using the new material
---
*This guide is a living document — update as the visual direction evolves.*
+16 -8
View File
@@ -2,14 +2,22 @@
## Scope
Art pass, baked lighting, performance profiling, LOD + occlusion culling for the competitive tactical shooter.
## Dependency Chain (Kanban)
```
t_p3_artkit (modular wall/floor kit + PBR materials 1K + art style guide)
└── t_p3_lighting (LightmapGI baked lighting + reflection probes)
└── t_p3_profile (perf budget + LOD + occlusion culling)
```
|
|## Dependency Chain (Kanban)
|
|```
|t_p3_artkit (modular wall/floor kit + PBR materials 1K + art style guide) ✅ DONE
| └── t_p3_lighting (LightmapGI baked lighting + reflection probes)
| └── t_p3_profile (perf budget + LOD + occlusion culling)
|```
|
|## Deliverables
|
|- **Art style guide**: [docs/art-style-guide.md](docs/art-style-guide.md) — visual direction, modular grid specs, material palette, mapping guidelines
|- **Modular kit scenes**: `client/assets/scenes/modular/` — 11 CSG-based wall/floor/structural pieces + kit_demo showcase scene
|- **PBR materials**: `client/assets/materials/` — 7 StandardMaterial3D .tres files
|- **1K textures**: `client/assets/textures/` — 28 procedural PBR maps (basecolor, normal, roughness, metallic × 7 materials)
|- **Project config**: `client/project.godot` — Godot 4 Forward+ renderer, 128-tick physics, LightmapGI defaults
## Art Style