lots of hacking and gutting, but it compiles
This commit is contained in:
@@ -27,7 +27,7 @@ noise = "0.6"
|
||||
ddsfile = "0.4"
|
||||
wgpu-subscriber = "0.1.0"
|
||||
tobj = "2.0.3"
|
||||
|
||||
legion = "0.3.1"
|
||||
|
||||
|
||||
|
||||
|
||||
127
src/main.rs
127
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<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(_) => {
|
||||
|
||||
111
src/render.rs
111
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<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));
|
||||
}
|
||||
// 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
103
src/runtime.rs
103
src/runtime.rs
@@ -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
|
||||
|
||||
|
||||
1
wgpu-diagram
Normal file
1
wgpu-diagram
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user