citro3d/
light.rs

1//! Bindings for accessing the lighting part of the GPU pipeline
2//!
3//! The hardware at play is shown in [this diagram][hardware], you should probably have
4//! it open as a reference for the documentation in this module.
5//!
6//! # Hardware lights
7//! There are 8 lights in the GPU's pipeline each of which have 4 colour fields and 1 spotlight colour,
8//! you can set all of them at once with [`LightEnv::set_material`]. When rendering for example you call
9//! `set_material` in your preparation code before the actual draw call.
10//!
11//! For things like specular lighting we need to go a bit deeper
12//!
13//! # LUTS
14//! LUTS are lookup tables, in this case for the GPU. They are created ahead of time and stored in [`Lut`]'s,
15//! [`Lut::from_fn`] essentially memoises the given function with the input changing depending on what
16//! input it is bound to when setting it on the [`LightEnv`].
17//!
18//! ## Example
19//! Lets say we have this code
20//!
21//! ```
22//! # use citro3d::{Instance, light::{LutId, LightInput, Lut}};
23//! let mut inst = Instance::new();
24//! let mut env = inst.light_env_mut();
25//! env.as_mut().connect_lut(
26//!     LutInputId::D0,
27//!     LutInput::NormalView,
28//!     Lut::from_fn(|x| x.powf(10.0)),
29//! );
30//! ```
31//!
32//! This places the LUT in `D0` (refer to [the diagram][hardware]) and connects the input wire as the dot product
33//! of the normal and view vectors. `x` is effectively the dot product of the normal and view for every vertex and
34//! the return of the closure goes out on the corresponding wire
35//! (which in the case of `D0` is used for specular lighting after being combined with with specular0)
36//!
37//!
38//!
39//! [hardware]: https://raw.githubusercontent.com/wwylele/misc-3ds-diagram/master/pica-pipeline.svg
40
41use std::{marker::PhantomPinned, mem::MaybeUninit, ops::Range, pin::Pin};
42
43use pin_array::PinArray;
44
45use crate::{
46    color::Color,
47    math::{FVec3, FVec4},
48};
49
50/// Index for one of the 8 hardware lights in the [lighting environment](LightEnv).
51///
52/// Usually you don't want to construct one of these directly but use [`LightEnv::create_light`].
53// Note we use a u8 here since usize is overkill and it saves a few bytes
54#[derive(Clone, Copy, PartialEq, Eq, Hash)]
55pub struct LightIndex(u8);
56
57const NB_LIGHTS: usize = 8;
58
59impl LightIndex {
60    /// Manually create a `LightIndex` with a specific index
61    ///
62    /// # Panics
63    /// if `idx` out of range for the number of lights (>=8)
64    pub fn new(idx: usize) -> Self {
65        assert!(idx < NB_LIGHTS);
66        Self(idx as u8)
67    }
68
69    /// Converts the index back into a raw integer.
70    pub fn as_usize(self) -> usize {
71        self.0 as usize
72    }
73}
74
75type LightArray = PinArray<Option<Light>, NB_LIGHTS>;
76
77/// Lighting environment, passed as one of the fragment stages.
78///
79/// A [`LightEnv`] is comprised of 8 different [lights](Light) governed by the same lighting algorithm.
80pub struct LightEnv {
81    raw: citro3d_sys::C3D_LightEnv,
82    /// The actual light data pointed to by the lights element of `raw`
83    ///
84    /// Note this is `Pin` as well, because `raw` means we are _actually_ self-referential which
85    /// is horrible but the best bad option in this case. Moving the one of these elements would
86    /// break the pointers in `raw`
87    lights: LightArray,
88    luts: [Option<Lut>; 6],
89    _pin: PhantomPinned,
90}
91
92/// Light source, used by a [`LightEnv`].
93///
94/// Lights can be simple omnidirectional point lights or setup with a directional [spotlight](Light::set_spotlight) effect.
95pub struct Light {
96    raw: citro3d_sys::C3D_Light,
97    spotlight: Option<Spotlight>,
98    distance_attenuation: Option<DistanceAttenuation>,
99    _pin: PhantomPinned,
100}
101
102/// Lighting and surface material used by the fragment stage.
103#[derive(Debug, Default, Clone, Copy)]
104pub struct Material {
105    pub ambient: Option<Color>,
106    pub diffuse: Option<Color>,
107    pub specular0: Option<Color>,
108    pub specular1: Option<Color>,
109    pub emission: Option<Color>,
110}
111
112impl Material {
113    pub fn to_raw(self) -> citro3d_sys::C3D_Material {
114        citro3d_sys::C3D_Material {
115            ambient: self.ambient.unwrap_or_default().to_parts_bgr(),
116            diffuse: self.diffuse.unwrap_or_default().to_parts_bgr(),
117            specular0: self.specular0.unwrap_or_default().to_parts_bgr(),
118            specular1: self.specular1.unwrap_or_default().to_parts_bgr(),
119            emission: self.emission.unwrap_or_default().to_parts_bgr(),
120        }
121    }
122}
123
124impl LightEnv {
125    /// Constructs a new lighting environment.
126    ///
127    /// Due to the internal representation of a lighting environment,
128    /// the lighting environment and the various lights are all pinned objects.
129    pub fn new_pinned() -> Pin<Box<LightEnv>> {
130        Box::pin({
131            let raw = unsafe {
132                let mut env = MaybeUninit::zeroed();
133                citro3d_sys::C3D_LightEnvInit(env.as_mut_ptr());
134                env.assume_init()
135            };
136
137            Self {
138                raw,
139                lights: Default::default(),
140                luts: Default::default(),
141                _pin: Default::default(),
142            }
143        })
144    }
145
146    /// Setup the environment material.
147    ///
148    /// This material is inherited by all internal lights.
149    #[doc(alias = "C3D_LightEnvMaterial")]
150    pub fn set_material(self: Pin<&mut Self>, mat: Material) {
151        let raw = mat.to_raw();
152        // Safety: This takes a pointer but it actually memcpy's it so this doesn't dangle
153        unsafe {
154            citro3d_sys::C3D_LightEnvMaterial(self.as_raw_mut() as *mut _, (&raw) as *const _);
155        }
156    }
157
158    /// Returns a reference to the array of (pinned) lights.
159    pub fn lights(&self) -> &LightArray {
160        &self.lights
161    }
162
163    /// Returns a mutable reference to the array of (pinned) lights.
164    pub fn lights_mut(self: Pin<&mut Self>) -> Pin<&mut LightArray> {
165        unsafe { self.map_unchecked_mut(|s| &mut s.lights) }
166    }
167
168    /// Returns a mutable reference to the light at a particular index.
169    pub fn light_mut(self: Pin<&mut Self>, idx: LightIndex) -> Option<Pin<&mut Light>> {
170        self.lights_mut()
171            .get_pin(idx.0 as usize)
172            .unwrap()
173            .as_pin_mut()
174    }
175
176    /// Sets up a new light source and returns its index for use.
177    ///
178    /// If no more light sources can be created, [`None`] is returned.
179    #[doc(alias = "C3D_LightInit")]
180    pub fn create_light(mut self: Pin<&mut Self>) -> Option<LightIndex> {
181        let idx = self
182            .lights()
183            .iter()
184            .enumerate()
185            .find(|(_, l)| l.is_none())
186            .map(|(n, _)| n)?;
187
188        self.as_mut()
189            .lights_mut()
190            .get_pin(idx)
191            .unwrap()
192            .set(Some(Light::new(unsafe {
193                MaybeUninit::zeroed().assume_init()
194            })));
195
196        let target = unsafe {
197            self.as_mut()
198                .lights_mut()
199                .get_pin(idx)
200                .unwrap()
201                .map_unchecked_mut(|p| p.as_mut().unwrap())
202        };
203        let r = unsafe {
204            citro3d_sys::C3D_LightInit(
205                target.get_unchecked_mut().as_raw_mut(),
206                self.as_raw_mut() as *mut _,
207            )
208        };
209
210        assert!(
211            r >= 0,
212            "C3D_LightInit should only fail if there are no free light slots but we checked that already, how did this happen?"
213        );
214        assert_eq!(
215            r as usize, idx,
216            "citro3d chose a different light to us? this shouldn't be possible"
217        );
218
219        Some(LightIndex::new(idx))
220    }
221
222    /// Uninitalizes and disables the light at a specific index.
223    pub fn destroy_light(mut self: Pin<&mut Self>, idx: LightIndex) {
224        self.as_mut()
225            .lights_mut()
226            .get_pin(idx.0 as usize)
227            .unwrap()
228            .set(None);
229
230        // Set the environment as dirty to update the changes (light data would still be available in GPU memory).
231        let env = self.as_raw_mut();
232        env.lights[idx.0 as usize] = std::ptr::null_mut();
233        env.flags |= citro3d_sys::C3DF_LightEnv_LCDirty as u32;
234    }
235
236    fn lut_id_to_index(id: LutId) -> Option<usize> {
237        match id {
238            LutId::D0 => Some(0),
239            LutId::D1 => Some(1),
240            LutId::Spotlight => None,
241            LutId::Fresnel => Some(2),
242            LutId::ReflectBlue => Some(3),
243            LutId::ReflectGreen => Some(4),
244            LutId::ReflectRed => Some(5),
245            LutId::DistanceAttenuation => None,
246        }
247    }
248
249    /// Attempts to disconnect a light lookup-table.
250    ///
251    /// This function returns [`None`] if no LUT was connected for `id` and `input`.
252    /// Otherwise, returns the disconnected LUT.
253    pub fn disconnect_lut(mut self: Pin<&mut Self>, id: LutId, input: LutInput) -> Option<Lut> {
254        let idx = Self::lut_id_to_index(id);
255        let me = unsafe { self.as_mut().get_unchecked_mut() };
256        let lut = idx.and_then(|i| me.luts[i].take());
257
258        if lut.is_some() {
259            unsafe {
260                citro3d_sys::C3D_LightEnvLut(
261                    &mut me.raw,
262                    id as u8,
263                    input as u8,
264                    false,
265                    std::ptr::null_mut(),
266                );
267            }
268        }
269
270        lut
271    }
272
273    /// Connects a light lookup-table at the given index, with a given input.
274    #[doc(alias = "C3D_LightEnvLut")]
275    pub fn connect_lut(mut self: Pin<&mut Self>, id: LutId, input: LutInput, data: Lut) {
276        let idx = Self::lut_id_to_index(id);
277        let (raw, lut) = unsafe {
278            // this is needed to do structural borrowing as otherwise
279            // the compiler rejects the reborrow needed with the pin
280            let me = self.as_mut().get_unchecked_mut();
281            let lut = idx.map(|i| me.luts[i].insert(data));
282            let raw = &mut me.raw;
283            let lut = match lut {
284                Some(l) => (&mut l.0) as *mut _,
285                None => core::ptr::null_mut(),
286            };
287            (raw, lut)
288        };
289
290        unsafe {
291            citro3d_sys::C3D_LightEnvLut(raw, id as u8, input as u8, false, lut);
292        }
293    }
294
295    /// Sets the fresnel for the lighting environment.
296    #[doc(alias = "C3D_LightEnvFresnel")]
297    pub fn set_fresnel(self: Pin<&mut Self>, sel: FresnelSelector) {
298        unsafe { citro3d_sys::C3D_LightEnvFresnel(self.as_raw_mut(), sel as _) }
299    }
300
301    /// Returns a reference to the raw Citro3D representation.
302    pub fn as_raw(&self) -> &citro3d_sys::C3D_LightEnv {
303        &self.raw
304    }
305
306    /// Returns a mutable reference to the raw Citro3D representation.
307    pub fn as_raw_mut(self: Pin<&mut Self>) -> &mut citro3d_sys::C3D_LightEnv {
308        unsafe { &mut self.get_unchecked_mut().raw }
309    }
310}
311
312impl Light {
313    fn new(raw: citro3d_sys::C3D_Light) -> Self {
314        Self {
315            raw,
316            spotlight: None,
317            distance_attenuation: None,
318            _pin: Default::default(),
319        }
320    }
321
322    /// Returns a reference to the raw Citro3D representation.
323    pub fn as_raw(&self) -> &citro3d_sys::C3D_Light {
324        &self.raw
325    }
326
327    /// Returns a mutable reference to the raw Citro3D representation.
328    ///
329    /// # Notes
330    ///
331    /// This does not take Pin<&mut Self> and rather borrows the value directly.
332    /// If you need the raw from a pinned light you must use `unsafe` and ensure you uphold the pinning
333    /// restrictions of the original `Light`.
334    pub fn as_raw_mut(&mut self) -> &mut citro3d_sys::C3D_Light {
335        &mut self.raw
336    }
337
338    /// Sets the position, in 3D space, of the light source.
339    #[doc(alias = "C3D_LightPosition")]
340    pub fn set_position(self: Pin<&mut Self>, p: FVec3) {
341        let mut p = FVec4::new(p.x(), p.y(), p.z(), 1.0);
342        unsafe { citro3d_sys::C3D_LightPosition(self.get_unchecked_mut().as_raw_mut(), &mut p.0) }
343    }
344
345    /// Sets the color of the light source.
346    #[doc(alias = "C3D_LightColor")]
347    pub fn set_color(self: Pin<&mut Self>, color: Color) {
348        unsafe {
349            citro3d_sys::C3D_LightColor(
350                self.get_unchecked_mut().as_raw_mut(),
351                color.r,
352                color.g,
353                color.b,
354            )
355        }
356    }
357
358    /// Enables/disables the light source.
359    #[doc(alias = "C3D_LightEnable")]
360    pub fn set_enabled(self: Pin<&mut Self>, enabled: bool) {
361        unsafe { citro3d_sys::C3D_LightEnable(self.get_unchecked_mut().as_raw_mut(), enabled) }
362    }
363
364    /// Enables/disables the light source's shadow emission.
365    #[doc(alias = "C3D_LightShadowEnable")]
366    pub fn set_shadow(self: Pin<&mut Self>, shadow: bool) {
367        unsafe { citro3d_sys::C3D_LightShadowEnable(self.get_unchecked_mut().as_raw_mut(), shadow) }
368    }
369
370    /// Sets the [distance attenuation](DistanceAttenuation) behaviour of the light.
371    #[doc(alias = "C3D_LightDistAttn")]
372    #[doc(alias = "C3D_LightDistAttnEnable")]
373    pub fn set_distance_attenutation(mut self: Pin<&mut Self>, lut: Option<DistanceAttenuation>) {
374        {
375            let me = unsafe { self.as_mut().get_unchecked_mut() };
376            me.distance_attenuation = lut;
377        }
378        // this is a bit of a mess because we need to be _reallly_ careful we don't trip aliasing rules
379        // reusing `me` here I think trips them because we have multiple live mutable references to
380        // the same region
381        let (raw, c_lut) = {
382            let me = unsafe { self.as_mut().get_unchecked_mut() };
383            let raw = &mut me.raw;
384            let c_lut = me.distance_attenuation.as_mut().map(|d| &mut d.raw);
385            (raw, c_lut)
386        };
387
388        unsafe {
389            citro3d_sys::C3D_LightDistAttn(
390                raw,
391                match c_lut {
392                    Some(l) => l,
393                    None => std::ptr::null_mut(),
394                },
395            );
396        }
397    }
398
399    /// Sets the [spotlight](Spotlight) behaviour of the light.
400    #[doc(alias = "C3D_LightSpotLut")]
401    #[doc(alias = "C3D_LightSpotEnable")]
402    pub fn set_spotlight(mut self: Pin<&mut Self>, lut: Option<Spotlight>) {
403        {
404            let me = unsafe { self.as_mut().get_unchecked_mut() };
405            me.spotlight = lut;
406        }
407
408        let (raw, c_lut) = {
409            let me = unsafe { self.as_mut().get_unchecked_mut() };
410            let raw = &mut me.raw;
411            let c_lut = me.spotlight.as_mut().map(|d| &mut d.lut.0);
412            (raw, c_lut)
413        };
414
415        match c_lut {
416            Some(l) => unsafe {
417                citro3d_sys::C3D_LightSpotLut(raw, l);
418            },
419            None => unsafe {
420                citro3d_sys::C3D_LightSpotLut(raw, std::ptr::null_mut());
421
422                // The "Spotlight-Dirty" flag in Citro3D is used to check whether the LUT has to be loaded onto the GPU.
423                // However, if a spotlight is set and immediately unset, the bit stays dirty, and the passed null pointer is accessed.
424                // Distance attenuation is not affected by the same issue since the lut is not set if the pointer is null.
425                //
426                // Here, we manually unset the dirty bit to make sure no null pointer is accessed.
427                // Reference: https://github.com/devkitPro/citro3d/blob/9f21cf7b380ce6f9e01a0420f19f0763e5443ca7/source/lightenv.c#L120
428                raw.flags &= !citro3d_sys::C3DF_Light_SPDirty;
429            },
430        }
431    }
432
433    /// Sets the spotlight direction of the light (relatively to the light's source [position](Light::set_position)).
434    #[doc(alias = "C3D_LightSpotDir")]
435    pub fn set_spotlight_direction(self: Pin<&mut Self>, direction: FVec3) {
436        unsafe {
437            // References:
438            //  https://github.com/devkitPro/citro3d/blob/9f21cf7b380ce6f9e01a0420f19f0763e5443ca7/source/light.c#L116
439            //  https://github.com/devkitPro/libctru/blob/e09a49a08fa469bc08fb62e9d29bfe6407c0232a/libctru/include/3ds/gpu/enums.h#L395
440            let raw = self.get_unchecked_mut().as_raw_mut();
441            let spot_enabled = (*raw.parent).conf.config[1] & (0b1 << (raw.id + 8));
442
443            citro3d_sys::C3D_LightSpotDir(raw, direction.x(), direction.y(), direction.z());
444
445            // For internal Citro3D reasons, setting the spotlight direction also enables the spotlight itself (even if no LUT was set).
446            // To avoid unexpected behaviour and crashes, we disable the spotlight if it were not enabled before.
447            if spot_enabled != 0 {
448                citro3d_sys::C3D_LightSpotEnable(raw, false);
449            }
450        }
451    }
452}
453
454/// Lookup-table for light data.
455///
456/// Lighting behaviour is memoized by a LUT which is used during the fragment stage by the GPU.
457/// This struct represents a generic LUT, which can be used for different parts of the lighting environment.
458#[derive(Clone, Copy, Debug)]
459#[repr(transparent)]
460pub struct Lut(citro3d_sys::C3D_LightLut);
461
462impl PartialEq for Lut {
463    fn eq(&self, other: &Self) -> bool {
464        self.0.data == other.0.data
465    }
466}
467impl Eq for Lut {}
468
469impl std::hash::Hash for Lut {
470    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
471        self.0.data.hash(state);
472    }
473}
474
475#[cfg(test)]
476extern "C" fn c_powf(a: f32, b: f32) -> f32 {
477    a.powf(b)
478}
479
480const LUT_LEN: i32 = 256;
481const LUT_HALF_LEN: i32 = LUT_LEN / 2;
482
483type LutArray = [u32; LUT_LEN as usize];
484
485impl Lut {
486    /// Create a LUT by memoizing a function.
487    ///
488    /// # Notes
489    ///
490    /// The input of the function is a number between `0.0` and `1.0`, or `-1.0` and `1.0` if `negative` is asserted.
491    /// The input is sampled 256 times for interpolation.
492    /// What the input actually represents depends on the [`LutInput`] used when binding the LUT.
493    #[doc(alias = "LightLut_FromFn")]
494    pub fn from_fn(mut f: impl FnMut(f32) -> f32, negative: bool) -> Self {
495        let (start, end, scale) = if negative {
496            (-LUT_HALF_LEN, LUT_HALF_LEN, 1.0 / LUT_HALF_LEN as f32)
497        } else {
498            (0, LUT_LEN, 1.0 / LUT_LEN as f32)
499        };
500
501        assert_eq!(start.abs_diff(end), LUT_LEN as u32);
502
503        // This data buffer is double the actual LUT length since we also store
504        // the deltas between values to use for interpolation (in the second half of the indices).
505        let mut data = [0.0f32; LUT_LEN as usize * 2];
506        let mut last_idx: usize = 0;
507
508        for i in start..=end {
509            let x = i as f32 * scale;
510            let v = f(x);
511
512            // The result for each negative x is saved in indices 128..=255, while positive values are saved in indices 0..=127
513            let idx: usize = if negative { i & 0xFF } else { i } as usize;
514
515            if i < end {
516                data[idx] = v;
517            }
518
519            if i > start {
520                data[idx + LUT_LEN as usize - 1] = v - data[last_idx];
521            }
522
523            last_idx = idx;
524        }
525
526        let lut = unsafe {
527            let mut lut = MaybeUninit::zeroed();
528            citro3d_sys::LightLut_FromArray(lut.as_mut_ptr(), data.as_mut_ptr());
529            lut.assume_init()
530        };
531        Self(lut)
532    }
533
534    /// Returns a reference to the raw LUT data.
535    pub fn data(&self) -> &LutArray {
536        &self.0.data
537    }
538
539    /// Returns a mutable reference to the raw LUT data.
540    pub fn data_mut(&mut self) -> &mut LutArray {
541        &mut self.0.data
542    }
543
544    #[cfg(test)]
545    fn phong_citro3d(shininess: f32) -> Self {
546        let lut = unsafe {
547            let mut lut = MaybeUninit::uninit();
548            citro3d_sys::LightLut_FromFunc(lut.as_mut_ptr(), Some(c_powf), shininess, false);
549            lut.assume_init()
550        };
551        Self(lut)
552    }
553}
554
555/// Lookup-table (plus some additional information) to handle distance attenuation of a light source.
556#[doc(alias = "C3D_LightLutDA")]
557pub struct DistanceAttenuation {
558    raw: citro3d_sys::C3D_LightLutDA,
559}
560
561impl DistanceAttenuation {
562    /// Creates a new distance attenuation table relative to a range of the clip space and a function based on the distance.
563    ///
564    /// # Notes
565    ///
566    /// The function takes only positive values as input.
567    /// Refer to [`Lut::from_fn`] for more information.
568    pub fn new(range: Range<f32>, mut f: impl FnMut(f32) -> f32) -> Self {
569        let mut raw: citro3d_sys::C3D_LightLutDA = unsafe { MaybeUninit::zeroed().assume_init() };
570        let dist = range.end - range.start;
571        raw.scale = 1.0 / dist;
572        raw.bias = -range.start * raw.scale;
573        let lut = Lut::from_fn(|x| f(range.start + dist * x), false);
574        raw.lut = citro3d_sys::C3D_LightLut { data: *lut.data() };
575        Self { raw }
576    }
577}
578
579/// Lookup-table to handle the spotlight area of a light source.
580pub struct Spotlight {
581    lut: Lut,
582}
583
584impl Spotlight {
585    /// Creates a new directional spotlight.
586    ///
587    /// The input of the `f` function is the cosine of angle from the direction of the spotlight,
588    /// while the output (between `0.0` and `1.0`) is the intensity of the light in that point.
589    ///
590    /// # Notes
591    ///
592    /// The function takes negative and positive values as input.
593    /// Refer to [`Lut::from_fn`] for more information.
594    pub fn new(f: impl FnMut(f32) -> f32) -> Self {
595        Self {
596            lut: Lut::from_fn(f, true),
597        }
598    }
599
600    /// Creates a new directional spotlight with drastic cutoff.
601    ///
602    /// Within the cutoff angle (in radians), from the direction of the spotlight, intensity is 1.
603    /// Outside, intensity is 0.
604    pub fn with_cutoff(cutoff_angle: f32) -> Self {
605        let lut = Lut::from_fn(
606            |angle| {
607                if angle >= cutoff_angle.cos() {
608                    1.0
609                } else {
610                    0.0
611                }
612            },
613            true,
614        );
615
616        Self { lut }
617    }
618}
619
620/// This is used to decide what the input should be to a [`Lut`]
621#[doc(alias = "GPU_LIGHTLUTINPUT")]
622#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
623#[repr(u8)]
624pub enum LutInput {
625    /// Cosine of the angle from the light direction (spotlights).
626    CosPhi = ctru_sys::GPU_LUTINPUT_CP,
627    /// Dot product of the light and normal vectors.
628    LightNormal = ctru_sys::GPU_LUTINPUT_LN,
629    /// Half the normal.
630    NormalHalf = ctru_sys::GPU_LUTINPUT_NH,
631    /// Dot product of the view and normal.
632    NormalView = ctru_sys::GPU_LUTINPUT_NV,
633    /// Dot product of the spotlight colour and light vector.
634    LightSpotLight = ctru_sys::GPU_LUTINPUT_SP,
635    /// Half the view vector.
636    ViewHalf = ctru_sys::GPU_LUTINPUT_VH,
637}
638
639impl TryFrom<u8> for LutInput {
640    type Error = String;
641    fn try_from(value: u8) -> Result<Self, Self::Error> {
642        match value {
643            ctru_sys::GPU_LUTINPUT_NH => Ok(Self::NormalHalf),
644            ctru_sys::GPU_LUTINPUT_VH => Ok(Self::ViewHalf),
645            ctru_sys::GPU_LUTINPUT_NV => Ok(Self::NormalView),
646            ctru_sys::GPU_LUTINPUT_LN => Ok(Self::LightNormal),
647            ctru_sys::GPU_LUTINPUT_SP => Ok(Self::LightSpotLight),
648            ctru_sys::GPU_LUTINPUT_CP => Ok(Self::CosPhi),
649            _ => Err("invalid value for LutInput".to_string()),
650        }
651    }
652}
653
654/// Identifier/index for the various LUTs associated to a [`LightEnv`].
655///
656/// # Notes
657///
658/// `Spotlight` and `DistanceAttenuation` are associated to specific light sources,
659/// and thus are not associated to an internal lighting index.
660#[doc(alias = "GPU_LIGHTLUTID")]
661#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
662#[repr(u8)]
663pub enum LutId {
664    /// Specular 0.
665    D0 = ctru_sys::GPU_LUT_D0,
666    /// Specular 1.
667    D1 = ctru_sys::GPU_LUT_D1,
668    /// Spotlight attenuation (used for [`Spotlight`]).
669    Spotlight = ctru_sys::GPU_LUT_SP,
670    /// Fresnel.
671    Fresnel = ctru_sys::GPU_LUT_FR,
672    /// Blue reflection component.
673    ReflectBlue = ctru_sys::GPU_LUT_RB,
674    /// Green reflection component.
675    ReflectGreen = ctru_sys::GPU_LUT_RG,
676    /// Red reflection component.
677    ReflectRed = ctru_sys::GPU_LUT_RR,
678    /// Distance attenuation (used for [`DistanceAttenuation`]).
679    DistanceAttenuation = ctru_sys::GPU_LUT_DA,
680}
681
682impl TryFrom<u8> for LutId {
683    type Error = String;
684    fn try_from(value: u8) -> Result<Self, Self::Error> {
685        match value {
686            ctru_sys::GPU_LUT_D0 => Ok(Self::D0),
687            ctru_sys::GPU_LUT_D1 => Ok(Self::D1),
688            ctru_sys::GPU_LUT_SP => Ok(Self::Spotlight),
689            ctru_sys::GPU_LUT_FR => Ok(Self::Fresnel),
690            ctru_sys::GPU_LUT_RB => Ok(Self::ReflectBlue),
691            ctru_sys::GPU_LUT_RG => Ok(Self::ReflectGreen),
692            ctru_sys::GPU_LUT_RR => Ok(Self::ReflectRed),
693            ctru_sys::GPU_LUT_DA => Ok(Self::DistanceAttenuation),
694            _ => Err("invalid value for LutId".to_string()),
695        }
696    }
697}
698
699#[doc(alias = "GPU_FRESNELSEL")]
700#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
701#[repr(u8)]
702pub enum FresnelSelector {
703    /// No fresnel selection.
704    None = ctru_sys::GPU_NO_FRESNEL,
705    /// Use as selector for primary colour unit alpha.
706    PrimaryAlpha = ctru_sys::GPU_PRI_ALPHA_FRESNEL,
707    /// Use as selector for secondary colour unit alpha.
708    SecondaryAlpha = ctru_sys::GPU_SEC_ALPHA_FRESNEL,
709    /// Use as selector for both colour units.
710    Both = ctru_sys::GPU_PRI_SEC_ALPHA_FRESNEL,
711}
712
713impl TryFrom<u8> for FresnelSelector {
714    type Error = String;
715    fn try_from(value: u8) -> Result<Self, Self::Error> {
716        match value {
717            ctru_sys::GPU_NO_FRESNEL => Ok(Self::None),
718            ctru_sys::GPU_PRI_ALPHA_FRESNEL => Ok(Self::PrimaryAlpha),
719            ctru_sys::GPU_SEC_ALPHA_FRESNEL => Ok(Self::SecondaryAlpha),
720            ctru_sys::GPU_PRI_SEC_ALPHA_FRESNEL => Ok(Self::Both),
721            _ => Err("invalid value for FresnelSelector".to_string()),
722        }
723    }
724}
725
726/// LUT scaling factors.
727#[repr(u8)]
728#[derive(Debug, Clone, Copy, PartialEq, Eq)]
729#[doc(alias = "GPU_LIGHTLUTSCALER")]
730pub enum LutScale {
731    /// 1x scale.
732    #[doc(alias = "GPU_LUTSCALER_1x")]
733    OneX = ctru_sys::GPU_LUTSCALER_1x,
734
735    /// 2x scale.
736    #[doc(alias = "GPU_LUTSCALER_2x")]
737    TwoX = ctru_sys::GPU_LUTSCALER_2x,
738
739    /// 4x scale.
740    #[doc(alias = "GPU_LUTSCALER_4x")]
741    FourX = ctru_sys::GPU_LUTSCALER_4x,
742
743    /// 8x scale.
744    #[doc(alias = "GPU_LUTSCALER_8x")]
745    EightX = ctru_sys::GPU_LUTSCALER_8x,
746
747    /// 0.25x scale.
748    #[doc(alias = "GPU_LUTSCALER_0_25x")]
749    QuarterX = ctru_sys::GPU_LUTSCALER_0_25x,
750
751    /// 0.5x scale.
752    #[doc(alias = "GPU_LUTSCALER_0_5x")]
753    HalfX = ctru_sys::GPU_LUTSCALER_0_5x,
754}
755
756impl TryFrom<u8> for LutScale {
757    type Error = String;
758    fn try_from(value: u8) -> Result<Self, Self::Error> {
759        match value {
760            ctru_sys::GPU_LUTSCALER_1x => Ok(Self::OneX),
761            ctru_sys::GPU_LUTSCALER_2x => Ok(Self::TwoX),
762            ctru_sys::GPU_LUTSCALER_4x => Ok(Self::FourX),
763            ctru_sys::GPU_LUTSCALER_8x => Ok(Self::EightX),
764            ctru_sys::GPU_LUTSCALER_0_25x => Ok(Self::QuarterX),
765            ctru_sys::GPU_LUTSCALER_0_5x => Ok(Self::HalfX),
766            _ => Err("invalid value for LutScale".to_string()),
767        }
768    }
769}
770
771/// Bump map modes.
772#[repr(u8)]
773#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774#[doc(alias = "GPU_BUMPMODE")]
775pub enum BumpMappingMode {
776    /// Disabled.
777    #[doc(alias = "GPU_BUMP_NOT_USED")]
778    NotUsed = ctru_sys::GPU_BUMP_NOT_USED,
779
780    /// Bump as bump mapping.
781    #[doc(alias = "GPU_BUMP_AS_BUMP")]
782    AsBump = ctru_sys::GPU_BUMP_AS_BUMP,
783
784    /// Bump as tangent/normal mapping.
785    #[doc(alias = "GPU_BUMP_AS_TANG")]
786    AsTangent = ctru_sys::GPU_BUMP_AS_TANG,
787}
788
789impl TryFrom<u8> for BumpMappingMode {
790    type Error = String;
791    fn try_from(value: u8) -> Result<Self, Self::Error> {
792        match value {
793            ctru_sys::GPU_BUMP_NOT_USED => Ok(BumpMappingMode::NotUsed),
794            ctru_sys::GPU_BUMP_AS_BUMP => Ok(BumpMappingMode::AsBump),
795            ctru_sys::GPU_BUMP_AS_TANG => Ok(BumpMappingMode::AsTangent),
796            _ => Err("invalid value for BumpMappingMode".to_string()),
797        }
798    }
799}
800
801#[cfg(test)]
802mod tests {
803    use super::Lut;
804
805    #[test]
806    fn lut_data_phong_matches_for_own_and_citro3d() {
807        let c3d = Lut::phong_citro3d(30.0);
808        let rs = Lut::from_fn(|i| i.powf(30.0), false);
809        assert_eq!(c3d, rs);
810    }
811}