diff --git a/client/assets/materials/accent_team_blue.tres b/client/assets/materials/accent_team_blue.tres new file mode 100644 index 0000000..6d7c279 --- /dev/null +++ b/client/assets/materials/accent_team_blue.tres @@ -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) diff --git a/client/assets/materials/accent_team_red.tres b/client/assets/materials/accent_team_red.tres new file mode 100644 index 0000000..68a3d58 --- /dev/null +++ b/client/assets/materials/accent_team_red.tres @@ -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) diff --git a/client/assets/materials/floor_concrete_01.tres b/client/assets/materials/floor_concrete_01.tres new file mode 100644 index 0000000..3d2ade4 --- /dev/null +++ b/client/assets/materials/floor_concrete_01.tres @@ -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) diff --git a/client/assets/materials/floor_tile_01.tres b/client/assets/materials/floor_tile_01.tres new file mode 100644 index 0000000..4cc02be --- /dev/null +++ b/client/assets/materials/floor_tile_01.tres @@ -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) diff --git a/client/assets/materials/generate_materials.py b/client/assets/materials/generate_materials.py new file mode 100644 index 0000000..1bec1ef --- /dev/null +++ b/client/assets/materials/generate_materials.py @@ -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() diff --git a/client/assets/materials/metal_structural_01.tres b/client/assets/materials/metal_structural_01.tres new file mode 100644 index 0000000..0d44b63 --- /dev/null +++ b/client/assets/materials/metal_structural_01.tres @@ -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) diff --git a/client/assets/materials/wall_concrete_01.tres b/client/assets/materials/wall_concrete_01.tres new file mode 100644 index 0000000..b6fe0ff --- /dev/null +++ b/client/assets/materials/wall_concrete_01.tres @@ -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) diff --git a/client/assets/materials/wall_concrete_02.tres b/client/assets/materials/wall_concrete_02.tres new file mode 100644 index 0000000..850f942 --- /dev/null +++ b/client/assets/materials/wall_concrete_02.tres @@ -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) diff --git a/client/assets/scenes/modular/accent_blue_panel.tscn b/client/assets/scenes/modular/accent_blue_panel.tscn new file mode 100644 index 0000000..57abd2d --- /dev/null +++ b/client/assets/scenes/modular/accent_blue_panel.tscn @@ -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 diff --git a/client/assets/scenes/modular/accent_red_panel.tscn b/client/assets/scenes/modular/accent_red_panel.tscn new file mode 100644 index 0000000..c3d6d28 --- /dev/null +++ b/client/assets/scenes/modular/accent_red_panel.tscn @@ -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 diff --git a/client/assets/scenes/modular/beam_01.tscn b/client/assets/scenes/modular/beam_01.tscn new file mode 100644 index 0000000..5800a22 --- /dev/null +++ b/client/assets/scenes/modular/beam_01.tscn @@ -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 diff --git a/client/assets/scenes/modular/floor_tile_01.tscn b/client/assets/scenes/modular/floor_tile_01.tscn new file mode 100644 index 0000000..d1855b4 --- /dev/null +++ b/client/assets/scenes/modular/floor_tile_01.tscn @@ -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 diff --git a/client/assets/scenes/modular/floor_tile_ceramic_01.tscn b/client/assets/scenes/modular/floor_tile_ceramic_01.tscn new file mode 100644 index 0000000..a01df5f --- /dev/null +++ b/client/assets/scenes/modular/floor_tile_ceramic_01.tscn @@ -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 diff --git a/client/assets/scenes/modular/generate_scenes.py b/client/assets/scenes/modular/generate_scenes.py new file mode 100644 index 0000000..9282820 --- /dev/null +++ b/client/assets/scenes/modular/generate_scenes.py @@ -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() diff --git a/client/assets/scenes/modular/kit_demo.gd b/client/assets/scenes/modular/kit_demo.gd new file mode 100644 index 0000000..58b377f --- /dev/null +++ b/client/assets/scenes/modular/kit_demo.gd @@ -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 diff --git a/client/assets/scenes/modular/kit_demo.tscn b/client/assets/scenes/modular/kit_demo.tscn new file mode 100644 index 0000000..35db4a0 --- /dev/null +++ b/client/assets/scenes/modular/kit_demo.tscn @@ -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 diff --git a/client/assets/scenes/modular/pillar_01.tscn b/client/assets/scenes/modular/pillar_01.tscn new file mode 100644 index 0000000..0478599 --- /dev/null +++ b/client/assets/scenes/modular/pillar_01.tscn @@ -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 diff --git a/client/assets/scenes/modular/wall_corner_01.tscn b/client/assets/scenes/modular/wall_corner_01.tscn new file mode 100644 index 0000000..6a9e200 --- /dev/null +++ b/client/assets/scenes/modular/wall_corner_01.tscn @@ -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) diff --git a/client/assets/scenes/modular/wall_doorway_01.tscn b/client/assets/scenes/modular/wall_doorway_01.tscn new file mode 100644 index 0000000..8702323 --- /dev/null +++ b/client/assets/scenes/modular/wall_doorway_01.tscn @@ -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) diff --git a/client/assets/scenes/modular/wall_endcap_01.tscn b/client/assets/scenes/modular/wall_endcap_01.tscn new file mode 100644 index 0000000..11ef0e4 --- /dev/null +++ b/client/assets/scenes/modular/wall_endcap_01.tscn @@ -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 diff --git a/client/assets/scenes/modular/wall_straight_01.tscn b/client/assets/scenes/modular/wall_straight_01.tscn new file mode 100644 index 0000000..67c3b30 --- /dev/null +++ b/client/assets/scenes/modular/wall_straight_01.tscn @@ -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 diff --git a/client/assets/scenes/modular/wall_window_01.tscn b/client/assets/scenes/modular/wall_window_01.tscn new file mode 100644 index 0000000..36ba9c1 --- /dev/null +++ b/client/assets/scenes/modular/wall_window_01.tscn @@ -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) diff --git a/client/assets/textures/accent_team_blue/basecolor.png b/client/assets/textures/accent_team_blue/basecolor.png new file mode 100644 index 0000000..fbc3837 Binary files /dev/null and b/client/assets/textures/accent_team_blue/basecolor.png differ diff --git a/client/assets/textures/accent_team_blue/metallic.png b/client/assets/textures/accent_team_blue/metallic.png new file mode 100644 index 0000000..e2e12b8 Binary files /dev/null and b/client/assets/textures/accent_team_blue/metallic.png differ diff --git a/client/assets/textures/accent_team_blue/normal.png b/client/assets/textures/accent_team_blue/normal.png new file mode 100644 index 0000000..9c578e6 Binary files /dev/null and b/client/assets/textures/accent_team_blue/normal.png differ diff --git a/client/assets/textures/accent_team_blue/roughness.png b/client/assets/textures/accent_team_blue/roughness.png new file mode 100644 index 0000000..5230bdd Binary files /dev/null and b/client/assets/textures/accent_team_blue/roughness.png differ diff --git a/client/assets/textures/accent_team_red/basecolor.png b/client/assets/textures/accent_team_red/basecolor.png new file mode 100644 index 0000000..ba05f9d Binary files /dev/null and b/client/assets/textures/accent_team_red/basecolor.png differ diff --git a/client/assets/textures/accent_team_red/metallic.png b/client/assets/textures/accent_team_red/metallic.png new file mode 100644 index 0000000..e2e12b8 Binary files /dev/null and b/client/assets/textures/accent_team_red/metallic.png differ diff --git a/client/assets/textures/accent_team_red/normal.png b/client/assets/textures/accent_team_red/normal.png new file mode 100644 index 0000000..ba851f9 Binary files /dev/null and b/client/assets/textures/accent_team_red/normal.png differ diff --git a/client/assets/textures/accent_team_red/roughness.png b/client/assets/textures/accent_team_red/roughness.png new file mode 100644 index 0000000..5230bdd Binary files /dev/null and b/client/assets/textures/accent_team_red/roughness.png differ diff --git a/client/assets/textures/floor_concrete_01/basecolor.png b/client/assets/textures/floor_concrete_01/basecolor.png new file mode 100644 index 0000000..cb0fb2a Binary files /dev/null and b/client/assets/textures/floor_concrete_01/basecolor.png differ diff --git a/client/assets/textures/floor_concrete_01/metallic.png b/client/assets/textures/floor_concrete_01/metallic.png new file mode 100644 index 0000000..fe0e124 Binary files /dev/null and b/client/assets/textures/floor_concrete_01/metallic.png differ diff --git a/client/assets/textures/floor_concrete_01/normal.png b/client/assets/textures/floor_concrete_01/normal.png new file mode 100644 index 0000000..daf55d3 Binary files /dev/null and b/client/assets/textures/floor_concrete_01/normal.png differ diff --git a/client/assets/textures/floor_concrete_01/roughness.png b/client/assets/textures/floor_concrete_01/roughness.png new file mode 100644 index 0000000..acafb6e Binary files /dev/null and b/client/assets/textures/floor_concrete_01/roughness.png differ diff --git a/client/assets/textures/floor_tile_01/basecolor.png b/client/assets/textures/floor_tile_01/basecolor.png new file mode 100644 index 0000000..40a25b7 Binary files /dev/null and b/client/assets/textures/floor_tile_01/basecolor.png differ diff --git a/client/assets/textures/floor_tile_01/metallic.png b/client/assets/textures/floor_tile_01/metallic.png new file mode 100644 index 0000000..fe0e124 Binary files /dev/null and b/client/assets/textures/floor_tile_01/metallic.png differ diff --git a/client/assets/textures/floor_tile_01/normal.png b/client/assets/textures/floor_tile_01/normal.png new file mode 100644 index 0000000..f0742a1 Binary files /dev/null and b/client/assets/textures/floor_tile_01/normal.png differ diff --git a/client/assets/textures/floor_tile_01/roughness.png b/client/assets/textures/floor_tile_01/roughness.png new file mode 100644 index 0000000..a519c44 Binary files /dev/null and b/client/assets/textures/floor_tile_01/roughness.png differ diff --git a/client/assets/textures/generate_textures.py b/client/assets/textures/generate_textures.py new file mode 100644 index 0000000..c23af4f --- /dev/null +++ b/client/assets/textures/generate_textures.py @@ -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.") diff --git a/client/assets/textures/metal_structural_01/basecolor.png b/client/assets/textures/metal_structural_01/basecolor.png new file mode 100644 index 0000000..bad30c5 Binary files /dev/null and b/client/assets/textures/metal_structural_01/basecolor.png differ diff --git a/client/assets/textures/metal_structural_01/metallic.png b/client/assets/textures/metal_structural_01/metallic.png new file mode 100644 index 0000000..a38d325 Binary files /dev/null and b/client/assets/textures/metal_structural_01/metallic.png differ diff --git a/client/assets/textures/metal_structural_01/normal.png b/client/assets/textures/metal_structural_01/normal.png new file mode 100644 index 0000000..256fcb8 Binary files /dev/null and b/client/assets/textures/metal_structural_01/normal.png differ diff --git a/client/assets/textures/metal_structural_01/roughness.png b/client/assets/textures/metal_structural_01/roughness.png new file mode 100644 index 0000000..5c51683 Binary files /dev/null and b/client/assets/textures/metal_structural_01/roughness.png differ diff --git a/client/assets/textures/wall_concrete_01/basecolor.png b/client/assets/textures/wall_concrete_01/basecolor.png new file mode 100644 index 0000000..64ebcc1 Binary files /dev/null and b/client/assets/textures/wall_concrete_01/basecolor.png differ diff --git a/client/assets/textures/wall_concrete_01/metallic.png b/client/assets/textures/wall_concrete_01/metallic.png new file mode 100644 index 0000000..fe0e124 Binary files /dev/null and b/client/assets/textures/wall_concrete_01/metallic.png differ diff --git a/client/assets/textures/wall_concrete_01/normal.png b/client/assets/textures/wall_concrete_01/normal.png new file mode 100644 index 0000000..d2e521b Binary files /dev/null and b/client/assets/textures/wall_concrete_01/normal.png differ diff --git a/client/assets/textures/wall_concrete_01/roughness.png b/client/assets/textures/wall_concrete_01/roughness.png new file mode 100644 index 0000000..795dce1 Binary files /dev/null and b/client/assets/textures/wall_concrete_01/roughness.png differ diff --git a/client/assets/textures/wall_concrete_02/basecolor.png b/client/assets/textures/wall_concrete_02/basecolor.png new file mode 100644 index 0000000..64ebcc1 Binary files /dev/null and b/client/assets/textures/wall_concrete_02/basecolor.png differ diff --git a/client/assets/textures/wall_concrete_02/metallic.png b/client/assets/textures/wall_concrete_02/metallic.png new file mode 100644 index 0000000..fe0e124 Binary files /dev/null and b/client/assets/textures/wall_concrete_02/metallic.png differ diff --git a/client/assets/textures/wall_concrete_02/normal.png b/client/assets/textures/wall_concrete_02/normal.png new file mode 100644 index 0000000..3a50d85 Binary files /dev/null and b/client/assets/textures/wall_concrete_02/normal.png differ diff --git a/client/assets/textures/wall_concrete_02/roughness.png b/client/assets/textures/wall_concrete_02/roughness.png new file mode 100644 index 0000000..5a820b0 Binary files /dev/null and b/client/assets/textures/wall_concrete_02/roughness.png differ diff --git a/client/scripts/bake_lighting.gd b/client/scripts/bake_lighting.gd new file mode 100644 index 0000000..62b68a8 --- /dev/null +++ b/client/scripts/bake_lighting.gd @@ -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) diff --git a/client/scripts/test_headless.gd b/client/scripts/test_headless.gd new file mode 100644 index 0000000..a45cf77 --- /dev/null +++ b/client/scripts/test_headless.gd @@ -0,0 +1,6 @@ +extends Node + +func _ready(): + print("Hello from Godot headless script!") + print("Testing: ", Engine.get_version_info()) + get_tree().quit(0) diff --git a/client/test_materials.gd b/client/test_materials.gd new file mode 100644 index 0000000..6f61a60 --- /dev/null +++ b/client/test_materials.gd @@ -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) diff --git a/client/test_materials.gd.uid b/client/test_materials.gd.uid new file mode 100644 index 0000000..17dbbd3 --- /dev/null +++ b/client/test_materials.gd.uid @@ -0,0 +1 @@ +uid://dpbnyepcip8vb diff --git a/docs/art-style-guide.md b/docs/art-style-guide.md new file mode 100644 index 0000000..0bc1294 --- /dev/null +++ b/docs/art-style-guide.md @@ -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 | 1500–2000/frame | +| LOD0 tris/wall | 500–2000 | + +### LOD Strategy (for future mesh pass) + +When modular CSG pieces are replaced with custom meshes: + +| LOD | Distance | Triangle Budget | Notes | +|-----|----------|-----------------|-------| +| LOD0 | 0–15m | 100% | Full detail | +| LOD1 | 15–40m | 50% | Remove panel gaps, merge edges | +| LOD2 | 40–80m | 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: 4–8 per unit on walls, 8–16 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//`. +- 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.* diff --git a/docs/visuals-architecture.md b/docs/visuals-architecture.md index 9d4e03a..57550b7 100644 --- a/docs/visuals-architecture.md +++ b/docs/visuals-architecture.md @@ -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