fix(assets): tune fog shader alpha and add zone temperature tint per D-059

Ticket #563. Light fog alpha tuned to 0.25-0.35 range (was 0.25-0.55),
deep fog alpha set to 0.55-0.70 with zone temperature tint from
zone_tint_tex (bar=warm #2a1f15, hub=cool #1a1f2e, corridor=neutral
#1a1a1a). Two Perlin noise cycles: 8-10s light, 15-20s deep.
Zone tint texture now populated per-tile from server zone_id in
fog_state.gd with preservation across texture resizes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 14:11:56 +01:00
co-authored by Claude Opus 4.6
parent 073c5a317c
commit dddfceeb5a
3 changed files with 157 additions and 86 deletions
+42 -67
View File
@@ -2,10 +2,28 @@
Ticket: #430 | Decision: D-059 | Sprint: 6
Author: Tyre (architecture) | Implementer: Stig
Updated: Sprint 22 (#569 simplification + #563 alpha tuning) | Stig + Araminta
## Current State (Sprint 22)
The fog system was simplified from the original 5-layer spec in Sprint 22 (#569):
| Original D-059 Layer | Sprint 22 Status | Notes |
|----------------------|------------------|-------|
| 1. Clear (forward cone) | Implemented | Soft Gaussian gradient (7x7, sigma 2.0) |
| 2. Light fog (peripheral sector) | Simplified — merged into gradient | Peripheral sector removed from server; gradient provides soft transition |
| 3. Deep fog (previously explored) | Implemented — EXP_EXPLORED | Alpha 0.54-0.70 with zone temperature tint (#563) |
| 4. Unexplored + maps app | Deferred | v0.1.2+, requires mapped_tiles in ObserverSnapshot |
| 5. Unexplored (no maps) | Implemented | Solid near-black #12141a |
**Alpha values (Sprint 22, #563):**
- Light fog zone (near cone gradient, vis 0.0-0.3): alpha 0.26-0.34, breathing ±0.04 (8-10s)
- Deep fog zone (EXP_EXPLORED, vis≈0): alpha 0.54-0.70, breathing ±0.08 (15-20s)
- Zone temperature tint active in deep fog zone (D-059/D-046/D-077)
## Overview
Complete rewrite of the fog system. Delete `fog_renderer.gd` (TileMapLayer-based, 2-state binary fog) and replace with a shader-driven, 5-layer fog system on a CanvasGroup.
Complete rewrite of the fog system. Delete `fog_renderer.gd` (TileMapLayer-based, 2-state binary fog) and replace with a shader-driven fog system on a CanvasGroup.
The fog is **knowledge-graph-driven** — the same fog shows different information per character based on their KG. "Fog is not darkness — it's the absence of your attention."
@@ -104,79 +122,36 @@ func update_from_state() -> void:
File: `client/shaders/fog.gdshader`
The shader determines fog layer per pixel based on the visibility and exploration textures.
The shader determines fog state per pixel based on the visibility and exploration textures.
Two explored sub-zones are distinguished by the blurred `vis` value (proximity to the forward cone):
```glsl
shader_type canvas_item;
// Light fog noise — fast cycle (8-10s), subtle ±0.04 breathing
float noise_fast = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r;
// Deep fog noise — slow cycle (15-20s), more pronounced ±0.08 breathing
float noise_slow = texture(noise_tex, tile * 0.02 + vec2(time * 0.05, time * 0.035)).r;
uniform sampler2D visibility_tex : filter_nearest;
uniform sampler2D exploration_tex : filter_nearest;
uniform sampler2D zone_tint_tex : filter_nearest;
uniform vec2 map_offset; // World position of texture origin (in pixels)
uniform vec2 map_size; // Texture size in tiles
uniform float tile_size; // Pixels per tile
uniform float time; // Engine TIME for noise animation
// Light fog: alpha 0.26-0.34 (near cone gradient)
float light_fog_alpha = 0.30 + noise_fast * 0.04;
// Deep fog: alpha 0.54-0.70 (far from cone, EXP_EXPLORED)
float deep_fog_alpha = 0.62 + noise_slow * 0.08;
// Fog layer colors
const vec4 FOG_UNEXPLORED = vec4(0.071, 0.078, 0.102, 1.0); // #12141a
const vec4 FOG_WIREFRAME = vec4(0.2, 0.2, 0.251, 1.0); // #333340
const float LIGHT_FOG_DESAT = 0.45; // 40-50% desaturation
const float LIGHT_FOG_DIM = 0.7; // brightness -30%
const float DEEP_FOG_DESAT = 0.9; // near-monochrome
const float DEEP_FOG_DIM = 0.25; // heavy dimming
const float ZONE_TINT_STRENGTH = 0.1; // ~10% zone temperature tint
// Blend deep <-> light fog by proximity to cone:
// vis=0 (far from cone) → deep_factor=1 → deep fog color + alpha
// vis=0.3 (cone gradient) → deep_factor=0 → light fog color + alpha
// vis=0.85+ (inside cone) → clarity=1 → transparent (clear)
float deep_factor = 1.0 - smoothstep(0.0, 0.30, vis);
float fog_alpha = mix(light_fog_alpha, deep_fog_alpha, deep_factor);
// Perlin noise (simplified — use Godot's NoiseTexture2D for production quality)
// Alternatively: pass a pre-generated noise texture as another uniform.
void fragment() {
// Map screen pixel to tile coordinate
vec2 world_pos = (SCREEN_UV * vec2(textureSize(visibility_tex, 0))) ;
vec2 tile_uv = world_pos / map_size;
// Sample textures
float vis = texture(visibility_tex, tile_uv).r; // 0-1: current visibility
float explored = texture(exploration_tex, tile_uv).r; // 0-1: exploration state
vec3 zone_tint = texture(zone_tint_tex, tile_uv).rgb;
// Determine fog layer:
// vis > 0.7 → Layer 1: Clear (vision cone) — soft gradient edge
// vis > 0.3 → Layer 2: Light fog (peripheral) — desaturated, noise
// explored > 0.4 → Layer 3: Deep fog (previously explored) — monochrome + tint
// explored > 0.1 → Layer 4: Unexplored + maps — wireframe outlines
// else → Layer 5: Unexplored, no maps — solid near-black
if (vis > 0.7) {
// Layer 1: Clear — soft gradient at edge
float edge = smoothstep(0.7, 1.0, vis);
COLOR = vec4(0.0, 0.0, 0.0, 1.0 - edge); // Transparent in clear zone
} else if (vis > 0.3) {
// Layer 2: Light fog — desaturated + dim + animated noise
float noise = _perlin(world_pos * 0.02 + vec2(time * 0.1, time * 0.05));
float alpha = mix(0.4, 0.6, noise); // Animated fog density
COLOR = vec4(0.02, 0.02, 0.05, alpha);
} else if (explored > 0.4) {
// Layer 3: Deep fog — near-monochrome + zone tint + breathing noise
float noise = _perlin(world_pos * 0.01 + vec2(time * 0.03, time * 0.02));
vec3 tint = mix(vec3(0.05), zone_tint, ZONE_TINT_STRENGTH);
float alpha = mix(0.75, 0.85, noise); // Fog breathes
COLOR = vec4(tint, alpha);
} else if (explored > 0.1) {
// Layer 4: Unexplored + maps app — geometric wireframe
COLOR = FOG_WIREFRAME;
// TODO: wireframe grid line overlay (1px every tile_size pixels)
} else {
// Layer 5: Unexplored, no maps — information zero
COLOR = FOG_UNEXPLORED;
}
}
// Zone temperature tint (D-059/D-046/D-077): deep fog color = zone tint
vec3 zone_tint = texture(zone_tint_tex, tex_uv).rgb;
vec3 fog_color = mix(DARK_OVERLAY, zone_tint, deep_factor);
```
**Note:** This is the architectural skeleton. The actual shader will need:
- A proper noise function or noise texture uniform (Godot's `NoiseTexture2D` resource)
- Correct world-to-UV coordinate mapping using `SCREEN_UV`, `CANVAS_MATRIX`, or vertex-passed world coords
- The gradient edge for Layer 1 should span 6-8 sim tiles (D-059/D-066)
- Layer 4 wireframe can use `mod()` on world coords for grid lines
**Zone temperature palette (D-046):**
- `hub` / `workplace`: `#1a1f2e` — cool blue-dark (institutional/terminal)
- `bar`: `#2a1f15` — warm amber-dark (social/inhabited)
- `corridor`: `#1a1a1a` — neutral dark (transitional/maintenance)
### Coordinate Mapping