Add the optional Forward+ desktop HQ build
Package a Forward+ preset with volumetric fog, SDFGI and a volumetric flashlight for a separate Linux/Windows Desktop HQ zip. Gate the effects on a real RenderingDevice, tune LATE fog, and adapt local light falloff for Godot 4.7. Record the Compatibility/Forward+ A/B captures and metrics, add the artifact to the build/publish scripts, and keep the default Compatibility/Web path unchanged. Headless 231/231, Python 87/87; exported HQ binary boots with Vulkan.
This commit is contained in:
1 parent
64f0c6a198
commit
28f2a48811
23 files changed
+337
-46
No files matched your search
@@ -54,7 +54,7 @@ const PHASE_LIGHTING := {
|
||||
"sky_ground": Color(0.45, 0.5, 0.55), "ambient": Color(0.75, 0.78, 0.85), "night": false,
|
||||
"exposure": 0.6, "glow": 0.45, "fog_color": Color(0.87, 0.83, 0.76),
|
||||
"fog_density": 0.0034, "fog_energy": 0.5, "contrast": 1.05, "saturation": 1.04,
|
||||
"ambient_energy": 0.1,
|
||||
"ambient_energy": 0.1, "volumetric_density": 0.004, "volumetric_energy": 0.0,
|
||||
},
|
||||
Rules.Phase.MIDDAY: {
|
||||
"sun_energy": 1.0, "sun_angle": -62.0, "sun_yaw": -30.0, "sun_color": Color(1.0, 0.99, 0.96),
|
||||
@@ -62,7 +62,7 @@ const PHASE_LIGHTING := {
|
||||
"sky_ground": Color(0.5, 0.54, 0.58), "ambient": Color(0.85, 0.88, 0.95), "night": false,
|
||||
"exposure": 0.6, "glow": 0.4, "fog_color": Color(0.88, 0.87, 0.83),
|
||||
"fog_density": 0.0028, "fog_energy": 0.5, "contrast": 1.06, "saturation": 1.04,
|
||||
"ambient_energy": 0.28,
|
||||
"ambient_energy": 0.28, "volumetric_density": 0.003, "volumetric_energy": 0.0,
|
||||
},
|
||||
# Evening is dusk, not a bright sunset: the sun is almost down and the street
|
||||
# is carried by the marquee, the lamps and the lit windows (realism pass).
|
||||
@@ -72,7 +72,7 @@ const PHASE_LIGHTING := {
|
||||
"sky_ground": Color(0.1, 0.09, 0.12), "ambient": Color(0.2, 0.19, 0.26), "night": true,
|
||||
"exposure": 0.72, "glow": 0.8, "fog_color": Color(0.34, 0.27, 0.3),
|
||||
"fog_density": 0.0026, "fog_energy": 0.45, "contrast": 1.1, "saturation": 1.05,
|
||||
"ambient_energy": 0.12,
|
||||
"ambient_energy": 0.12, "volumetric_density": 0.014, "volumetric_energy": 0.10,
|
||||
},
|
||||
Rules.Phase.LATE: {
|
||||
"sun_energy": 0.02, "sun_angle": -8.0, "sun_color": Color(0.3, 0.35, 0.5),
|
||||
@@ -80,7 +80,7 @@ const PHASE_LIGHTING := {
|
||||
"sky_ground": Color(0.03, 0.03, 0.05), "ambient": Color(0.06, 0.07, 0.12), "night": true,
|
||||
"exposure": 0.95, "glow": 1.15, "fog_color": Color(0.08, 0.1, 0.2),
|
||||
"fog_density": 0.0028, "fog_energy": 0.45, "contrast": 1.12, "saturation": 1.0,
|
||||
"ambient_energy": 0.22,
|
||||
"ambient_energy": 0.22, "volumetric_density": 0.020, "volumetric_energy": 0.14,
|
||||
},
|
||||
}
|
||||
var hud: Hud = null
|
||||
@@ -196,11 +196,14 @@ func _build_guard() -> GuardController:
|
||||
# Aim straight ahead: tilting it down made the cone hit the ground right in
|
||||
# front of the guard instead of the person or door being checked.
|
||||
flashlight.rotation_degrees = Vector3.ZERO
|
||||
flashlight.spot_range = 20.0
|
||||
flashlight.spot_range = 30.0 if Materials.modern_light_falloff() else 20.0
|
||||
flashlight.spot_angle = 14.0
|
||||
flashlight.light_energy = 4.2
|
||||
flashlight.spot_attenuation = 0.9
|
||||
flashlight.spot_attenuation = 0.0 if Materials.modern_light_falloff() else 0.9
|
||||
flashlight.spot_angle_attenuation = 0.9
|
||||
# The volumetric shaft is a Forward+ (desktop HQ) effect. Compatibility and
|
||||
# headless builds leave the light non-volumetric, so their look is unchanged.
|
||||
flashlight.light_volumetric_fog_energy = 3.0 if RenderingServer.get_rendering_device() != null else 0.0
|
||||
head.add_child(flashlight)
|
||||
var collider := CollisionShape3D.new()
|
||||
var shape := CapsuleShape3D.new()
|
||||
|
||||
+34
-1
@@ -152,9 +152,24 @@ static func window_pane() -> StandardMaterial3D:
|
||||
mat.emission_energy_multiplier = 0.0
|
||||
return mat
|
||||
|
||||
## Godot 4.7 changed the Compatibility light falloff in practice: the same
|
||||
## attenuation/range that read well on 4.3 leaves the night scene nearly black.
|
||||
## Use this switch for local lights until the project moves its baseline to 4.7.
|
||||
static func modern_light_falloff() -> bool:
|
||||
var info := Engine.get_version_info()
|
||||
return int(info.get("major", 4)) > 4 \
|
||||
or (int(info.get("major", 4)) == 4 and int(info.get("minor", 0)) >= 7)
|
||||
|
||||
## True only when a real RenderingDevice exists. The Compatibility renderer and
|
||||
## the headless dummy renderer return null, so the desktop-only effects can be
|
||||
## gated without Web/CI builds ever enabling them.
|
||||
static func forward_plus_active() -> bool:
|
||||
return RenderingServer.get_rendering_device() != null
|
||||
|
||||
## Environment look shared by every phase: ACES tonemapping, soft glow, aerial
|
||||
## fog for depth, and a mild contrast/saturation lift. Phase values come from
|
||||
## `GameManager.PHASE_LIGHTING`.
|
||||
## `GameManager.PHASE_LIGHTING`. Volumetric fog and SDFGI are only enabled when
|
||||
## Forward+ is actually available (the optional desktop preset).
|
||||
static func configure_environment(env: Environment, cfg: Dictionary) -> void:
|
||||
if env == null:
|
||||
return
|
||||
@@ -180,6 +195,24 @@ static func configure_environment(env: Environment, cfg: Dictionary) -> void:
|
||||
env.fog_density = float(cfg.get("fog_density", 0.0035))
|
||||
env.fog_aerial_perspective = 0.25
|
||||
env.fog_sky_affect = 0.4
|
||||
var advanced := forward_plus_active()
|
||||
env.volumetric_fog_enabled = advanced
|
||||
env.sdfgi_enabled = advanced
|
||||
if advanced:
|
||||
# Desktop HQ: a shallow, cool fog gives the torch a visible shaft while
|
||||
# keeping the entrance facade readable. Phase values are tuned per phase.
|
||||
env.volumetric_fog_density = float(cfg.get("volumetric_density", 0.012))
|
||||
env.volumetric_fog_albedo = cfg.get("volumetric_albedo", Color(0.55, 0.62, 0.72))
|
||||
env.volumetric_fog_emission = cfg.get("volumetric_emission", Color.BLACK)
|
||||
env.volumetric_fog_emission_energy = float(cfg.get("volumetric_energy", 0.0))
|
||||
env.volumetric_fog_anisotropy = 0.25
|
||||
env.volumetric_fog_length = 48.0
|
||||
env.volumetric_fog_ambient_inject = 0.15
|
||||
env.volumetric_fog_sky_affect = 0.2
|
||||
env.sdfgi_use_occlusion = true
|
||||
env.sdfgi_cascades = 4
|
||||
env.sdfgi_min_cell_size = 0.2
|
||||
env.sdfgi_energy = 0.7
|
||||
env.adjustment_enabled = true
|
||||
env.adjustment_brightness = 1.0
|
||||
env.adjustment_contrast = float(cfg.get("contrast", 1.06))
|
||||
|
||||
@@ -548,7 +548,8 @@ func _build_bulb_canopy() -> void:
|
||||
for x in [-2.2, 2.2]:
|
||||
var omni := OmniLight3D.new()
|
||||
omni.position = Vector3(x, 5.15, front + 1.4)
|
||||
omni.omni_range = 8.0
|
||||
omni.omni_range = 12.0 if Materials.modern_light_falloff() else 8.0
|
||||
omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1.0, 0.72, 0.36)
|
||||
root.add_child(omni)
|
||||
@@ -586,7 +587,8 @@ func _add_wall_lantern(pos: Vector3, with_light: bool) -> void:
|
||||
var omni := OmniLight3D.new()
|
||||
omni.name = "LanternGlow"
|
||||
omni.position = pos + Vector3(0.0, -0.1, 0.45)
|
||||
omni.omni_range = 9.0
|
||||
omni.omni_range = 14.0 if Materials.modern_light_falloff() else 9.0
|
||||
omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1.0, 0.72, 0.38)
|
||||
root.add_child(omni)
|
||||
@@ -666,7 +668,8 @@ func _build_street_furniture() -> void:
|
||||
holder.name = "Lamp_%d" % int(side)
|
||||
var omni := OmniLight3D.new()
|
||||
omni.position = Vector3(-0.85, 4.65, 0)
|
||||
omni.omni_range = 12.0
|
||||
omni.omni_range = 16.0 if Materials.modern_light_falloff() else 12.0
|
||||
omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0
|
||||
omni.light_energy = 0.0
|
||||
omni.light_color = Color(1, 0.93, 0.75)
|
||||
holder.add_child(omni)
|
||||
|
||||
@@ -30,6 +30,9 @@ const HOODED_TYPES := [5, 6, 7] # TRESPASSER, VIOLENT_THREAT, MENTAL_CRISIS
|
||||
|
||||
## Optional `--portrait-type=N` picks the NpcType a portrait should feature.
|
||||
var _portrait_type := -1
|
||||
## Optional `--flashlight` turns the guard lamp on for night stages; the desktop
|
||||
## HQ A/B uses it to show the volumetric shaft.
|
||||
var _flashlight := false
|
||||
## Where the portrait subject is placed, relative to the guard's spawn.
|
||||
var _portrait_spot := Vector3(0.0, 0.5, 9.0)
|
||||
var _portrait_subject: Node3D = null
|
||||
@@ -114,6 +117,8 @@ func _parse_args() -> void:
|
||||
_only_stages[name.strip_edges()] = true
|
||||
elif text.begins_with("--portrait-type="):
|
||||
_portrait_type = int(text.substr(16))
|
||||
elif text == "--flashlight":
|
||||
_flashlight = true
|
||||
|
||||
func _pair(text: String, into: Dictionary) -> void:
|
||||
var cut := text.find("=", 6) # skip the "res://" scheme
|
||||
@@ -168,6 +173,10 @@ func _begin_stage() -> void:
|
||||
if venue != "" and venue != str(gm.get("active_scenario_id")):
|
||||
gm.call("switch_scenario", venue)
|
||||
gm.coordinator.elapsed_s = float(stage["elapsed"])
|
||||
if _flashlight:
|
||||
var want := bool(stage["night"])
|
||||
if want != bool(gm.flashlight_on):
|
||||
gm.call("_toggle_flashlight")
|
||||
_apply(swaps)
|
||||
if bool(stage["night"]):
|
||||
_apply(night_swaps)
|
||||
|
||||
@@ -43,8 +43,11 @@ func test_flashlight_aims_a_tight_forward_spot() -> bool:
|
||||
var c := TestUtils.check(lamp != null and lamp.spot_angle <= 20.0,
|
||||
"flashlight cone is narrow enough to read as a torch")
|
||||
var d := TestUtils.check(lamp != null and not lamp.visible, "flashlight starts off")
|
||||
var e := TestUtils.check(lamp != null
|
||||
and is_zero_approx(lamp.light_volumetric_fog_energy),
|
||||
"headless/compatibility builds leave the flashlight non-volumetric")
|
||||
root.free()
|
||||
return a and b and c and d
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_flashlight_widens_perception() -> bool:
|
||||
## Without the flashlight, an NPC at AWARE_RADIUS + 3 m is unaware.
|
||||
|
||||
@@ -77,14 +77,19 @@ func test_environment_tuning_applies_phase_values() -> bool:
|
||||
var f := TestUtils.check(
|
||||
env.fog_light_color.is_equal_approx(late["fog_color"]),
|
||||
"phase fog colour is used")
|
||||
return a and b and c and d and e and f
|
||||
var advanced := Materials.forward_plus_active()
|
||||
var g := TestUtils.check(env.volumetric_fog_enabled == advanced,
|
||||
"volumetric fog is gated to Forward+ (advanced=%s)" % advanced)
|
||||
var h := TestUtils.check(env.sdfgi_enabled == advanced,
|
||||
"SDFGI is gated to Forward+ (advanced=%s)" % advanced)
|
||||
return a and b and c and d and e and f and g and h
|
||||
|
||||
func test_every_phase_has_environment_keys() -> bool:
|
||||
var ok := true
|
||||
for phase in [Rules.Phase.EARLY, Rules.Phase.MIDDAY, Rules.Phase.EVENING, Rules.Phase.LATE]:
|
||||
var cfg: Dictionary = GameManager.PHASE_LIGHTING.get(phase, {})
|
||||
for key in ["exposure", "glow", "fog_color", "fog_density", "fog_energy",
|
||||
"contrast", "saturation"]:
|
||||
"contrast", "saturation", "volumetric_density", "volumetric_energy"]:
|
||||
if not cfg.has(key):
|
||||
ok = false
|
||||
return TestUtils.check(ok, "all phases set exposure/glow/fog/grading")
|
||||
|
||||
Reference in new issue
Block a user