diff --git a/citro3d/Cargo.toml b/citro3d/Cargo.toml index b96ca1e..2998a81 100644 --- a/citro3d/Cargo.toml +++ b/citro3d/Cargo.toml @@ -16,6 +16,7 @@ ctru-rs = { git = "https://github.com/rust3ds/ctru-rs.git" } ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } document-features = "0.2.7" libc = "0.2.125" +pin_array = { version = "0.1.1" } [features] default = ["glam"] diff --git a/citro3d/examples/assets/frag-shader.pica b/citro3d/examples/assets/frag-shader.pica new file mode 100644 index 0000000..79d357b --- /dev/null +++ b/citro3d/examples/assets/frag-shader.pica @@ -0,0 +1,74 @@ +; modified version of https://github.com/devkitPro/3ds-examples/blob/ea519187782397c279609da80310e0f8c7e80f09/graphics/gpu/fragment_light/source/vshader.v.pica +; Example PICA200 vertex shader + +; Uniforms +.fvec projection[4], modelView[4] + +; Constants +.constf myconst(0.0, 1.0, -1.0, 0.5) +.alias zeros myconst.xxxx ; Vector full of zeros +.alias ones myconst.yyyy ; Vector full of ones +.alias half myconst.wwww + +; Outputs +.out outpos position +.out outtex texcoord0 +.out outclr color +.out outview view +.out outnq normalquat + +; Inputs (defined as aliases for convenience) +.in inpos +.in innrm +.in intex + +.proc main + ; Force the w component of inpos to be 1.0 + mov r0.xyz, inpos + mov r0.w, ones + + ; r1 = modelView * inpos + dp4 r1.x, modelView[0], r0 + dp4 r1.y, modelView[1], r0 + dp4 r1.z, modelView[2], r0 + dp4 r1.w, modelView[3], r0 + + ; outview = -r1 + mov outview, -r1 + + ; outpos = projection * r1 + dp4 outpos.x, projection[0], r1 + dp4 outpos.y, projection[1], r1 + dp4 outpos.z, projection[2], r1 + dp4 outpos.w, projection[3], r1 + + ; outtex = intex + mov outtex, intex + + ; Transform the normal vector with the modelView matrix + ; TODO: use a separate normal matrix that is the transpose of the inverse of modelView + dp3 r14.x, modelView[0], innrm + dp3 r14.y, modelView[1], innrm + dp3 r14.z, modelView[2], innrm + dp3 r6.x, r14, r14 + rsq r6.x, r6.x + mul r14.xyz, r14.xyz, r6.x + + mov r0, myconst.yxxx + add r4, ones, r14.z + mul r4, half, r4 + cmp zeros, ge, ge, r4.x + rsq r4, r4.x + mul r5, half, r14 + jmpc cmp.x, degenerate + + rcp r0.z, r4.x + mul r0.xy, r5, r4 + +degenerate: + mov outnq, r0 + mov outclr, ones + + ; We're finished + end +.end \ No newline at end of file diff --git a/citro3d/examples/fragment-light.rs b/citro3d/examples/fragment-light.rs new file mode 100644 index 0000000..4909250 --- /dev/null +++ b/citro3d/examples/fragment-light.rs @@ -0,0 +1,455 @@ +#![feature(allocator_api)] +use std::f32::consts::PI; + +use citro3d::{ + attrib, buffer, + light::{FresnelSelector, LightEnv, LightLut, LightLutDistAtten, LightLutId, LutInput}, + material::{Color, Material}, + math::{AspectRatio, ClipPlanes, FVec3, FVec4, Matrix4, Projection, StereoDisplacement}, + render::{self, ClearFlags}, + shader, texenv, +}; +use citro3d_macros::include_shader; +use ctru::services::{ + apt::Apt, + gfx::{Gfx, RawFrameBuffer, Screen, TopScreen3D}, + hid::{Hid, KeyPad}, + soc::Soc, +}; + +#[repr(C)] +#[derive(Copy, Clone)] +struct Vec3 { + x: f32, + y: f32, + z: f32, +} + +impl Vec3 { + const fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } +} +#[derive(Copy, Clone)] +#[repr(C)] +struct Vec2 { + x: f32, + y: f32, +} + +impl Vec2 { + const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct Vertex { + pos: Vec3, + normal: Vec3, + uv: Vec2, +} + +impl Vertex { + const fn new(pos: Vec3, normal: Vec3, uv: Vec2) -> Self { + Self { pos, normal, uv } + } +} + +static SHADER_BYTES: &[u8] = include_shader!("assets/frag-shader.pica"); + +const VERTICES: &[Vertex] = &[ + Vertex::new( + Vec3::new(-0.5, -0.5, 0.5), + Vec3::new(0.0, 0.0, 1.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, -0.5, 0.5), + Vec3::new(0.0, 0.0, 1.0), + Vec2::new(1.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, 0.5), + Vec3::new(0.0, 0.0, 1.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, 0.5), + Vec3::new(0.0, 0.0, 1.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, 0.5, 0.5), + Vec3::new(0.0, 0.0, 1.0), + Vec2::new(0.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, 0.5), + Vec3::new(0.0, 0.0, 1.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, -0.5), + Vec3::new(0.0, 0.0, -1.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(-0.5, 0.5, -0.5), + Vec3::new(0.0, 0.0, -1.0), + Vec2::new(1.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, -0.5), + Vec3::new(0.0, 0.0, -1.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, -0.5), + Vec3::new(0.0, 0.0, -1.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, -0.5, -0.5), + Vec3::new(0.0, 0.0, -1.0), + Vec2::new(0.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, -0.5), + Vec3::new(0.0, 0.0, -1.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, -0.5, -0.5), + Vec3::new(1.0, 0.0, 0.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, -0.5), + Vec3::new(1.0, 0.0, 0.0), + Vec2::new(1.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, 0.5), + Vec3::new(1.0, 0.0, 0.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, 0.5), + Vec3::new(1.0, 0.0, 0.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, -0.5, 0.5), + Vec3::new(1.0, 0.0, 0.0), + Vec2::new(0.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, -0.5, -0.5), + Vec3::new(1.0, 0.0, 0.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, -0.5), + Vec3::new(-1.0, 0.0, 0.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, 0.5), + Vec3::new(-1.0, 0.0, 0.0), + Vec2::new(1.0, 0.0), + ), + Vertex::new( + Vec3::new(-0.5, 0.5, 0.5), + Vec3::new(-1.0, 0.0, 0.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, 0.5, 0.5), + Vec3::new(-1.0, 0.0, 0.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, 0.5, -0.5), + Vec3::new(-1.0, 0.0, 0.0), + Vec2::new(0.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, -0.5), + Vec3::new(-1.0, 0.0, 0.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(-0.5, 0.5, -0.5), + Vec3::new(0.0, 1.0, 0.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(-0.5, 0.5, 0.5), + Vec3::new(0.0, 1.0, 0.0), + Vec2::new(1.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, 0.5), + Vec3::new(0.0, 1.0, 0.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, 0.5), + Vec3::new(0.0, 1.0, 0.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, 0.5, -0.5), + Vec3::new(0.0, 1.0, 0.0), + Vec2::new(0.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, 0.5, -0.5), + Vec3::new(0.0, 1.0, 0.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, -0.5), + Vec3::new(0.0, -1.0, 0.0), + Vec2::new(0.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, -0.5, -0.5), + Vec3::new(0.0, -1.0, 0.0), + Vec2::new(1.0, 0.0), + ), + Vertex::new( + Vec3::new(0.5, -0.5, 0.5), + Vec3::new(0.0, -1.0, 0.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(0.5, -0.5, 0.5), + Vec3::new(0.0, -1.0, 0.0), + Vec2::new(1.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, 0.5), + Vec3::new(0.0, -1.0, 0.0), + Vec2::new(0.0, 1.0), + ), + Vertex::new( + Vec3::new(-0.5, -0.5, -0.5), + Vec3::new(0.0, -1.0, 0.0), + Vec2::new(0.0, 0.0), + ), +]; + +fn main() { + { + let prev = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + std::fs::write("panic.log", info.to_string()); + prev(info); + })); + } + let mut soc = Soc::new().expect("failed to get SOC"); + drop(soc.redirect_to_3dslink(true, true)); + + let gfx = Gfx::with_formats_shared( + ctru::services::gspgpu::FramebufferFormat::Rgba8, + ctru::services::gspgpu::FramebufferFormat::Rgba8, + ) + .expect("Couldn't obtain GFX controller"); + let mut hid = Hid::new().expect("Couldn't obtain HID controller"); + let apt = Apt::new().expect("Couldn't obtain APT controller"); + + let mut instance = citro3d::Instance::new().expect("failed to initialize Citro3D"); + + let top_screen = TopScreen3D::from(&gfx.top_screen); + + let (mut top_left, mut top_right) = top_screen.split_mut(); + + let RawFrameBuffer { width, height, .. } = top_left.raw_framebuffer(); + let mut top_left_target = instance + .render_target( + width, + height, + top_left, + Some(render::DepthFormat::Depth24Stencil8), + ) + .expect("failed to create render target"); + + let RawFrameBuffer { width, height, .. } = top_right.raw_framebuffer(); + let mut top_right_target = instance + .render_target( + width, + height, + top_right, + Some(render::DepthFormat::Depth24Stencil8), + ) + .expect("failed to create render target"); + + let mut bottom_screen = gfx.bottom_screen.borrow_mut(); + let RawFrameBuffer { width, height, .. } = bottom_screen.raw_framebuffer(); + + let mut bottom_target = instance + .render_target( + width, + height, + bottom_screen, + Some(render::DepthFormat::Depth24Stencil8), + ) + .expect("failed to create bottom screen render target"); + + let shader = shader::Library::from_bytes(SHADER_BYTES).unwrap(); + let vertex_shader = shader.get(0).unwrap(); + + let program = shader::Program::new(vertex_shader).unwrap(); + instance.bind_program(&program); + + let mut vbo_data = Vec::with_capacity_in(VERTICES.len(), ctru::linear::LinearAllocator); + vbo_data.extend_from_slice(VERTICES); + + let mut buf_info = buffer::Info::new(); + let (attr_info, vbo_data) = prepare_vbos(&mut buf_info, &vbo_data); + let mut light_env = instance.light_env_mut(); + light_env.as_mut().connect_lut( + LightLutId::D0, + LutInput::LightNormal, + LightLut::from_fn(|v| v.powf(10.0), false), + ); + light_env.as_mut().set_material(Material { + ambient: Some(Color::new(0.2, 0.2, 0.2)), + diffuse: Some(Color::new(1.0, 0.4, 1.0)), + specular0: Some(Color::new(0.8, 0.8, 0.8)), + ..Default::default() + }); + let light = light_env.as_mut().create_light().unwrap(); + let mut light = light_env.as_mut().light_mut(light).unwrap(); + light.as_mut().set_color(1.0, 1.0, 1.0); + light.as_mut().set_position(FVec3::new(0.0, 0.0, -0.5)); + light + .as_mut() + .set_distance_attenutation(Some(LightLutDistAtten::new(0.0..400.0, |d| { + (1.0 / (4.0 * PI * d * d)).min(1.0) + }))); + let mut c = Matrix4::identity(); + let model_idx = program.get_uniform("modelView").unwrap(); + c.translate(0.0, 0.0, -2.0); + instance.bind_vertex_uniform(model_idx, &c); + + // Configure the first fragment shading substage to just pass through the vertex color + // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight + let stage0 = texenv::Stage::new(0).unwrap(); + instance + .texenv(stage0) + .src( + texenv::Mode::BOTH, + texenv::Source::FragmentPrimaryColor, + Some(texenv::Source::FragmentSecondaryColor), + None, + ) + .func(texenv::Mode::BOTH, texenv::CombineFunc::Add); + + let projection_uniform_idx = program.get_uniform("projection").unwrap(); + + while apt.main_loop() { + hid.scan_input(); + + if hid.keys_down().contains(KeyPad::START) { + break; + } + + instance.render_frame_with(|instance| { + let mut render_to = |target: &mut render::Target, projection| { + target.clear(ClearFlags::ALL, 0, 0); + + instance + .select_render_target(target) + .expect("failed to set render target"); + + instance.bind_vertex_uniform(projection_uniform_idx, projection); + instance.bind_vertex_uniform(model_idx, &c); + + instance.set_attr_info(&attr_info); + + instance.draw_arrays(buffer::Primitive::Triangles, vbo_data); + }; + + let Projections { + left_eye, + right_eye, + center, + } = calculate_projections(); + + render_to(&mut top_left_target, &left_eye); + render_to(&mut top_right_target, &right_eye); + render_to(&mut bottom_target, ¢er); + }); + c.translate(0.0, 0.0, 2.0); + c.rotate_y(1.0f32.to_radians()); + c.translate(0.0, 0.0, -2.0); + } +} + +fn prepare_vbos<'a>( + buf_info: &'a mut buffer::Info, + vbo_data: &'a [Vertex], +) -> (attrib::Info, buffer::Slice<'a>) { + // Configure attributes for use with the vertex shader + let mut attr_info = attrib::Info::new(); + + let reg0 = attrib::Register::new(0).unwrap(); + let reg1 = attrib::Register::new(1).unwrap(); + let reg2 = attrib::Register::new(2).unwrap(); + + attr_info + .add_loader(reg0, attrib::Format::Float, 3) + .unwrap(); + + attr_info + .add_loader(reg1, attrib::Format::Float, 3) + .unwrap(); + + attr_info + .add_loader(reg2, attrib::Format::Float, 2) + .unwrap(); + + let buf_idx = buf_info.add(vbo_data, &attr_info).unwrap(); + + (attr_info, buf_idx) +} + +struct Projections { + left_eye: Matrix4, + right_eye: Matrix4, + center: Matrix4, +} + +fn calculate_projections() -> Projections { + // TODO: it would be cool to allow playing around with these parameters on + // the fly with D-pad, etc. + let slider_val = ctru::os::current_3d_slider_state(); + let interocular_distance = slider_val / 2.0; + + let vertical_fov = 40.0_f32.to_radians(); + let screen_depth = 2.0; + + let clip_planes = ClipPlanes { + near: 0.01, + far: 100.0, + }; + + let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth); + + let (left_eye, right_eye) = + Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes) + .stereo_matrices(left, right); + + let center = + Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into(); + + Projections { + left_eye, + right_eye, + center, + } +} diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index 452b89d..0742ae6 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -19,6 +19,8 @@ pub mod attrib; pub mod buffer; pub mod error; +pub mod light; +pub mod material; pub mod math; pub mod render; pub mod shader; @@ -27,6 +29,7 @@ pub mod uniform; use std::cell::{OnceCell, RefMut}; use std::fmt; +use std::pin::Pin; use std::rc::Rc; use ctru::services::gfx::Screen; @@ -47,6 +50,7 @@ pub mod macros { pub struct Instance { texenvs: [OnceCell; texenv::TEXENV_COUNT], queue: Rc, + light_env: Pin>, } /// Representation of `citro3d`'s internal render queue. This is something that @@ -79,6 +83,12 @@ impl Instance { #[doc(alias = "C3D_Init")] pub fn with_cmdbuf_size(size: usize) -> Result { if unsafe { citro3d_sys::C3D_Init(size) } { + let mut light_env = Box::pin(light::LightEnv::new()); + unsafe { + // setup the light env slot, since this is a pointer copy it will stick around even with we swap + // out light_env later + citro3d_sys::C3D_LightEnvBind(light_env.as_mut().as_raw_mut()); + } Ok(Self { texenvs: [ // thank goodness there's only six of them! @@ -90,6 +100,7 @@ impl Instance { OnceCell::new(), ], queue: Rc::new(RenderQueue), + light_env, }) } else { Err(Error::FailedToInitialize) @@ -208,6 +219,9 @@ impl Instance { citro3d_sys::C3D_BindProgram(program.as_raw().cast_mut()); } } + pub fn light_env_mut(&mut self) -> Pin<&mut light::LightEnv> { + self.light_env.as_mut() + } /// Bind a uniform to the given `index` in the vertex shader for the next draw call. /// diff --git a/citro3d/src/light.rs b/citro3d/src/light.rs new file mode 100644 index 0000000..ab69fcb --- /dev/null +++ b/citro3d/src/light.rs @@ -0,0 +1,457 @@ +//! Bindings for accessing the lighting part of the GPU pipeline +//! +//! The hardware at play is shown in [this diagram][hardware], you should probably have +//! it open as a reference for the documentation in this module. +//! +//! # Hardware lights +//! There are 8 lights in the GPU's pipeline each of which have 4 colour fields and 1 spotlight colour, +//! you can set all of them at once with [`LightEnv::set_material`]. When rendering for example you call +//! `set_material` in your preparation code before the actual draw call. +//! +//! For things like specular lighting we need to go a bit deeper +//! +//! # LUTS +//! LUTS are lookup tables, in this case for the GPU. They are created ahead of time and stored in [`LightLut`]'s, +//! [`LightLut::from_fn`] essentially memoises the given function with the input changing depending on what +//! input it is bound to when setting it on the [`LightEnv`]. +//! +//! ## Example +//! Lets say we have this code +//! +//! ``` +//! # use citro3d::{Instance, light::{LightLutId, LightInput, LightLut}}; +//! let mut inst = Instance::new(); +//! let mut env = inst.light_env_mut(); +//! env.as_mut().connect_lut( +//! LutInputId::D0, +//! LutInput::NormalView, +//! LightLut::from_fn(|x| x.powf(10.0)), +//! ); +//! ``` +//! +//! This places the LUT in `D0` (refer to [the diagram][hardware]) and connects the input wire as the dot product +//! of the normal and view vectors. `x` is effectively the dot product of the normal and view for every vertex and +//! the return of the closure goes out on the corresponding wire +//! (which in the case of `D0` is used for specular lighting after being combined with with specular0) +//! +//! +//! +//! [hardware]: https://raw.githubusercontent.com/wwylele/misc-3ds-diagram/master/pica-pipeline.svg + +use std::{marker::PhantomPinned, mem::MaybeUninit, ops::Range, pin::Pin}; + +use pin_array::PinArray; + +use crate::{ + material::Material, + math::{FVec3, FVec4}, +}; + +/// Index for one of the 8 hardware lights in the GPU pipeline +/// +/// Usually you don't want to construct one of these directly but use [`LightEnv::create_light`] +// Note we use a u8 here since usize is overkill and it saves a few bytes +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct LightIndex(u8); + +const NB_LIGHTS: usize = 8; + +impl LightIndex { + /// Manually create a `LightIndex` with a specific index + /// + /// # Panics + /// if `idx` out of range for the number of lights (>=8) + pub fn new(idx: usize) -> Self { + assert!(idx < NB_LIGHTS); + Self(idx as u8) + } + pub fn as_usize(self) -> usize { + self.0 as usize + } +} + +type LightArray = PinArray, NB_LIGHTS>; + +pub struct LightEnv { + raw: citro3d_sys::C3D_LightEnv, + /// The actual light data pointed to by the lights element of `raw` + /// + /// Note this is `Pin` as well, because `raw` means we are _actually_ self-referential which + /// is horrible but the best bad option in this case. Moving the one of these elements would + /// break the pointers in `raw` + lights: LightArray, + luts: [Option; 6], + _pin: PhantomPinned, +} + +pub struct Light { + raw: citro3d_sys::C3D_Light, + // todo: implement spotlight support + _spot: Option, + diffuse_atten: Option, + _pin: PhantomPinned, +} + +impl Default for LightEnv { + fn default() -> Self { + let raw = unsafe { + let mut env = MaybeUninit::zeroed(); + citro3d_sys::C3D_LightEnvInit(env.as_mut_ptr()); + env.assume_init() + }; + Self { + raw, + lights: Default::default(), + luts: Default::default(), + _pin: Default::default(), + } + } +} +impl LightEnv { + pub fn new() -> Self { + Self::default() + } + pub fn set_material(self: Pin<&mut Self>, mat: Material) { + let raw = mat.to_raw(); + // Safety: This takes a pointer but it actually memcpy's it so this doesn't dangle + unsafe { + citro3d_sys::C3D_LightEnvMaterial(self.as_raw_mut() as *mut _, (&raw) as *const _); + } + } + + pub fn lights(&self) -> &LightArray { + &self.lights + } + + pub fn lights_mut(self: Pin<&mut Self>) -> Pin<&mut LightArray> { + unsafe { self.map_unchecked_mut(|s| &mut s.lights) } + } + + pub fn light_mut(self: Pin<&mut Self>, idx: LightIndex) -> Option> { + self.lights_mut() + .get_pin(idx.0 as usize) + .unwrap() + .as_pin_mut() + } + pub fn create_light(mut self: Pin<&mut Self>) -> Option { + let idx = self + .lights() + .iter() + .enumerate() + .find(|(_, n)| n.is_none()) + .map(|(n, _)| n)?; + + self.as_mut() + .lights_mut() + .get_pin(idx) + .unwrap() + .set(Some(Light::new(unsafe { + MaybeUninit::zeroed().assume_init() + }))); + + let target = unsafe { + self.as_mut() + .lights_mut() + .get_pin(idx) + .unwrap() + .map_unchecked_mut(|p| p.as_mut().unwrap()) + }; + let r = unsafe { + citro3d_sys::C3D_LightInit( + target.get_unchecked_mut().as_raw_mut(), + self.as_raw_mut() as *mut _, + ) + }; + assert!(r >= 0, "C3D_LightInit should only fail if there are no free light slots but we checked that already, how did this happen?"); + assert_eq!( + r as usize, idx, + "citro3d chose a different light to us? this shouldn't be possible" + ); + Some(LightIndex::new(idx)) + } + fn lut_id_to_index(id: LightLutId) -> Option { + match id { + LightLutId::D0 => Some(0), + LightLutId::D1 => Some(1), + LightLutId::SpotLightAttenuation => None, + LightLutId::Fresnel => Some(2), + LightLutId::ReflectBlue => Some(3), + LightLutId::ReflectGreen => Some(4), + LightLutId::ReflectRed => Some(5), + LightLutId::DistanceAttenuation => None, + } + } + /// Attempt to disconnect a light lut + /// + /// # Note + /// This function will not panic if the lut does not exist for `id` and `input`, it will just return `None` + pub fn disconnect_lut( + mut self: Pin<&mut Self>, + id: LightLutId, + input: LutInput, + ) -> Option { + let idx = Self::lut_id_to_index(id); + let me = unsafe { self.as_mut().get_unchecked_mut() }; + let lut = idx.and_then(|i| me.luts[i].take()); + if lut.is_some() { + unsafe { + citro3d_sys::C3D_LightEnvLut( + &mut me.raw, + id as u8, + input as u8, + false, + std::ptr::null_mut(), + ); + } + } + lut + } + pub fn connect_lut(mut self: Pin<&mut Self>, id: LightLutId, input: LutInput, data: LightLut) { + let idx = Self::lut_id_to_index(id); + let (raw, lut) = unsafe { + // this is needed to do structural borrowing as otherwise + // the compiler rejects the reborrow needed with the pin + let me = self.as_mut().get_unchecked_mut(); + let lut = idx.map(|i| me.luts[i].insert(data)); + let raw = &mut me.raw; + let lut = match lut { + Some(l) => (&mut l.0) as *mut _, + None => core::ptr::null_mut(), + }; + (raw, lut) + }; + unsafe { + citro3d_sys::C3D_LightEnvLut(raw, id as u8, input as u8, false, lut); + } + } + pub fn set_fresnel(self: Pin<&mut Self>, sel: FresnelSelector) { + unsafe { citro3d_sys::C3D_LightEnvFresnel(self.as_raw_mut(), sel as _) } + } + + pub fn as_raw(&self) -> &citro3d_sys::C3D_LightEnv { + &self.raw + } + + pub fn as_raw_mut(self: Pin<&mut Self>) -> &mut citro3d_sys::C3D_LightEnv { + unsafe { &mut self.get_unchecked_mut().raw } + } +} + +impl Light { + fn new(raw: citro3d_sys::C3D_Light) -> Self { + Self { + raw, + _spot: Default::default(), + diffuse_atten: Default::default(), + _pin: Default::default(), + } + } + + /// Get a reference to the underlying raw `C3D_Light` + pub fn as_raw(&self) -> &citro3d_sys::C3D_Light { + &self.raw + } + + /// Get a raw mut to the raw `C3D_Light` + /// + /// note: This does not take Pin<&mut Self>, if you need the raw from a pinned light you must use `unsafe` and ensure you uphold the pinning + /// restrictions of the original `Light` + pub fn as_raw_mut(&mut self) -> &mut citro3d_sys::C3D_Light { + &mut self.raw + } + + pub fn set_position(self: Pin<&mut Self>, p: FVec3) { + let mut p = FVec4::new(p.x(), p.y(), p.z(), 1.0); + unsafe { citro3d_sys::C3D_LightPosition(self.get_unchecked_mut().as_raw_mut(), &mut p.0) } + } + pub fn set_color(self: Pin<&mut Self>, r: f32, g: f32, b: f32) { + unsafe { citro3d_sys::C3D_LightColor(self.get_unchecked_mut().as_raw_mut(), r, g, b) } + } + #[doc(alias = "C3D_LightEnable")] + pub fn set_enabled(self: Pin<&mut Self>, enabled: bool) { + unsafe { citro3d_sys::C3D_LightEnable(self.get_unchecked_mut().as_raw_mut(), enabled) } + } + #[doc(alias = "C3D_LightShadowEnable")] + pub fn set_shadow(self: Pin<&mut Self>, shadow: bool) { + unsafe { citro3d_sys::C3D_LightShadowEnable(self.get_unchecked_mut().as_raw_mut(), shadow) } + } + pub fn set_distance_attenutation(mut self: Pin<&mut Self>, lut: Option) { + { + let me = unsafe { self.as_mut().get_unchecked_mut() }; + me.diffuse_atten = lut; + } + // this is a bit of a mess because we need to be _reallly_ careful we don't trip aliasing rules + // reusing `me` here I think trips them because we have multiple live mutable references to + // the same region + let (raw, c_lut) = { + let me = unsafe { self.as_mut().get_unchecked_mut() }; + let raw = &mut me.raw; + let c_lut = me.diffuse_atten.as_mut().map(|d| &mut d.raw); + (raw, c_lut) + }; + unsafe { + citro3d_sys::C3D_LightDistAttn( + raw, + match c_lut { + Some(l) => l, + None => std::ptr::null_mut(), + }, + ); + } + } +} + +// Safety: I am 99% sure these are safe. That 1% is if citro3d does something weird I missed +// which is not impossible +unsafe impl Send for Light {} +unsafe impl Sync for Light {} + +unsafe impl Send for LightEnv {} +unsafe impl Sync for LightEnv {} + +/// Lookup table for light data +/// +/// For more refer to the module documentation +#[derive(Clone, Copy, Debug)] +#[repr(transparent)] +pub struct LightLut(citro3d_sys::C3D_LightLut); + +impl PartialEq for LightLut { + fn eq(&self, other: &Self) -> bool { + self.0.data == other.0.data + } +} +impl Eq for LightLut {} + +impl std::hash::Hash for LightLut { + fn hash(&self, state: &mut H) { + self.0.data.hash(state); + } +} + +#[cfg(test)] +extern "C" fn c_powf(a: f32, b: f32) -> f32 { + a.powf(b) +} + +type LutArray = [u32; 256]; +const LUT_BUF_SZ: usize = 512; + +impl LightLut { + /// Create a LUT by memoizing a function + pub fn from_fn(mut f: impl FnMut(f32) -> f32, negative: bool) -> Self { + let base: i32 = 128; + let diff = if negative { 0 } else { base }; + let min = -128 + diff; + let max = base + diff; + assert_eq!(min.abs_diff(max), 2 * base as u32); + let mut data = [0.0f32; LUT_BUF_SZ]; + for i in min..=max { + let x = i as f32 / max as f32; + let v = f(x); + let idx = if negative { i & 0xFF } else { i } as usize; + if i < max { + data[idx] = v; + } + if i > min { + data[idx + 255] = v - data[idx - 1]; + } + } + let lut = unsafe { + let mut lut = MaybeUninit::zeroed(); + citro3d_sys::LightLut_FromArray(lut.as_mut_ptr(), data.as_mut_ptr()); + lut.assume_init() + }; + Self(lut) + } + + /// Get a reference to the underlying data + pub fn data(&self) -> &LutArray { + &self.0.data + } + + /// Get a mutable reference to the underlying data + pub fn data_mut(&mut self) -> &mut LutArray { + &mut self.0.data + } + + #[cfg(test)] + fn phong_citro3d(shininess: f32) -> Self { + let lut = unsafe { + let mut lut = MaybeUninit::uninit(); + citro3d_sys::LightLut_FromFunc(lut.as_mut_ptr(), Some(c_powf), shininess, false); + lut.assume_init() + }; + Self(lut) + } +} + +pub struct LightLutDistAtten { + raw: citro3d_sys::C3D_LightLutDA, +} + +impl LightLutDistAtten { + pub fn new(range: Range, mut f: impl FnMut(f32) -> f32) -> Self { + let mut raw: citro3d_sys::C3D_LightLutDA = unsafe { MaybeUninit::zeroed().assume_init() }; + let dist = range.end - range.start; + raw.scale = 1.0 / dist; + raw.bias = -range.start * raw.scale; + let lut = LightLut::from_fn(|x| f(range.start + dist * x), false); + raw.lut = citro3d_sys::C3D_LightLut { data: *lut.data() }; + Self { raw } + } +} + +/// This is used to decide what the input should be to a [`LightLut`] +#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[repr(u8)] +pub enum LutInput { + CosPhi = ctru_sys::GPU_LUTINPUT_CP, + /// Dot product of the light and normal vectors + LightNormal = ctru_sys::GPU_LUTINPUT_LN, + /// Half the normal + NormalHalf = ctru_sys::GPU_LUTINPUT_NH, + /// Dot product of the view and normal + NormalView = ctru_sys::GPU_LUTINPUT_NV, + /// Dot product of the spotlight colour and light vector + LightSpotLight = ctru_sys::GPU_LUTINPUT_SP, + /// Half the view vector + ViewHalf = ctru_sys::GPU_LUTINPUT_VH, +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[repr(u8)] +pub enum LightLutId { + D0 = ctru_sys::GPU_LUT_D0, + D1 = ctru_sys::GPU_LUT_D1, + SpotLightAttenuation = ctru_sys::GPU_LUT_SP, + Fresnel = ctru_sys::GPU_LUT_FR, + ReflectBlue = ctru_sys::GPU_LUT_RB, + ReflectGreen = ctru_sys::GPU_LUT_RG, + ReflectRed = ctru_sys::GPU_LUT_RR, + DistanceAttenuation = ctru_sys::GPU_LUT_DA, +} +#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[repr(u8)] +pub enum FresnelSelector { + /// No fresnel selection + None = ctru_sys::GPU_NO_FRESNEL, + /// Use as selector for primary colour unit alpha + PrimaryAlpha = ctru_sys::GPU_PRI_ALPHA_FRESNEL, + /// Use as selector for secondary colour unit alpha + SecondaryAlpha = ctru_sys::GPU_SEC_ALPHA_FRESNEL, + /// Use as selector for both colour units + Both = ctru_sys::GPU_PRI_SEC_ALPHA_FRESNEL, +} + +#[cfg(test)] +mod tests { + use super::LightLut; + + #[test] + fn lut_data_phong_matches_for_own_and_citro3d() { + let c3d = LightLut::phong_citro3d(30.0); + let rs = LightLut::from_fn(|i| i.powf(30.0), false); + assert_eq!(c3d, rs); + } +} diff --git a/citro3d/src/material.rs b/citro3d/src/material.rs new file mode 100644 index 0000000..08dbf79 --- /dev/null +++ b/citro3d/src/material.rs @@ -0,0 +1,41 @@ +#[derive(Debug, Default, Clone, Copy)] +pub struct Material { + pub ambient: Option, + pub diffuse: Option, + pub specular0: Option, + pub specular1: Option, + pub emission: Option, +} +impl Material { + pub fn to_raw(self) -> citro3d_sys::C3D_Material { + citro3d_sys::C3D_Material { + ambient: self.ambient.unwrap_or_default().to_parts_bgr(), + diffuse: self.diffuse.unwrap_or_default().to_parts_bgr(), + specular0: self.specular0.unwrap_or_default().to_parts_bgr(), + specular1: self.specular1.unwrap_or_default().to_parts_bgr(), + emission: self.emission.unwrap_or_default().to_parts_bgr(), + } + } +} + +/// RGB color in linear space ([0, 1]) +#[derive(Debug, Default, Clone, Copy)] +pub struct Color { + pub r: f32, + pub g: f32, + pub b: f32, +} + +impl Color { + pub fn new(r: f32, g: f32, b: f32) -> Self { + Self { r, g, b } + } + /// Split into BGR ordered parts + /// + /// # Reason for existence + /// The C version of [`Material`] expects colours in BGR order (don't ask why it is beyond my comprehension) + /// so we have to reorder when converting + pub fn to_parts_bgr(self) -> [f32; 3] { + [self.b, self.g, self.r] + } +}