big
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
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
|
||||
@@ -15,82 +17,205 @@ static func add_to(level: Node, sky_variant: String = "day") -> WorldEnvironment
|
||||
env.environment = make_environment(sky_variant)
|
||||
level.add_child(env)
|
||||
|
||||
if not level.has_node("Sun"):
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.name = "Sun"
|
||||
sun.rotation_degrees = Vector3(-52, 38, 0)
|
||||
sun.light_color = Color(1.0, 0.97, 0.88)
|
||||
sun.light_energy = 1.4
|
||||
sun.shadow_enabled = true
|
||||
sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS
|
||||
sun.directional_shadow_max_distance = 120.0
|
||||
level.add_child(sun)
|
||||
|
||||
var fill := DirectionalLight3D.new()
|
||||
fill.name = "FillLight"
|
||||
fill.rotation_degrees = Vector3(-30, -142, 0)
|
||||
fill.light_color = Color(0.6, 0.7, 1.0)
|
||||
fill.light_energy = 0.25
|
||||
fill.shadow_enabled = false
|
||||
level.add_child(fill)
|
||||
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)
|
||||
var ink_mat := ShaderMaterial.new()
|
||||
ink_mat.shader = load("res://assets/shaders/ink_edge.gdshader")
|
||||
quad.material = ink_mat
|
||||
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
|
||||
|
||||
# Quiet ambient bed so the arena never sits in dead air.
|
||||
if not level.has_node("AmbientBed") and ResourceLoader.exists("res://assets/sounds/wind.wav"):
|
||||
var amb := AudioStreamPlayer.new()
|
||||
amb.name = "AmbientBed"
|
||||
amb.stream = load("res://assets/sounds/wind.wav")
|
||||
if amb.stream is AudioStreamWAV:
|
||||
amb.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
amb.stream.loop_end = amb.stream.data.size() / 2
|
||||
amb.volume_db = -30.0
|
||||
amb.pitch_scale = 0.6
|
||||
amb.bus = "SFX"
|
||||
amb.autoplay = true
|
||||
level.add_child(amb)
|
||||
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
|
||||
var sky := Sky.new()
|
||||
var sky_mat := ProceduralSkyMaterial.new()
|
||||
match sky_variant:
|
||||
"sunset":
|
||||
sky_mat.sky_top_color = Color(0.25, 0.2, 0.5)
|
||||
sky_mat.sky_horizon_color = Color(0.95, 0.6, 0.45)
|
||||
sky_mat.ground_bottom_color = Color(0.18, 0.12, 0.2)
|
||||
sky_mat.ground_horizon_color = Color(0.85, 0.55, 0.45)
|
||||
sky_mat.sun_curve = 0.12
|
||||
_:
|
||||
# Bold anime day sky: deep saturated blue up top, bright cyan horizon.
|
||||
sky_mat.sky_top_color = Color(0.18, 0.4, 0.85)
|
||||
sky_mat.sky_horizon_color = Color(0.72, 0.88, 0.98)
|
||||
sky_mat.ground_bottom_color = Color(0.22, 0.2, 0.24)
|
||||
sky_mat.ground_horizon_color = Color(0.62, 0.7, 0.75)
|
||||
sky_mat.sun_curve = 0.1
|
||||
sky_mat.sun_angle_max = 20.0
|
||||
sky.sky_material = sky_mat
|
||||
environment.sky = 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.
|
||||
@@ -104,16 +229,68 @@ static func make_environment(sky_variant: String = "day") -> Environment:
|
||||
environment.glow_bloom = 0.05
|
||||
environment.glow_hdr_threshold = 1.45
|
||||
|
||||
# Cel color grade: a LIGHT saturation lift only. Anything stronger shifts
|
||||
# hues — authored ginger hair grades into fire-truck red, pastels go neon —
|
||||
# and the whole game reads "filtered". Let the textures' own colors carry
|
||||
# the vibrancy.
|
||||
environment.adjustment_enabled = true
|
||||
environment.adjustment_saturation = 1.08
|
||||
environment.adjustment_contrast = 1.03
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user