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.
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
After Width: | Height: | Size: 540 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 973 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 539 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 973 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 972 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
@@ -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.")
|
||||
|
After Width: | Height: | Size: 936 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
extends Node
|
||||
|
||||
func _ready():
|
||||
print("Hello from Godot headless script!")
|
||||
print("Testing: ", Engine.get_version_info())
|
||||
get_tree().quit(0)
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dpbnyepcip8vb
|
||||
@@ -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/<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.*
|
||||
@@ -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
|
||||
|
||||
|
||||