Bake library AO and lift environment textures
Full build / Full desktop build + optional release (push) Successful in 3m43s
Full build / Import + headless tests (push) Successful in 29s

Phase 4 of the v0.6.0 graphics plan:

- tools/bake_ao.py bakes 256² AO maps for every Blender character/gear/prop
  (Cycles, existing UVs) next to each GLB; Materials.apply_ao() wires the red
  channel into character slots, gear, and props. Compatibility has no SSAO.
- Regenerated brick/concrete/asphalt at 1024² with OpenRouter, derived tangent
  normal maps via tools/make_normal_maps.py, and wired them through
  SurfaceMaterials.textured() without changing UV scales.
- Evaluated Forward+: boots on desktop Vulkan, but the renderer is project-wide
  and Web must stay GL Compatibility; v0.6.0 keeps Compatibility (decision in
  MEMORY/plan). A desktop-only override is deferred.
- Recaptured screenshots; compare 11 good / 0 bad. Fixed marquee text alignment.
- Tests: baked AO maps exist for all 46 assets; prop/character AO material
  checks; headless 222/222, Python 69/69, all smokes pass.
- Version 0.6.0; ADR-0013 accepted; README/architecture/blender-assets/plan and
  TODO/DONE updated.
This commit is contained in:
zwitschi committed 2026-11-01 15:07:27 +01:00
1 parent d8cfddbf89
commit d872034388
78 files changed
+390 -28

No files matched your search

+33 -4
View File
@@ -13,6 +13,9 @@ 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")
const T_BRICK_NORMAL := preload("res://assets/textures/ai/game_ready/brick_facade_normal.png")
const T_CONCRETE_NORMAL := preload("res://assets/textures/ai/game_ready/concrete_normal.png")
const T_ASPHALT_NORMAL := preload("res://assets/textures/ai/game_ready/asphalt_normal.png")
## Roughness / metallic pairs per surface role. Exposed so tests and tooling can
## assert the profiles instead of hard-coding numbers twice.
@@ -40,14 +43,40 @@ static func npc_slot(source: Material, tint: Color) -> StandardMaterial3D:
material.albedo_color = tint
return material
## Wire a baked AO map (red channel) into every surface of an imported mesh.
## Compatibility has no SSAO, so the library assets carry their own AO.
static func apply_ao(mesh_instance: MeshInstance3D, texture: Texture2D) -> void:
if mesh_instance == null or mesh_instance.mesh == null or texture == null:
return
for index in mesh_instance.mesh.get_surface_count():
var base := mesh_instance.get_surface_override_material(index)
if base == null:
base = mesh_instance.mesh.surface_get_material(index)
var material := base.duplicate() as StandardMaterial3D
if material == null:
continue
material.ao_enabled = true
material.ao_texture = texture
material.ao_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED
mesh_instance.set_surface_override_material(index, material)
static func load_ao(path: String) -> Texture2D:
if not ResourceLoader.exists(path):
return null
return ResourceLoader.load(path) as Texture2D
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:
role: String = "concrete", normal: Texture2D = null) -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.albedo_texture = texture
if normal != null:
mat.normal_enabled = true
mat.normal_texture = normal
mat.normal_scale = 0.8
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
@@ -56,13 +85,13 @@ static func textured(texture: Texture2D, tint: Color, uv_scale: Vector2,
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")
return textured(T_BRICK, tint, uv_scale, "brick", T_BRICK_NORMAL)
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")
return textured(T_CONCRETE, tint, uv_scale, "concrete", T_CONCRETE_NORMAL)
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")
var mat := textured(T_ASPHALT, tint, uv_scale, "asphalt", T_ASPHALT_NORMAL)
# Damp asphalt at night picks up the street lamps and the marquee.
mat.metallic_specular = 0.6
return mat