Release 0.9.5 texture and material refinement
Full build / Import + headless tests (push) Successful in 46s
Full build / Full desktop build + optional release (push) Successful in 4m54s

This commit is contained in:
zwitschi committed 2026-11-02 21:04:48 +01:00
1 parent fb3da52438
commit 4bc6390e9f
28 files changed
+160 -78

No files matched your search

+45 -15
View File
@@ -81,15 +81,16 @@ const SHOTS_PORTRAIT := [
]
## Stage list: shift time selects the phase, `venue` switches the scenario first.
const STAGES := [
{"name": "day", "elapsed": 0.0, "venue": "", "shots": SHOTS_DAY, "night": false},
{"name": "surfaces", "elapsed": 0.0, "venue": "", "shots": SHOTS_SURFACES, "night": false},
{"name": "surfaces_joke", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_SURFACES_JOKE, "night": false},
# Portrait next, while the day crowd is still on the street.
{"name": "portrait", "elapsed": 0.0, "venue": "", "shots": SHOTS_PORTRAIT,
{"name": "day", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_DAY, "night": false},
{"name": "surfaces", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_SURFACES, "night": false},
# Portrait before the venue switch: re-staging a close-up after a rebuild is
# not reliable, and this keeps the day crowd on Hollywood.
{"name": "portrait", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_PORTRAIT,
"night": false},
{"name": "evening", "elapsed": 620.0, "venue": "", "shots": SHOTS_EVENING, "night": true},
{"name": "late", "elapsed": 760.0, "venue": "", "shots": SHOTS_LATE, "night": true},
{"name": "evening", "elapsed": 620.0, "venue": "hollywood_building", "shots": SHOTS_EVENING, "night": true},
{"name": "late", "elapsed": 760.0, "venue": "hollywood_building", "shots": SHOTS_LATE, "night": true},
{"name": "joke_shop_day", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_JOKE_DAY, "night": false},
{"name": "surfaces_joke", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_SURFACES_JOKE, "night": false},
{"name": "joke_shop", "elapsed": 700.0, "venue": "joke_shop", "shots": SHOTS_JOKE, "night": true},
{"name": "studio", "elapsed": 700.0, "venue": "tv_studio", "shots": SHOTS_STUDIO, "night": true},
]
@@ -180,6 +181,7 @@ func _process(delta: float) -> bool:
return false
func _begin_stage() -> void:
_clear_capture_pause()
_stage += 1
while _stage < STAGES.size() and not _only_stages.is_empty() \
and not _only_stages.has(str(STAGES[_stage]["name"])):
@@ -209,6 +211,7 @@ func _begin_stage() -> void:
print("capture_realism: stage %s at %d ms" % [stage["name"], Time.get_ticks_msec()])
func _run_queue(delta: float) -> void:
_clear_capture_pause()
if _queue.is_empty():
_phase = "stage"
return
@@ -241,9 +244,18 @@ func _run_queue(delta: float) -> void:
_queue.pop_front()
_shot_frames = 0
## Capture staging must not inherit a pause overlay from the previous stage or a
## window-focus change; a paused frame hides the very surfaces the pass audits.
func _clear_capture_pause() -> void:
if gm == null:
return
var pause = gm.get("pause_overlay")
if pause != null and pause.visible:
gm.call("_close_pause")
## Frame a nearby NPC from the front, with the sun behind the camera so the face
## is lit. The subject's own yaw is re-written every frame by its FSM, so the
## portrait turns the visual `Rig` child instead and stands on the sunlit side.
## is lit. The subject's FSM and idle head-turn are frozen so the still keeps the
## face toward the lens; photo/film poses are skipped because their props cover it.
func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
var guard_pos: Vector3 = gm.guard.global_position
var light := _sun_direction()
@@ -254,7 +266,17 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
var npc := node as Node3D
if npc == null or _faces_covered(npc):
continue
if _portrait_type >= 0 and int(_profile_of(npc).get("npc_type")) != _portrait_type:
var profile = _profile_of(npc)
if profile == null:
continue
if _portrait_type >= 0 and int(profile.get("npc_type")) != _portrait_type:
continue
# A film/photo pose holds a camera or phone in front of the face; keep the
# default portrait on an idle person so the texture pass sees skin.
var pose := ""
if npc.has_method("theater_pose"):
pose = str(npc.call("theater_pose"))
if _portrait_type < 0 and pose in ["photo", "film", "perform"]:
continue
var distance: float = npc.global_position.distance_to(guard_pos)
if distance > 25.0:
@@ -280,9 +302,10 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
continue
other.set_physics_process(false)
other.global_position += Vector3(0.0, 0.0, 140.0)
# Freeze the subject: its FSM re-writes the yaw every physics tick, which
# would turn it away from the camera again.
# Freeze the subject: its FSM re-writes the yaw every physics tick, and the
# idle head-turn would still look away after the body is aimed at the camera.
best.set_physics_process(false)
best.set_process(false)
# Clear standing spot on the sidewalk, away from the queue: the close-up is a
# character still, not a slice of the scene, and the crowd would block it.
var spot := Vector3(0.0, 0.0, 0.0)
@@ -290,6 +313,11 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
spot = gm.guard.get("spawn_position")
best.global_position = spot + _portrait_spot
best.rotation.y = 0.0
var subject_head := best.get_node_or_null("Rig/Head") as Node3D
if subject_head == null:
subject_head = best.find_child("Head", true, false) as Node3D
if subject_head != null:
subject_head.rotation.y = 0.0
# Stand between the subject and the sun, so the face is front-lit.
var to_light := -light
to_light = to_light.rotated(Vector3.UP, deg_to_rad(angle_deg))
@@ -305,10 +333,12 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
# Face the camera: from the subject, the guard lies in the -look direction.
# Use the drawn rig so an FSM yaw on the root cannot fight the pose.
var rig := best.find_child("Rig", true, false) as Node3D
# NPC art faces +Z; yaw the root toward the guard so the face, not the
# back, is under the camera.
var face := atan2(-look.x, -look.z)
best.rotation.y = face
if rig != null:
rig.rotation.y = atan2(-look.x, -look.z)
else:
best.rotation.y = atan2(-look.x, -look.z)
rig.rotation.y = 0.0
return true
+12 -3
View File
@@ -42,10 +42,19 @@ func _process(delta: float) -> bool:
if frame >= WARMUP_FRAMES + SAMPLE_FRAMES:
var average := sample_ms / SAMPLE_FRAMES
var meshes := 0
var triangles := 0
for avatar in avatars:
meshes += avatar.find_children("*", "MeshInstance3D", true, false).size()
print("NPC budget: %d avatars, %d meshes, avg frame %.2f ms (budget %.0f ms)" % [
COUNT, meshes, average, BUDGET_MS])
for node in avatar.find_children("*", "MeshInstance3D", true, false):
var mi := node as MeshInstance3D
if mi == null or mi.mesh == null:
continue
meshes += 1
for surface in mi.mesh.get_surface_count():
var arrays := mi.mesh.surface_get_arrays(surface)
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
triangles += indices.size() / 3
print("NPC budget: %d avatars, %d meshes, %d triangles, avg frame %.2f ms (budget %.0f ms)" % [
COUNT, meshes, triangles, average, BUDGET_MS])
if average > BUDGET_MS:
printerr("SMOKE FAIL: frame time %.1f ms exceeds the %.0f ms budget" % [
average, BUDGET_MS])