batched resource creation, vertex buffer macro

This commit is contained in:
Carter Anderson 2020-03-21 18:12:30 -07:00
parent 8f4296c4ff
commit 7660b8bf3f
12 changed files with 386 additions and 58 deletions

View File

@ -4,7 +4,7 @@ use darling::FromMeta;
use inflector::Inflector;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Field, Fields};
use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Field, Fields, Type};
#[derive(FromMeta, Debug, Default)]
struct EntityArchetypeAttributeArgs {
@ -67,7 +67,9 @@ struct UniformAttributeArgs {
#[darling(default)]
pub shader_def: Option<bool>,
#[darling(default)]
pub instanceable: Option<bool>,
pub instance: Option<bool>,
#[darling(default)]
pub vertex: Option<bool>,
}
#[proc_macro_derive(Uniforms, attributes(uniform))]
@ -152,6 +154,53 @@ pub fn derive_uniforms(input: TokenStream) -> TokenStream {
.map(|field| field.ident.as_ref().unwrap().to_string())
.collect::<Vec<String>>();
let vertex_buffer_fields = uniform_fields
.iter()
.map(|(field, attrs)| {
(
field,
match attrs {
Some(attrs) => (
(match attrs.instance {
Some(instance) => instance,
None => false,
}),
(match attrs.vertex {
Some(vertex) => vertex,
None => false,
}),
),
None => (false, false),
},
)
})
.filter(|(_f, (instance, vertex))| *instance || *vertex);
let vertex_buffer_field_types = vertex_buffer_fields
.clone()
.map(|(f, _)| &f.ty)
.collect::<Vec<&Type>>();
let vertex_buffer_field_names_pascal = vertex_buffer_fields
.map(|(f, (instance, _vertex))| {
let pascal_field = f.ident.as_ref().unwrap().to_string().to_pascal_case();
if instance {
format!(
"I_{}_{}",
struct_name,
pascal_field
)
} else {
format!(
"{}_{}",
struct_name,
pascal_field
)
}
})
.collect::<Vec<String>>();
let mut uniform_name_strings = Vec::new();
let mut texture_and_sampler_name_strings = Vec::new();
let mut texture_and_sampler_name_idents = Vec::new();
@ -175,8 +224,8 @@ pub fn derive_uniforms(input: TokenStream) -> TokenStream {
texture_and_sampler_name_idents.push(f.ident.clone());
texture_and_sampler_name_idents.push(f.ident.clone());
let is_instanceable = match attrs {
Some(attrs) => match attrs.instanceable {
Some(instanceable) => instanceable,
Some(attrs) => match attrs.instance {
Some(instance) => instance,
None => false,
},
None => false,
@ -197,16 +246,39 @@ pub fn derive_uniforms(input: TokenStream) -> TokenStream {
static #vertex_buffer_descriptor_ident: bevy::once_cell::sync::Lazy<bevy::render::pipeline::VertexBufferDescriptor> =
bevy::once_cell::sync::Lazy::new(|| {
use bevy::render::pipeline::AsVertexFormats;
// let vertex_formats = vec![
// #(#active_uniform_field_types::as_vertex_formats(),)*
// ];
use bevy::render::pipeline::{VertexFormat, AsVertexFormats, VertexAttributeDescriptor};
let mut vertex_formats: Vec<(&str,&[VertexFormat])> = vec![
#((#vertex_buffer_field_names_pascal, <#vertex_buffer_field_types>::as_vertex_formats()),)*
];
let mut shader_location = 0;
let mut offset = 0;
let vertex_attribute_descriptors = vertex_formats.drain(..).map(|(name, formats)| {
formats.iter().enumerate().map(|(i, format)| {
let size = format.get_size();
let formatted_name = if formats.len() > 1 {
format!("{}_{}", name, i)
} else {
format!("{}", name)
};
let descriptor = VertexAttributeDescriptor {
name: formatted_name,
offset,
format: *format,
shader_location,
};
offset += size;
shader_location += 1;
descriptor
}).collect::<Vec<VertexAttributeDescriptor>>()
}).flatten().collect::<Vec<VertexAttributeDescriptor>>();
bevy::render::pipeline::VertexBufferDescriptor {
attributes: Vec::new(),
attributes: vertex_attribute_descriptors,
name: #struct_name_string.to_string(),
step_mode: bevy::render::pipeline::InputStepMode::Instance,
stride: 0,
stride: offset,
}
});

View File

@ -26,14 +26,14 @@ fn setup(world: &mut World, resources: &mut Resources) {
.add_entity(MeshEntity {
mesh: plane_handle,
material: plane_material_handle,
// renderable: Renderable::instanced(),
renderable: Renderable::instanced(),
..Default::default()
})
// cube
.add_entity(MeshEntity {
mesh: cube_handle,
material: cube_material_handle,
// renderable: Renderable::instanced(),
renderable: Renderable::instanced(),
translation: Translation::new(-1.5, 0.0, 1.0),
..Default::default()
})
@ -41,7 +41,7 @@ fn setup(world: &mut World, resources: &mut Resources) {
.add_entity(MeshEntity {
mesh: cube_handle,
material: cube_material_handle,
// renderable: Renderable::instanced(),
renderable: Renderable::instanced(),
translation: Translation::new(1.5, 0.0, 1.0),
..Default::default()
})

View File

@ -6,7 +6,7 @@ use std::{
hash::{Hash, Hasher},
};
use std::{collections::HashMap, marker::PhantomData};
use std::{collections::HashMap, marker::PhantomData, any::TypeId};
pub type HandleId = usize;
@ -65,6 +65,32 @@ impl<T> Clone for Handle<T> {
}
}
#[derive(Hash, Copy, Clone, Eq, PartialEq, Debug)]
pub struct HandleUntyped {
pub id: HandleId,
pub type_id: TypeId,
}
impl<T> From<Handle<T>> for HandleUntyped where T: 'static {
fn from(handle: Handle<T>) -> Self {
HandleUntyped {
id: handle.id,
type_id: TypeId::of::<T>(),
}
}
}
impl<T> From<HandleUntyped> for Handle<T> where T: 'static {
fn from(handle: HandleUntyped) -> Self {
if TypeId::of::<T>() != handle.type_id {
panic!("attempted to convert untyped handle to incorrect typed handle");
}
Handle::new(handle.id)
}
}
pub trait Asset<D> {
fn load(descriptor: D) -> Self;
}

View File

@ -6,6 +6,33 @@ pub trait GetBytes {
fn get_bytes_ref(&self) -> Option<&[u8]>;
}
impl GetBytes for [f32; 2] {
fn get_bytes(&self) -> Vec<u8> {
self.as_bytes().to_vec()
}
fn get_bytes_ref(&self) -> Option<&[u8]> {
Some(self.as_bytes())
}
}
impl GetBytes for [f32; 3] {
fn get_bytes(&self) -> Vec<u8> {
self.as_bytes().to_vec()
}
fn get_bytes_ref(&self) -> Option<&[u8]> {
Some(self.as_bytes())
}
}
impl GetBytes for [f32; 4] {
fn get_bytes(&self) -> Vec<u8> {
self.as_bytes().to_vec()
}
fn get_bytes_ref(&self) -> Option<&[u8]> {
Some(self.as_bytes())
}
}
impl GetBytes for Vec4 {
fn get_bytes(&self) -> Vec<u8> {
let vec4_array: [f32; 4] = (*self).into();

View File

@ -17,15 +17,16 @@ impl DrawTarget for AssignedBatchesDrawTarget {
&self,
_world: &World,
resources: &Resources,
_render_pass: &mut dyn RenderPass,
render_pass: &mut dyn RenderPass,
_pipeline_handle: Handle<PipelineDescriptor>,
) {
let asset_batches = resources.get::<AssetBatchers>().unwrap();
// let renderer = render_pass.get_renderer();
// println!("Drawing batches");
for batch in asset_batches.get_batches() {
// render_resources.get
// println!("{:?}", batch);
// render_pass.set_bind_groups(batch.render_resource_assignments.as_ref());
// render_pass.draw_indexed(0..1, 0, 0..1);
}
// println!();

View File

@ -1,4 +1,7 @@
use crate::math::{Mat4, Vec2, Vec3, Vec4};
use crate::{
math::{Mat4, Vec2, Vec3, Vec4},
render::Color,
};
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub enum VertexFormat {
@ -109,3 +112,27 @@ impl AsVertexFormats for Mat4 {
]
}
}
impl AsVertexFormats for Color {
fn as_vertex_formats() -> &'static [VertexFormat] {
&[VertexFormat::Float4]
}
}
impl AsVertexFormats for [f32; 2] {
fn as_vertex_formats() -> &'static [VertexFormat] {
&[VertexFormat::Float2]
}
}
impl AsVertexFormats for [f32; 3] {
fn as_vertex_formats() -> &'static [VertexFormat] {
&[VertexFormat::Float3]
}
}
impl AsVertexFormats for [f32; 4] {
fn as_vertex_formats() -> &'static [VertexFormat] {
&[VertexFormat::Float4]
}
}

View File

@ -1,5 +1,5 @@
use super::{RenderResourceAssignments};
use crate::asset::{Handle, HandleId};
use super::RenderResourceAssignments;
use crate::asset::{Handle, HandleId, HandleUntyped};
use legion::prelude::Entity;
use std::{any::TypeId, collections::HashMap, hash::Hash};
@ -30,6 +30,7 @@ impl EntitySetState2 {
#[derive(PartialEq, Eq, Debug, Default)]
pub struct Batch {
pub handles: Vec<HandleUntyped>,
pub entity_indices: HashMap<Entity, usize>,
pub current_index: usize,
pub render_resource_assignments: Option<RenderResourceAssignments>,
@ -73,6 +74,16 @@ impl AssetSetBatcher2 {
}
None => {
let mut batch = Batch::default();
batch.handles.push(HandleUntyped {
id: key.handle1,
type_id: self.key.handle1_type,
});
batch.handles.push(HandleUntyped {
id: key.handle2,
type_id: self.key.handle2_type,
});
batch.add_entity(entity);
self.set_batches.insert(key, batch);
}
@ -141,6 +152,10 @@ impl AssetBatcher for AssetSetBatcher2 {
fn get_batches<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Batch> + 'a> {
Box::new(self.set_batches.values())
}
fn get_batches_mut<'a>(&'a mut self) -> Box<dyn Iterator<Item = &'a mut Batch> + 'a> {
Box::new(self.set_batches.values_mut())
}
}
pub trait AssetBatcher {
@ -149,12 +164,14 @@ pub trait AssetBatcher {
// TODO: add pipeline handle here
fn get_batches2(&self) -> std::collections::hash_map::Iter<'_, BatchKey2, Batch>;
fn get_batches<'a>(&'a self) -> Box<dyn Iterator<Item = &Batch> + 'a>;
fn get_batches_mut<'a>(&'a mut self) -> Box<dyn Iterator<Item = &mut Batch> + 'a>;
}
#[derive(Default)]
pub struct AssetBatchers {
asset_batchers: Vec<Box<dyn AssetBatcher + Send + Sync>>,
asset_batcher_indices2: HashMap<AssetSetBatcherKey2, usize>,
handle_batchers: HashMap<TypeId, Vec<usize>>,
}
impl AssetBatchers {
@ -163,8 +180,10 @@ impl AssetBatchers {
T: 'static,
{
let handle_type = TypeId::of::<T>();
for asset_batcher in self.asset_batchers.iter_mut() {
asset_batcher.set_entity_handle(entity, handle_type, handle.id);
if let Some(batcher_indices) = self.handle_batchers.get(&handle_type) {
for index in batcher_indices.iter() {
self.asset_batchers[*index].set_entity_handle(entity, handle_type, handle.id);
}
}
}
@ -181,8 +200,21 @@ impl AssetBatchers {
self.asset_batchers
.push(Box::new(AssetSetBatcher2::new(key.clone())));
self.asset_batcher_indices2
.insert(key, self.asset_batchers.len() - 1);
let index = self.asset_batchers.len() - 1;
let handle1_batchers = self
.handle_batchers
.entry(key.handle1_type.clone())
.or_insert_with(|| Vec::new());
handle1_batchers.push(index);
let handle2_batchers = self
.handle_batchers
.entry(key.handle2_type.clone())
.or_insert_with(|| Vec::new());
handle2_batchers.push(index);
self.asset_batcher_indices2.insert(key, index);
}
pub fn get_batches2<T1, T2>(
@ -226,14 +258,58 @@ impl AssetBatchers {
}
}
pub fn get_batches<'a>(&'a self) -> Box<dyn Iterator<Item = &Batch> + 'a> {
Box::new(
pub fn get_batches(&self) -> impl Iterator<Item = &Batch> {
self.asset_batchers
.iter()
.map(|a| a.get_batches())
.flatten(),
)
.flatten()
}
pub fn get_batcher_indices<T>(&self) -> impl Iterator<Item = &usize>
where
T: 'static,
{
let handle_type = TypeId::of::<T>();
self.handle_batchers
.get(&handle_type)
.unwrap()
.iter()
}
pub fn get_batches_from_batcher(&self, index: usize) -> impl Iterator<Item = &Batch>
{
self.asset_batchers[index].get_batches()
}
pub fn get_batches_from_batcher_mut(&mut self, index: usize) -> impl Iterator<Item = &mut Batch>
{
self.asset_batchers[index].get_batches_mut()
}
// pub fn get_handle_batches<T>(&self) -> Option<impl Iterator<Item = &Batch>>
// where
// T: 'static,
// {
// let handle_type = TypeId::of::<T>();
// if let Some(batcher_indices) = self.handle_batchers.get(&handle_type) {
// Some(
// self.asset_batchers
// .iter()
// .enumerate()
// .filter(|(index, a)| {
// let handle_type = TypeId::of::<T>();
// self.handle_batchers
// .get(&handle_type)
// .unwrap()
// .contains(index)
// })
// .map(|(index, a)| a.get_batches())
// .flatten(),
// )
// } else {
// None
// }
// }
}
#[cfg(test)]
@ -263,7 +339,10 @@ mod tests {
assert_eq!(asset_batchers.get_batch2(a1, b1), None);
asset_batchers.set_entity_handle(entities[0], b1);
// entity[0] is added to batch when it has both Handle<A> and Handle<B>
let mut expected_batch = Batch::default();
let mut expected_batch = Batch {
handles: vec![a1.into(), b1.into()],
..Default::default()
};
expected_batch.add_entity(entities[0]);
assert_eq!(asset_batchers.get_batch2(a1, b1).unwrap(), &expected_batch);
asset_batchers.set_entity_handle(entities[0], c1);
@ -272,7 +351,10 @@ mod tests {
asset_batchers.set_entity_handle(entities[1], b1);
// all entities with Handle<A> and Handle<B> are returned
let mut expected_batch = Batch::default();
let mut expected_batch = Batch {
handles: vec![a1.into(), b1.into()],
..Default::default()
};
expected_batch.add_entity(entities[0]);
expected_batch.add_entity(entities[1]);
assert_eq!(asset_batchers.get_batch2(a1, b1).unwrap(), &expected_batch);
@ -290,10 +372,16 @@ mod tests {
.collect::<Vec<(&BatchKey2, &Batch)>>();
batches.sort_by(|a, b| a.0.cmp(b.0));
let mut expected_batch1 = Batch::default();
let mut expected_batch1 = Batch {
handles: vec![a1.into(), b1.into()],
..Default::default()
};
expected_batch1.add_entity(entities[0]);
expected_batch1.add_entity(entities[1]);
let mut expected_batch2 = Batch::default();
let mut expected_batch2 = Batch {
handles: vec![a2.into(), b2.into()],
..Default::default()
};
expected_batch2.add_entity(entities[2]);
let mut expected_batches = vec![
(

View File

@ -11,6 +11,8 @@ pub trait ResourceProvider {
}
fn update(&mut self, _renderer: &mut dyn Renderer, _world: &mut World, _resources: &Resources) {
}
fn finish_update(&mut self, _renderer: &mut dyn Renderer, _world: &mut World, _resources: &Resources) {
}
fn resize(
&mut self,
_renderer: &mut dyn Renderer,

View File

@ -3,9 +3,10 @@ use crate::{
render::{
pipeline::BindType,
render_resource::{
AssetBatchers, BufferArrayInfo, BufferInfo, BufferUsage,
AssetBatchers, BufferArrayInfo, BufferDynamicUniformInfo, BufferInfo, BufferUsage,
EntityRenderResourceAssignments, RenderResource, RenderResourceAssignments,
RenderResourceAssignmentsProvider, ResourceInfo, ResourceProvider, BufferDynamicUniformInfo,
RenderResourceAssignmentsId, RenderResourceAssignmentsProvider, ResourceInfo,
ResourceProvider,
},
renderer::Renderer,
shader::{AsUniforms, UniformInfoIter},
@ -27,8 +28,14 @@ where
{
_marker: PhantomData<T>,
// PERF: somehow remove this HashSet
uniform_buffer_info_resources:
HashMap<String, (Option<RenderResource>, usize, HashSet<Entity>)>,
uniform_buffer_info_resources: HashMap<
String,
(
Option<RenderResource>,
usize,
HashSet<RenderResourceAssignmentsId>,
),
>,
asset_resources: HashMap<Handle<T>, HashMap<String, RenderResource>>,
resource_query: Query<
(Read<T>, Read<Renderable>),
@ -94,8 +101,7 @@ where
entity_render_resource_assignments.get_mut(entity).unwrap()
};
if let Some(uniforms) = asset_storage.get(&handle) {
self.setup_entity_uniform_resources(
entity,
self.setup_uniform_resources(
uniforms,
renderer,
resources,
@ -111,9 +117,8 @@ where
self.handle_query = Some(handle_query);
}
fn setup_entity_uniform_resources(
fn setup_uniform_resources(
&mut self,
entity: Entity,
uniforms: &T,
renderer: &mut dyn Renderer,
resources: &Resources,
@ -131,11 +136,12 @@ where
.insert(uniform_info.name.to_string(), (None, 0, HashSet::new()));
}
let (_resource, counts, entities) = self
let (_resource, counts, render_resource_assignments_ids) = self
.uniform_buffer_info_resources
.get_mut(uniform_info.name)
.unwrap();
entities.insert(entity);
render_resource_assignments_ids
.insert(render_resource_assignments.get_id());
*counts += 1;
} else {
let handle = asset_handle.expect(
@ -269,9 +275,8 @@ where
world: &World,
resources: &Resources,
) {
let entity_render_resource_assignments = resources
.get::<EntityRenderResourceAssignments>()
.unwrap();
let entity_render_resource_assignments =
resources.get::<EntityRenderResourceAssignments>().unwrap();
// allocate uniform buffers
for (name, (resource, count, _entities)) in self.uniform_buffer_info_resources.iter_mut() {
let count = *count as u64;
@ -317,6 +322,7 @@ where
..
})) = resource_info
{
// TODO: properly handle alignments > BIND_BUFFER_ALIGNMENT
let size = BIND_BUFFER_ALIGNMENT * *count as u64;
let alignment = BIND_BUFFER_ALIGNMENT as usize;
let mut offset = 0usize;
@ -324,14 +330,21 @@ where
// TODO: only mem-map entities if their data has changed
// PERF: These hashmap inserts are pretty expensive (10 fps for 10000 entities)
for (entity, (_, renderable)) in self.resource_query.iter_entities(world) {
if renderable.is_instanced || !entities.contains(&entity) {
if renderable.is_instanced {
continue;
}
if let Some(render_resource) = entity_render_resource_assignments.get(entity) {
dynamic_uniform_info.offsets.insert(render_resource.get_id(), offset as u32);
// this unwrap is safe because the assignments were created in the calling function
let render_resource_assignments =
entity_render_resource_assignments.get(entity).unwrap();
if !entities.contains(&render_resource_assignments.get_id()) {
continue;
}
dynamic_uniform_info
.offsets
.insert(render_resource_assignments.get_id(), offset as u32);
offset += alignment;
}
@ -342,9 +355,14 @@ where
continue;
}
if let Some(render_resource) = entity_render_resource_assignments.get(entity) {
dynamic_uniform_info.offsets.insert(render_resource.get_id(), offset as u32);
let render_resource_assignments =
entity_render_resource_assignments.get(entity).unwrap();
if !entities.contains(&render_resource_assignments.get_id()) {
continue;
}
dynamic_uniform_info
.offsets
.insert(render_resource_assignments.get_id(), offset as u32);
offset += alignment;
}
@ -363,7 +381,13 @@ where
for (entity, (uniforms, renderable)) in
self.resource_query.iter_entities(world)
{
if renderable.is_instanced || !entities.contains(&entity) {
if renderable.is_instanced {
continue;
}
let render_resource_assignments =
entity_render_resource_assignments.get(entity).unwrap();
if !entities.contains(&render_resource_assignments.get_id()) {
continue;
}
if let Some(uniform_bytes) = uniforms.get_uniform_bytes_ref(&name) {
@ -381,7 +405,13 @@ where
for (entity, (handle, renderable)) in
self.handle_query.as_ref().unwrap().iter_entities(world)
{
if renderable.is_instanced || !entities.contains(&entity) {
if renderable.is_instanced {
continue;
}
let render_resource_assignments =
entity_render_resource_assignments.get(entity).unwrap();
if !entities.contains(&render_resource_assignments.get_id()) {
continue;
}
@ -412,6 +442,7 @@ where
let vertex_buffer_descriptor = T::get_vertex_buffer_descriptor();
if let Some(vertex_buffer_descriptor) = vertex_buffer_descriptor {
if let None = renderer.get_vertex_buffer_descriptor(&vertex_buffer_descriptor.name) {
println!("{:#?}", vertex_buffer_descriptor);
renderer.set_vertex_buffer_descriptor(vertex_buffer_descriptor.clone());
}
}
@ -473,8 +504,7 @@ where
.set(entity, render_resource_assignments_provider.next());
entity_render_resource_assignments.get_mut(entity).unwrap()
};
self.setup_entity_uniform_resources(
entity,
self.setup_uniform_resources(
&uniforms,
renderer,
resources,
@ -508,4 +538,48 @@ where
}
}
}
fn finish_update(
&mut self,
renderer: &mut dyn Renderer,
_world: &mut World,
resources: &Resources,
) {
if let Some(asset_storage) = resources.get::<AssetStorage<T>>() {
let handle_type = std::any::TypeId::of::<T>();
let mut asset_batchers = resources.get_mut::<AssetBatchers>().unwrap();
let mut render_resource_assignments_provider = resources
.get_mut::<RenderResourceAssignmentsProvider>()
.unwrap();
// TODO: work out lifetime issues here so allocation isn't necessary
for index in asset_batchers
.get_batcher_indices::<T>()
.map(|i| *i)
.collect::<Vec<usize>>()
{
for batch in asset_batchers.get_batches_from_batcher_mut(index) {
let handle: Handle<T> = batch
.handles
.iter()
.find(|h| h.type_id == handle_type)
.map(|h| (*h).into())
.unwrap();
let render_resource_assignments = batch
.render_resource_assignments
.get_or_insert_with(|| render_resource_assignments_provider.next());
if let Some(uniforms) = asset_storage.get(&handle) {
self.setup_uniform_resources(
uniforms,
renderer,
resources,
render_resource_assignments,
false,
Some(handle),
);
}
}
}
}
}
}

View File

@ -19,7 +19,7 @@ use crate::{
renderer::Renderer,
shader::{Shader},
texture::{SamplerDescriptor, TextureDescriptor},
update_shader_assignments,
update_shader_assignments, Vertex,
},
};
use std::{collections::HashMap, ops::Deref};
@ -428,6 +428,10 @@ impl Renderer for WgpuRenderer {
resource_provider.update(self, world, resources);
}
for resource_provider in render_graph.resource_providers.iter_mut() {
resource_provider.finish_update(self, world, resources);
}
update_shader_assignments(world, resources, render_graph);
for (name, texture_descriptor) in render_graph.queued_textures.drain(..) {

View File

@ -8,6 +8,7 @@ use bevy_derive::Uniforms;
#[derive(Uniforms)]
pub struct StandardMaterial {
#[uniform(instance)]
pub albedo: Color,
#[uniform(shader_def)]
pub albedo_texture: Option<Handle<Texture>>,

View File

@ -1,11 +1,17 @@
use std::convert::From;
use zerocopy::{AsBytes, FromBytes};
use crate as bevy;
use bevy_derive::Uniforms;
#[repr(C)]
#[derive(Clone, Copy, AsBytes, FromBytes)]
#[derive(Clone, Copy, AsBytes, FromBytes, Uniforms)]
pub struct Vertex {
#[uniform(vertex)]
pub position: [f32; 4],
#[uniform(vertex)]
pub normal: [f32; 4],
#[uniform(vertex)]
pub uv: [f32; 2],
}