Noise-driven fire light rig with bounce, core, embers and glow

This commit is contained in:
enne2
2026-10-11 02:26:08 +02:00
parent e15deef84f
commit 70c82df450
11 changed files with 440 additions and 22 deletions
+36
View File
@@ -0,0 +1,36 @@
extends SceneTree
## Cattura il bidone acceso a fasi diverse di flicker e di scarto, per confrontare
## ombre, rimbalzo, nucleo, braci e glow. Lo spawn delle ondate viene bloccato e gli
## zombie congelati, altrimenti finiscono davanti all'obiettivo e falsano l'immagine.
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
var world = load("res://scenes/main.tscn").instantiate()
root.add_child(world)
world.spawning = true # impedisce l'avvio di nuove ondate durante le riprese
world.player.set_physics_process(false)
world.player.hp = 100000
var office = world.get_node("OfficeAtmosphere")
var rig = office.fire_rig
var shots := [
["basso", -1.0, 0.0, Vector3(-4, 0.35, 4)],
["alto", 1.0, 0.0, Vector3(-4, 0.35, 4)],
["alto-scarto", 1.0, 0.45, Vector3(-4, 0.35, 4)],
["insieme", 0.0, 0.2, Vector3(-4, 1.0, 4)],
]
for shot in shots:
for zombie in get_nodes_in_group("live_zombies"):
zombie.set_physics_process(false)
rig._pulse = shot[1]
rig._time = shot[2]
rig._apply()
world.player.position = Vector3(-1.6, 0.1, 2.9) if shot[0] != "insieme" else Vector3(0, 0.1, -0.5)
world.player.look_at(shot[3])
await create_timer(0.35).timeout
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png("/tmp/zombie-fire-%s.png" % shot[0])
print("SHOT: 4 immagini in /tmp/zombie-fire-*.png")
world.queue_free()
await process_frame
quit()
+1
View File
@@ -0,0 +1 @@
uid://cossye8uv1oba
+99
View File
@@ -0,0 +1,99 @@
extends SceneTree
## Verifica il rig del fuoco: rumore continuo, assenza di strobing, coerenza fra le
## luci e modulazione della fiamma. Il campione di rumore e' puro, quindi il test
## simula 60 s a 60 Hz senza dipendere dal frame rate reale.
const FireLight = preload("res://scripts/fire_light.gd")
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
var world = load("res://scenes/main.tscn").instantiate()
root.add_child(world)
var rig = world.get_node("OfficeAtmosphere").fire_rig
var main_light: OmniLight3D = rig.main_light
var bounce: OmniLight3D = rig.bounce_light
var core: OmniLight3D = rig.core_light
# --- limiti, variazione e assenza di strobing -----------------------------
var previous := -1.0
var maximum_step := 0.0
var minimum := INF
var maximum := -INF
var total := 0.0
var squares := 0.0
var samples := 0
rig._pulse = 0.0
for i in 3600: # 60 s a 60 Hz
rig._process(1.0 / 60.0)
var energy := main_light.light_energy
minimum = minf(minimum, energy)
maximum = maxf(maximum, energy)
total += energy
squares += energy * energy
samples += 1
if previous >= 0.0:
maximum_step = maxf(maximum_step, absf(energy - previous))
previous = energy
var mean := total / samples
var deviation := sqrt(squares / samples - mean * mean)
assert(minimum >= rig.MIN_ENERGY and maximum <= rig.MAX_ENERGY)
assert(deviation > 0.05) # non e' una luce fissa
assert(maximum_step < 0.05) # nessun salto: niente strobing
assert(maximum - minimum > 0.4) # la gamma viene usata davvero
print("PASS: flicker entro [%.2f, %.2f], deviazione %.3f, salto massimo per frame %.3f" % [minimum, maximum, deviation, maximum_step])
# --- coerenza fra le tre luci ---------------------------------------------
assert(bounce.light_energy > 0.0 and core.light_energy > 0.0)
assert(bounce.light_energy < main_light.light_energy)
assert(core.omni_range < main_light.omni_range)
assert(is_equal_approx(main_light.omni_range, rig.BASE_RANGE)) # raggio mai animato
rig._pulse = 1.0
rig._apply()
var bright := [main_light.light_energy, bounce.light_energy, core.light_energy]
rig._pulse = -1.0
rig._apply()
var dim := [main_light.light_energy, bounce.light_energy, core.light_energy]
for i in 3:
assert(bright[i] > dim[i])
print("PASS: principale, rimbalzo e nucleo pulsano insieme e il raggio resta fisso")
# --- ricetta ufficiale del rimbalzo e unicita' delle ombre ----------------
assert(not bounce.shadow_enabled and not core.shadow_enabled)
assert(bounce.light_specular == 0.0 and core.light_specular == 0.0)
assert(is_equal_approx(bounce.omni_range, rig.BASE_RANGE * rig.BOUNCE_RANGE_FACTOR))
assert(bounce.get_parent() == main_light and core.get_parent() == main_light)
assert(main_light.omni_shadow_mode == OmniLight3D.SHADOW_CUBE) # il dual paraboloid non esiste in Compatibility
assert(main_light.shadow_enabled and not bounce.shadow_enabled)
print("PASS: rimbalzo figlio senza ombre ne' specular, raggio +40%, ombre solo sulla principale")
# --- micro-scarto e fiamma ------------------------------------------------
var maximum_offset := 0.0
var flame_heights: Array[float] = []
rig._time = 0.0
for i in 600:
rig._process(1.0 / 60.0)
maximum_offset = maxf(maximum_offset, Vector2(main_light.position.x, main_light.position.z).length())
flame_heights.append(rig._flame_mesh.size.y)
assert(maximum_offset <= rig.JITTER * 1.45) # scarto piccolo e limitato
assert(maximum_offset > 0.005) # le ombre si muovono davvero
assert(flame_heights.max() > flame_heights.min())
assert(flame_heights.max() < rig._flame_height * 1.2)
print("PASS: scarto massimo %.3f m, altezza fiamma da %.2f a %.2f" % [maximum_offset, flame_heights.min(), flame_heights.max()])
# --- braci e determinismo -------------------------------------------------
assert(rig.embers is CPUParticles3D and rig.embers.amount == 8 and rig.embers.emitting)
assert(rig.embers.mesh.material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
var same := Node3D.new()
same.set_script(FireLight)
world.add_child(same)
var different := false
for i in 200:
var t := 0.01 * i
if not is_equal_approx(rig.pulse_at(t), same.pulse_at(t)):
different = true
assert(not different) # stesso seed -> stessa sequenza, test riproducibili
print("PASS: braci presenti e flicker deterministico con seed fisso")
world.queue_free()
await process_frame
quit()
+1
View File
@@ -0,0 +1 @@
uid://c6xrj86f687kq
+9 -4
View File
@@ -21,18 +21,23 @@ func run_test() -> void:
var flames = office.get_node("FireBarrel/BarrelFlames")
assert(flames is CPUParticles3D and flames.amount == 28 and flames.emitting)
assert(office.fire_light.shadow_enabled)
assert(office.fire_rig is Node3D and office.fire_rig.flame == flames)
for i in 100:
office._process(0.016)
assert(office.fire_light.light_energy >= office.FIRE_MIN and office.fire_light.light_energy <= office.FIRE_MAX)
office.fire_rig._process(0.016)
var energy: float = office.fire_light.light_energy
assert(energy >= office.fire_rig.MIN_ENERGY and energy <= office.fire_rig.MAX_ENERGY)
print("PASS: fiamme CPU leggere e flicker continuo entro limiti")
var lights: Array[Node] = world.find_children("*", "OmniLight3D", true, false)
assert(lights.size() == 4) # due lampade, bidone, muzzle flash
assert(lights.size() == 6) # due lampade, principale + rimbalzo + nucleo, muzzle flash
assert(world.find_children("DeskLampLight", "OmniLight3D", true, false).size() == 2)
var shadow_casters := lights.filter(func(light): return light.shadow_enabled)
assert(shadow_casters.size() == 1 and shadow_casters[0] == office.fire_light)
for node in world.get_children():
assert(not node is OmniLight3D) # nessuna plafoniera
if node is WorldEnvironment:
assert(node.environment.ambient_light_energy <= 0.15)
print("PASS: plafoniera rimossa, ambient basso e quattro luci totali")
assert(node.environment.glow_enabled and node.environment.glow_hdr_threshold >= 1.2)
print("PASS: plafoniera rimossa, ambient basso, una sola luce con ombre su sei totali")
await physics_frame
await physics_frame
var space = world.get_world_3d().direct_space_state