1use alloc::vec;
8use alloc::vec::Vec;
9
10use super::bake::{CubeBake, prefilter_mip0};
11use super::schedule::RowScheduler;
12use super::{
13 DEFAULT_IRRADIANCE_PHI_SAMPLES, DEFAULT_IRRADIANCE_THETA_SAMPLES, max_mip_count,
14 prefilter_roughness, serialise_payload,
15};
16use crate::math::{acos, atan2, exp, floor, powi, sqrt};
17
18#[derive(Debug, Clone)]
20pub struct HdrImage {
21 pub width: u32,
23 pub height: u32,
25 pub pixels: Vec<[f32; 3]>,
27}
28
29pub fn equirect_to_cube(hdr: &HdrImage, face_size: u32) -> [Vec<f32>; 6] {
33 let f = face_size as usize;
34 let mut faces: [Vec<f32>; 6] = core::array::from_fn(|_| vec![0.0; f * f * 4]);
35 for (face, face_buf) in faces.iter_mut().enumerate() {
36 for y in 0..f {
37 for x in 0..f {
38 let u = (x as f32 + 0.5) / face_size as f32 * 2.0 - 1.0;
40 let v = (y as f32 + 0.5) / face_size as f32 * 2.0 - 1.0;
41 let dir = face_uv_to_dir(face, u, v);
42 let sample = sample_equirect(hdr, dir);
43 let off = (y * f + x) * 4;
44 face_buf[off] = sample[0];
45 face_buf[off + 1] = sample[1];
46 face_buf[off + 2] = sample[2];
47 face_buf[off + 3] = 1.0;
48 }
49 }
50 }
51 faces
52}
53
54fn face_uv_to_dir(face: usize, u: f32, v: f32) -> [f32; 3] {
57 let d = match face {
58 0 => [1.0, -v, -u],
59 1 => [-1.0, -v, u],
60 2 => [u, 1.0, v],
61 3 => [u, -1.0, -v],
62 4 => [u, -v, 1.0],
63 5 => [-u, -v, -1.0],
64 _ => unreachable!("invalid cube face index {}", face),
65 };
66 normalize3(d)
67}
68
69fn normalize3(v: [f32; 3]) -> [f32; 3] {
70 let l = length(v).max(1e-20);
71 [v[0] / l, v[1] / l, v[2] / l]
72}
73
74fn length(v: [f32; 3]) -> f32 {
75 sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
76}
77
78fn sample_equirect(hdr: &HdrImage, dir: [f32; 3]) -> [f32; 3] {
79 let phi = atan2(dir[2], dir[0]); let theta = acos(dir[1].clamp(-1.0, 1.0)); let u = phi / (2.0 * core::f32::consts::PI) + 0.5;
82 let v = theta / core::f32::consts::PI;
83 let fx = u * hdr.width as f32 - 0.5;
84 let fy = v * hdr.height as f32 - 0.5;
85 let x0 = floor(fx) as i32;
86 let y0 = floor(fy) as i32;
87 let dx = fx - x0 as f32;
88 let dy = fy - y0 as f32;
89 let x1 = x0 + 1;
90 let y1 = y0 + 1;
91 let w00 = (1.0 - dx) * (1.0 - dy);
92 let w10 = dx * (1.0 - dy);
93 let w01 = (1.0 - dx) * dy;
94 let w11 = dx * dy;
95 let p00 = fetch_wrap(hdr, x0, y0);
96 let p10 = fetch_wrap(hdr, x1, y0);
97 let p01 = fetch_wrap(hdr, x0, y1);
98 let p11 = fetch_wrap(hdr, x1, y1);
99 [
100 p00[0] * w00 + p10[0] * w10 + p01[0] * w01 + p11[0] * w11,
101 p00[1] * w00 + p10[1] * w10 + p01[1] * w01 + p11[1] * w11,
102 p00[2] * w00 + p10[2] * w10 + p01[2] * w01 + p11[2] * w11,
103 ]
104}
105
106fn fetch_wrap(hdr: &HdrImage, x: i32, y: i32) -> [f32; 3] {
107 let w = hdr.width as i32;
109 let h = hdr.height as i32;
110 let xw = x.rem_euclid(w);
111 let yc = y.clamp(0, h - 1);
112 hdr.pixels[(yc * w + xw) as usize]
113}
114
115pub fn bake_payload<S: RowScheduler>(
122 hdr: &HdrImage,
123 prefilter_face: u32,
124 irradiance_face: u32,
125 prefilter_samples: u32,
126 prefilter_clamp: f32,
127 rows: &S,
128) -> Vec<u8> {
129 let source_cube = equirect_to_cube(hdr, prefilter_face);
130 let prefilter_mips = max_mip_count(prefilter_face);
131 let irradiance = CubeBake::irradiance(
132 &source_cube,
133 prefilter_face,
134 irradiance_face,
135 DEFAULT_IRRADIANCE_PHI_SAMPLES,
136 DEFAULT_IRRADIANCE_THETA_SAMPLES,
137 )
138 .bake(rows);
139 let mut prefilter = Vec::with_capacity(prefilter_mips as usize);
141 prefilter.push(prefilter_mip0(
142 &source_cube,
143 prefilter_face,
144 prefilter_clamp,
145 false,
146 ));
147 for mip in 1..prefilter_mips {
148 prefilter.push(
149 CubeBake::ggx(
150 &source_cube,
151 prefilter_face,
152 prefilter_face >> mip,
153 prefilter_roughness(mip, prefilter_mips),
154 prefilter_samples,
155 prefilter_clamp,
156 )
157 .bake(rows),
158 );
159 }
160 serialise_payload(
161 irradiance_face,
162 prefilter_face,
163 prefilter_mips,
164 &irradiance,
165 &prefilter,
166 )
167}
168
169pub fn generate_sky_equirect() -> HdrImage {
175 let width = 256u32;
176 let height = 128u32;
177 let zenith = [0.012, 0.105, 0.526];
179 let mid = [0.142, 0.355, 0.708];
180 let horizon = [0.563, 0.726, 0.857];
181 let sun_u = 0.25_f32;
183 let sun_v = 0.35_f32;
184 let sun_color = [3.0, 2.6, 2.1];
185 let mut pixels = Vec::with_capacity((width * height) as usize);
186 for row in 0..height {
187 let v = row as f32 / (height - 1) as f32;
188 let t = if v < 0.5 { 1.0 - v * 2.0 } else { 0.0 };
191 let base = if t > 0.5 {
192 let s = (t - 0.5) * 2.0;
193 [
194 lerp(mid[0], zenith[0], s),
195 lerp(mid[1], zenith[1], s),
196 lerp(mid[2], zenith[2], s),
197 ]
198 } else {
199 let s = t * 2.0;
200 let warm = powi(1.0 - s, 2) * 0.07;
201 [
202 lerp(horizon[0], mid[0], s) + warm * 0.5,
203 lerp(horizon[1], mid[1], s) + warm * 0.25,
204 lerp(horizon[2], mid[2], s),
205 ]
206 };
207 for col in 0..width {
208 let u = col as f32 / (width - 1) as f32;
209 let du = (u - sun_u).abs();
211 let du = du.min(1.0 - du); let dv = v - sun_v;
213 let d2 = du * du + dv * dv;
214 let sun_amt = exp(-d2 / 0.0006);
215 let r = base[0] + sun_color[0] * sun_amt;
216 let g = base[1] + sun_color[1] * sun_amt;
217 let b = base[2] + sun_color[2] * sun_amt;
218 pixels.push([r, g, b]);
219 }
220 }
221 HdrImage {
222 width,
223 height,
224 pixels,
225 }
226}
227
228fn lerp(a: f32, b: f32, t: f32) -> f32 {
229 a + (b - a) * t
230}
231
232#[cfg(test)]
233mod tests {
234 use super::super::deserialise;
235 use super::super::schedule::Serial;
236 use super::*;
237
238 #[test]
239 fn sky_generator_bakes_into_a_full_payload_serially() {
240 let hdr = generate_sky_equirect();
241 assert_eq!((hdr.width, hdr.height), (256, 128));
242 let payload = bake_payload(&hdr, 16, 8, 32, 12.0, &Serial);
243 let view = deserialise(&payload).expect("deserialise");
244 assert_eq!(view.irradiance_face, 8);
245 assert_eq!(view.prefilter_face, 16);
246 assert_eq!(view.prefilter_mip_bytes.len(), 3);
248 }
249
250 #[test]
251 fn equirect_solid_color_produces_solid_cube() {
252 let pixel = [0.8f32, 0.4, 0.1];
253 let hdr = HdrImage {
254 width: 32,
255 height: 16,
256 pixels: vec![pixel; 32 * 16],
257 };
258 let faces = equirect_to_cube(&hdr, 16);
259 for (idx, face) in faces.iter().enumerate() {
260 assert_eq!(face.len(), 16 * 16 * 4);
261 for px in face.chunks_exact(4) {
262 assert!((px[0] - pixel[0]).abs() < 1e-4, "face {} R", idx);
263 assert!((px[1] - pixel[1]).abs() < 1e-4, "face {} G", idx);
264 assert!((px[2] - pixel[2]).abs() < 1e-4, "face {} B", idx);
265 assert!((px[3] - 1.0).abs() < 1e-6, "face {} A", idx);
266 }
267 }
268 }
269
270 #[test]
271 fn equirect_red_seam_lights_only_the_minus_x_face() {
272 let mut pixels = vec![[0.0f32; 3]; 32 * 16];
277 for y in 0..16 {
278 for &x in &[30usize, 31, 0, 1] {
279 pixels[y * 32 + x] = [10.0, 0.0, 0.0];
280 }
281 }
282 let hdr = HdrImage {
283 width: 32,
284 height: 16,
285 pixels,
286 };
287 let faces = equirect_to_cube(&hdr, 16);
288 let mean_red = |face: &[f32]| -> f32 {
289 let n = face.len() / 4;
290 face.chunks_exact(4).map(|p| p[0]).sum::<f32>() / n as f32
291 };
292 let plus_x = mean_red(&faces[0]);
293 let minus_x = mean_red(&faces[1]);
294 assert!(
295 minus_x > 5.0 * plus_x.max(0.001),
296 "-X mean red ({}) should dwarf +X mean red ({})",
297 minus_x,
298 plus_x
299 );
300 }
301
302 #[test]
303 #[should_panic(expected = "invalid cube face index 6")]
304 fn face_uv_to_dir_rejects_an_out_of_range_face() {
305 let _ = face_uv_to_dir(6, 0.0, 0.0);
306 }
307}