saumwacht-godot/tools/verify_material_sides.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

82 lines
3.6 KiB
GDScript

extends Node
## Render actual presentation materials from both sides; headless cannot prove visibility.
var checks: int = 0
var failures: Array[String] = []
func check(ok: bool, label: String) -> void:
checks += 1
if not ok:
failures.append(label)
printerr("MATERIAL_FAIL: ", label)
func _ready() -> void:
call_deferred("run")
func run() -> void:
if DisplayServer.get_name() == "headless":
printerr("This material test requires a rendering display.")
await Soundscape.quit_game(1)
return
Soundscape.enabled = false
var view := SubViewport.new()
view.size = Vector2i(512, 256)
view.own_world_3d = true
view.render_target_update_mode = SubViewport.UPDATE_ALWAYS
add_child(view)
var world := WorldEnvironment.new()
world.environment = Environment.new()
world.environment.background_mode = Environment.BG_COLOR
world.environment.background_color = Color(.01, .01, .01)
world.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
world.environment.ambient_light_color = Color.WHITE
world.environment.ambient_light_energy = 1.0
world.environment.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED
view.add_child(world)
var camera := Camera3D.new()
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
camera.size = 2.4
camera.position = Vector3(0, 0, 3)
view.add_child(camera)
var front := MeshInstance3D.new()
front.mesh = QuadMesh.new()
front.position.x = -.6
view.add_child(front)
var back := MeshInstance3D.new()
back.mesh = front.mesh
back.position.x = .6
back.rotation.y = PI
view.add_child(back)
var source: Node3D = load("res://assets/characters/quellgeflecht.glb").instantiate()
var surfaces: Dictionary = {}
for node: Node in source.find_children("*", "MeshInstance3D", true, false):
for index: int in range(node.mesh.get_surface_count()):
var material: Material = node.get_active_material(index)
if material.resource_name in ["QG_Leaf", "QG_Moss", "QG_Petal"]:
surfaces[material.resource_name] = material
check(surfaces.size() == 3, "all three imported foliage materials present")
var measurements: Dictionary = {}
for key: String in surfaces:
var original: StandardMaterial3D = surfaces[key]
check(original.cull_mode == BaseMaterial3D.CULL_DISABLED, key + " authored double-sided")
var material: Material = AssetLibrary.presentation_material(original)
front.material_override = material
back.material_override = material
for frame in range(5):
await get_tree().process_frame
await RenderingServer.frame_post_draw
var pixels := view.get_texture().get_image()
var front_pixel := camera.unproject_position(front.position)
var back_pixel := camera.unproject_position(back.position)
var front_light := pixels.get_pixelv(Vector2i(front_pixel)).get_luminance()
var back_light := pixels.get_pixelv(Vector2i(back_pixel)).get_luminance()
check(front_light > .15, key + " front is visible")
check(back_light > .15, key + " reverse side is visible")
check(absf(front_light - back_light) < .08, key + " sides have consistent ambient lighting")
measurements[key] = {"front_luminance": front_light, "back_luminance": back_light}
pixels.save_png("res://docs/screenshots/phase17/" + key.to_lower() + "-sides.png")
source.free()
var report := FileAccess.open("res://docs/phase17-material-verification.json", FileAccess.WRITE)
report.store_string(JSON.stringify({"checks": checks, "failures": failures, "renderer": RenderingServer.get_current_rendering_method(), "materials": measurements}, "\t"))
report.close()
print("MATERIAL_PASS: ", checks, " checks; ", failures.size(), " failures")
await Soundscape.quit_game(0 if failures.is_empty() else 1)