-
Notifications
You must be signed in to change notification settings - Fork 11
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
[Feature] Indexed drawing #42
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
Comment on lines
+145
to
+146
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe to enforce lifetimes for index buffers we could have something like let (attr_info, _vbo_data) = prepare_vbos(&mut buf_info, &vbo_data);
let mut index_buffer = buf_info.index_buffer();
// This possibly also checks that all the new indices are valid for the vertex data:
index_buffer.extend_from_slice(&indices_a); Edit: actually, I guess we'd also need to make sure the vertex buffer was added at some point, so maybe this would be a method on |
||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this just leftover from copy-pasting the existing triangle example? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, good catch |
||
}; | ||
|
||
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, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
Comment on lines
+169
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'm fine with having a new |
||
/// | ||
/// # 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<IndexType<'a>>, | ||
) { | ||
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" | ||
); | ||
ian-h-chamberlain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/// Check if pointer is in linear memory | ||
pub fn is_linear_ptr<P>(p: *const P) -> bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does seem useful to have — I've also thought about having some kind of type safety around this, e.g. trait Linear {}
impl<T> Linear for Vec<T, LinearAllocator> {} The problem is that other APIs would end up with somewhat inconvenient definitions, e.g. pub fn add<'this, 'a, T, V>(
&'this mut self,
vbo_data: &T,
attrib_info: &attrib::Info,
) -> crate::Result<Slice<'a>>
where
'this: 'a,
T: Deref<Target = [V]> + Linear,
{ But maybe the type safety would be worth it and then we wouldn't need panics or |
||
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) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might call this
cube-indexed.rs
or something just to make it clear that it uses an index buffer as compared to the existing triangle xample