Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add indexed drawing #52

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions citro3d/examples/cube.rs
Original file line number Diff line number Diff line change
@@ -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, &center);
});
}
}

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,
}
}
65 changes: 65 additions & 0 deletions citro3d/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<I>(&self, indices: &[I]) -> Result<Indices<I>, Error>
where
I: Index + Copy + Into<libc::c_int>,
{
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<I: Index + Clone>(&self, indices: &[I]) -> Indices<I> {
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<I, LinearAllocator>,
_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).
Expand Down
7 changes: 7 additions & 0 deletions citro3d/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TryFromIntError> for Error {
Expand Down
Loading
Loading