297 lines
14 KiB
GDScript
297 lines
14 KiB
GDScript
extends Object
|
|
class_name LevelEnvironment
|
|
|
|
const CLOUD_SUN_SYNC_SCRIPT := preload("res://scenes/maps/cloud_sun_sync.gd")
|
|
|
|
## Shared stylized environment for every level: saturated anime-sky gradient,
|
|
## bloom for emissives (tracers, plasma), and a color grade that pushes the
|
|
## cel-shaded look (high saturation, slight contrast lift). Builders call
|
|
## add_to(level) instead of hand-rolling WorldEnvironment + sun.
|
|
|
|
|
|
## Creates and adds a WorldEnvironment + key sun + cool fill light.
|
|
## Returns the WorldEnvironment node.
|
|
static func add_to(level: Node, sky_variant: String = "day") -> WorldEnvironment:
|
|
var env := WorldEnvironment.new()
|
|
env.name = "WorldEnvironment"
|
|
env.environment = make_environment(sky_variant)
|
|
level.add_child(env)
|
|
|
|
var sun := level.get_node_or_null("Sun") as DirectionalLight3D
|
|
if sun == null:
|
|
sun = _build_light_rig(level, sky_variant)
|
|
|
|
# Screen-space ink edge pass: drawn outlines on every model for whatever
|
|
# camera renders (fullscreen POSITION-override quad, never culled).
|
|
var ink_material: ShaderMaterial
|
|
if not level.has_node("InkEdgePost"):
|
|
var ink := MeshInstance3D.new()
|
|
ink.name = "InkEdgePost"
|
|
var quad := QuadMesh.new()
|
|
quad.size = Vector2(2, 2)
|
|
ink_material = ShaderMaterial.new()
|
|
ink_material.shader = load("res://assets/shaders/ink_edge.gdshader")
|
|
_apply_sky_palette(ink_material, sky_variant, true)
|
|
quad.material = ink_material
|
|
ink.mesh = quad
|
|
ink.extra_cull_margin = 16384.0
|
|
level.add_child(ink)
|
|
else:
|
|
var ink := level.get_node("InkEdgePost") as MeshInstance3D
|
|
if ink and ink.mesh:
|
|
ink_material = ink.mesh.material as ShaderMaterial
|
|
|
|
if sun and ink_material and not level.has_node("CloudSunSync"):
|
|
var sync := CLOUD_SUN_SYNC_SCRIPT.new()
|
|
sync.name = "CloudSunSync"
|
|
level.add_child(sync)
|
|
sync.configure(sun, ink_material)
|
|
|
|
# Deliberately no global broadband ambience. Constant air/noise masks the
|
|
# physical cues that matter in a competitive shooter and was perceived as
|
|
# television static. Authored map emitters may add localized machinery;
|
|
# traversal wind remains silent until the player is genuinely moving fast.
|
|
return env
|
|
|
|
|
|
## The anime light rig.
|
|
##
|
|
## Not one key and a token fill. The reference names four lights and every one
|
|
## of them is doing a specific job that the cel ramp cannot do without it:
|
|
##
|
|
## key Warm, and the only one casting shadows. It is what the ramp
|
|
## quantises, so it alone decides where every band edge in the frame
|
|
## falls.
|
|
## bounce A STRONG cool light from the opposite quarter — roughly a third of
|
|
## the key, not the tenth a PBR fill would be. This is the single most
|
|
## important light in an anime rig: it is what puts a distinct, cool,
|
|
## non-black value on every shadow plane, so a surface turning away
|
|
## from the sun turns INTO another colour rather than into darkness.
|
|
## up A weak violet up-light standing in for ground bounce, which keeps
|
|
## undersides — eaves, canopies, the underside of a train — from
|
|
## collapsing into one flat mass with the shadow planes.
|
|
## ground The hemisphere's ground colour, carried here by ambient (see
|
|
## make_environment): violet rather than grey, for the same reason the
|
|
## cel ramp's shadow tint is.
|
|
##
|
|
## The energies look high because the cel shaders divide by PI in light(), the
|
|
## same as any Lambert term: a key at 2.6 lands its top band at 2.6/PI ≈ 0.83 of
|
|
## the surface albedo, which is what "fully lit" should mean.
|
|
static func _build_light_rig(level: Node, sky_variant: String) -> DirectionalLight3D:
|
|
var warm := Color(1.0, 0.945, 0.847) # PAL.sun 0xfff1d8
|
|
var cool := Color(0.663, 0.741, 0.961) # PAL.fill 0xa9bdf5
|
|
var key_energy := 2.6
|
|
var bounce_energy := 0.95
|
|
var key_angle := Vector3(-48, 34, 0)
|
|
if sky_variant == "sunset":
|
|
warm = Color(1.0, 0.78, 0.58)
|
|
cool = Color(0.52, 0.56, 0.92)
|
|
key_energy = 2.2
|
|
bounce_energy = 1.1
|
|
key_angle = Vector3(-16, 28, 0)
|
|
|
|
var sun := DirectionalLight3D.new()
|
|
sun.name = "Sun"
|
|
sun.rotation_degrees = key_angle
|
|
sun.light_color = warm
|
|
sun.light_energy = key_energy
|
|
sun.shadow_enabled = true
|
|
sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS
|
|
sun.directional_shadow_max_distance = 160.0
|
|
level.add_child(sun)
|
|
|
|
# The opposite quarter: the key's yaw turned 180 degrees, and a shallower
|
|
# pitch, so the bounce rakes across the shadow planes instead of lighting
|
|
# them like a second sun.
|
|
var bounce := DirectionalLight3D.new()
|
|
bounce.name = "FillLight"
|
|
bounce.rotation_degrees = Vector3(-24, key_angle.y + 180.0, 0)
|
|
bounce.light_color = cool
|
|
bounce.light_energy = bounce_energy
|
|
bounce.shadow_enabled = false
|
|
level.add_child(bounce)
|
|
|
|
var up := DirectionalLight3D.new()
|
|
up.name = "UpLight"
|
|
up.rotation_degrees = Vector3(74, key_angle.y - 60.0, 0)
|
|
up.light_color = Color(0.714, 0.651, 0.776) # PAL.hemiGround 0xb6a6c6
|
|
up.light_energy = 0.30
|
|
up.shadow_enabled = false
|
|
level.add_child(up)
|
|
|
|
return sun
|
|
|
|
|
|
## Cel sky with rounded, flat-toned anime clouds.
|
|
##
|
|
## This replaces ProceduralSkyMaterial, whose smooth two-colour ramp was the
|
|
## largest single flat area in every frame — tools/levels.py measured a quarter
|
|
## to a third of each shot sitting at one shade, and almost all of it was sky.
|
|
## Clouds give the top half of the picture something to be. They use a layered
|
|
## 2D field and painted directional tones rather than volumetric ray marching,
|
|
## keeping the richer silhouette practical for a competitive shooter.
|
|
static func make_sky(sky_variant: String = "day") -> Sky:
|
|
var sky := Sky.new()
|
|
var mat := ShaderMaterial.new()
|
|
mat.shader = load("res://assets/shaders/anime_sky.gdshader")
|
|
_apply_sky_palette(mat, sky_variant)
|
|
sky.sky_material = mat
|
|
# The sky animates, so its radiance would otherwise be re-rendered in full
|
|
# every frame for the maps that take ambient from it. INCREMENTAL spreads
|
|
# that across frames, which is invisible at a drift this slow.
|
|
sky.process_mode = Sky.PROCESS_MODE_INCREMENTAL
|
|
return sky
|
|
|
|
|
|
## Keep the Environment sky (ambient/reflections) and the ink pass's final
|
|
## background composite on one authored palette.
|
|
static func _apply_sky_palette(
|
|
mat: ShaderMaterial, sky_variant: String, ink_pass: bool = false) -> void:
|
|
var prefix := "sky_" if ink_pass else ""
|
|
match sky_variant:
|
|
"sakura":
|
|
# A hand-painted anime background sky, which means PALE. The bold
|
|
# saturated blue the other variants use is a video-game sky; a cel
|
|
# background painter keeps the top of the picture light and low in
|
|
# contrast so the ink line work and the blossom read against it. The
|
|
# horizon carries the reference's pink haze (PAL.skyHaze), which is
|
|
# what makes the air itself look like it has petals in it.
|
|
mat.set_shader_parameter(prefix + "top_color", Color(0.561, 0.741, 0.918))
|
|
mat.set_shader_parameter(prefix + "horizon_color", Color(0.949, 0.910, 0.933))
|
|
mat.set_shader_parameter(prefix + "ground_color", Color(0.714, 0.651, 0.776))
|
|
mat.set_shader_parameter(prefix + "cloud_color", Color(0.992, 0.980, 0.973))
|
|
mat.set_shader_parameter(prefix + "cloud_shadow", Color(0.902, 0.902, 0.949))
|
|
mat.set_shader_parameter(prefix + "cloud_cover", 0.520)
|
|
mat.set_shader_parameter(prefix + "cloud_scale", 2.2)
|
|
# Harder cloud edges than the other skies. A cel cloud is a shape
|
|
# with a drawn outline, not a soft mass.
|
|
mat.set_shader_parameter(prefix + "cloud_softness", 0.014)
|
|
mat.set_shader_parameter(prefix + "horizon_falloff", 0.62)
|
|
if ink_pass:
|
|
mat.set_shader_parameter("sky_sun_color", Color(1.0, 0.945, 0.847))
|
|
mat.set_shader_parameter("sky_sun_halo", 0.25)
|
|
mat.set_shader_parameter("sky_sun_direction", Vector3(0.46, 0.52, -0.72))
|
|
"sunset":
|
|
mat.set_shader_parameter(prefix + "top_color", Color(0.25, 0.20, 0.50))
|
|
mat.set_shader_parameter(prefix + "horizon_color", Color(0.95, 0.60, 0.45))
|
|
mat.set_shader_parameter(prefix + "ground_color", Color(0.18, 0.11, 0.25))
|
|
# Sunset clouds are lit from underneath and from the side, so their
|
|
# lit face takes the sun's colour and their shadow goes violet
|
|
# rather than grey.
|
|
mat.set_shader_parameter(prefix + "cloud_color", Color(1.0, 0.80, 0.62))
|
|
mat.set_shader_parameter(prefix + "cloud_shadow", Color(0.52, 0.38, 0.55))
|
|
mat.set_shader_parameter(prefix + "cloud_cover", 0.515)
|
|
mat.set_shader_parameter(prefix + "cloud_scale", 2.8)
|
|
mat.set_shader_parameter(prefix + "cloud_softness", 0.020)
|
|
mat.set_shader_parameter(prefix + "sun_halo", 0.55)
|
|
if ink_pass:
|
|
mat.set_shader_parameter("sky_sun_color", Color(1.0, 0.62, 0.42))
|
|
mat.set_shader_parameter("sky_sun_direction", Vector3(0.55, 0.20, -0.81))
|
|
_:
|
|
# Bold anime day sky: deep saturated blue up top, bright cyan horizon.
|
|
mat.set_shader_parameter(prefix + "top_color", Color(0.18, 0.40, 0.85))
|
|
mat.set_shader_parameter(prefix + "horizon_color", Color(0.72, 0.88, 0.98))
|
|
mat.set_shader_parameter(prefix + "ground_color", Color(0.22, 0.20, 0.28))
|
|
mat.set_shader_parameter(prefix + "cloud_color", Color(1.0, 0.99, 0.97))
|
|
mat.set_shader_parameter(prefix + "cloud_shadow", Color(0.72, 0.75, 0.88))
|
|
mat.set_shader_parameter(prefix + "cloud_cover", 0.500)
|
|
mat.set_shader_parameter(prefix + "cloud_scale", 2.5)
|
|
mat.set_shader_parameter(prefix + "cloud_softness", 0.020)
|
|
|
|
|
|
static func make_environment(sky_variant: String = "day") -> Environment:
|
|
var environment := Environment.new()
|
|
environment.background_mode = Environment.BG_SKY
|
|
environment.sky = make_sky(sky_variant)
|
|
|
|
# Flat-ish ambient keeps toon shadow bands readable (sun does the shaping).
|
|
environment.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
|
environment.ambient_light_energy = 0.55
|
|
|
|
if sky_variant == "sakura":
|
|
# The rig's bounce and up-light already put a cool value on every shadow
|
|
# plane, and the cel ramp's darkest stop is 0.36 rather than zero. Left
|
|
# at 0.55 on top of that, ambient becomes a flood: the bands converge,
|
|
# the violet shadow tint washes out, and the picture goes milky. This is
|
|
# the hemisphere's SKY half only — its ground half is the UpLight.
|
|
environment.ambient_light_energy = 0.22
|
|
|
|
# Filmic with a high white point: near-linear through the cel bands (they
|
|
# stay crisp) but a soft shoulder above 1.0. LINEAR clipped lit white
|
|
# surfaces and then bloomed the clip — every facade glowed like porcelain.
|
|
environment.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
|
environment.tonemap_white = 2.4
|
|
|
|
# Bloom for genuine emissives only (tracers, plasma, neon) — lit geometry
|
|
# must never cross this threshold or it reads as glowing glass.
|
|
environment.glow_enabled = true
|
|
environment.glow_intensity = 0.4
|
|
environment.glow_bloom = 0.05
|
|
environment.glow_hdr_threshold = 1.45
|
|
|
|
# The colour grade moved into the ink pass, where it can be a SPLIT-tone
|
|
# (cool violet into the darks, warm paper white into the lights) instead of
|
|
# a single saturation number. Environment's adjustment stage runs after that
|
|
# pass, so leaving it at 1.08 would grade the frame twice and re-introduce
|
|
# exactly the hue shifting the note below warns about.
|
|
environment.adjustment_enabled = false
|
|
environment.adjustment_saturation = 1.0
|
|
environment.adjustment_contrast = 1.0
|
|
|
|
# A touch of depth haze for scale; far enough to not gray the arena.
|
|
environment.fog_enabled = true
|
|
environment.fog_light_color = Color(0.65, 0.75, 0.9)
|
|
environment.fog_density = 0.0012
|
|
|
|
if sky_variant == "sakura":
|
|
# Haze, not fog. The reference's far hills are pale and low-contrast
|
|
# because everything more than a hundred metres out is painted as
|
|
# atmosphere; the density is chosen so a two-hundred-metre view still
|
|
# reads as air rather than as weather.
|
|
environment.fog_light_color = Color(0.902, 0.925, 0.969) # PAL.fog
|
|
environment.fog_density = 0.0022
|
|
|
|
_add_grounding(environment)
|
|
return environment
|
|
|
|
|
|
## Contact darkening. This is what was missing more than anything else.
|
|
##
|
|
## Ambient here is a flood — a single energy applied to every surface no matter
|
|
## what is standing over it — so a lamp post met the pavement with no darkening
|
|
## at all, and so did every building, every crate and every character's feet.
|
|
## `tools/levels.py` puts a number on it: the Neon Alley aerial measured a 1st
|
|
## percentile of 119, meaning the DARKEST few pixels in the whole frame were
|
|
## still mid-grey. Nothing was dark, so nothing looked like it was resting on
|
|
## anything.
|
|
##
|
|
## Lowering the old 1.35 ambient flood restores form, but it cannot solve
|
|
## contact on its own. The rest of the fix is to take ambient away only where
|
|
## geometry is in the way, which is exactly what SSAO does.
|
|
##
|
|
## The tuning is what keeps it anime rather than "PBR game with dirt in the
|
|
## corners":
|
|
##
|
|
## radius 0.7 Contact, not global. A big radius produces the soft grey
|
|
## haze under everything that reads as smog; a tight one draws
|
|
## a dark seam right where two surfaces meet, which is what a
|
|
## cel artist would ink in by hand.
|
|
## power 3.0 Sharpens the falloff so the occlusion is nearly gone a
|
|
## half-metre out. Combined with the small radius this gives a
|
|
## defined contact line rather than a gradient.
|
|
## sharpness .98 Keeps the AO from bleeding across depth discontinuities.
|
|
## Blurry AO over a hard-edged cel look is instantly wrong.
|
|
## light_affect Only 0.08. Direct sunlight must keep landing in clean flat
|
|
## bands — that IS the style. AO belongs in the ambient term,
|
|
## where it darkens what the sun never reached anyway.
|
|
static func _add_grounding(environment: Environment) -> void:
|
|
environment.ssao_enabled = true
|
|
environment.ssao_radius = 0.7
|
|
environment.ssao_intensity = 3.2
|
|
environment.ssao_power = 3.0
|
|
environment.ssao_detail = 0.6
|
|
environment.ssao_horizon = 0.1
|
|
environment.ssao_sharpness = 0.98
|
|
environment.ssao_light_affect = 0.08
|
|
environment.ssao_ao_channel_affect = 0.0
|