nannou-org / nannou

A Creative Coding Framework for Rust.
https://nannou.cc/
6.04k stars 305 forks source link

Static geometry mesh #20

Closed freesig closed 6 years ago

freesig commented 7 years ago

Unit sized static mesh

const fixed size arrays Indicies array Variants

freesig commented 7 years ago

So I'm trying to figure out the most efficient way to implement the mesh. There's two types of mesh. Static For example a unit square mesh. This will never change if the tessellation (resolution) changes. Dynamic For example a sphere. This will change as tessellation changes.

For the static mesh its faster to put the data on the stack as a const array. Like this: const UnitSquare: [f32; 12] = [-0.5, 0.5, 0.0, 0.5, 0.5, 0.0, 0.5, -0.5, 0.0, -0.5, -0.5, 0.0];

But this can't be done with the dynamic mesh that will be different if the tessellation changes. So that needs to be created on the heap using some thing like:

struct Vertex{
    position: f32,
}
struct Mesh{
    verticies: Vec<Vertex>,
}

Question

How can I do this so for the person using the Mesh struct they have the same interface. Basically I want to be able to use a mesh struct and be unaware how its stored so that we can put static shapes on the stack where possible.

freesig commented 7 years ago

One idea I was thinking is to have a Vec<&Vertex> and then the Vertex can be either stored in a const array or in a dynamic Vec. But I think this leads to some complicated lifetimes and maybe we loose the advantage of having the it on the stack

freesig commented 7 years ago

Maybe something along these lines

use cgmath::*;

const UnitSquare: [Vertex; 12] = [Vertex::new(-0.5, 0.5, 0.0),
Vertex::new(0.5, 0.5, 0.0),
Vertex::new(0.5, -0.5, 0.0),
Vertex::new(-0.5, -0.5, 0.0)];

struct Vertex{
    position: Vector3<f32>,
}

impl Vertex{
    fn new(x: f32, y: f32, z:f32) -> Self {
        Vertex{ position: Vector3(x, y, z) }
    }
}

struct Mesh{
    verticies: Vec<Vertex>,
}
freesig commented 7 years ago

Or Mesh could hold a slice. like &[Vertex] then we could pull that from either a Vec or a const array?

mitchmindtree commented 7 years ago

Hmmm maybe we could start at an even more basic level than the Mesh type with this geom module? I think the way you point out some shapes are inherently dynamic depending on the level of tesselation while others can be easily represented in a const array is maybe a good indication for this.

Maybe a nicer way to go about this is to write "the most efficient representation" for each individual kind of geometry (e.g. a const arrays for squares and triangles, Iterators for spheres, ovals, etc) and then once we have these to work with we can think about the simplest way to unify these representations with a Mesh type?

I'm pretty sure that theoretically we should be able to have const arrays for spheres/circles if the given tesselation level is const (and calculated the number of verts needed at the const-level), but rust's support for const generic types and associated consts in fixed-size-arrays is super limited atm.

mitchmindtree commented 7 years ago

Or Mesh could hold a slice. like &[Vertex] then we could pull that from either a Vec or a const array?

Yeah I like the idea of Mesh being generic in the sense that it can either borrow existing buffers or own its buffers :+1:

freesig commented 7 years ago

Yeh I agree, if I just make all the shapes in their most efficient way, then maybe it will become obvious how to package them in a generic Mesh type

mitchmindtree commented 7 years ago

Here's a mega rough sketch of where I was thinking of going with the Mesh type (could be way off as I've barely used meshes much!):

struct Mesh<V, I, N=(), T=()>
where
    V: Vertices,
    I: Indices,
    N: Normals,
    T: TexCoords,
{
    vertices: V,
    indices: I,
    normals: Option<N>,
    tex_coords: Option<T>,
}

trait Vertices: std::ops::Index<usize, Output=Vertex> {}

trait Indices {
    type Iter: Iterator<Item=usize>;
    fn indices(self) -> Self::Iter;
}

// Not sure how normals/texcoords are normally referenced but maybe that could have similar traits?

// Auto implement the Vertices trait for Vec, slices and other types indexable by `usize`.
impl<T> Vertices for T where T: std::ops::Index<usize, Output=Vertex> {}

// Auto implement the `Indices` trait for `&Vec<usize>`, `&[usize]` and other  types that may be 
// borrowed to produce an iterator yielding `usize`s.
impl<'a, T> Indices for &'a T
where
    &'a T: IntoIterator<Item=&'a usize>,
{
    type Iter = std::iter::Cloned<Self::IntoIter>;
    fn indices(self) -> Self::Iter {
        self.into_iter().cloned()
    }
}

Maybe we could use default type params and type aliases for Mesh to simplify its in-practise use and remove some of the generic type noise?

Also I'm defs not sure if this is the best approach for handling normals and tex coords. Perhaps a better approach might be something like this?

trait MeshType {
    type Vertices: Vertices;
    type Indices: Indices;
    fn vertices(&self) -> Self::Vertices;
    fn indices(&self) -> Self::Indices;
}

struct Mesh<V, I>
where
    V: Vertices,
    I: Indices,
{
    vertices: V,
    indices: I,
}

struct Textured<M, T=Vec<TexCoord>>
where
    M: MeshType,
    T: TexCoords,
{
    mesh: M
    tex_coords: T,
}

struct WithNormals<M, N=Vec<Normal>>
where
    M: MeshType,
    N: Normals,
{
    mesh: M,
    normals: N,
}

impl MeshType for Mesh ...
impl MeshType for Textured ...
impl MeshType for WithNormals ...

This way you could maybe have stuff like Textured<Mesh<V, I>>, WithNormals<Mesh<V, I>>, or Textured<WithNormals<Mesh<V, I>>>? You could maybe build these like:

Mesh::new(vertices, indices)
    .with_normals(normals)
    .textured(tex_coords)

I don't really know how practical this approach would actually be though in practise - at this point I'm just drafting a mess, feel free to disregard it! Just thought I'd get down some thoughts.

freesig commented 7 years ago

I think having Option doesn't really solve the problem because if Option is None it's still the size of a normal. Which means you don't save the memory. And if the mesh is large that's a lot of normals.

Usually a normal is just a vec3 and a texture_coordinate would be a vec2 or something.

I'm a little confused how the indicies trait would work. It's basically just an array of index positions that map across the verticies and tell opengl where to draw the each triangle. You usually pass this array to opengl along with the actual mesh data. I think it's better to store this data in memory over generating it on the fly because memory is less of a concern then add cpu cycles.

Kinda depends how you actually send the VBO to the gpu.

I think the fastest way is to send static mesh to the gpu. For animated you mesh, you can either modify it using transforms in the shader or update the gpu memory each frame. Either way you probably want the indicies with the mesh data so you can pass it together.

mitchmindtree commented 7 years ago

I think having Option doesn't really solve the problem because if Option is None it's still the size of a normal. Which means you don't save the memory. And if the mesh is large that's a lot of normals.

Remember this would only be the case for Option<[Normal; N]>, but for Option<Vec<Normal>> a None would be very small (as a Vec's size on the stack is very small - just a pointer, a length and a capacity).

Usually a normal is just a vec3 and a texture_coordinate would be a vec2 or something.

Aye true, just thought I'd juse Normal and TexCoord as aliases to make it a bit more obvious what I was talking about.

I'm a little confused how the indicies trait would work. It's basically just an array of index positions that map across the verticies and tell opengl where to draw the each triangle. You usually pass this array to opengl along with the actual mesh data. I think it's better to store this data in memory over generating it on the fly because memory is less of a concern then add cpu cycles.

True true, we could make the Indices trait something like this instead:

trait Indices: std::ops::Deref<Target=[usize]> {}
// Auto-implement for arrays, Vec, slices or any type that deref's to a slice of indices.
impl<T> Indices for T where T: std::ops::Deref<Target=[usize]> {}

Kinda depends how you actually send the VBO to the gpu.

Yeah totally, we'll probably continue to guage what works better performance-wise as we go on - I imagine that in some cases it will be more efficient to collect loads of tiny primitives into a single re-usable mesh so that there's only one draw call where the cpu has to sync with the gpu, whereas in other cases where we have a lot of really big meshes where some are textured and some aren't it might make sense to submit them individually rather than spend the cycles on moving them all into one giant mesh.

This makes me wonder if there's a reliable way we can setup a bunch of benchmarks for this... would be sweet if we could generate a big table which could demonstrate which methods are most efficient in which cases!

mitchmindtree commented 6 years ago

Closed via #67 - Supports creating meshes from both borrowed or owned memory, or a mixture. Channels can be added and removed in a type-safe manner and the API guarantes that the length of each channel is always the same (besides vertex index channels which may vary). A higher-level nannou::draw::Mesh type has been added which is composed of lower-level components from the nannou::mesh module. It would be nice to add an example of creating a custom mesh using the mesh module someday!