Rendering and presentation - Shared surface-material module (src/game/materials.gd) with per-surface roughness/metallic/uv profiles; ACES tonemapping, glow, fog and phase-driven sky energy. WorldBuilder rebuilt on those profiles with facade windows that light up, door trim/handles, curb, lane dashes, bollards, bench, bin and planter foliage. - NPC silhouettes: per-role clothing palettes, caps/hoods/backpacks/sunglasses, hands and shoes, arm/leg walk swing. - Six generated low-poly GLB props (bollards, bench, bin, planter, newspaper box, hydrant) built by tools/model_pipeline.py, loaded by WorldBuilder with a procedural fallback; ADR-0012. Selection and movement - Focus ranking by view angle then distance (NpcFocus.rank_indices); Tab and the mouse wheel cycle nearby people initially, right click clears focus with a re-acquire grace, HUD shows the candidate count, Shift walks at half speed. ADR-0011. Windowed smoke extended to cover wheel cycling and clearing. QA tooling - screen_designs.py catalogue grown to 19 screens (8 scene, 11 UI) plus a `list` subcommand; draft generation keeps existing AI references unless --force. Web export - New Web preset using the non-threaded Godot template (static hosting, no COOP/COEP). tools/build_releases.sh exports build/web and packages build/step-line-web-<ver>.zip; publish_release.sh uploads it with the desktop artifacts. test_web_export.gd covers the preset and release wiring. Verification - Headless suite 177 -> 181, Python tooling 53 tests, all eight smokes pass (including the windowed focus/wheel smoke). Web bundle serves index.html, index.wasm (35 MB) and index.pck (30 MB) with HTTP 200 from a static server. - Version 0.3.0; README, PLAN, RELEASING, TODO/DONE and MEMORY updated.
138 lines
6.0 KiB
GDScript
138 lines
6.0 KiB
GDScript
class_name SurfaceMaterials
|
|
extends RefCounted
|
|
## Shared surface recipes for the procedural world (ADR-0012).
|
|
##
|
|
## Godot's `StandardMaterial3D` defaults are flat: every surface comes out with
|
|
## the same roughness and a plain albedo, which is what made the street read as
|
|
## grey boxes. These helpers give each surface a physically plausible profile
|
|
## (brick is rough, glass is smooth, metal is metallic) and keep the textures,
|
|
## UV scaling, and texture filtering in one place so the world builder, the NPC
|
|
## avatars, and the props all look like one scene.
|
|
|
|
const T_BRICK := preload("res://assets/textures/ai/game_ready/brick_facade.png")
|
|
const T_CONCRETE := preload("res://assets/textures/ai/game_ready/concrete.png")
|
|
const T_ASPHALT := preload("res://assets/textures/ai/game_ready/asphalt.png")
|
|
const T_AWNING := preload("res://assets/textures/ai/game_ready/awning.png")
|
|
|
|
## Roughness / metallic pairs per surface role. Exposed so tests and tooling can
|
|
## assert the profiles instead of hard-coding numbers twice.
|
|
const PROFILES := {
|
|
"brick": {"roughness": 0.88, "metallic": 0.0},
|
|
"concrete": {"roughness": 0.94, "metallic": 0.0},
|
|
"asphalt": {"roughness": 0.72, "metallic": 0.0},
|
|
"awning": {"roughness": 0.80, "metallic": 0.0},
|
|
"stone": {"roughness": 0.88, "metallic": 0.0},
|
|
"metal": {"roughness": 0.32, "metallic": 0.75},
|
|
"painted_metal": {"roughness": 0.48, "metallic": 0.35},
|
|
"wood": {"roughness": 0.70, "metallic": 0.0},
|
|
"cloth": {"roughness": 0.85, "metallic": 0.0},
|
|
"skin": {"roughness": 0.62, "metallic": 0.0},
|
|
"glass": {"roughness": 0.12, "metallic": 0.10},
|
|
"rubber": {"roughness": 0.95, "metallic": 0.0},
|
|
}
|
|
|
|
static func profile(role: String) -> Dictionary:
|
|
return PROFILES.get(role, PROFILES["concrete"])
|
|
|
|
## Base textured surface with a named profile.
|
|
static func textured(texture: Texture2D, tint: Color, uv_scale: Vector2,
|
|
role: String = "concrete") -> StandardMaterial3D:
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_texture = texture
|
|
mat.albedo_color = tint
|
|
mat.uv1_scale = Vector3(uv_scale.x, uv_scale.y, 1.0)
|
|
mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
|
|
mat.roughness = float(profile(role).get("roughness", 0.9))
|
|
mat.metallic = float(profile(role).get("metallic", 0.0))
|
|
return mat
|
|
|
|
static func brick(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(10, 5)) -> StandardMaterial3D:
|
|
return textured(T_BRICK, tint, uv_scale, "brick")
|
|
|
|
static func concrete(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(9, 2.4)) -> StandardMaterial3D:
|
|
return textured(T_CONCRETE, tint, uv_scale, "concrete")
|
|
|
|
static func asphalt(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(30, 7)) -> StandardMaterial3D:
|
|
var mat := textured(T_ASPHALT, tint, uv_scale, "asphalt")
|
|
# Damp asphalt at night picks up the street lamps and the marquee.
|
|
mat.metallic_specular = 0.6
|
|
return mat
|
|
|
|
static func awning(tint: Color = Color(1, 1, 1), uv_scale: Vector2 = Vector2(4, 1)) -> StandardMaterial3D:
|
|
return textured(T_AWNING, tint, uv_scale, "awning")
|
|
|
|
## Flat-coloured surface with a profile (props, trim, limbs, clothing).
|
|
static func flat(color: Color, role: String = "concrete") -> StandardMaterial3D:
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = color
|
|
mat.roughness = float(profile(role).get("roughness", 0.9))
|
|
mat.metallic = float(profile(role).get("metallic", 0.0))
|
|
return mat
|
|
|
|
## Self-lit surface (marquee bars, lamp bulbs, lock indicator).
|
|
static func emissive(color: Color, energy: float = 1.4) -> StandardMaterial3D:
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = color
|
|
mat.emission_enabled = true
|
|
mat.emission = color
|
|
mat.emission_energy_multiplier = energy
|
|
mat.roughness = 0.4
|
|
return mat
|
|
|
|
## Dark glass for windows and the entrance door. The alpha keeps the interior
|
|
## visible, and the low roughness gives the facade a highlight instead of a hole.
|
|
static func glass(color: Color = Color(0.42, 0.56, 0.72, 0.75)) -> StandardMaterial3D:
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = color
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
mat.roughness = float(profile("glass").get("roughness", 0.12))
|
|
mat.metallic = 0.15
|
|
mat.metallic_specular = 0.8
|
|
return mat
|
|
|
|
## Window pane that can be lit from the inside at night. Solid dark glass reads
|
|
## as a window at midday; alpha glass just looked like white paper on the brick.
|
|
static func window_pane() -> StandardMaterial3D:
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = Color(0.09, 0.12, 0.17)
|
|
mat.roughness = 0.18
|
|
mat.metallic = 0.35
|
|
mat.metallic_specular = 0.6
|
|
mat.emission_enabled = true
|
|
mat.emission = Color(1.0, 0.86, 0.62)
|
|
mat.emission_energy_multiplier = 0.0
|
|
return mat
|
|
|
|
## Environment look shared by every phase: ACES tonemapping, soft glow, aerial
|
|
## fog for depth, and a mild contrast/saturation lift. Phase values come from
|
|
## `GameManager.PHASE_LIGHTING`.
|
|
static func configure_environment(env: Environment, cfg: Dictionary) -> void:
|
|
if env == null:
|
|
return
|
|
env.background_mode = Environment.BG_SKY
|
|
env.tonemap_mode = Environment.TONE_MAPPER_ACES
|
|
env.tonemap_exposure = float(cfg.get("exposure", 1.0))
|
|
env.tonemap_white = 6.0
|
|
env.ambient_light_energy = float(cfg.get("ambient_energy", 0.5))
|
|
env.reflected_light_source = Environment.REFLECTION_SOURCE_SKY
|
|
# Glow makes the marquee, lamp bulbs, and windows read as light sources.
|
|
env.glow_enabled = true
|
|
env.glow_normalized = true
|
|
env.glow_intensity = float(cfg.get("glow", 0.55))
|
|
env.glow_strength = 1.0
|
|
env.glow_bloom = 0.08
|
|
env.glow_hdr_threshold = 1.0
|
|
env.glow_blend_mode = Environment.GLOW_BLEND_MODE_SOFTLIGHT
|
|
# Aerial perspective: the skyline ring sits ~200 m out, so light fog gives the
|
|
# distance its haze without hiding the street.
|
|
env.fog_enabled = true
|
|
env.fog_light_color = cfg.get("fog_color", Color(0.72, 0.78, 0.88))
|
|
env.fog_light_energy = float(cfg.get("fog_energy", 0.7))
|
|
env.fog_density = float(cfg.get("fog_density", 0.0035))
|
|
env.fog_aerial_perspective = 0.25
|
|
env.fog_sky_affect = 0.4
|
|
env.adjustment_enabled = true
|
|
env.adjustment_brightness = 1.0
|
|
env.adjustment_contrast = float(cfg.get("contrast", 1.06))
|
|
env.adjustment_saturation = float(cfg.get("saturation", 1.08))
|