Files
Papay-Shooter/scenes/maps/level_environment.gd
T
Nicholas ButzkeandClaude Fable 5 d46530bd8c fix: color-accurate grade — ginger hair stays ginger, characters stay matte
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]>
2026-07-20 22:55:58 -04:00

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