1use 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#[derive(Clone, Copy, PartialEq, Eq, Hash)]
55pub struct LightIndex(u8);
56
57const NB_LIGHTS: usize = 8;
58
59impl LightIndex {
60 pub fn new(idx: usize) -> Self {
65 assert!(idx < NB_LIGHTS);
66 Self(idx as u8)
67 }
68
69 pub fn as_usize(self) -> usize {
71 self.0 as usize
72 }
73}
74
75type LightArray = PinArray<Option<Light>, NB_LIGHTS>;
76
77pub struct LightEnv {
81 raw: citro3d_sys::C3D_LightEnv,
82 lights: LightArray,
88 luts: [Option<Lut>; 6],
89 _pin: PhantomPinned,
90}
91
92pub struct Light {
96 raw: citro3d_sys::C3D_Light,
97 spotlight: Option<Spotlight>,
98 distance_attenuation: Option<DistanceAttenuation>,
99 _pin: PhantomPinned,
100}
101
102#[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 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 #[doc(alias = "C3D_LightEnvMaterial")]
150 pub fn set_material(self: Pin<&mut Self>, mat: Material) {
151 let raw = mat.to_raw();
152 unsafe {
154 citro3d_sys::C3D_LightEnvMaterial(self.as_raw_mut() as *mut _, (&raw) as *const _);
155 }
156 }
157
158 pub fn lights(&self) -> &LightArray {
160 &self.lights
161 }
162
163 pub fn lights_mut(self: Pin<&mut Self>) -> Pin<&mut LightArray> {
165 unsafe { self.map_unchecked_mut(|s| &mut s.lights) }
166 }
167
168 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 #[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 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 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 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 #[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 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 #[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 pub fn as_raw(&self) -> &citro3d_sys::C3D_LightEnv {
303 &self.raw
304 }
305
306 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 pub fn as_raw(&self) -> &citro3d_sys::C3D_Light {
324 &self.raw
325 }
326
327 pub fn as_raw_mut(&mut self) -> &mut citro3d_sys::C3D_Light {
335 &mut self.raw
336 }
337
338 #[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 #[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 #[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 #[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 #[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 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 #[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 raw.flags &= !citro3d_sys::C3DF_Light_SPDirty;
429 },
430 }
431 }
432
433 #[doc(alias = "C3D_LightSpotDir")]
435 pub fn set_spotlight_direction(self: Pin<&mut Self>, direction: FVec3) {
436 unsafe {
437 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 if spot_enabled != 0 {
448 citro3d_sys::C3D_LightSpotEnable(raw, false);
449 }
450 }
451 }
452}
453
454#[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 #[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 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 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 pub fn data(&self) -> &LutArray {
536 &self.0.data
537 }
538
539 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#[doc(alias = "C3D_LightLutDA")]
557pub struct DistanceAttenuation {
558 raw: citro3d_sys::C3D_LightLutDA,
559}
560
561impl DistanceAttenuation {
562 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
579pub struct Spotlight {
581 lut: Lut,
582}
583
584impl Spotlight {
585 pub fn new(f: impl FnMut(f32) -> f32) -> Self {
595 Self {
596 lut: Lut::from_fn(f, true),
597 }
598 }
599
600 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#[doc(alias = "GPU_LIGHTLUTINPUT")]
622#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
623#[repr(u8)]
624pub enum LutInput {
625 CosPhi = ctru_sys::GPU_LUTINPUT_CP,
627 LightNormal = ctru_sys::GPU_LUTINPUT_LN,
629 NormalHalf = ctru_sys::GPU_LUTINPUT_NH,
631 NormalView = ctru_sys::GPU_LUTINPUT_NV,
633 LightSpotLight = ctru_sys::GPU_LUTINPUT_SP,
635 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#[doc(alias = "GPU_LIGHTLUTID")]
661#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
662#[repr(u8)]
663pub enum LutId {
664 D0 = ctru_sys::GPU_LUT_D0,
666 D1 = ctru_sys::GPU_LUT_D1,
668 Spotlight = ctru_sys::GPU_LUT_SP,
670 Fresnel = ctru_sys::GPU_LUT_FR,
672 ReflectBlue = ctru_sys::GPU_LUT_RB,
674 ReflectGreen = ctru_sys::GPU_LUT_RG,
676 ReflectRed = ctru_sys::GPU_LUT_RR,
678 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 None = ctru_sys::GPU_NO_FRESNEL,
705 PrimaryAlpha = ctru_sys::GPU_PRI_ALPHA_FRESNEL,
707 SecondaryAlpha = ctru_sys::GPU_SEC_ALPHA_FRESNEL,
709 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#[repr(u8)]
728#[derive(Debug, Clone, Copy, PartialEq, Eq)]
729#[doc(alias = "GPU_LIGHTLUTSCALER")]
730pub enum LutScale {
731 #[doc(alias = "GPU_LUTSCALER_1x")]
733 OneX = ctru_sys::GPU_LUTSCALER_1x,
734
735 #[doc(alias = "GPU_LUTSCALER_2x")]
737 TwoX = ctru_sys::GPU_LUTSCALER_2x,
738
739 #[doc(alias = "GPU_LUTSCALER_4x")]
741 FourX = ctru_sys::GPU_LUTSCALER_4x,
742
743 #[doc(alias = "GPU_LUTSCALER_8x")]
745 EightX = ctru_sys::GPU_LUTSCALER_8x,
746
747 #[doc(alias = "GPU_LUTSCALER_0_25x")]
749 QuarterX = ctru_sys::GPU_LUTSCALER_0_25x,
750
751 #[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#[repr(u8)]
773#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774#[doc(alias = "GPU_BUMPMODE")]
775pub enum BumpMappingMode {
776 #[doc(alias = "GPU_BUMP_NOT_USED")]
778 NotUsed = ctru_sys::GPU_BUMP_NOT_USED,
779
780 #[doc(alias = "GPU_BUMP_AS_BUMP")]
782 AsBump = ctru_sys::GPU_BUMP_AS_BUMP,
783
784 #[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}