lots of hacking and gutting, but it compiles

master
mitchellhansen 4 years ago
parent 3585c053ae
commit f933fe8312

@ -27,7 +27,7 @@ noise = "0.6"
ddsfile = "0.4"
wgpu-subscriber = "0.1.0"
tobj = "2.0.3"
legion = "0.3.1"

@ -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<f32> = 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<Idx> {
pub start: Idx,
pub end: Idx,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct DirectionalLight {
color: wgpu::Color,
fov: f32,
depth: RangeCopy<f32>
}
#[derive(Clone, Debug)]
struct Mesh {
index_buffer: Rc<Buffer>,
vertex_buffer: Rc<Buffer>,
}
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::<f32>().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::<Position>::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(_) => {

@ -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<Device>,
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<Buffer>, Rc<Buffer>) {
pub fn load_mesh_to_buffer(device: Rc<wgpu::Device>, filepath: &str) -> (Rc<Buffer>, Rc<Buffer>) {
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<wgpu::Device>, sc_desc: &wgpu::SwapChainDescriptor) -> Renderer {
let entity_uniform_size = mem::size_of::<EntityUniforms>() 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::<LightRaw>()) 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::<LightRaw>()) 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
}

@ -20,13 +20,15 @@ struct Entity {
mx_world: cgmath::Matrix4<f32>,
rotation_speed: f32,
color: wgpu::Color,
vertex_buf: Rc<wgpu::Buffer>,
// Could probably tie this along with index & count to some resource handle in the renderer
index_buf: Rc<wgpu::Buffer>,
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<wgpu::Buffer>,
index_buf: Rc<wgpu::Buffer>,
}
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

@ -0,0 +1 @@
<mxfile host="Electron" modified="2021-02-01T07:58:02.975Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.1.8 Chrome/87.0.4280.88 Electron/11.1.1 Safari/537.36" etag="2NJNGVERa2KLkA7RE-2K" version="14.1.8" type="device"><diagram id="LqEz0sV-94yUqItN7aiY" name="Page-1">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</diagram></mxfile>
Loading…
Cancel
Save