From f933fe83122e872983f046a8885dd62404ecc049 Mon Sep 17 00:00:00 2001 From: mitchellhansen Date: Sun, 31 Jan 2021 23:59:23 -0800 Subject: [PATCH] lots of hacking and gutting, but it compiles --- Cargo.toml | 2 +- src/main.rs | 127 +++++++++++++++++++++++++++++++++++++++++++------ src/render.rs | 113 ++++++++++++++++++++++++------------------- src/runtime.rs | 103 ++++++++++++++++++++------------------- wgpu-diagram | 1 + 5 files changed, 232 insertions(+), 114 deletions(-) create mode 100644 wgpu-diagram diff --git a/Cargo.toml b/Cargo.toml index fb7b02c..246aadf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ noise = "0.6" ddsfile = "0.4" wgpu-subscriber = "0.1.0" tobj = "2.0.3" - +legion = "0.3.1" diff --git a/src/main.rs b/src/main.rs index fe80da8..1a2ff04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,13 +4,21 @@ extern crate winit; #[cfg(not(target_arch = "wasm32"))] use std::time::{Duration, Instant}; + use futures::task::LocalSpawn; use wgpu_subscriber; use winit::{ event::{self, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; + use crate::render::Renderer; +use bytemuck::__core::ops::Range; +use cgmath::Point3; +use std::rc::Rc; +use wgpu::Buffer; +use winit::platform::unix::x11::ffi::Time; +use legion::*; mod framework; mod geometry; @@ -48,8 +56,6 @@ ECS */ - - #[cfg_attr(rustfmt, rustfmt_skip)] #[allow(unused)] pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( @@ -84,7 +90,40 @@ pub enum ShaderStage { queue: wgpu::Queue, */ -async fn main() { +// a component is any type that is 'static, sized, send and sync +#[derive(Clone, Copy, Debug, PartialEq)] +struct Position { + x: f32, + y: f32, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct Velocity { + dx: f32, + dy: f32, +} + +#[derive(Clone, Default, PartialEq, Eq, Hash, Copy, Debug)] +pub struct RangeCopy { + pub start: Idx, + pub end: Idx, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct DirectionalLight { + color: wgpu::Color, + fov: f32, + depth: RangeCopy +} + +#[derive(Clone, Debug)] +struct Mesh { + index_buffer: Rc, + vertex_buffer: Rc, +} + +fn main() { + // #[cfg(not(target_arch = "wasm32"))] // { @@ -96,6 +135,66 @@ async fn main() { // #[cfg(target_arch = "wasm32")] // console_log::init().expect("could not initialize logger"); + use legion::*; + let mut world = World::default(); + + // This could be used for relationships between entities...??? + let entity: Entity = world.push(( + cgmath::Point3 { + x: -5.0, + y: 7.0, + z: 10.0, + }, + DirectionalLight { + color: wgpu::Color { + r: 1.0, + g: 0.5, + b: 0.5, + a: 1.0, + }, + fov: 45.0, + depth: RangeCopy { start: 1.0, end: 20.0 }, + } + )); + + let entities: &[Entity] = world.extend(vec![ + (Position { x: 0.0, y: 0.0 }, Velocity { dx: 0.0, dy: 0.0 }), + (Position { x: 1.0, y: 1.0 }, Velocity { dx: 0.0, dy: 0.0 }), + (Position { x: 2.0, y: 2.0 }, Velocity { dx: 0.0, dy: 0.0 }), + ]); + + /* + Querying entities by their handle + + // entries return `None` if the entity does not exist + if let Some(mut entry) = world.entry(entity) { + // access information about the entity's archetype + //println!("{:?} has {:?}", entity, entry.archetype().layout().component_types()); + + // add an extra component + //entry.add_component(12f32); + + // access the entity's components, returns `None` if the entity does not have the component + //assert_eq!(entry.get_component::().unwrap(), &12f32); + + } + */ + + // construct a schedule (you should do this on init) + let mut schedule = Schedule::builder() + // .add_system(Renderer::render_test) + .build(); + + // run our schedule (you should do this each update) + //schedule.execute(&mut world, &mut resources); + + // Querying entities by component is just defining the component type! + let mut query = Read::::query(); + + // you can then iterate through the components found in the world + for position in query.iter(&world) { + println!("{:?}", position); + } let event_loop = EventLoop::new(); let mut builder = winit::window::WindowBuilder::new(); @@ -119,15 +218,14 @@ async fn main() { let surface = instance.create_surface(&window); (size, surface) }; - let adapter = async { + let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: Some(&surface), - }) - .await - .unwrap(); - }; + }); + + let adapter = futures::executor::block_on(adapter).unwrap(); let optional_features = Renderer::optional_features(); let required_features = Renderer::required_features(); @@ -144,7 +242,7 @@ async fn main() { let trace_dir = std::env::var("WGPU_TRACE"); // And then get the device we want - let (device, queue) = adapter + let device = adapter .request_device( &wgpu::DeviceDescriptor { features: (optional_features & adapter_features) | required_features, @@ -152,10 +250,11 @@ async fn main() { shader_validation: true, }, trace_dir.ok().as_ref().map(std::path::Path::new), - ) - .unwrap(); + ); + let (device, queue) = futures::executor::block_on(device).unwrap(); + let device = Rc::new(device); #[cfg(not(target_arch = "wasm32"))] let (mut pool, spawner) = { let local_pool = futures::executor::LocalPool::new(); @@ -219,9 +318,9 @@ async fn main() { log::info!("Entering render loop..."); // Load up the renderer (and the resources) - let mut renderer = render::Renderer::init(&device, &sc_desc); + let mut renderer = render::Renderer::init(device.clone(), &sc_desc); - let (plane_vertex_buffer, plane_index_buffer) = Renderer::load_mesh_to_buffer(device, "plane.obj"); + let (plane_vertex_buffer, plane_index_buffer) = Renderer::load_mesh_to_buffer(device.clone(), "plane.obj"); // Init, this wants the references to the buffers... let mut runtime = runtime::Runtime::init(&sc_desc, &device, &queue); @@ -291,7 +390,7 @@ async fn main() { *control_flow = ControlFlow::Exit; } _ => { - renderer.update(event); + //renderer.update(event); } }, event::Event::RedrawRequested(_) => { diff --git a/src/render.rs b/src/render.rs index 2ff4523..5a6dabb 100644 --- a/src/render.rs +++ b/src/render.rs @@ -2,11 +2,13 @@ use bytemuck::{Pod, Zeroable}; use bytemuck::__core::mem; use wgpu::util::DeviceExt; use std::{iter, num::NonZeroU32, ops::Range, rc::Rc}; -use crate::OPENGL_TO_WGPU_MATRIX; +use crate::{OPENGL_TO_WGPU_MATRIX, Velocity}; use crate::light::LightRaw; use crate::geometry::{Vertex, import_mesh, create_plane}; -use wgpu::Buffer; - +use wgpu::{Buffer, Device}; +use winit::dpi::Position; +use winit::platform::unix::x11::ffi::Time; +use legion::*; #[repr(C)] #[derive(Clone, Copy)] @@ -42,16 +44,16 @@ pub struct Pass { } pub struct Renderer { - device: Device, + device: Rc, lights_are_dirty: bool, shadow_pass: Pass, forward_pass: Pass, forward_depth: wgpu::TextureView, light_uniform_buf: wgpu::Buffer, - plane_uniform_buf: wgpu::Buffer, - plane_vertex_buf: wgpu::Buffer, - plane_index_buf: wgpu::Buffer, + // plane_uniform_buf: wgpu::Buffer, + // plane_vertex_buf: wgpu::Buffer, + // plane_index_buf: wgpu::Buffer, } impl Renderer { @@ -122,13 +124,13 @@ impl Renderer { } - pub fn load_mesh_to_buffer(device: &wgpu::Device, filepath: &str) -> (Rc, Rc) { + pub fn load_mesh_to_buffer(device: Rc, filepath: &str) -> (Rc, Rc) { let (vertices, indices) = import_mesh(filepath); - Renderer::create_buffer(device, indices, vertices) + Renderer::create_buffer(&device, indices, vertices) } - pub fn init(device: &wgpu::Device, sc_desc: &wgpu::SwapChainDescriptor) -> Renderer { + pub fn init(device: Rc, sc_desc: &wgpu::SwapChainDescriptor) -> Renderer { let entity_uniform_size = mem::size_of::() as wgpu::BufferAddress; @@ -469,17 +471,23 @@ impl Renderer { }); Renderer { - device, + device: device, lights_are_dirty: false, shadow_pass, forward_pass, forward_depth: depth_texture.create_view(&wgpu::TextureViewDescriptor::default()), light_uniform_buf, - plane_uniform_buf, - plane_vertex_buf: (), - plane_index_buf: () + // plane_uniform_buf, + // plane_vertex_buf: (), + // plane_index_buf: () } } + // + // #[system(for_each)] + // pub fn render_test(pos: &mut Position, vel: &Velocity) { + // //pos.x += vel.dx * time.elapsed_seconds; + // //pos.y += vel.dy * time.elapsed_seconds; + // } pub fn render( &mut self, @@ -490,39 +498,42 @@ impl Renderer { ) { // update uniforms - for entity in self.entities.iter_mut() { - if entity.rotation_speed != 0.0 { - let rotation = cgmath::Matrix4::from_angle_x(cgmath::Deg(entity.rotation_speed)); - entity.mx_world = entity.mx_world * rotation; - } - let data = EntityUniforms { - model: entity.mx_world.into(), - color: [ - entity.color.r as f32, - entity.color.g as f32, - entity.color.b as f32, - entity.color.a as f32, - ], - }; - queue.write_buffer(&entity.uniform_buf, 0, bytemuck::bytes_of(&data)); - } - - if self.lights_are_dirty { - self.lights_are_dirty = false; - for (i, light) in self.lights.iter().enumerate() { - queue.write_buffer( - &self.light_uniform_buf, - (i * mem::size_of::()) as wgpu::BufferAddress, - bytemuck::bytes_of(&light.to_raw()), - ); - } - } + // for entity in self.entities.iter_mut() { + // + // // Revolve the entity by the rotation speed, only if it is non-zero + // if entity.rotation_speed != 0.0 { + // let rotation = cgmath::Matrix4::from_angle_x(cgmath::Deg(entity.rotation_speed)); + // entity.mx_world = entity.mx_world * rotation; + // } + // + // let data = EntityUniforms { + // model: entity.mx_world.into(), + // color: [ + // entity.color.r as f32, + // entity.color.g as f32, + // entity.color.b as f32, + // entity.color.a as f32, + // ], + // }; + // queue.write_buffer(&entity.uniform_buf, 0, bytemuck::bytes_of(&data)); + // } + + // if self.lights_are_dirty { + // self.lights_are_dirty = false; + // for (i, light) in self.lights.iter().enumerate() { + // queue.write_buffer( + // &self.light_uniform_buf, + // (i * mem::size_of::()) as wgpu::BufferAddress, + // bytemuck::bytes_of(&light.to_raw()), + // ); + // } + // } let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); encoder.push_debug_group("shadow passes"); - for (i, light) in self.lights.iter().enumerate() { + /*for (i, light) in self.lights.iter().enumerate() { encoder.push_debug_group(&format!( "shadow pass {} (light at position {:?})", i, light.pos @@ -565,7 +576,7 @@ impl Renderer { } encoder.pop_debug_group(); - } + }*/ encoder.pop_debug_group(); // forward pass @@ -597,18 +608,22 @@ impl Renderer { pass.set_pipeline(&self.forward_pass.pipeline); pass.set_bind_group(0, &self.forward_pass.bind_group, &[]); - for entity in &self.entities { - pass.set_bind_group(1, &entity.bind_group, &[]); - pass.set_index_buffer(entity.index_buf.slice(..)); - pass.set_vertex_buffer(0, entity.vertex_buf.slice(..)); - pass.draw_indexed(0..entity.index_count as u32, 0, 0..1); - } + // for entity in &self.entities { + // pass.set_bind_group(1, &entity.bind_group, &[]); + // pass.set_index_buffer(entity.index_buf.slice(..)); + // pass.set_vertex_buffer(0, entity.vertex_buf.slice(..)); + // pass.draw_indexed(0..entity.index_count as u32, 0, 0..1); + // } } encoder.pop_debug_group(); queue.submit(iter::once(encoder.finish())); } + pub(crate) fn required_features() -> wgpu::Features { + wgpu::Features::empty() + } + pub fn optional_features() -> wgpu::Features { wgpu::Features::DEPTH_CLAMPING } diff --git a/src/runtime.rs b/src/runtime.rs index 22b880a..460784d 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -20,13 +20,15 @@ struct Entity { mx_world: cgmath::Matrix4, rotation_speed: f32, color: wgpu::Color, - vertex_buf: Rc, - // Could probably tie this along with index & count to some resource handle in the renderer - index_buf: Rc, + index_count: usize, bind_group: wgpu::BindGroup, // This is a little weird to have in the entity isn't it? + + // uniform buf is tough... uniform_buf: wgpu::Buffer, + vertex_buf: Rc, + index_buf: Rc, } pub struct Runtime { @@ -68,6 +70,7 @@ impl Runtime { label: None, }); */ + // Defines the Uniform buffer for the Vertex and Fragment shaders let local_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -88,23 +91,23 @@ impl Runtime { let mut entities = Vec::default(); - entities.push(Entity { - mx_world: cgmath::Matrix4::identity(), - rotation_speed: 0.0, - color: wgpu::Color::WHITE, - vertex_buf: Rc::new(plane_vertex_buf), - index_buf: Rc::new(plane_index_buf), - index_count: plane_index_data.len(), - bind_group: device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &local_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(plane_uniform_buf.slice(..)), - }], - label: None, - }), - uniform_buf: plane_uniform_buf, - }); + // entities.push(Entity { + // mx_world: cgmath::Matrix4::identity(), + // rotation_speed: 0.0, + // color: wgpu::Color::WHITE, + // vertex_buf: Rc::new(plane_vertex_buf), + // index_buf: Rc::new(plane_index_buf), + // index_count: plane_index_data.len(), + // bind_group: device.create_bind_group(&wgpu::BindGroupDescriptor { + // layout: &local_bind_group_layout, + // entries: &[wgpu::BindGroupEntry { + // binding: 0, + // resource: wgpu::BindingResource::Buffer(plane_uniform_buf.slice(..)), + // }], + // label: None, + // }), + // uniform_buf: plane_uniform_buf, + // }); struct CubeDesc { @@ -124,36 +127,36 @@ impl Runtime { ]; - for cube in &cube_descs { - let transform = Decomposed { - disp: cube.offset.clone(), - rot: Quaternion::from_axis_angle(cube.offset.normalize(), Deg(cube.angle)), - scale: cube.scale, - }; - let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor { - label: None, - size: entity_uniform_size, - usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, - mapped_at_creation: false, - }); - entities.push(Entity { - mx_world: cgmath::Matrix4::from(transform), - rotation_speed: cube.rotation, - color: wgpu::Color::GREEN, - vertex_buf: Rc::clone(&cube_vertex_buf), - index_buf: Rc::clone(&cube_index_buf), - index_count: cube_index_data.len(), - bind_group: device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &local_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(uniform_buf.slice(..)), - }], - label: None, - }), - uniform_buf, - }); - } + // for cube in &cube_descs { + // let transform = Decomposed { + // disp: cube.offset.clone(), + // rot: Quaternion::from_axis_angle(cube.offset.normalize(), Deg(cube.angle)), + // scale: cube.scale, + // }; + // let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor { + // label: None, + // size: entity_uniform_size, + // usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, + // mapped_at_creation: false, + // }); + // entities.push(Entity { + // mx_world: cgmath::Matrix4::from(transform), + // rotation_speed: cube.rotation, + // color: wgpu::Color::GREEN, + // vertex_buf: Rc::clone(&cube_vertex_buf), + // index_buf: Rc::clone(&cube_index_buf), + // index_count: cube_index_data.len(), + // bind_group: device.create_bind_group(&wgpu::BindGroupDescriptor { + // layout: &local_bind_group_layout, + // entries: &[wgpu::BindGroupEntry { + // binding: 0, + // resource: wgpu::BindingResource::Buffer(uniform_buf.slice(..)), + // }], + // label: None, + // }), + // uniform_buf, + // }); + //} // Create other resources diff --git a/wgpu-diagram b/wgpu-diagram new file mode 100644 index 0000000..60b11f0 --- /dev/null +++ b/wgpu-diagram @@ -0,0 +1 @@ +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 \ No newline at end of file