Skip to main content

mirage_engine/
skybox.rs

1//! The sky a frame draws where nothing else was drawn, and the light every
2//! surface takes from it.
3
4use core::f32::consts::{PI, TAU};
5use core::fmt;
6use core::fmt::Debug;
7use core::hash::Hash;
8
9use crate::light::rgb;
10use crate::math::{UVec2, Vec3, Vec4};
11use crate::{Assets, Catalog, Color, TextureData};
12
13/// The color of the sky a frame that sets none is drawn and lit by: one
14/// neutral dark grey, the whole way around.
15const DEFAULT: Color = Color::rgb(0.1, 0.1, 0.1);
16
17/// The equirect a gradient is drawn into at startup. A gradient changes
18/// smoothly, so one this small holds it.
19const RASTERIZED: UVec2 = UVec2::new(64, 32);
20
21/// The widest a mip may be for the nine coefficients to be taken from it,
22/// in texels.
23const COEFFICIENT_WIDTH: u32 = 256;
24
25/// How far under the horizon a ground color reaches its whole share: the
26/// `y` of a direction three degrees under level, so the horizon reads as a
27/// line and not a step.
28const GROUND_BLEND: f32 = 0.052_336;
29
30/// What each coefficient's shape is scaled by, taken twice because a
31/// fragment applies none of it; in the order a fragment reads them back.
32const SHAPES: [f32; 9] = [
33    0.282_095, 0.488_603, 0.488_603, 0.488_603, 1.092_548, 1.092_548, 0.315_392, 1.092_548,
34    0.546_274,
35];
36
37/// The share of the light from every direction each coefficient takes,
38/// over `pi`, so a sky of one color lands exactly that color on a surface
39/// facing anywhere.
40const TAKEN: [f32; 9] = [
41    1.0,
42    2.0 / 3.0,
43    2.0 / 3.0,
44    2.0 / 3.0,
45    0.25,
46    0.25,
47    0.25,
48    0.25,
49    0.25,
50];
51
52/// Turns a game's value into a skybox.
53///
54/// Values are cache keys: equal values must build the same sky. [`Catalog`]
55/// proves every named asset loads before the first frame, and a startup
56/// error names a skybox by its [`Debug`].
57pub trait Skyboxes: Catalog + Clone + Debug + Eq + Hash {
58    /// Builds this value's sky, the first time a frame is drawn by it.
59    ///
60    /// Must not read a file or the network; loaded data is in `assets`.
61    fn build(&self, assets: &Assets) -> SkyboxData;
62}
63
64/// The vocabulary of a game with no sky of its own to name.
65///
66/// No value of it exists, so such a game draws and is lit by the default
67/// sky.
68#[derive(Clone, Debug, Eq, Hash, PartialEq)]
69pub enum NoSkyboxes {}
70
71impl Catalog for NoSkyboxes {
72    fn catalog() -> Vec<Self> {
73        Vec::new()
74    }
75}
76
77impl Skyboxes for NoSkyboxes {
78    fn build(&self, _assets: &Assets) -> SkyboxData {
79        match *self {}
80    }
81}
82
83/// The sky one value of a game's vocabulary is drawn and lit from: one of
84/// two kinds, an image of the whole sky or a gradient between three colors,
85/// the fraction of its own light it lands on a surface, and the ground
86/// color under its horizon where a game states one.
87#[derive(Clone, Debug)]
88pub struct SkyboxData {
89    kind: Kind,
90    light: f32,
91    ground: Option<Color>,
92}
93
94/// The closed set of skies; [`SkyboxData`] has one constructor for each.
95#[derive(Clone, Debug)]
96enum Kind {
97    Equirect(TextureData),
98    Gradient(Gradient),
99}
100
101impl SkyboxData {
102    /// The sky `image` holds: the whole way around across it, and zenith to
103    /// nadir down it, so it is twice as wide as it is tall.
104    ///
105    /// Required if you want a sky an image holds. An image not twice as wide
106    /// as it is tall stops startup naming the skybox.
107    pub fn equirect(image: TextureData) -> Self {
108        Self::of(Kind::Equirect(image))
109    }
110
111    /// The sky `zenith` straight up, `horizon` level, and `nadir` straight
112    /// down, each texel between them the share its own elevation takes.
113    ///
114    /// Required if you want a sky with no image at all: one color in all
115    /// three lands that color on every surface, from every direction.
116    pub fn gradient(zenith: Color, horizon: Color, nadir: Color) -> Self {
117        Self::of(Kind::Gradient(Gradient::new(zenith, horizon, nadir)))
118    }
119
120    /// The same sky landing `fraction` of its own light on every surface,
121    /// and a surface that reflects it reflecting that share. The sky the
122    /// frame draws is unchanged.
123    ///
124    /// Required if you want the frame's own lights to show under a bright
125    /// sky. `1.0` where never called, held at zero and above; past `1.0`
126    /// lands more light than the sky holds.
127    pub fn lit_by(mut self, fraction: f32) -> Self {
128        self.light = fraction.max(0.0);
129        self
130    }
131
132    /// The same sky reading as `color` in every direction under the horizon,
133    /// blended over the three degrees below it: what a metal reflects there
134    /// and what the sky lands on a surface facing down take that color, and
135    /// so does the sky the frame draws where nothing covers it.
136    ///
137    /// Required if you want a metal to reflect the floor it stands on rather
138    /// than the sky's own underside. No ground where never called.
139    pub fn with_ground(mut self, color: Color) -> Self {
140        self.ground = Some(color);
141        self
142    }
143
144    fn of(kind: Kind) -> Self {
145        Self {
146            kind,
147            light: 1.0,
148            ground: None,
149        }
150    }
151
152    /// This sky as the engine keeps it for the run, or the error its image
153    /// is no sky for.
154    pub(crate) fn resident(&self) -> Result<Resident, SkyboxError> {
155        let largest = match &self.kind {
156            Kind::Equirect(image) => Mip::of(image)?,
157            Kind::Gradient(gradient) => Mip::between(*gradient),
158        };
159        let largest = match self.ground {
160            Some(ground) => largest.over(rgb(ground)),
161            None => largest,
162        };
163
164        Ok(Resident::of(largest).lit_by(self.light))
165    }
166}
167
168impl Default for SkyboxData {
169    /// The sky a name no source holds returns: black the whole way around.
170    fn default() -> Self {
171        Self::equirect(TextureData::rgba8(UVec2::new(2, 1), vec![0; 8]))
172    }
173}
174
175/// The three colors a gradient sky is drawn between.
176#[derive(Clone, Copy, Debug, PartialEq)]
177pub(crate) struct Gradient {
178    /// Straight up.
179    zenith: Color,
180    /// Level with the horizon.
181    horizon: Color,
182    /// Straight down.
183    nadir: Color,
184}
185
186impl Gradient {
187    /// The sky `zenith` straight up, `horizon` level, and `nadir` straight
188    /// down.
189    pub(crate) const fn new(zenith: Color, horizon: Color, nadir: Color) -> Self {
190        Self {
191            zenith,
192            horizon,
193            nadir,
194        }
195    }
196}
197
198impl Default for Gradient {
199    /// The sky a frame that sets none is drawn and lit by: one neutral dark
200    /// grey, the whole way around.
201    fn default() -> Self {
202        Self::new(DEFAULT, DEFAULT, DEFAULT)
203    }
204}
205
206/// Why an image is no sky at all, which startup reports under the name of
207/// the skybox value it was built for.
208#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
209pub(crate) enum SkyboxError {
210    /// It is not twice as wide as it is tall.
211    Sides { width: u32, height: u32 },
212    /// It holds another count of bytes than the four its every texel needs.
213    Pixels {
214        width: u32,
215        height: u32,
216        bytes: usize,
217    },
218}
219
220impl fmt::Display for SkyboxError {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        match self {
223            Self::Sides { width, height } => write!(
224                f,
225                "is {width}x{height}; a skybox image is twice as wide as it is tall"
226            ),
227            Self::Pixels {
228                width,
229                height,
230                bytes,
231            } => write!(
232                f,
233                "is {width}x{height} over {bytes} bytes, where every texel needs four"
234            ),
235        }
236    }
237}
238
239/// One sky as the engine keeps it: the mips it is drawn and reflected from,
240/// largest first, and the light it lands on a surface.
241pub(crate) struct Resident {
242    largest: Mip,
243    /// Every halving of `largest`, down to the one that is a single texel.
244    halved: Vec<Mip>,
245    irradiance: Irradiance,
246    /// What a surface's reflection of this sky is scaled by.
247    light: f32,
248}
249
250impl Resident {
251    /// The sky `gradient` holds, drawn into an equirect of its own.
252    pub(crate) fn gradient(gradient: Gradient) -> Self {
253        Self::of(Mip::between(gradient))
254    }
255
256    /// Halves `largest` down to a single texel, and takes the nine
257    /// coefficients from the first mip no wider than [`COEFFICIENT_WIDTH`].
258    fn of(largest: Mip) -> Self {
259        let mut halved: Vec<Mip> = Vec::new();
260        while let Some(mip) = halved.last().unwrap_or(&largest).halved() {
261            halved.push(mip);
262        }
263        // What a surface takes changes smoothly over the sky, so a small
264        // copy of it holds all nine.
265        let narrow = halved.iter().fold(&largest, |narrowest, mip| {
266            match narrowest.size.x <= COEFFICIENT_WIDTH {
267                true => narrowest,
268                false => mip,
269            }
270        });
271
272        Self {
273            irradiance: Irradiance::of(narrow),
274            largest,
275            halved,
276            light: 1.0,
277        }
278    }
279
280    /// The same sky landing `fraction` of its light: the coefficients are
281    /// scaled here, and the frame's own values hold the fraction for the
282    /// reflection.
283    fn lit_by(mut self, fraction: f32) -> Self {
284        self.irradiance = self.irradiance.scaled(fraction);
285        self.light = fraction;
286        self
287    }
288
289    /// What a surface's reflection of this sky is scaled by; the
290    /// coefficients already hold it.
291    pub(crate) fn share(&self) -> f32 {
292        self.light
293    }
294
295    /// The size of its largest mip, in texels.
296    pub(crate) fn size(&self) -> UVec2 {
297        self.largest.size
298    }
299
300    /// Every mip of this sky, largest first.
301    pub(crate) fn mips(&self) -> impl Iterator<Item = &Mip> {
302        core::iter::once(&self.largest).chain(&self.halved)
303    }
304
305    /// How many mips that is.
306    pub(crate) fn mip_count(&self) -> u32 {
307        1 + self.halved.len() as u32
308    }
309
310    /// The mip level a fully rough surface reflects this sky from: the
311    /// smallest mip's, counting `0.0` at the largest.
312    pub(crate) fn top_mip(&self) -> f32 {
313        self.halved.len() as f32
314    }
315
316    /// The light this sky lands on a surface, by the direction the surface
317    /// faces.
318    pub(crate) fn irradiance(&self) -> Irradiance {
319        self.irradiance
320    }
321}
322
323/// One mip of a sky: its size, and its linear texels row by row from the
324/// top left.
325pub(crate) struct Mip {
326    size: UVec2,
327    texels: Vec<Vec3>,
328}
329
330impl Mip {
331    /// The light `image` holds, or the error that image is no sky for.
332    fn of(image: &TextureData) -> Result<Self, SkyboxError> {
333        let size = image.size();
334        let (width, height) = (size.x, size.y);
335        if height == 0 || width != 2 * height {
336            return Err(SkyboxError::Sides { width, height });
337        }
338        // A texture built by hand holds whatever bytes it was given, and
339        // every mip under this one reads one texel per texel of its size.
340        let bytes = image.pixels().len();
341        if bytes as u64 != 4 * u64::from(width) * u64::from(height) {
342            return Err(SkyboxError::Pixels {
343                width,
344                height,
345                bytes,
346            });
347        }
348
349        Ok(Self {
350            size,
351            texels: image
352                .pixels()
353                .chunks_exact(4)
354                .map(|texel| rgb(Color::of_srgb([texel[0], texel[1], texel[2]])))
355                .collect(),
356        })
357    }
358
359    /// The sky `gradient` holds: each texel the share its own elevation
360    /// takes of the color above it and the color below it.
361    fn between(gradient: Gradient) -> Self {
362        let zenith = rgb(gradient.zenith);
363        let horizon = rgb(gradient.horizon);
364        let nadir = rgb(gradient.nadir);
365
366        Self {
367            size: RASTERIZED,
368            texels: Grid::of(RASTERIZED)
369                .directions()
370                .map(|direction| match direction.y >= 0.0 {
371                    true => horizon.lerp(zenith, direction.y),
372                    false => horizon.lerp(nadir, -direction.y),
373                })
374                .collect(),
375        }
376    }
377
378    /// This mip with `ground` under the horizon, blended across
379    /// [`GROUND_BLEND`] below it.
380    fn over(self, ground: Vec3) -> Self {
381        let texels = Grid::of(self.size)
382            .directions()
383            .zip(self.texels)
384            .map(|(direction, texel)| {
385                let share = (-direction.y / GROUND_BLEND).clamp(0.0, 1.0);
386                texel.lerp(ground, share)
387            })
388            .collect();
389
390        Self {
391            size: self.size,
392            texels,
393        }
394    }
395
396    /// This mip at half its size, each texel the average of the four it
397    /// covers; `None` for one that is a single texel.
398    fn halved(&self) -> Option<Self> {
399        if self.size.max_element() <= 1 {
400            return None;
401        }
402        let size = (self.size / 2).max(UVec2::ONE);
403
404        Some(Self {
405            size,
406            texels: (0..size.y)
407                .flat_map(|down| (0..size.x).map(move |across| UVec2::new(across, down)))
408                .map(|at| self.averaged(at))
409                .collect(),
410        })
411    }
412
413    /// This mip's size in texels.
414    pub(crate) fn size(&self) -> UVec2 {
415        self.size
416    }
417
418    /// Its texels, row by row from the top left.
419    pub(crate) fn texels(&self) -> &[Vec3] {
420        &self.texels
421    }
422
423    /// The average of the four texels this mip holds under texel `at` of
424    /// the mip half its size.
425    fn averaged(&self, at: UVec2) -> Vec3 {
426        let corner = at * 2;
427        let covered: Vec3 = [UVec2::ZERO, UVec2::X, UVec2::Y, UVec2::ONE]
428            .into_iter()
429            .map(|step| self.texel(corner + step))
430            .sum();
431
432        covered / 4.0
433    }
434
435    /// The texel `at`, held within this mip.
436    fn texel(&self, at: UVec2) -> Vec3 {
437        let held = at.min(self.size - UVec2::ONE);
438
439        self.texels[(held.y * self.size.x + held.x) as usize]
440    }
441}
442
443/// The texel grid of one equirect image: the direction each texel holds,
444/// and how much of the sky it covers.
445///
446/// `forward.wgsl` reads a direction back through the same mapping.
447#[derive(Clone, Copy)]
448struct Grid {
449    size: UVec2,
450}
451
452impl Grid {
453    fn of(size: UVec2) -> Self {
454        Self { size }
455    }
456
457    /// Each texel's direction, row by row from the top left.
458    fn directions(self) -> impl Iterator<Item = Vec3> {
459        self.rows().flat_map(Row::directions)
460    }
461
462    /// Each texel's direction and the fraction of the whole sky it covers,
463    /// row by row from the top left.
464    fn texels(self) -> impl Iterator<Item = (Vec3, f32)> {
465        self.rows().flat_map(|row| {
466            row.directions()
467                .map(move |direction| (direction, row.covered))
468        })
469    }
470
471    /// The rows from the top, each at its own angle from the zenith.
472    fn rows(self) -> impl Iterator<Item = Row> {
473        let size = self.size.as_vec2();
474        let texel = (PI / size.y) * (TAU / size.x);
475
476        (0..self.size.y).map(move |down| {
477            let latitude = (down as f32 + 0.5) / size.y * PI;
478            let (latitude_sin, latitude_cos) = latitude.sin_cos();
479            Row {
480                latitude_sin,
481                latitude_cos,
482                columns: self.size.x,
483                covered: latitude_sin * texel,
484            }
485        })
486    }
487}
488
489/// One row of a [`Grid`]: its angle from the zenith, and how much of the
490/// sky each of its texels covers.
491#[derive(Clone, Copy)]
492struct Row {
493    latitude_sin: f32,
494    latitude_cos: f32,
495    columns: u32,
496    covered: f32,
497}
498
499impl Row {
500    /// Each texel's direction, from the left.
501    fn directions(self) -> impl Iterator<Item = Vec3> {
502        let columns = self.columns as f32;
503
504        (0..self.columns).map(move |across| {
505            let longitude = ((across as f32 + 0.5) / columns - 0.5) * TAU;
506            let (longitude_sin, longitude_cos) = longitude.sin_cos();
507            Vec3::new(
508                self.latitude_sin * longitude_sin,
509                self.latitude_cos,
510                -self.latitude_sin * longitude_cos,
511            )
512        })
513    }
514}
515
516/// The light a sky lands on a surface, as nine coefficients a fragment
517/// reads through the direction the surface faces.
518///
519/// A coefficient's shape is its own factor at a direction `n`: `1`, `n.y`,
520/// `n.z`, `n.x`, `n.x * n.y`, `n.y * n.z`, `3 * n.z * n.z - 1`, `n.x * n.z`
521/// and `n.x * n.x - n.y * n.y`, in that order. The coefficients hold the
522/// sky's own light with the share a flat surface takes of each direction
523/// already scaled into them, so what a fragment reads is what the surface
524/// takes.
525#[derive(Clone, Copy, Debug)]
526pub(crate) struct Irradiance([Vec3; 9]);
527
528impl Irradiance {
529    /// The nine coefficients of `mip`.
530    fn of(mip: &Mip) -> Self {
531        let mut coefficients = [Vec3::ZERO; 9];
532        for (texel, (direction, covered)) in mip.texels().iter().zip(Grid::of(mip.size).texels()) {
533            let light = *texel * covered;
534            for (coefficient, shape) in coefficients.iter_mut().zip(Self::shapes(direction)) {
535                *coefficient += light * shape;
536            }
537        }
538
539        Self(core::array::from_fn(|at| {
540            coefficients[at] * TAKEN[at] * SHAPES[at] * SHAPES[at]
541        }))
542    }
543
544    /// The same light, `fraction` of it.
545    fn scaled(self, fraction: f32) -> Self {
546        Self(self.0.map(|coefficient| coefficient * fraction))
547    }
548
549    /// The nine coefficients as the frame's own values hold them, each in
550    /// the first three lanes of a `Vec4` of its own.
551    pub(crate) fn lanes(&self) -> [Vec4; 9] {
552        self.0.map(|coefficient| coefficient.extend(0.0))
553    }
554
555    /// The shape of each coefficient at `direction`, in the order a
556    /// fragment reads them back.
557    fn shapes(direction: Vec3) -> [f32; 9] {
558        let Vec3 { x, y, z } = direction;
559
560        [
561            1.0,
562            y,
563            z,
564            x,
565            x * y,
566            y * z,
567            3.0 * z * z - 1.0,
568            x * z,
569            x * x - y * y,
570        ]
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    /// A sky of one color, as a gradient and as an image of that color.
579    fn flat(color: Color) -> (Resident, Resident) {
580        let encoded = |linear: f32| {
581            let encoded = match linear <= 0.003_130_8 {
582                true => linear * 12.92,
583                false => 1.055 * linear.powf(1.0 / 2.4) - 0.055,
584            };
585            (encoded * 255.0).round() as u8
586        };
587        let texel = [
588            encoded(color.red),
589            encoded(color.green),
590            encoded(color.blue),
591            u8::MAX,
592        ];
593        let image = TextureData::rgba8(UVec2::new(16, 8), texel.repeat(16 * 8));
594
595        (
596            Resident::gradient(Gradient::new(color, color, color)),
597            SkyboxData::equirect(image)
598                .resident()
599                .expect("that image is a sky"),
600        )
601    }
602
603    #[test]
604    fn a_sky_of_one_color_lands_that_color_on_a_surface_facing_anywhere() {
605        let color = Color::rgb(0.25, 0.5, 0.75);
606        let (gradient, image) = flat(color);
607
608        for sky in [gradient, image] {
609            let coefficients = sky.irradiance().0;
610
611            for (read, held) in [coefficients[0].x, coefficients[0].y, coefficients[0].z]
612                .into_iter()
613                .zip([color.red, color.green, color.blue])
614            {
615                assert!(
616                    (read - held).abs() < 0.01,
617                    "the first coefficient holds the color itself, got {read} against {held}"
618                );
619            }
620            for coefficient in &coefficients[1..] {
621                assert!(
622                    coefficient.length() < 0.01,
623                    "and no other holds anything, got {coefficient}"
624                );
625            }
626        }
627    }
628
629    #[test]
630    fn a_sky_bright_above_lands_more_light_on_a_surface_facing_up() {
631        let coefficients =
632            Resident::gradient(Gradient::new(Color::WHITE, Color::BLACK, Color::BLACK))
633                .irradiance()
634                .0;
635
636        assert!(
637            coefficients[1].y > 0.0,
638            "the coefficient a normal reads through its own `+Y` rises with the \
639             light above, got {}",
640            coefficients[1].y
641        );
642        assert!(
643            coefficients[0].y > 0.0,
644            "and the sky lands light on a surface facing anywhere"
645        );
646    }
647
648    #[test]
649    fn an_image_not_twice_as_wide_as_it_is_tall_is_no_sky_at_all() {
650        let square = TextureData::rgba8(UVec2::splat(4), vec![u8::MAX; 4 * 4 * 4]);
651
652        assert_eq!(
653            SkyboxData::equirect(square).resident().err(),
654            Some(SkyboxError::Sides {
655                width: 4,
656                height: 4
657            })
658        );
659        assert_eq!(
660            SkyboxData::equirect(TextureData::default())
661                .resident()
662                .err(),
663            Some(SkyboxError::Sides {
664                width: 0,
665                height: 0
666            }),
667            "and an image with no texels at all is none either"
668        );
669    }
670
671    #[test]
672    fn a_sky_is_halved_down_to_one_texel() {
673        let sky = Resident::gradient(Gradient::default());
674        let sizes: Vec<UVec2> = sky.mips().map(Mip::size).collect();
675
676        assert_eq!(sizes.first(), Some(&RASTERIZED));
677        assert_eq!(sizes.last(), Some(&UVec2::ONE));
678        assert_eq!(sizes.len(), 7, "one mip per halving of the widest side");
679        assert_eq!(sky.mip_count(), 7);
680        assert_eq!(sky.top_mip(), 6.0, "the smallest of them is the last");
681        assert!(
682            sky.mips()
683                .all(|mip| mip.texels().len() == (mip.size().x * mip.size().y) as usize),
684            "each of them holds its own texels"
685        );
686    }
687
688    #[test]
689    fn the_smallest_mip_holds_the_average_of_the_sky() {
690        let sky = Resident::gradient(Gradient::new(Color::WHITE, Color::BLACK, Color::BLACK));
691        let smallest = sky.mips().last().expect("the mips end at one texel");
692
693        assert_eq!(smallest.size(), UVec2::ONE);
694        let average = smallest.texels()[0].x;
695        assert!(
696            (0.1..0.4).contains(&average),
697            "a sky white above and black below averages between them, got {average}"
698        );
699    }
700
701    #[test]
702    fn a_ground_colors_every_texel_under_the_horizon_and_none_above_it() {
703        let sky = SkyboxData::gradient(Color::WHITE, Color::WHITE, Color::WHITE)
704            .with_ground(Color::rgb(0.5, 0.0, 0.0))
705            .resident()
706            .expect("a gradient is a sky");
707        let largest = sky.mips().next().expect("a sky has a largest mip");
708        let size = largest.size();
709        let top = largest.texel(UVec2::new(size.x / 2, 0));
710        let bottom = largest.texel(UVec2::new(size.x / 2, size.y - 1));
711
712        assert!(
713            top.abs_diff_eq(Vec3::ONE, 0.01),
714            "the zenith keeps the sky's own color, got {top}"
715        );
716        assert!(
717            bottom.abs_diff_eq(Vec3::new(0.5, 0.0, 0.0), 0.01),
718            "and the nadir reads as the ground, got {bottom}"
719        );
720    }
721
722    #[test]
723    fn a_ground_darker_than_the_sky_lands_less_light_on_a_surface_facing_down() {
724        let coefficients = SkyboxData::gradient(Color::WHITE, Color::WHITE, Color::WHITE)
725            .with_ground(Color::BLACK)
726            .resident()
727            .expect("a gradient is a sky")
728            .irradiance()
729            .0;
730
731        assert!(
732            coefficients[1].x > 0.0,
733            "the coefficient a normal reads through its own `+Y` rises with the \
734             light above and not below, got {}",
735            coefficients[1].x
736        );
737    }
738}