roxlap_render/light.rs
1//! Dynamic lighting (stage DL) — runtime sun + point lights + stylized
2//! voxel shadows. **GPU-only**: the CPU rasterizer ignores everything
3//! here (it keeps multiplying the baked ambient byte). See
4//! `PORTING-DYNLIGHT.md`.
5//!
6//! Lighting is per-frame: a host builds a [`LightRig`] each frame and
7//! hands it to the renderer via [`crate::FrameParams::lights`]. There are
8//! deliberately no stateful lighting setters on [`crate::SceneRenderer`]
9//! (mirroring how sky / fog / side-shades already flow through
10//! [`crate::FrameParams`]). `lights: None` ⇒ exactly the pre-DL render.
11
12/// The sun — a single directional light for the whole scene. World space.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct DirectionalLight {
15 /// Direction the light **travels** (from the sun toward the scene),
16 /// world space. Need not be normalized — the backend normalizes. The
17 /// N·L term uses the negation (the direction *to* the sun).
18 pub direction: [f32; 3],
19 /// Linear RGB, `0..1` (may exceed 1 for extra punch; the output is
20 /// clamped). `[1.0; 3]` is neutral white.
21 pub color: [f32; 3],
22 /// Scalar brightness multiplier on `color`.
23 pub intensity: f32,
24 /// Whether this light casts stylized hard shadows (DL.3). The sun is
25 /// the natural first shadow caster.
26 pub casts_shadow: bool,
27}
28
29impl Default for DirectionalLight {
30 fn default() -> Self {
31 Self {
32 direction: [0.0, 0.0, 1.0],
33 color: [1.0; 3],
34 intensity: 1.0,
35 casts_shadow: false,
36 }
37 }
38}
39
40/// A colored point light. World space, with a hard radius cutoff (it
41/// contributes nothing beyond `radius`).
42#[derive(Clone, Copy, Debug, PartialEq)]
43pub struct PointLight {
44 /// World-space position (voxel units).
45 pub position: [f32; 3],
46 /// Linear RGB, `0..1`.
47 pub color: [f32; 3],
48 /// Scalar brightness multiplier on `color`.
49 pub intensity: f32,
50 /// Hard cutoff distance in world units; past it the light is zero.
51 pub radius: f32,
52 /// Whether this light casts stylized hard shadows (DL.3). Only the
53 /// first few flagged lights actually cast (see the renderer's
54 /// shadow-caster cap); the rest are silently demoted to shadowless
55 /// with a log warning — never truncated.
56 pub casts_shadow: bool,
57}
58
59impl Default for PointLight {
60 fn default() -> Self {
61 Self {
62 position: [0.0; 3],
63 color: [1.0; 3],
64 intensity: 1.0,
65 radius: 32.0,
66 casts_shadow: false,
67 }
68 }
69}
70
71/// The whole per-frame light environment, borrowed into
72/// [`crate::FrameParams`]. GPU-only.
73#[derive(Clone, Copy, Debug)]
74pub struct LightRig<'a> {
75 /// The sun. `None` ⇒ no directional light this frame.
76 pub sun: Option<DirectionalLight>,
77 /// Point lights. The backend caps the count (excess dropped with a
78 /// log warning), so this slice may be longer than the GPU honours.
79 pub points: &'a [PointLight],
80 /// Multiplier applied to the baked ambient byte — the global ambient
81 /// level / tint. `[1.0; 3]` uses the baked byte as-is.
82 pub ambient: [f32; 3],
83 /// Stylized-shadow strength `0..1`: the fraction of a caster's light
84 /// removed in shadow (`1.0` = full black, `0.0` = no visible shadow).
85 pub shadow_strength: f32,
86 /// Shadow-ray origin bias along the surface normal, in voxel units —
87 /// kills self-shadow acne. ~1.5 is a good default.
88 pub shadow_bias_voxels: f32,
89 /// Sun shadow-ray length cap, world units (point-light shadow rays
90 /// stop at the light instead).
91 pub shadow_max_dist: f32,
92 /// DL.6 — **stylized lighting**. `0` ⇒ smooth (physically-ish) diffuse
93 /// (the default). `≥1` ⇒ retro cel look: the sun's key term and each
94 /// point light's diffuse factor quantize to `bands + 1` discrete levels
95 /// (terraced light instead of a smooth gradient), and the banded sun key
96 /// drives a **gradient map** from [`shadow_tint`](Self::shadow_tint)
97 /// (cool, unlit) to the sun's colour (warm, lit) — hue-shifted shadows
98 /// rather than plain darkening. Avoids the "generic Phong" read that
99 /// flattens the voxel/retro identity.
100 pub bands: u32,
101 /// DL.6 — the cool shadow/ambient tint the stylized ramp starts from
102 /// (the unlit end). Multiplied by the baked ambient/AO byte. Ignored
103 /// when `bands == 0` (then [`ambient`](Self::ambient) is used instead).
104 pub shadow_tint: [f32; 3],
105}
106
107impl Default for LightRig<'_> {
108 fn default() -> Self {
109 Self {
110 sun: None,
111 points: &[],
112 ambient: [1.0; 3],
113 shadow_strength: 0.7,
114 shadow_bias_voxels: 1.5,
115 shadow_max_dist: 512.0,
116 bands: 0,
117 shadow_tint: [0.12, 0.14, 0.2],
118 }
119 }
120}