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
+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()