The saturation grade (1.3) was shifting authored hues — Taila's auburn ginger hair rendered fire-truck red and the whole game read filtered. Saturation 1.3 -> 1.08, contrast 1.05 -> 1.03: textures now carry their own vibrancy. Verified by sampling the rendered hair against the source texture: lit hue 18-20 deg vs authored ~20 deg. Character rim/specular cut again (rim 0.15/0.4 -> 0.08/0.32, spec 0.06 -> 0.025 @ 64): hair and cloth read fully matte, no wet sheen. Co-Authored-By: Claude Fable 5 <[email protected]>
120 lines
4.5 KiB
GDScript
120 lines
4.5 KiB
GDScript
extends Object
|
|
class_name LevelEnvironment
|
|
|
|
## 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)
|
|
|
|
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)
|
|
|
|
# Screen-space ink edge pass: drawn outlines on every model for whatever
|
|
# camera renders (fullscreen POSITION-override quad, never culled).
|
|
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.mesh = quad
|
|
ink.extra_cull_margin = 16384.0
|
|
level.add_child(ink)
|
|
|
|
# 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)
|
|
return env
|
|
|
|
|
|
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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
return environment
|