diff --git a/citro3d/examples/cube.rs b/citro3d/examples/cube.rs new file mode 100644 index 0000000..ba58afb --- /dev/null +++ b/citro3d/examples/cube.rs @@ -0,0 +1,243 @@ +//! This example demonstrates the most basic usage of `citro3d`: rendering a simple +//! RGB triangle (sometimes called a "Hello triangle") to the 3DS screen. + +#![feature(allocator_api)] + +use citro3d::macros::include_shader; +use citro3d::math::{ + AspectRatio, ClipPlanes, CoordinateOrientation, FVec3, Matrix4, Projection, StereoDisplacement, +}; +use citro3d::render::ClearFlags; +use citro3d::{attrib, buffer, render, shader, texenv}; +use ctru::prelude::*; +use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; + +#[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 } + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct Vertex { + pos: Vec3, + color: Vec3, +} + +// borrowed from https://bevyengine.org/examples/3D%20Rendering/generate-custom-mesh/ +const VERTS: &[[f32; 3]] = &[ + // top (facing towards +y) + [-0.5, 0.5, -0.5], // vertex with index 0 + [0.5, 0.5, -0.5], // vertex with index 1 + [0.5, 0.5, 0.5], // etc. until 23 + [-0.5, 0.5, 0.5], + // bottom (-y) + [-0.5, -0.5, -0.5], + [0.5, -0.5, -0.5], + [0.5, -0.5, 0.5], + [-0.5, -0.5, 0.5], + // right (+x) + [0.5, -0.5, -0.5], + [0.5, -0.5, 0.5], + [0.5, 0.5, 0.5], // This vertex is at the same position as vertex with index 2, but they'll have different UV and normal + [0.5, 0.5, -0.5], + // left (-x) + [-0.5, -0.5, -0.5], + [-0.5, -0.5, 0.5], + [-0.5, 0.5, 0.5], + [-0.5, 0.5, -0.5], + // back (+z) + [-0.5, -0.5, 0.5], + [-0.5, 0.5, 0.5], + [0.5, 0.5, 0.5], + [0.5, -0.5, 0.5], + // forward (-z) + [-0.5, -0.5, -0.5], + [-0.5, 0.5, -0.5], + [0.5, 0.5, -0.5], + [0.5, -0.5, -0.5], +]; + +static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica"); +const CLEAR_COLOR: u32 = 0x68_B0_D8_FF; + +fn main() { + let mut soc = Soc::new().expect("failed to get SOC"); + drop(soc.redirect_to_3dslink(true, true)); + + let gfx = Gfx::new().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, None) + .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, None) + .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, None) + .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(VERTS.len(), ctru::linear::LinearAllocator); + for vert in VERTS.iter().enumerate().map(|(i, v)| Vertex { + pos: Vec3 { + x: v[0], + y: v[1], + z: v[2], + }, + color: { + // Give each vertex a slightly different color just to highlight edges/corners + let value = i as f32 / VERTS.len() as f32; + Vec3::new(1.0, 0.7 * value, 0.5) + }, + }) { + vbo_data.push(vert); + } + + let attr_info = build_attrib_info(); + + let mut buf_info = buffer::Info::new(); + let vbo_slice = buf_info.add(&vbo_data, &attr_info).unwrap(); + + // 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::PrimaryColor, None, None) + .func(texenv::Mode::BOTH, texenv::CombineFunc::Replace); + + let projection_uniform_idx = program.get_uniform("projection").unwrap(); + let camera_transform = Matrix4::looking_at( + FVec3::new(1.8, 1.8, 1.8), + FVec3::new(0.0, 0.0, 0.0), + FVec3::new(0.0, 1.0, 0.0), + CoordinateOrientation::RightHanded, + ); + let indices: &[u8] = &[ + 0, 3, 1, 1, 3, 2, // triangles making up the top (+y) facing side. + 4, 5, 7, 5, 6, 7, // bottom (-y) + 8, 11, 9, 9, 11, 10, // right (+x) + 12, 13, 15, 13, 14, 15, // left (-x) + 16, 19, 17, 17, 19, 18, // back (+z) + 20, 21, 23, 21, 22, 23, // forward (-z) + ]; + let index_buffer = vbo_slice.index_buffer(indices).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, CLEAR_COLOR, 0); + + instance + .select_render_target(target) + .expect("failed to set render target"); + + instance.bind_vertex_uniform(projection_uniform_idx, projection * camera_transform); + + instance.set_attr_info(&attr_info); + unsafe { + instance.draw_elements(buffer::Primitive::Triangles, vbo_slice, &index_buffer); + } + }; + + 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); + }); + } +} + +fn build_attrib_info() -> attrib::Info { + // 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(); + + attr_info + .add_loader(reg0, attrib::Format::Float, 3) + .unwrap(); + + attr_info + .add_loader(reg1, attrib::Format::Float, 3) + .unwrap(); + + attr_info +} + +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/buffer.rs b/citro3d/src/buffer.rs index 0a19ff3..afaa426 100644 --- a/citro3d/src/buffer.rs +++ b/citro3d/src/buffer.rs @@ -5,7 +5,10 @@ use std::mem::MaybeUninit; +use ctru::linear::LinearAllocator; + use crate::attrib; +use crate::Error; /// Vertex buffer info. This struct is used to describe the shape of the buffer /// data to be sent to the GPU for rendering. @@ -46,6 +49,68 @@ impl Slice<'_> { pub fn info(&self) -> &Info { self.buf_info } + + /// Get an index buffer for this slice using the given indices. + /// + /// # Errors + /// + /// Returns an error if: + /// - any of the given indices are out of bounds. + /// - the given slice is too long for its length to fit in a `libc::c_int`. + pub fn index_buffer(&self, indices: &[I]) -> Result, Error> + where + I: Index + Copy + Into, + { + if libc::c_int::try_from(indices.len()).is_err() { + return Err(Error::InvalidSize); + } + + for &idx in indices { + let idx = idx.into(); + let len = self.len(); + if idx >= len { + return Err(Error::IndexOutOfBounds { idx, len }); + } + } + + Ok(unsafe { self.index_buffer_unchecked(indices) }) + } + + /// Get an index buffer for this slice using the given indices without + /// bounds checking. + /// + /// # Safety + /// + /// If any indices are outside this buffer it can cause an invalid access by the GPU + /// (this crashes citra). + pub unsafe fn index_buffer_unchecked(&self, indices: &[I]) -> Indices { + let mut buffer = Vec::with_capacity_in(indices.len(), LinearAllocator); + buffer.extend_from_slice(indices); + Indices { + buffer, + _slice: *self, + } + } +} + +/// An index buffer for indexed drawing. See [`Slice::index_buffer`] to obtain one. +pub struct Indices<'buf, I> { + pub(crate) buffer: Vec, + _slice: Slice<'buf>, +} + +/// A type that can be used as an index for indexed drawing. +pub trait Index: crate::private::Sealed { + /// The data type of the index, as used by [`citro3d_sys::C3D_DrawElements`]'s `type_` parameter. + const TYPE: libc::c_int; +} + +impl Index for u8 { + const TYPE: libc::c_int = citro3d_sys::C3D_UNSIGNED_BYTE as _; +} + +impl Index for u16 { + const TYPE: libc::c_int = citro3d_sys::C3D_UNSIGNED_SHORT as _; } /// The geometric primitive to draw (i.e. what shapes the buffer data describes). diff --git a/citro3d/src/error.rs b/citro3d/src/error.rs index 9a99089..65ca430 100644 --- a/citro3d/src/error.rs +++ b/citro3d/src/error.rs @@ -36,6 +36,13 @@ pub enum Error { InvalidName, /// The requested resource could not be found. NotFound, + /// Attempted to use an index that was out of bounds. + IndexOutOfBounds { + /// The index used. + idx: libc::c_int, + /// The length of the collection. + len: libc::c_int, + }, } impl From for Error { diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index 452b89d..b8748c6 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -1,5 +1,6 @@ #![feature(custom_test_frameworks)] #![test_runner(test_runner::run_gdb)] +#![feature(allocator_api)] #![feature(doc_cfg)] #![feature(doc_auto_cfg)] #![doc(html_root_url = "https://rust3ds.github.io/citro3d-rs/crates")] @@ -32,6 +33,7 @@ use std::rc::Rc; use ctru::services::gfx::Screen; pub use error::{Error, Result}; +use self::buffer::{Index, Indices}; use self::texenv::TexEnv; use self::uniform::Uniform; @@ -40,6 +42,12 @@ pub mod macros { pub use citro3d_macros::*; } +mod private { + pub trait Sealed {} + impl Sealed for u8 {} + impl Sealed for u16 {} +} + /// The single instance for using `citro3d`. This is the base type that an application /// should instantiate to use this library. #[non_exhaustive] @@ -190,7 +198,6 @@ impl Instance { self.set_buffer_info(vbo_data.info()); // TODO: should we also require the attrib info directly here? - unsafe { citro3d_sys::C3D_DrawArrays( primitive as ctru_sys::GPU_Primitive_t, @@ -199,6 +206,38 @@ impl Instance { ); } } + /// Indexed drawing + /// + /// Draws the vertices in `buf` indexed by `indices`. `indices` must be linearly allocated + /// + /// # Safety + // TODO: #41 might be able to solve this: + /// If `indices` goes out of scope before the current frame ends it will cause a + /// use-after-free (possibly by the GPU). + /// + /// # Panics + /// + /// If the given index buffer is too long to have its length converted to `i32`. + #[doc(alias = "C3D_DrawElements")] + pub unsafe fn draw_elements( + &mut self, + primitive: buffer::Primitive, + vbo_data: buffer::Slice, + indices: &Indices, + ) { + self.set_buffer_info(vbo_data.info()); + + let indices = &indices.buffer; + let elements = indices.as_ptr().cast(); + + citro3d_sys::C3D_DrawElements( + primitive as ctru_sys::GPU_Primitive_t, + indices.len().try_into().unwrap(), + // flag bit for short or byte + I::TYPE, + elements, + ); + } /// Use the given [`shader::Program`] for subsequent draw calls. pub fn bind_program(&mut self, program: &shader::Program) { diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs index 229210e..7949189 100644 --- a/citro3d/src/math/ops.rs +++ b/citro3d/src/math/ops.rs @@ -212,7 +212,7 @@ impl AbsDiffEq for Matrix4 { fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { self.rows_wzyx() .into_iter() - .zip(other.rows_wzyx().into_iter()) + .zip(other.rows_wzyx()) .all(|(l, r)| l.abs_diff_eq(&r, epsilon)) } }