diff --git a/citro3d/examples/cube.rs b/citro3d/examples/cube.rs new file mode 100644 index 0000000..69d9f80 --- /dev/null +++ b/citro3d/examples/cube.rs @@ -0,0 +1,250 @@ +//! 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}; +use citro3d::{texenv, IndexType}; +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 = + render::Target::new(width, height, top_left, None).expect("failed to create render target"); + + let RawFrameBuffer { width, height, .. } = top_right.raw_framebuffer(); + let mut top_right_target = render::Target::new(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 = render::Target::new(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: Vec3::new(1.0, 0.7, 0.5), + }) { + vbo_data.push(vert); + } + + let mut buf_info = buffer::Info::new(); + let (attr_info, vbo_data) = prepare_vbos(&mut buf_info, &vbo_data); + + // 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 indecies_a = [ + 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 mut indecies = Vec::with_capacity_in(indecies_a.len(), ctru::linear::LinearAllocator); + indecies.extend_from_slice(&indecies_a); + + 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.clone()), + ); + + instance.set_attr_info(&attr_info); + unsafe { + instance.draw_elements( + buffer::Primitive::Triangles, + &buf_info, + IndexType::U16(&indecies), + ); + } + + //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); + }); + } +} + +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(); + + attr_info + .add_loader(reg0, attrib::Format::Float, 3) + .unwrap(); + + attr_info + .add_loader(reg1, attrib::Format::Float, 3) + .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 c18e6fc..7117a85 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -17,11 +17,13 @@ pub mod render; pub mod shader; pub mod texenv; pub mod uniform; +mod util; use std::cell::OnceCell; use std::fmt; pub use error::{Error, Result}; +use util::is_linear_ptr; use self::texenv::TexEnv; use self::uniform::Uniform; @@ -152,7 +154,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, @@ -161,6 +162,44 @@ impl Instance { ); } } + /// Indexed drawing + /// + /// Draws the vertices in `buf` indexed by `indices`. `indices` must be linearly allocated + /// + /// # Safety + /// If `indices` goes out of scope before the current frame ends it will cause a use-after-free (possibly by the GPU) + /// If `buf` does not contain all the vertices references by `indices` it will cause an invalid access by the GPU (this crashes citra) + /// + /// # Panics + /// If `indices` is not allocated in linear memory + #[doc(alias = "C3D_DrawElements")] + pub unsafe fn draw_elements<'a>( + &mut self, + primitive: buffer::Primitive, + buf: &buffer::Info, + indices: impl Into>, + ) { + self.set_buffer_info(buf); + let indices: IndexType<'a> = indices.into(); + let elements = match indices { + IndexType::U16(v) => v.as_ptr() as *const _, + IndexType::U8(v) => v.as_ptr() as *const _, + }; + assert!( + is_linear_ptr(elements), + "draw_elements requires linear allocated indices buffer" + ); + citro3d_sys::C3D_DrawElements( + primitive as ctru_sys::GPU_Primitive_t, + indices.len() as i32, + // flag bit for short or byte + match indices { + IndexType::U16(_) => citro3d_sys::C3D_UNSIGNED_SHORT, + IndexType::U8(_) => citro3d_sys::C3D_UNSIGNED_BYTE, + } as i32, + elements, + ); + } /// Use the given [`shader::Program`] for subsequent draw calls. pub fn bind_program(&mut self, program: &shader::Program) { @@ -237,3 +276,28 @@ impl Drop for Instance { } } } + +pub enum IndexType<'a> { + U16(&'a [u16]), + U8(&'a [u8]), +} +impl IndexType<'_> { + fn len(&self) -> usize { + match self { + IndexType::U16(a) => a.len(), + IndexType::U8(a) => a.len(), + } + } +} + +impl<'a> From<&'a [u8]> for IndexType<'a> { + fn from(v: &'a [u8]) -> Self { + Self::U8(v) + } +} + +impl<'a> From<&'a [u16]> for IndexType<'a> { + fn from(v: &'a [u16]) -> Self { + Self::U16(v) + } +} diff --git a/citro3d/src/util.rs b/citro3d/src/util.rs new file mode 100644 index 0000000..727e8a2 --- /dev/null +++ b/citro3d/src/util.rs @@ -0,0 +1,6 @@ +/// Check if pointer is in linear memory +pub fn is_linear_ptr

(p: *const P) -> bool { + let addr = p as usize; + addr >= ctru_sys::OS_FCRAM_VADDR as usize + && addr < (ctru_sys::OS_FCRAM_VADDR as usize + ctru_sys::OS_FCRAM_SIZE as usize) +}