diff --git a/Cargo.lock b/Cargo.lock index a90381f..5e4598f 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -1142,6 +1142,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1290,6 +1299,7 @@ dependencies = [ "glam", "image", "instant", + "itertools", "lyra-resource", "petgraph", "quote", diff --git a/Cargo.toml b/Cargo.toml old mode 100755 new mode 100644 index c6be1f7..73cf6fd --- a/Cargo.toml +++ b/Cargo.toml @@ -37,4 +37,5 @@ aligned-vec = "0.5.0" tracing-appender = "0.2.2" stopwatch = "0.0.7" petgraph = "0.6.4" -uuid = { version = "1.5.0", features = ["v4", "fast-rng"] } \ No newline at end of file +uuid = { version = "1.5.0", features = ["v4", "fast-rng"] } +itertools = "0.11.0" diff --git a/examples/testbed/src/free_fly_camera.rs b/examples/testbed/src/free_fly_camera.rs index fafb145..1bbbea4 100644 --- a/examples/testbed/src/free_fly_camera.rs +++ b/examples/testbed/src/free_fly_camera.rs @@ -20,7 +20,7 @@ pub struct FreeFlyCamera { impl Default for FreeFlyCamera { fn default() -> Self { Self { - speed: 3.0, + speed: 4.0, look_speed: 0.09, mouse_sensitivity: 0.4, look_with_keys: false, diff --git a/examples/testbed/src/main.rs b/examples/testbed/src/main.rs index 710d01d..fb9384d 100644 --- a/examples/testbed/src/main.rs +++ b/examples/testbed/src/main.rs @@ -1,4 +1,4 @@ -use lyra_engine::{math::{self, Vec3}, ecs::{World, components::{transform::TransformComponent, camera::CameraComponent, model::ModelComponent, DeltaTime}, EventQueue, SimpleSystem, Component, Criteria, CriteriaSchedule, BatchedSystem}, math::Transform, input::{KeyCode, InputButtons, MouseMotion, ActionHandler, Layout, Action, ActionKind, LayoutId, ActionMapping, Binding, ActionSource, ActionMappingId, InputActionPlugin, ActionState}, game::Game, plugin::Plugin, render::window::{CursorGrabMode, WindowOptions}, change_tracker::Ct}; +use lyra_engine::{math::{self, Vec3}, ecs::{World, components::{transform::TransformComponent, camera::CameraComponent, model::ModelComponent, DeltaTime}, EventQueue, SimpleSystem, Component, Criteria, CriteriaSchedule, BatchedSystem}, math::Transform, input::{KeyCode, InputButtons, MouseMotion, ActionHandler, Layout, Action, ActionKind, LayoutId, ActionMapping, Binding, ActionSource, ActionMappingId, InputActionPlugin, ActionState}, game::Game, plugin::Plugin, render::{window::{CursorGrabMode, WindowOptions}, light::PointLight}, change_tracker::Ct}; use lyra_engine::assets::{ResourceManager, Model}; mod free_fly_camera; @@ -69,7 +69,7 @@ async fn main() { let mut resman = world.get_resource_mut::().unwrap(); //let diffuse_texture = resman.request::("assets/happy-tree.png").unwrap(); let antique_camera_model = resman.request::("assets/AntiqueCamera.glb").unwrap(); - //let cube_model = resman.request::("assets/texture-sep/texture-sep.gltf").unwrap(); + let cube_model = resman.request::("assets/cube-texture-bin.glb").unwrap(); drop(resman); /* world.spawn(( @@ -82,6 +82,38 @@ async fn main() { TransformComponent::from(Transform::from_xyz(0.0, -5.0, -10.0)), )); + /* let light = PointLight { + color: Vec3::new(1.0, 1.0, 1.0), + position: Vec3::new(0.0, -5.0, -8.0), + constant: 1.0, + linear: 0.09, + quadratic: 0.032, + }; + world.spawn((light,)); */ + world.spawn(( + PointLight { + color: Vec3::new(1.0, 1.0, 1.0), + intensity: 1.0, + constant: 1.0, + linear: 0.045, + quadratic: 0.0075, + }, + TransformComponent::from(Transform::from_xyz(-2.5, 0.0, -10.0)), + ModelComponent(cube_model.clone()), + )); + + world.spawn(( + PointLight { + color: Vec3::new(0.361, 0.984, 0.0), + intensity: 1.0, + constant: 1.0, + linear: 0.045, + quadratic: 0.0075, + }, + TransformComponent::from(Transform::from_xyz(2.5, 0.0, -10.0)), + ModelComponent(cube_model), + )); + let mut camera = CameraComponent::new_3d(); camera.transform.translation += math::Vec3::new(0.0, 0.0, 7.5); //camera.transform.rotate_y(Angle::Degrees(-25.0)); diff --git a/src/render/camera.rs b/src/render/camera.rs index fa72afd..b7560fd 100755 --- a/src/render/camera.rs +++ b/src/render/camera.rs @@ -35,6 +35,23 @@ impl Projection { } } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct CameraUniform { + pub view_proj: glam::Mat4, + // vec4 is used because of the uniforms 16 byte spacing requirement + pub view_pos: glam::Vec4, +} + +impl Default for CameraUniform { + fn default() -> Self { + Self { + view_proj: glam::Mat4::IDENTITY, + view_pos: Default::default() + } + } +} + #[derive(Debug, Clone)] pub struct RenderCamera { view_proj: glam::Mat4, diff --git a/src/render/light/mod.rs b/src/render/light/mod.rs new file mode 100644 index 0000000..8f084d1 --- /dev/null +++ b/src/render/light/mod.rs @@ -0,0 +1,209 @@ +pub mod point; +use std::{collections::{VecDeque, HashMap}, num::NonZeroU64, marker::PhantomData}; + +use edict::query::EpochOf; +pub use point::*; +use tracing::debug; +use wgpu::util::DeviceExt; + +use std::mem; + +use crate::{math::Transform, ecs::components::TransformComponent}; + +const MAX_LIGHT_COUNT: usize = 32; + +pub struct LightBuffer { + _phantom: PhantomData, + /// The max amount of light casters that could fit in this buffer. + pub max_count: usize, + /// The current amount of light casters in this buffer. + pub current_count: usize, + /// The buffer index for a specific entity/caster. + used_indexes: HashMap, + /// Indexes that were being used but are no longer needed. + dead_indexes: VecDeque, +} + +impl LightBuffer { + pub fn new(layout: &wgpu::BindGroupLayout, max_count: usize) -> Self { + Self { + _phantom: PhantomData::default(), + max_count, + current_count: 0, + used_indexes: HashMap::new(), + dead_indexes: VecDeque::new(), + } + } + + pub fn has_light(&self, entity: edict::EntityId) -> bool { + self.used_indexes.contains_key(&entity) + } + + /// Update an existing light in the light buffer. + pub fn update_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: edict::EntityId, light: U) { + let buffer_idx = *self.used_indexes.get(&entity) + .expect("Entity for Light is not in buffer!"); + + lights_buffer[buffer_idx] = light; + } + + /// Add a new light to the light buffer. + pub fn add_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: edict::EntityId, light: U) { + let buffer_idx = match self.dead_indexes.pop_front() { + Some(i) => i, + None => { + let i = self.current_count; + self.current_count += 1; + i + }, + }; + + self.used_indexes.insert(entity, buffer_idx); + self.update_light(lights_buffer, entity, light); + } + + /// Update, or add a new caster, to the light buffer. + pub fn update_or_add(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: edict::EntityId, light: U) { + if self.used_indexes.contains_key(&entity) { + self.update_light(lights_buffer, entity, light); + } else { + self.add_light(lights_buffer, entity, light); + } + } + + /// Remove a caster from the buffer, returns true if it was removed. + pub fn remove_light(&mut self, lights_buffer: &mut [U; MAX_LIGHT_COUNT], entity: edict::EntityId) -> bool { + if let Some(removed_idx) = self.used_indexes.remove(&entity) { + self.dead_indexes.push_back(removed_idx); + self.current_count -= 1; + lights_buffer[removed_idx] = U::default(); + + true + } else { + false + } + } +} + +pub struct LightUniformBuffers { + pub buffer: wgpu::Buffer, + pub bindgroup_layout: wgpu::BindGroupLayout, + pub bindgroup: wgpu::BindGroup, + pub lights_uniform: LightsUniform, + pub point_lights: LightBuffer, +} + +impl LightUniformBuffers { + pub fn new(device: &wgpu::Device) -> Self { + let buffer = device.create_buffer( + &wgpu::BufferDescriptor { + label: Some("Lights Uniform buffer"), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + size: mem::size_of::() as u64, + mapped_at_creation: false, + } + ); + + let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + } + ], + label: Some("lights_bind_group_layout"), + }); + + let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &bindgroup_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: &buffer, + offset: 0, + size: Some(NonZeroU64::new(mem::size_of::() as u64).unwrap()) + } + ) + } + ], + label: Some("light_bind_group"), + }); + + let point_lights = LightBuffer::new(&bindgroup_layout, MAX_LIGHT_COUNT); + + Self { + buffer, + bindgroup_layout, + bindgroup, + lights_uniform: LightsUniform::default(), + point_lights, + } + } + + pub fn update_lights(&mut self, queue: &wgpu::Queue, world_epoch: edict::epoch::EpochId, world: &edict::World) { + for (entity, point_light, transform, light_epoch, transform_epoch) + in world.query::<(edict::Entities, &PointLight, &TransformComponent, EpochOf, EpochOf)>().iter() { + + if !self.point_lights.has_light(entity) || light_epoch == world_epoch || transform_epoch == world_epoch { + let uniform = PointLightUniform::from_bundle(point_light, &transform.transform); + self.point_lights.update_or_add(&mut self.lights_uniform.point_lights, entity, uniform); + } + } + + self.lights_uniform.point_light_count = self.point_lights.current_count as u32; + queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[self.lights_uniform])); + } +} + +#[repr(C)] +#[derive(Default, Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct LightsUniform { + point_lights: [PointLightUniform; MAX_LIGHT_COUNT], + point_light_count: u32, + _padding: [u32; 3], +} + +#[repr(C)] +#[derive(Default, Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct PointLightUniform { + /// The position of the light + /// vec4 is used here for gpu padding, w is ignored in the shader + pub position: glam::Vec4, + /// The color of the light + /// vec4 is used here for gpu padding, w is ignored in the shader + pub color: glam::Vec4, + /// The intensity of the light + /// This works by just multiplying the result of the lighting + /// calculations by this scalar + pub intensity: f32, + /// The constant used in the quadratic attenuation calculation. Its best to leave this at 1.0 + pub constant: f32, + /// The linear factor used in the quadratic attenuation calculation. + pub linear: f32, + /// The quadratic factor used in the quadratic attenuation calculation. + pub quadratic: f32, +} + +impl PointLightUniform { + /// Create the PointLightUniform from an ECS bundle + pub fn from_bundle(light: &PointLight, transform: &Transform) -> Self { + Self { + position: glam::Vec4::new(transform.translation.x, transform.translation.y, transform.translation.z, 0.0), + //_padding: 0, + color: glam::Vec4::new(light.color.x, light.color.y, light.color.z, 0.0), + //_padding2: 0, + intensity: light.intensity, + constant: light.constant, + linear: light.linear, + quadratic: light.quadratic, + } + } +} \ No newline at end of file diff --git a/src/render/light/point.rs b/src/render/light/point.rs new file mode 100644 index 0000000..a2b1f2c --- /dev/null +++ b/src/render/light/point.rs @@ -0,0 +1,9 @@ +#[repr(C)] +#[derive(Default, Debug, Copy, Clone, edict::Component)] +pub struct PointLight { + pub color: glam::Vec3, + pub intensity: f32, + pub constant: f32, + pub linear: f32, + pub quadratic: f32, +} \ No newline at end of file diff --git a/src/render/mod.rs b/src/render/mod.rs index e6fe716..0830bba 100755 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -10,4 +10,5 @@ pub mod shader_loader; pub mod material; pub mod camera; pub mod window; -pub mod transform_buffer_storage; \ No newline at end of file +pub mod transform_buffer_storage; +pub mod light; \ No newline at end of file diff --git a/src/render/renderer.rs b/src/render/renderer.rs index 4aea4f9..bc740e1 100755 --- a/src/render/renderer.rs +++ b/src/render/renderer.rs @@ -8,6 +8,7 @@ use edict::query::EpochOf; use edict::{EntityId, Entities}; use glam::Vec3; use instant::Instant; +use itertools::izip; use tracing::{debug, warn}; use wgpu::{BindGroup, BindGroupLayout, Limits}; use wgpu::util::DeviceExt; @@ -16,10 +17,12 @@ use winit::window::Window; use crate::ecs::components::camera::CameraComponent; use crate::ecs::components::model::ModelComponent; use crate::ecs::components::transform::TransformComponent; -use crate::math::Transform; +use crate::math::{Transform, self}; +use crate::render::light::PointLightUniform; -use super::camera::RenderCamera; +use super::camera::{RenderCamera, CameraUniform}; use super::desc_buf_lay::DescVertexBufferLayout; +use super::light::{PointLight, LightUniformBuffers}; use super::texture::RenderTexture; use super::transform_buffer_storage::{TransformBufferIndices, TransformBuffers}; use super::vertex::Vertex; @@ -87,6 +90,8 @@ pub struct BasicRenderer { texture_bind_group_layout: BindGroupLayout, default_texture_bind_group: BindGroup, depth_buffer_texture: RenderTexture, + + light_buffers: LightUniformBuffers, } impl BasicRenderer { @@ -239,7 +244,7 @@ impl BasicRenderer { let camera_buffer = device.create_buffer_init( &wgpu::util::BufferInitDescriptor { label: Some("Camera Buffer"), - contents: bytemuck::cast_slice(&[glam::Mat4::IDENTITY]), + contents: bytemuck::cast_slice(&[CameraUniform::default()]), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, } ); @@ -248,7 +253,7 @@ impl BasicRenderer { entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, @@ -293,6 +298,8 @@ impl BasicRenderer { } ); + let light_uniform_buffers = LightUniformBuffers::new(&device); + let mut s = Self { window, surface, @@ -323,13 +330,16 @@ impl BasicRenderer { default_texture_bind_group: default_tex_bindgroup, depth_buffer_texture: depth_texture, entity_last_transforms: HashMap::new(), + + light_buffers: light_uniform_buffers, }; // create the default pipelines let mut pipelines = HashMap::new(); pipelines.insert(0, Arc::new(FullRenderPipeline::new(&s.device, &s.config, &shader, vec![super::vertex::Vertex::desc(),], - vec![&s.texture_bind_group_layout, &s.transform_bind_group_layout, &camera_bind_group_layout]))); + vec![&s.texture_bind_group_layout, &s.transform_bind_group_layout, &camera_bind_group_layout, + &s.light_buffers.bindgroup_layout]))); s.render_pipelines = pipelines; s @@ -376,12 +386,14 @@ impl BasicRenderer { let positions = mesh.position().unwrap(); let tex_coords: Vec = mesh.tex_coords().cloned() .unwrap_or_else(|| vec![glam::Vec2::new(0.0, 0.0); positions.len()]); + let normals = mesh.normals().unwrap(); - assert!(positions.len() == tex_coords.len()); + assert!(positions.len() == tex_coords.len() && positions.len() == normals.len()); - let vertex_inputs: Vec = std::iter::zip(positions, tex_coords) - .map(|(v, t)| Vertex::new(*v, t)) - .collect(); + let mut vertex_inputs = vec![]; + for (v, t, n) in izip!(positions.iter(), tex_coords.iter(), normals.iter()) { + vertex_inputs.push(Vertex::new(*v, *t, *n)); + } let vertex_buffer = self.device.create_buffer_init( &wgpu::util::BufferInitDescriptor { @@ -518,6 +530,7 @@ impl BasicRenderer { impl Renderer for BasicRenderer { fn prepare(&mut self, main_world: &mut edict::World) { let last_epoch = main_world.epoch(); + debug!("Last epoch: {last_epoch:?}"); let mut alive_entities = HashSet::new(); @@ -588,10 +601,17 @@ impl Renderer for BasicRenderer { if let Some(camera) = main_world.query_mut::<(&mut CameraComponent,)>().into_iter().next() { let view_proj = self.inuse_camera.update_view_projection(camera); - self.queue.write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[*view_proj])); + let pos = camera.transform.translation; + let uniform = CameraUniform { + view_proj: *view_proj, + view_pos: glam::Vec4::new(pos.x, pos.y, pos.z, 0.0), + }; + self.queue.write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[uniform])); } else { warn!("Missing camera!"); } + + self.light_buffers.update_lights(&self.queue, last_epoch, &main_world); } fn render(&mut self) -> Result<(), wgpu::SurfaceError> { @@ -650,6 +670,12 @@ impl Renderer for BasicRenderer { // Bind camera render_pass.set_bind_group(2, &self.camera_bind_group, &[]); + + // bind light + //render_pass.set_bind_group(3, &self.point_light_bind_group, &[]); + render_pass.set_bind_group(3, &self.light_buffers.bindgroup, &[]); + ////self.light_buffers.bind_lights(&mut render_pass, 3); + // if this mesh uses indices, use them to draw the mesh if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() { diff --git a/src/render/shaders/base.wgsl b/src/render/shaders/base.wgsl index c8e6ce1..e97c715 100755 --- a/src/render/shaders/base.wgsl +++ b/src/render/shaders/base.wgsl @@ -1,32 +1,63 @@ // Vertex shader +const max_light_count: u32 = 32u; + struct VertexInput { @location(0) position: vec3, @location(1) tex_coords: vec2, + @location(2) normal: vec3, } struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) tex_coords: vec2, + @location(1) world_position: vec3, + @location(2) world_normal: vec3, } struct CameraUniform { view_proj: mat4x4, + view_pos: vec4, }; +struct PointLight { + position: vec4, + color: vec4, + intensity: f32, + constant: f32, + linear: f32, + quadratic: f32, +}; + +struct Lights { + point_lights: array, + point_light_count: u32, +} + @group(1) @binding(0) var u_model_transform: mat4x4; @group(2) @binding(0) -var camera: CameraUniform; +var u_camera: CameraUniform; + +@group(3) @binding(0) +var u_lights: Lights; @vertex fn vs_main( model: VertexInput, ) -> VertexOutput { var out: VertexOutput; + out.tex_coords = model.tex_coords; - out.clip_position = camera.view_proj * u_model_transform * vec4(model.position, 1.0); + out.clip_position = u_camera.view_proj * u_model_transform * vec4(model.position, 1.0); + + out.world_normal = (u_model_transform * vec4(model.normal, 0.0)).xyz; + + //out.world_normal = model.normal; + var world_position: vec4 = u_model_transform * vec4(model.position, 1.0); + out.world_position = world_position.xyz; + return out; } @@ -39,5 +70,50 @@ var s_diffuse: sampler; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return textureSample(t_diffuse, s_diffuse, in.tex_coords); + let object_color: vec4 = textureSample(t_diffuse, s_diffuse, in.tex_coords); + + var light_res = vec3(0.0); + for (var i = 0u; i < u_lights.point_light_count; i++) { + light_res += blinn_phong_point_light(in.world_position, in.world_normal, u_lights.point_lights[i]); + } + let light_object_res = light_res * object_color.xyz; + + return vec4(light_object_res, object_color.a); } + +fn blinn_phong_point_light(world_pos: vec3, world_norm: vec3, point_light: PointLight) -> vec3 { + let light_color = point_light.color.xyz; + let light_pos = point_light.position.xyz; + let camera_view_pos = u_camera.view_pos.xyz; + + // We don't need (or want) much ambient light, so 0.1 is fine + let ambient_strength = 0.1; + var ambient_color = light_color * ambient_strength; + + //// diffuse //// + let light_dir = normalize(light_pos - world_pos); + + let diffuse_strength = max(dot(world_norm, light_dir), 0.0); + var diffuse_color = light_color * diffuse_strength; + //// end of diffuse //// + + //// specular //// + let view_dir = normalize(camera_view_pos - world_pos); + let half_dir = normalize(view_dir + light_dir); + + let specular_strength = pow(max(dot(world_norm, half_dir), 0.0), 32.0); + var specular_color = specular_strength * light_color; + //// end of specular //// + + //// point light attenuation //// + let distance = length(light_pos - world_pos); + let attenuation = 1.0 / (point_light.constant + point_light.linear * distance + + point_light.quadratic * (distance * distance)); + + ambient_color *= attenuation * point_light.intensity; + diffuse_color *= attenuation * point_light.intensity; + specular_color *= attenuation * point_light.intensity; + //// end of point light attenuation //// + + return (ambient_color + diffuse_color + specular_color); +} \ No newline at end of file diff --git a/src/render/vertex.rs b/src/render/vertex.rs index 2b53e9d..346fa6d 100755 --- a/src/render/vertex.rs +++ b/src/render/vertex.rs @@ -4,14 +4,15 @@ use super::desc_buf_lay::DescVertexBufferLayout; #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct Vertex { pub position: glam::Vec3, - pub tex_coords: glam::Vec2 + pub tex_coords: glam::Vec2, + pub normals: glam::Vec3, //pub color: [f32; 3], // TODO: add color again } impl Vertex { - pub fn new(position: glam::Vec3, tex_coords: glam::Vec2) -> Self { + pub fn new(position: glam::Vec3, tex_coords: glam::Vec2, normals: glam::Vec3) -> Self { Self { - position, tex_coords + position, tex_coords, normals } } } @@ -31,6 +32,11 @@ impl DescVertexBufferLayout for Vertex { offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, shader_location: 1, format: wgpu::VertexFormat::Float32x2, // Vec2 + }, + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 2, + format: wgpu::VertexFormat::Float32x3, // Vec3 } ] }