1use crate::prelude::{Color, Vec3, vec3};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct LitSettings {
10 pub light_direction: Vec3,
12 pub camera_position: Vec3,
13 pub fog_color: Color,
14 pub fog_start: f32,
15 pub fog_end: f32,
16 pub ambient: f32,
17}
18
19impl Default for LitSettings {
20 fn default() -> Self {
21 Self {
22 light_direction: vec3(-0.4, 0.85, 0.3),
23 camera_position: vec3(12.0, 10.0, 16.0),
24 fog_color: Color::from_rgba(14, 22, 35, 255),
25 fog_start: 20.0,
26 fog_end: 55.0,
27 ambient: 0.38,
28 }
29 }
30}
31
32impl LitSettings {
33 pub fn sanitized(self) -> Self {
36 let defaults = Self::default();
37 let finite = |value: f32, fallback: f32| {
38 if value.is_finite() { value } else { fallback }
39 };
40 Self {
41 light_direction: if self.light_direction.is_finite() {
42 self.light_direction.normalize_or_zero()
43 } else {
44 defaults.light_direction.normalize_or_zero()
45 },
46 camera_position: if self.camera_position.is_finite() {
47 self.camera_position
48 } else {
49 defaults.camera_position
50 },
51 fog_color: Color::new(
52 finite(self.fog_color.r, defaults.fog_color.r).clamp(0.0, 1.0),
53 finite(self.fog_color.g, defaults.fog_color.g).clamp(0.0, 1.0),
54 finite(self.fog_color.b, defaults.fog_color.b).clamp(0.0, 1.0),
55 finite(self.fog_color.a, defaults.fog_color.a).clamp(0.0, 1.0),
56 ),
57 fog_start: finite(self.fog_start, defaults.fog_start).max(0.0),
58 fog_end: finite(self.fog_end, defaults.fog_end).max(0.0),
59 ambient: finite(self.ambient, defaults.ambient).clamp(0.0, 1.0),
60 }
61 }
62
63 pub fn shade_vertex(self, position: Vec3, normal: Vec3, color: Color) -> Color {
69 let diffuse = normal
70 .normalize_or_zero()
71 .dot(self.light_direction)
72 .clamp(0.0, 1.0);
73 let shade = self.ambient + (1.0 - self.ambient) * diffuse;
74 let fog = ((position.distance(self.camera_position) - self.fog_start)
75 / (self.fog_end - self.fog_start).max(0.001))
76 .clamp(0.0, 1.0);
77 Color::new(
78 color.r * shade * (1.0 - fog) + self.fog_color.r * fog,
79 color.g * shade * (1.0 - fog) + self.fog_color.g * fog,
80 color.b * shade * (1.0 - fog) + self.fog_color.b * fog,
81 color.a,
82 )
83 }
84}
85
86#[derive(Clone, Copy, Debug, Default)]
91pub struct LitMaterial;
92
93impl LitMaterial {
94 pub fn new() -> Result<Self, String> {
96 Ok(Self)
97 }
98
99 pub fn begin(&self, settings: LitSettings) {
100 crate::set_lighting(Some(settings.sanitized()));
101 }
102
103 pub fn end(&self) {
104 crate::set_lighting(None);
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 fn settings() -> LitSettings {
113 LitSettings {
114 light_direction: Vec3::Y,
115 camera_position: Vec3::ZERO,
116 fog_color: Color::new(0.2, 0.4, 0.6, 1.0),
117 fog_start: 10.0,
118 fog_end: 20.0,
119 ambient: 0.25,
120 }
121 }
122
123 fn close(actual: Color, expected: Color) {
124 assert!(actual.to_vec().abs_diff_eq(expected.to_vec(), 0.00001));
125 }
126
127 #[test]
128 fn diffuse_faces_receive_light_back_faces_receive_ambient() {
129 let material = settings().sanitized();
130 let color = Color::new(0.8, 0.6, 0.4, 0.3);
131 close(
132 material.shade_vertex(Vec3::ZERO, Vec3::Y * 7.0, color),
133 color,
134 );
135 for normal in [-Vec3::Y, Vec3::X, Vec3::ZERO] {
136 close(
137 material.shade_vertex(Vec3::ZERO, normal, color),
138 Color::new(0.2, 0.15, 0.1, 0.3),
139 );
140 }
141 }
142
143 #[test]
144 fn fog_interpolates_by_world_distance_and_preserves_alpha() {
145 let material = settings().sanitized();
146 let color = Color::new(0.8, 0.6, 0.4, 0.3);
147 close(material.shade_vertex(Vec3::X * 10.0, Vec3::Y, color), color);
148 close(
149 material.shade_vertex(Vec3::X * 15.0, Vec3::Y, color),
150 Color::new(0.5, 0.5, 0.5, 0.3),
151 );
152 close(
153 material.shade_vertex(Vec3::X * 25.0, Vec3::Y, color),
154 material.fog_color.with_alpha(0.3),
155 );
156 let moved_eye = LitSettings {
157 camera_position: Vec3::X * 15.0,
158 ..material
159 };
160 close(
161 moved_eye.shade_vertex(Vec3::X * 15.0, Vec3::Y, color),
162 color,
163 );
164 }
165
166 #[test]
167 fn light_normalization_zero_light_and_collapsed_fog_are_defined() {
168 let color = Color::new(1.0, 1.0, 1.0, 0.7);
169 let material = LitSettings {
170 light_direction: Vec3::Y * 8.0,
171 fog_end: 5.0,
172 ..settings()
173 }
174 .sanitized();
175 close(material.shade_vertex(Vec3::ZERO, Vec3::Y, color), color);
176 close(
177 material.shade_vertex(Vec3::X * 11.0, Vec3::Y, color),
178 material.fog_color.with_alpha(0.7),
179 );
180 let ambient_only = LitSettings {
181 light_direction: Vec3::ZERO,
182 ..material
183 };
184 close(
185 ambient_only.shade_vertex(Vec3::ZERO, Vec3::Y, color),
186 Color::new(0.25, 0.25, 0.25, 0.7),
187 );
188 }
189
190 #[test]
191 fn invalid_settings_do_not_poison_recorded_vertex_colors() {
192 let material = LitSettings {
193 light_direction: Vec3::NAN,
194 camera_position: Vec3::NAN,
195 fog_color: Color::new(f32::NAN, f32::INFINITY, -1.0, 8.0),
196 fog_start: f32::NAN,
197 fog_end: f32::INFINITY,
198 ambient: f32::NAN,
199 }
200 .sanitized();
201 let color = material.shade_vertex(Vec3::ZERO, Vec3::Y, Color::new(1.0, 1.0, 1.0, 1.0));
202 assert!(color.to_vec().is_finite());
203 assert!(
204 color
205 .to_array()
206 .into_iter()
207 .all(|channel| (0.0..=1.0).contains(&channel))
208 );
209 assert_eq!(material.fog_color.b, 0.0);
210 assert_eq!(material.fog_color.a, 1.0);
211 }
212}