tirbofish/dropbear · commit
e2c482ba16d4ba5353b323fe8782da25a8f94415
refactor: organised code. Unverified
@@ -7,15 +7,7 @@ package.readme = "README.md" resolver = "3" members = [ - "crates/dropbear-engine", - "crates/dropbear-macro", - "crates/dropbear-shader", - "crates/dropbear-traits", - "crates/dropbear_future-queue", - "crates/eucalyptus-core", - "crates/eucalyptus-editor", - "crates/magna-carta", -# "crates/redback-runtime" + "crates/*" ] [workspace.dependencies] @@ -10,7 +10,6 @@ readme.workspace = true [dependencies] dropbear_future-queue = { path = "../dropbear_future-queue" } -dropbear-shader = { path = "../dropbear-shader" } dropbear-traits = { path = "../dropbear-traits" } dropbear-macro = { path = "../dropbear-macro" } @@ -6,9 +6,10 @@ use std::sync::{ use dashmap::DashMap; use crate::{ - graphics::{SharedGraphicsContext, Texture}, + graphics::{SharedGraphicsContext}, model::{Material, Mesh, Model, ModelId}, utils::ResourceReference, + texture::Texture, }; /// Opaque identifier returned from the [`AssetRegistry`] for stored assets. @@ -139,10 +140,9 @@ impl AssetRegistry { return Arc::clone(existing.value()); } - let texture = Texture::from_rgba_buffer(graphics, &rgba, (1, 1)); + let texture = Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba, Some((1, 1)), None, None, None); let texture = Arc::new(texture); - // Race-safe insert: if another thread beat us, reuse theirs. match self.solid_textures.entry(key) { dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()), dashmap::mapref::entry::Entry::Vacant(entry) => { @@ -2,6 +2,8 @@ use std::marker::PhantomData; +use wgpu::BufferUsages; + #[repr(C)] #[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)] pub struct Vertex { @@ -30,7 +32,7 @@ impl Vertex { } } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct ResizableBuffer<T> { buffer: wgpu::Buffer, capacity: usize, @@ -39,6 +41,84 @@ pub struct ResizableBuffer<T> { _marker: PhantomData<T>, } +#[derive(Debug, Clone)] +pub struct UniformBuffer<T> { + buffer: wgpu::Buffer, + label: String, + _marker: PhantomData<T>, +} + +impl<T: bytemuck::Pod> UniformBuffer<T> { + pub fn new(device: &wgpu::Device, label: &str) -> Self { + let size = (std::mem::size_of::<T>() as wgpu::BufferAddress).max(16); + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + Self { + buffer, + label: label.to_string(), + _marker: PhantomData, + } + } + + pub fn write(&self, queue: &wgpu::Queue, value: &T) { + queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value)); + } + + pub fn buffer(&self) -> &wgpu::Buffer { + &self.buffer + } + + pub fn label(&self) -> &str { + &self.label + } +} + +#[derive(Debug, Clone)] +pub struct StorageBuffer<T> { + buffer: wgpu::Buffer, + label: String, + _marker: PhantomData<T>, +} + +impl<T: bytemuck::Pod> StorageBuffer<T> { + /// Creates a storage buffer intended to be written by the CPU and read by the GPU. + /// + /// Note: whether it is bound as read-only is controlled by the bind group layout + /// (`BufferBindingType::Storage { read_only: true }`). + pub fn new(device: &wgpu::Device, label: &str) -> Self { + let size = (std::mem::size_of::<T>() as wgpu::BufferAddress).max(16); + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + Self { + buffer, + label: label.to_string(), + _marker: PhantomData, + } + } + + pub fn write(&self, queue: &wgpu::Queue, value: &T) { + queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value)); + } + + pub fn buffer(&self) -> &wgpu::Buffer { + &self.buffer + } + + pub fn label(&self) -> &str { + &self.label + } +} + impl<T: bytemuck::Pod> ResizableBuffer<T> { pub fn new( device: &wgpu::Device, @@ -63,6 +143,13 @@ impl<T: bytemuck::Pod> ResizableBuffer<T> { } } + pub fn uniform( + device: &wgpu::Device, + label: &str, + ) -> Self { + Self::new(device, 1, BufferUsages::UNIFORM | BufferUsages::COPY_DST, label) + } + pub fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[T]) { if data.is_empty() { return; @@ -5,11 +5,11 @@ use std::sync::Arc; use glam::{DMat4, DQuat, DVec3, Mat4}; use serde::{Deserialize, Serialize}; use wgpu::{ - BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, - BindGroupLayoutEntry, BindingType, Buffer, BufferBindingType, ShaderStages, + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, + BindingType, BufferBindingType, ShaderStages }; -use crate::graphics::SharedGraphicsContext; +use crate::{buffer::UniformBuffer, graphics::SharedGraphicsContext}; /// Matrix that converts OpenGL (from [`glam`]) to [`wgpu`] values #[rustfmt::skip] @@ -45,7 +45,7 @@ impl Default for CameraSettings { } /// The basic values of a Camera. -#[derive(Default, Debug, Clone)] +#[derive(Debug, Clone)] pub struct Camera { /// The name of the camera pub label: String, @@ -72,10 +72,9 @@ pub struct Camera { /// Uniform/interface for Rust and the GPU pub uniform: CameraUniform, - buffer: Option<Buffer>, - layout: Option<BindGroupLayout>, - bind_group: Option<BindGroup>, + buffer: UniformBuffer<CameraUniform>, + pub bind_group: BindGroup, /// View matrix pub view_mat: DMat4, @@ -95,19 +94,58 @@ pub struct CameraBuilder { } impl Camera { + pub const CAMERA_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = + BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX.union(ShaderStages::FRAGMENT), + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("camera_bind_group_layout"), + }; + /// Creates a new camera pub fn new( graphics: Arc<SharedGraphicsContext>, builder: CameraBuilder, label: Option<&str>, ) -> Self { - let uniform = CameraUniform::new(); + let mut uniform = CameraUniform::new(); let dir = (builder.target - builder.eye).normalize(); let pitch = dir.y.clamp(-1.0, 1.0).asin(); let yaw = dir.z.atan2(dir.x); - let mut camera = Self { + let view = DMat4::look_at_lh(builder.eye, builder.target, builder.up); + let proj = DMat4::perspective_infinite_reverse_lh( + builder.settings.fov_y.to_radians(), + builder.aspect, + builder.znear, + ); + + let view_mat = view; + let proj_mat = proj; + + let mvp = DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX) * proj * view; + uniform.view_proj = mvp.as_mat4().to_cols_array_2d(); + + let buffer = UniformBuffer::new(&graphics.device, "camera uniform"); + + let bind_group = graphics.device.create_bind_group(&BindGroupDescriptor { + layout: &graphics.layouts.camera_bind_group_layout, + entries: &[BindGroupEntry { + binding: 0, + resource: buffer.buffer().as_entire_binding(), + }], + label: Some("camera_bind_group"), + }); + + let camera = Self { eye: builder.eye, target: builder.target, up: builder.up, @@ -115,9 +153,8 @@ impl Camera { znear: builder.znear, zfar: builder.zfar, uniform, - buffer: None, - layout: None, - bind_group: None, + buffer, + bind_group, yaw, pitch, settings: builder.settings, @@ -126,12 +163,10 @@ impl Camera { } else { String::from("Camera") }, - ..Default::default() + view_mat, + proj_mat, }; - camera.update_view_proj(); - let buffer = graphics.create_uniform(camera.uniform, Some("Camera Uniform")); - camera.create_bind_group_layout(graphics.clone(), buffer.clone()); - camera.buffer = Some(buffer); + log::debug!("Created new camera{}", if let Some(l) = label { format!(" with the label {}", l) } else { String::new() } ); camera } @@ -144,7 +179,7 @@ impl Camera { eye: DVec3::new(0.0, 1.0, 2.0), target: DVec3::new(0.0, 0.0, 0.0), up: DVec3::Y, - aspect: (graphics.screen_size.0 / graphics.screen_size.1).into(), + aspect: (graphics.window.inner_size().width / graphics.window.inner_size().height).into(), znear: 0.1, zfar: 100.0, settings: CameraSettings::default(), @@ -159,18 +194,6 @@ impl Camera { yaw * pitch } - pub fn uniform_buffer(&self) -> &Buffer { - self.buffer.as_ref().unwrap() - } - - pub fn layout(&self) -> &BindGroupLayout { - self.layout.as_ref().unwrap() - } - - pub fn bind_group(&self) -> &BindGroup { - self.bind_group.as_ref().unwrap() - } - pub fn forward(&self) -> DVec3 { (self.target - self.eye).normalize() } @@ -207,47 +230,9 @@ impl Camera { DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX) * proj * view } - pub fn create_bind_group_layout( - &mut self, - graphics: Arc<SharedGraphicsContext>, - camera_buffer: Buffer, - ) { - let camera_bind_group_layout = - graphics - .device - .create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: Some("camera_bind_group_layout"), - }); - - let camera_bind_group = graphics.device.create_bind_group(&BindGroupDescriptor { - layout: &camera_bind_group_layout, - entries: &[BindGroupEntry { - binding: 0, - resource: camera_buffer.as_entire_binding(), - }], - label: Some("camera_bind_group"), - }); - self.layout = Some(camera_bind_group_layout); - self.bind_group = Some(camera_bind_group); - } - pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) { self.update_view_proj(); - graphics.queue.write_buffer( - self.buffer.as_ref().unwrap(), - 0, - bytemuck::cast_slice(&[self.uniform]), - ); + self.buffer.write(&graphics.queue, &self.uniform); } pub fn update_view_proj(&mut self) { @@ -10,9 +10,10 @@ use std::{ use crate::{ asset::{ASSET_REGISTRY, AssetHandle, AssetKind, AssetRegistry}, - graphics::{Instance, SharedGraphicsContext, Texture}, + graphics::{Instance, SharedGraphicsContext}, model::{LoadedModel, MODEL_CACHE, Model, ModelId}, utils::ResourceReference, + texture::Texture, }; use anyhow::anyhow; use dropbear_macro::SerializableComponent; @@ -1,4 +1,6 @@ use crate::shader::Shader; +use crate::texture::Texture; +use crate::{BindGroupLayouts, texture}; use crate::{ State, egui_renderer::EguiRenderer, @@ -7,20 +9,16 @@ use crate::{ use dropbear_future_queue::FutureQueue; use egui::{Context, TextureId}; use glam::{DMat4, DQuat, DVec3, Mat3}; -use image::GenericImageView; use parking_lot::Mutex; -use std::{fs, path::PathBuf, sync::Arc, time::Instant}; -use crate::utils::TextureWrapMode; +use std::sync::Arc; use wgpu::*; -use wgpu::util::*; use winit::window::Window; pub const NO_TEXTURE: &[u8] = include_bytes!("../../../resources/textures/no-texture.png"); -pub const NO_MODEL: &[u8] = include_bytes!("../../../resources/models/error.glb"); -pub struct RenderContext<'a> { - pub shared: Arc<SharedGraphicsContext>, - pub frame: FrameGraphicsContext<'a>, +pub struct FrameGraphicsContext<'a> { + pub view: TextureView, + pub encoder: &'a mut CommandEncoder, } pub struct SharedGraphicsContext { @@ -29,43 +27,18 @@ pub struct SharedGraphicsContext { pub surface: Arc<Surface<'static>>, pub surface_format: TextureFormat, pub instance: Arc<wgpu::Instance>, - pub texture_bind_layout: Arc<BindGroupLayout>, - pub material_tint_bind_layout: Arc<BindGroupLayout>, + pub layouts: Arc<BindGroupLayouts>, pub window: Arc<Window>, - pub viewport_texture: Arc<Texture>, + pub viewport_texture: texture::Texture, + pub depth_texture: texture::Texture, pub egui_renderer: Arc<Mutex<EguiRenderer>>, - pub diffuse_sampler: Arc<Sampler>, - pub screen_size: (f32, f32), pub texture_id: Arc<TextureId>, pub future_queue: Arc<FutureQueue>, } -pub struct FrameGraphicsContext<'a> { - pub encoder: &'a mut CommandEncoder, - pub view: &'a TextureView, - pub depth_texture: &'a Texture, - pub screen_size: (f32, f32), -} - impl SharedGraphicsContext { - pub fn get_egui_context(&self) -> Context { - self.egui_renderer.lock().context().clone() - } - - pub fn create_uniform<T>(&self, uniform: T, label: Option<&str>) -> Buffer - where - T: bytemuck::Pod + bytemuck::Zeroable, - { - self.device.create_buffer_init(&BufferInitDescriptor { - label, - contents: bytemuck::cast_slice(&[uniform]), - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - }) - } - - pub fn create_model_uniform_bind_group_layout(&self) -> BindGroupLayout { - self.device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + pub const MODEL_UNIFORM_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = + wgpu::BindGroupLayoutDescriptor { entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, @@ -77,49 +50,30 @@ impl SharedGraphicsContext { count: None, }], label: Some("model_uniform_bind_group_layout"), - }) + }; + + pub fn get_egui_context(&self) -> Context { + self.egui_renderer.lock().context().clone() } } -impl<'a> RenderContext<'a> { - pub fn from_state( - state: &'a mut State, - view: &'a TextureView, - encoder: &'a mut CommandEncoder, +impl SharedGraphicsContext { + pub(crate) fn from_state( + state: &State, ) -> Self { - let screen_size = (state.config.width as f32, state.config.height as f32); - let diffuse_sampler = Arc::new(state.device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - })); - Self { - shared: Arc::new(SharedGraphicsContext { - future_queue: state.future_queue.clone(), - device: state.device.clone(), - queue: state.queue.clone(), - instance: state.instance.clone(), - texture_bind_layout: Arc::new(state.texture_bind_layout.clone()), - material_tint_bind_layout: Arc::new(state.material_tint_bind_layout.clone()), - window: state.window.clone(), - viewport_texture: Arc::new(state.viewport_texture.clone()), - egui_renderer: state.egui_renderer.clone(), - diffuse_sampler, - screen_size, - texture_id: state.texture_id.clone(), - surface: state.surface.clone(), - surface_format: state.surface_format, - }), - frame: FrameGraphicsContext { - encoder, - view, - depth_texture: &state.depth_texture, - screen_size, - }, + SharedGraphicsContext { + future_queue: state.future_queue.clone(), + device: state.device.clone(), + queue: state.queue.clone(), + instance: state.instance.clone(), + layouts: state.layouts.clone(), + window: state.window.clone(), + viewport_texture: state.viewport_texture.clone(), + depth_texture: state.depth_texture.clone(), + egui_renderer: state.egui_renderer.clone(), + texture_id: state.texture_id.clone(), + surface: state.surface.clone(), + surface_format: state.surface_format, } } @@ -130,8 +84,7 @@ impl<'a> RenderContext<'a> { label: Option<&str>, ) -> RenderPipeline { let render_pipeline_layout = - self.shared - .device + self.device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some(label.unwrap_or("Render Pipeline Descriptor")), bind_group_layouts: bind_group_layouts.as_slice(), @@ -139,8 +92,7 @@ impl<'a> RenderContext<'a> { }); let render_pipeline = - self.shared - .device + self.device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(label.unwrap_or("Render Pipeline")), layout: Some(&render_pipeline_layout), @@ -164,8 +116,7 @@ impl<'a> RenderContext<'a> { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, - // cull_mode: Some(wgpu::Face::Back), // todo: change for improved performance - cull_mode: None, + cull_mode: Some(wgpu::Face::Back), polygon_mode: wgpu::PolygonMode::Fill, unclipped_depth: false, conservative: false, @@ -188,510 +139,6 @@ impl<'a> RenderContext<'a> { log::debug!("Created new render pipeline"); render_pipeline } - - pub fn clear_colour(&mut self, color: Color) -> RenderPass<'static> { - self.frame - .encoder - .begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Render Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: self.frame.view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(color), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &self.frame.depth_texture.view, - depth_ops: Some(Operations { - load: LoadOp::Clear(0.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - }) - .forget_lifetime() - } - - pub fn continue_pass(&mut self) -> RenderPass<'static> { - self.frame - .encoder - .begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Render Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: self.frame.view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &self.frame.depth_texture.view, - depth_ops: Some(Operations { - load: LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - }) - .forget_lifetime() - } - - pub fn begin_shadow_pass( - &mut self, - shadow_view: &wgpu::TextureView, - label: Option<&str>, - ) -> RenderPass<'static> { - self.frame - .encoder - .begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(label.unwrap_or("Shadow Pass")), - color_attachments: &[], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: shadow_view, - depth_ops: Some(Operations { - load: LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - }) - .forget_lifetime() - } -} - -#[derive(Clone)] -/// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture. -pub struct Texture { - pub texture: wgpu::Texture, - pub sampler: Sampler, - pub size: Extent3d, - pub view: TextureView, -} - -impl Texture { - /// Describes the depth format for all Texture related functions in WGPU to use. Makes life easier - pub const DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; - - fn align_up(value: u32, alignment: u32) -> u32 { - debug_assert!(alignment.is_power_of_two()); - (value + alignment - 1) & !(alignment - 1) - } - - fn write_rgba8_texture( - queue: &wgpu::Queue, - texture: &wgpu::Texture, - rgba_data: &[u8], - dimensions: (u32, u32), - ) { - let (width, height) = dimensions; - let texture_size = wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }; - - let unpadded_bytes_per_row = 4 * width; - let padded_bytes_per_row = - Self::align_up(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); - - debug_assert!(rgba_data.len() >= (unpadded_bytes_per_row * height) as usize); - - if padded_bytes_per_row == unpadded_bytes_per_row { - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - rgba_data, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(unpadded_bytes_per_row), - rows_per_image: Some(height), - }, - texture_size, - ); - return; - } - - let mut padded = vec![0u8; (padded_bytes_per_row * height) as usize]; - let src_stride = unpadded_bytes_per_row as usize; - let dst_stride = padded_bytes_per_row as usize; - for row in 0..height as usize { - let src_start = row * src_stride; - let dst_start = row * dst_stride; - padded[dst_start..dst_start + src_stride] - .copy_from_slice(&rgba_data[src_start..src_start + src_stride]); - } - - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - &padded, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(padded_bytes_per_row), - rows_per_image: Some(height), - }, - texture_size, - ); - } - - /// Creates a new Texture from the bytes of an image. This function is blocking, and takes roughly 4 seconds to - /// convert from the image to RGBA, which can cause issues. There are better options, such as doing it yourself. - /// - /// Once async is implemented, this will be a better use. - pub fn new(graphics: Arc<SharedGraphicsContext>, diffuse_bytes: &[u8]) -> Self { - let start = Instant::now(); - let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap(); - log::trace!("Loading image to memory: {:?}", start.elapsed()); - - let start = Instant::now(); - let diffuse_rgba = diffuse_image.to_rgba8(); - log::trace!( - "Converting diffuse image to rgba8 took {:?}", - start.elapsed() - ); - - let dimensions = diffuse_image.dimensions(); - let texture_size = wgpu::Extent3d { - width: dimensions.0, - height: dimensions.1, - depth_or_array_layers: 1, - }; - - let start = Instant::now(); - let diffuse_texture = Self::create_mipmapped_diffuse_texture(&graphics.device, texture_size); - log::trace!("Creating new diffuse texture took {:?}", start.elapsed()); - - let start = Instant::now(); - Self::write_rgba8_texture( - graphics.queue.as_ref(), - &diffuse_texture, - diffuse_rgba.as_raw(), - dimensions, - ); - log::trace!( - "Writing texture to graphics queue took {:?}", - start.elapsed() - ); - - let start = Instant::now(); - let diffuse_texture_view = diffuse_texture.create_view(&TextureViewDescriptor::default()); - let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - log::trace!("Creating sampler took {:?}", start.elapsed()); - - log::trace!("Done creating texture"); - Self { - texture: diffuse_texture, - sampler: diffuse_sampler, - size: texture_size, - view: diffuse_texture_view, - } - } - - pub fn new_with_wrap_mode( - graphics: Arc<SharedGraphicsContext>, - diffuse_bytes: &[u8], - wrap_mode: TextureWrapMode, - ) -> Self { - let address_mode = match wrap_mode { - TextureWrapMode::Repeat => wgpu::AddressMode::Repeat, - TextureWrapMode::Clamp => wgpu::AddressMode::ClampToEdge, - }; - - Self::new_with_sampler(graphics, diffuse_bytes, address_mode) - } - - /// Creates a new depth texture. This is an internal function. - // note: this should not be mipmapped - pub fn create_depth_texture( - config: &SurfaceConfiguration, - device: &Device, - label: Option<&str>, - ) -> Self { - let size = Extent3d { - width: config.width.max(1), - height: config.height.max(1), - depth_or_array_layers: 1, - }; - - let desc = TextureDescriptor { - label, - size, - mip_level_count: 1, // leave me alone - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: Self::DEPTH_FORMAT, - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }; - let texture = device.create_texture(&desc); - - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Nearest, - compare: Some(wgpu::CompareFunction::LessEqual), - lod_min_clamp: 0.0, - lod_max_clamp: 100.0, - ..Default::default() - }); - - Self { - texture, - sampler, - view, - size, - } - } - - /// Creates a viewport texture. This is an internal function. - // note: this should not be mipmapped - pub fn create_viewport_texture( - config: &SurfaceConfiguration, - device: &Device, - label: Option<&str>, - ) -> Self { - let size = Extent3d { - width: config.width.max(1), - height: config.height.max(1), - depth_or_array_layers: 1, - }; - - let desc = TextureDescriptor { - label, - size, - mip_level_count: 1, // leave me alone - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba16Float, - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }; - - let texture = device.create_texture(&desc); - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); - - Self { - texture, - sampler, - view, - size, - } - } - - /// Alternative to [`Texture::new()`], which uses an existing rgba data buffer compared to new which synchronously - /// converts the image to RGBA form. - pub(crate) fn from_rgba_buffer( - graphics: Arc<SharedGraphicsContext>, - rgba_data: &[u8], - dimensions: (u32, u32), - ) -> Texture { - let texture_size = wgpu::Extent3d { - width: dimensions.0, - height: dimensions.1, - depth_or_array_layers: 1, - }; - - let create_start = Instant::now(); - let diffuse_texture = Self::create_mipmapped_diffuse_texture(&graphics.device, texture_size); - log::trace!( - "Creating new diffuse texture took {:?}", - create_start.elapsed() - ); - - let write_start = Instant::now(); - Self::write_rgba8_texture(graphics.queue.as_ref(), &diffuse_texture, rgba_data, dimensions); - log::trace!( - "Writing texture to graphics queue took {:?}", - write_start.elapsed() - ); - - let sampler_start = Instant::now(); - let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - log::trace!("Creating sampler took {:?}", sampler_start.elapsed()); - - let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let bind_group_start = Instant::now(); - log::trace!( - "Creating diffuse bind group took {:?}", - bind_group_start.elapsed() - ); - - log::trace!("Done creating texture"); - - Texture { - texture: diffuse_texture, - sampler: diffuse_sampler, - view, - size: texture_size, - } - } - - fn create_mipmapped_diffuse_texture(device: &wgpu::Device, texture_size: wgpu::Extent3d) -> wgpu::Texture { - device.create_texture(&wgpu::TextureDescriptor { - label: Some("diffuse_texture"), - size: texture_size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }) - } - - /// Creates a new [`Texture`] with a specified sampler (wgpu) and already converted RGBA byte buffer. - pub fn new_with_sampler_with_rgba_buffer( - graphics: Arc<SharedGraphicsContext>, - rgba_data: &[u8], - dimensions: (u32, u32), - address_mode: wgpu::AddressMode, - ) -> Self { - let texture_size = wgpu::Extent3d { - width: dimensions.0, - height: dimensions.1, - depth_or_array_layers: 1, - }; - - let create_start = Instant::now(); - let diffuse_texture = Self::create_mipmapped_diffuse_texture(&graphics.device, texture_size); - log::trace!( - "Creating new diffuse texture took {:?}", - create_start.elapsed() - ); - - let write_start = Instant::now(); - Self::write_rgba8_texture(graphics.queue.as_ref(), &diffuse_texture, rgba_data, dimensions); - log::trace!( - "Writing texture to graphics queue took {:?}", - write_start.elapsed() - ); - - let sampler_start = Instant::now(); - let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: address_mode, - address_mode_v: address_mode, - address_mode_w: address_mode, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - log::trace!("Creating sampler took {:?}", sampler_start.elapsed()); - - let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let bind_group_start = Instant::now(); - log::trace!( - "Creating diffuse bind group took {:?}", - bind_group_start.elapsed() - ); - - log::trace!("Done creating texture"); - - Texture { - texture: diffuse_texture, - sampler: diffuse_sampler, - view, - size: texture_size, - } - } - - /// Creates a new [`Texture`] with a specified sampler (wgpu). - /// - /// This function decodes the image to RGBA, which can take a long time. This function is not - /// recommended to be used until you have async working. - pub fn new_with_sampler( - graphics: Arc<SharedGraphicsContext>, - diffuse_bytes: &[u8], - address_mode: wgpu::AddressMode, - ) -> Self { - let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap(); - let diffuse_rgba = diffuse_image.to_rgba8(); - - let dimensions = diffuse_image.dimensions(); - let texture_size = wgpu::Extent3d { - width: dimensions.0, - height: dimensions.1, - depth_or_array_layers: 1, - }; - - let diffuse_texture = Self::create_mipmapped_diffuse_texture(&graphics.device, texture_size); - - Self::write_rgba8_texture( - graphics.queue.as_ref(), - &diffuse_texture, - diffuse_rgba.as_raw(), - dimensions, - ); - - let diffuse_texture_view = diffuse_texture.create_view(&TextureViewDescriptor::default()); - let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: address_mode, - address_mode_v: address_mode, - address_mode_w: address_mode, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - - Self { - texture: diffuse_texture, - sampler: diffuse_sampler, - view: diffuse_texture_view, - size: texture_size, - } - } - - /// A helper function that loads the texture from a path. Still returns the same [`Texture`]. - pub async fn load_texture( - graphics: Arc<SharedGraphicsContext>, - path: &PathBuf, - ) -> anyhow::Result<Texture> { - let data = fs::read(path)?; - Ok(Self::new(graphics.clone(), &data)) - } } #[derive(Default, Clone)] @@ -699,8 +146,6 @@ pub struct Instance { pub position: DVec3, pub rotation: DQuat, pub scale: DVec3, - - buffer: Option<Buffer>, } impl Instance { @@ -709,7 +154,6 @@ impl Instance { position, rotation, scale, - buffer: None, } } @@ -722,17 +166,12 @@ impl Instance { } } - pub fn buffer(&self) -> &Buffer { - self.buffer.as_ref().unwrap() - } - pub fn from_matrix(mat: DMat4) -> Self { let (scale, rotation, position) = mat.to_scale_rotation_translation(); Instance { position, rotation, scale, - buffer: None, } } } @@ -8,7 +8,6 @@ pub mod entity; pub mod graphics; pub mod input; pub mod lighting; -pub mod mipmap; pub mod model; pub mod panic; pub mod procedural; @@ -16,12 +15,11 @@ pub mod resources; pub mod scene; pub mod shader; pub mod utils; +pub mod texture; pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new(); pub const PHYSICS_STEP_RATE: u32 = 120; const MAX_PHYSICS_STEPS_PER_FRAME: usize = 4; -/// Note: image size is 256x256 -pub const LOGO_AS_BYTES: &[u8] = include_bytes!("../../../resources/eucalyptus-editor.png"); use app_dirs2::{AppDataType, AppInfo}; use bytemuck::Contiguous; @@ -51,7 +49,11 @@ use winit::{ window::Window, }; -use crate::{egui_renderer::EguiRenderer, graphics::Texture}; +use crate::camera::Camera; +use crate::graphics::{FrameGraphicsContext, SharedGraphicsContext}; +use crate::lighting::{Light, LightManager}; +use crate::texture::Texture; +use crate::{egui_renderer::EguiRenderer}; pub use dropbear_future_queue as future; pub use gilrs; @@ -60,6 +62,15 @@ pub use winit; use winit::window::{WindowAttributes, WindowId}; use crate::scene::Scene; +pub struct BindGroupLayouts { + pub texture_bind_layout: BindGroupLayout, + pub material_tint_bind_layout: BindGroupLayout, + pub camera_bind_group_layout: BindGroupLayout, + pub light_bind_group_layout: BindGroupLayout, + pub light_array_bind_group_layout: BindGroupLayout, + pub light_cube_bind_group_layout: BindGroupLayout, +} + /// The backend information, such as the device, queue, config, surface, renderer, window and more. pub struct State { // keep top for drop order @@ -72,10 +83,9 @@ pub struct State { pub queue: Arc<Queue>, pub config: SurfaceConfiguration, pub is_surface_configured: bool, - pub depth_texture: Texture, - pub texture_bind_layout: BindGroupLayout, - pub material_tint_bind_layout: BindGroupLayout, + pub layouts: Arc<BindGroupLayouts>, pub egui_renderer: Arc<Mutex<EguiRenderer>>, + pub depth_texture: Texture, pub viewport_texture: Texture, pub texture_id: Arc<TextureId>, pub future_queue: Arc<FutureQueue>, @@ -85,18 +95,26 @@ pub struct State { pub scene_manager: scene::Manager, } -/// Generates the dropbear engine logo in a form that [winit::window::Icon] can accept. -/// -/// Returns (the bytes, width, height) in resp order. -pub fn gen_logo() -> anyhow::Result<(Vec<u8>, u32, u32)> { - let image = image::load_from_memory(LOGO_AS_BYTES)?.into_rgba8(); - let (width, height) = image.dimensions(); - let rgba = image.into_raw(); - Ok((rgba, width, height)) - -} - impl State { + /// As defined in `shader.wgsl` as + /// ``` + /// @group(3) @binding(0) + /// var<uniform> u_material: MaterialUniform; + /// ``` + const MATERIAL_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("material bind group layout"), + }; + /// Asynchronously initialised the state and sets up the backend and surface for wgpu to render to. pub async fn new(window: Arc<Window>, instance: Arc<Instance>, future_queue: Arc<FutureQueue>) -> anyhow::Result<Self> { let title = window.title(); @@ -196,64 +214,15 @@ Hardware: surface.configure(&device, &config); } - let depth_texture = Texture::create_depth_texture(&config, &device, Some("depth texture")); + let depth_texture = Texture::depth_texture(&config, &device, Some("depth texture")); let viewport_texture = - Texture::create_viewport_texture(&config, &device, Some("viewport texture")); + Texture::viewport(&config, &device, Some("viewport texture")); let texture_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - // normal map - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - label: Some("texture_bind_group_layout"), - }); + device.create_bind_group_layout(&texture::TEXTURE_BIND_GROUP_LAYOUT); let material_tint_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: Some("material_tint_bind_group_layout"), - }); + device.create_bind_group_layout(&Self::MATERIAL_BIND_GROUP_LAYOUT); let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new( &device, @@ -269,6 +238,12 @@ Hardware: .renderer() .register_native_texture(&device, &viewport_texture.view, wgpu::FilterMode::Linear); + let camera_bind_group_layout = device.create_bind_group_layout(&Camera::CAMERA_BIND_GROUP_LAYOUT); + let light_bind_group_layout = device.create_bind_group_layout(&Light::LIGHT_BIND_GROUP_LAYOUT); + let light_array_bind_group_layout = device.create_bind_group_layout(&LightManager::LIGHT_ARRAY_BIND_GROUP_LAYOUT); + let light_cube_bind_group_layout = device + .create_bind_group_layout(&LightManager::LIGHT_CUBE_BIND_GROUP_LAYOUT); + let result = Self { surface: Arc::new(surface), surface_format, @@ -277,8 +252,6 @@ Hardware: config, is_surface_configured, depth_texture, - texture_bind_layout: texture_bind_group_layout, - material_tint_bind_layout: material_tint_bind_group_layout, window, egui_renderer, viewport_texture, @@ -287,6 +260,14 @@ Hardware: instance, physics_accumulator: Duration::ZERO, scene_manager: scene::Manager::new(), + layouts: Arc::new(BindGroupLayouts { + texture_bind_layout: texture_bind_group_layout, + material_tint_bind_layout: material_tint_bind_group_layout, + camera_bind_group_layout, + light_bind_group_layout, + light_array_bind_group_layout, + light_cube_bind_group_layout, + }), }; Ok(result) @@ -302,9 +283,9 @@ Hardware: } self.depth_texture = - Texture::create_depth_texture(&self.config, &self.device, Some("depth texture")); + Texture::depth_texture(&self.config, &self.device, Some("depth texture")); self.viewport_texture = - Texture::create_viewport_texture(&self.config, &self.device, Some("viewport texture")); + Texture::viewport(&self.config, &self.device, Some("viewport texture")); self.egui_renderer .lock() .renderer() @@ -322,6 +303,7 @@ Hardware: &mut self, previous_dt: f32, event_loop: &ActiveEventLoop, + graphics: Arc<SharedGraphicsContext>, ) -> anyhow::Result<Vec<scene::SceneCommand>> { if !self.is_surface_configured { return Ok(Vec::new()); @@ -371,7 +353,10 @@ Hardware: label: Some("Render Encoder"), }); - let viewport_view = { &self.viewport_texture.view.clone() }; + let frame_ctx = FrameGraphicsContext { + view: view.clone(), + encoder: &mut encoder, + }; self.egui_renderer.lock().begin_frame(&self.window); @@ -382,11 +367,9 @@ Hardware: let mut physics_accumulator = self.physics_accumulator + frame_dt; let window_commands = { - let mut graphics = graphics::RenderContext::from_state(self, viewport_view, &mut encoder); - let mut steps = 0usize; while physics_accumulator >= physics_dt && steps < MAX_PHYSICS_STEPS_PER_FRAME { - scene_manager.physics_update(physics_dt.as_secs_f32(), &mut graphics); + scene_manager.physics_update(physics_dt.as_secs_f32(), graphics.clone()); physics_accumulator -= physics_dt; steps += 1; } @@ -395,8 +378,8 @@ Hardware: physics_accumulator = physics_accumulator.min(physics_dt); } - let commands = scene_manager.update(previous_dt, &mut graphics, event_loop); - scene_manager.render(&mut graphics); + let commands = scene_manager.update(previous_dt, graphics.clone(), event_loop); + scene_manager.render(graphics.clone(), frame_ctx); commands }; @@ -446,8 +429,7 @@ Hardware: drop(self.egui_renderer); drop(self.depth_texture); - drop(self.viewport_texture); - drop(self.texture_bind_layout); + drop(self.layouts); drop(self.surface); @@ -732,7 +714,7 @@ pub struct App { instance: Arc<Instance>, // multi-window management - windows: HashMap<WindowId, State>, + windows: HashMap<WindowId, (State, Arc<SharedGraphicsContext>)>, root_window_id: Option<WindowId>, windows_to_create: Vec<WindowData>, } @@ -787,7 +769,9 @@ impl App { let size = win_state.window.inner_size(); win_state.resize(size.width, size.height); - self.windows.insert(window_id, win_state); + let graphics = Arc::new(graphics::SharedGraphicsContext::from_state(&win_state)); + + self.windows.insert(window_id, (win_state, graphics)); Ok(window_id) } @@ -800,7 +784,7 @@ impl App { log::info!("Exiting app!"); let windows = std::mem::take(&mut self.windows); - for (_, state) in windows { + for (_, (state, _)) in windows { state.cleanup(event_loop); } self.root_window_id = None; @@ -821,7 +805,7 @@ impl ApplicationHandler for App { for window_data in windows_to_create { match self.create_window(event_loop, window_data.attributes) { Ok(window_id) => { - if let Some(state) = self.windows.get_mut(&window_id) { + if let Some((state, _)) = self.windows.get_mut(&window_id) { for (scene_name, scene) in window_data.scenes { state.scene_manager.add(&scene_name, scene.clone()); @@ -876,14 +860,14 @@ impl ApplicationHandler for App { self.quit(event_loop, None); } else { log::info!("Closing non-root window: {:?}", window_id); - if let Some(state) = self.windows.remove(&window_id) { + if let Some((state, _)) = self.windows.remove(&window_id) { state.cleanup(event_loop); } } return; } - let state = match self.windows.get_mut(&window_id) { + let (state, graphics) = match self.windows.get_mut(&window_id) { Some(canvas) => canvas, None => return, }; @@ -907,7 +891,7 @@ impl ApplicationHandler for App { self.input_manager.update(&mut self.gilrs); - let render_result = state.render(self.delta_time, event_loop); + let render_result = state.render(self.delta_time, event_loop, graphics.clone()); let window_commands = render_result.unwrap_or_else(|e| { log::error!("Render failed: {:?}", e); @@ -933,7 +917,7 @@ impl ApplicationHandler for App { log::info!("Scene requested new window creation"); match self.create_window(event_loop, window_data.attributes) { Ok(new_window_id) => { - if let Some(new_state) = self.windows.get_mut(&new_window_id) { + if let Some((new_state, _)) = self.windows.get_mut(&new_window_id) { for (scene_name, scene) in window_data.scenes { new_state.scene_manager.add(&scene_name, scene.clone()); @@ -983,7 +967,7 @@ impl ApplicationHandler for App { } } - for state in self.windows.values() { + for (state, _) in self.windows.values() { state.window.request_redraw(); } @@ -1034,7 +1018,7 @@ impl ApplicationHandler for App { } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - for window_state in self.windows.values() { + for (window_state, _) in self.windows.values() { window_state.window.request_redraw(); } } @@ -1,9 +1,8 @@ use crate::attenuation::{Attenuation, RANGE_50}; -use crate::buffer::ResizableBuffer; +use crate::buffer::{ResizableBuffer, StorageBuffer, UniformBuffer}; use crate::graphics::SharedGraphicsContext; use crate::shader::Shader; use crate::{ - camera::Camera, entity::{EntityTransform, Transform}, model::{self, Model, Vertex}, }; @@ -12,7 +11,7 @@ use dropbear_traits::SerializableComponent; use glam::{DMat4, DQuat, DVec3}; use std::fmt::{Display, Formatter}; use std::sync::Arc; -use wgpu::{BindGroup, BindGroupLayout, Buffer, BufferAddress, CompareFunction, DepthBiasState, RenderPipeline, StencilState, VertexBufferLayout, FilterMode}; +use wgpu::{BindGroup, Buffer, BufferAddress, CompareFunction, DepthBiasState, RenderPipeline, StencilState, VertexBufferLayout}; pub const MAX_LIGHTS: usize = 8; @@ -28,11 +27,6 @@ pub struct LightUniform { pub linear: f32, pub quadratic: f32, pub cutoff: f32, - - pub shadow_index: i32, - pub _padding: [u32; 3], - - pub(crate) proj: [[f32; 4]; 4], } fn dvec3_to_uniform_array(vec: DVec3) -> [f32; 4] { @@ -68,9 +62,6 @@ impl Default for LightUniform { linear: 0.0, quadratic: 0.0, cutoff: f32::cos(12.5_f32.to_radians()), - shadow_index: -1, - _padding: [0; 3], - proj: glam::Mat4::IDENTITY.to_cols_array_2d(), } } } @@ -251,13 +242,27 @@ pub struct Light { pub uniform: LightUniform, pub cube_model: Arc<Model>, pub label: String, - buffer: Option<Buffer>, - layout: Option<BindGroupLayout>, - bind_group: Option<BindGroup>, + pub buffer: UniformBuffer<LightUniform>, + pub bind_group: BindGroup, pub instance_buffer: ResizableBuffer<InstanceRaw>, } impl Light { + pub const LIGHT_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = + wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT), + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("light bind group layout descriptor"), + }; + pub async fn new( graphics: Arc<SharedGraphicsContext>, light: LightComponent, @@ -278,9 +283,6 @@ impl Light { linear: light.attenuation.linear, quadratic: light.attenuation.quadratic, cutoff: f32::cos(light.cutoff_angle.to_radians()), - shadow_index: -1, - _padding: Default::default(), - proj: Default::default(), }; log::trace!("Created new light uniform"); @@ -297,31 +299,15 @@ impl Light { let label_str = label.unwrap_or("Light").to_string(); - let buffer = graphics.create_uniform(uniform, label); - - let layout = graphics - .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, - }); + let buffer = UniformBuffer::new(&graphics.device, &label_str); let bind_group = graphics .device .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &layout, + layout: &graphics.layouts.light_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, - resource: buffer.as_entire_binding(), + resource: buffer.buffer().as_entire_binding(), }], label, }); @@ -347,9 +333,8 @@ impl Light { uniform, cube_model, label: label_str, - buffer: Some(buffer), - layout: Some(layout), - bind_group: Some(bind_group), + buffer, + bind_group, instance_buffer, } } @@ -370,48 +355,7 @@ impl Light { self.uniform.cutoff = f32::cos(light.cutoff_angle.to_radians()); - let safe_up = if direction.normalize_or_zero().dot(DVec3::Y).abs() > 0.99 { - DVec3::Z - } else { - DVec3::Y - }; - - let view = glam::DMat4::look_at_lh( - transform.position, - transform.position + direction, - safe_up, - ); - - let projection = match light.light_type { - LightType::Directional => { - let extent = 50.0; - glam::DMat4::orthographic_lh( - -extent, - extent, - -extent, - extent, - light.depth.start as f64, - light.depth.end as f64, - ) - } - LightType::Spot => glam::DMat4::perspective_lh( - light.outer_cutoff_angle.to_radians() as f64 * 2.0, - 1.0, - light.depth.start as f64, - light.depth.end as f64, - ), - // Point light shadows require cubemaps; not supported here. - LightType::Point => glam::DMat4::IDENTITY, - }; - - let light_vp = projection * view; - self.uniform.proj = light_vp.as_mat4().to_cols_array_2d(); - - if let Some(buffer) = &self.buffer { - graphics - .queue - .write_buffer(buffer, 0, bytemuck::cast_slice(&[self.uniform])); - } + self.buffer.write(&graphics.queue, &self.uniform); } pub fn uniform(&self) -> &LightUniform { @@ -425,32 +369,13 @@ impl Light { pub fn label(&self) -> &str { &self.label } - - pub fn bind_group(&self) -> &BindGroup { - self.bind_group.as_ref().unwrap() - } - - pub fn layout(&self) -> &BindGroupLayout { - self.layout.as_ref().unwrap() - } - - pub fn buffer(&self) -> &Buffer { - self.buffer.as_ref().unwrap() - } } #[derive(Clone)] pub struct LightManager { pub pipeline: Option<RenderPipeline>, - pub shadow_pipeline: Option<RenderPipeline>, - light_array_buffer: Option<Buffer>, + light_array_buffer: Option<StorageBuffer<LightArrayUniform>>, light_array_bind_group: Option<BindGroup>, - light_array_layout: Option<BindGroupLayout>, - - pub shadow_texture: Option<wgpu::Texture>, - pub shadow_view: Option<wgpu::TextureView>, - pub shadow_sampler: Option<wgpu::Sampler>, - pub shadow_target_views: Vec<wgpu::TextureView>, } impl Default for LightManager { @@ -460,216 +385,66 @@ impl Default for LightManager { } impl LightManager { - pub const SHADOW_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; - pub const SHADOW_SIZE: u32 = 2048; - - pub fn new() -> Self { - log::info!("Initialised lighting"); - Self { - pipeline: None, - shadow_pipeline: None, - light_array_buffer: None, - light_array_bind_group: None, - light_array_layout: None, - shadow_texture: None, - shadow_view: None, - shadow_sampler: None, - shadow_target_views: vec![], - } - } - - pub fn create_shadow_pipeline( - &mut self, - graphics: Arc<SharedGraphicsContext>, - shader_contents: &str, - label: Option<&str>, - ) { - let shader = Shader::new(graphics.clone(), shader_contents, label); - - // Layout compatible with `Light::bind_group()` (single uniform buffer). - let per_light_layout = graphics - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { + pub const LIGHT_ARRAY_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = + wgpu::BindGroupLayoutDescriptor { + entries: &[ + // light data + wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT), ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, count: None, - }], - label: Some("Shadow Per-Light Layout"), - }); - - let pipeline_layout = graphics - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(label.unwrap_or("Shadow Pipeline Layout")), - bind_group_layouts: &[&per_light_layout], - push_constant_ranges: &[], - }); - - let pipeline = graphics - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(label.unwrap_or("Shadow Pipeline")), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shader.module, - entry_point: Some("vs_main"), - buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()], - compilation_options: Default::default(), }, - fragment: None, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Front), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: Self::SHADOW_FORMAT, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::LessEqual, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState { - constant: 2, - slope_scale: 2.0, - clamp: 0.0, - }, - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, + ], + label: Some("Light Array Layout"), + }; + pub const LIGHT_CUBE_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = + wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT), + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, }, - multiview: None, - cache: None, - }); - - self.shadow_pipeline = Some(pipeline); - log::debug!("Created shadow render pipeline"); + count: None, + }], + label: Some("Per-Light Layout"), + }; + + pub fn new() -> Self { + log::info!("Initialised lighting"); + Self { + pipeline: None, + light_array_buffer: None, + light_array_bind_group: None, + } } pub fn create_light_array_resources(&mut self, graphics: Arc<SharedGraphicsContext>) { - let shadow_texture = graphics.device.create_texture(&wgpu::TextureDescriptor { - label: Some("Shadow Map Array"), - size: wgpu::Extent3d { - width: Self::SHADOW_SIZE, - height: Self::SHADOW_SIZE, - depth_or_array_layers: MAX_LIGHTS as u32, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: Self::SHADOW_FORMAT, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - - let shadow_view = shadow_texture.create_view(&wgpu::TextureViewDescriptor { - label: Some("Shadow Array View"), - dimension: Some(wgpu::TextureViewDimension::D2Array), - ..Default::default() - }); - - let shadow_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("Shadow Sampler"), - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: FilterMode::Nearest, - compare: Some(wgpu::CompareFunction::LessEqual), - ..Default::default() - }); - - self.shadow_target_views = (0..MAX_LIGHTS) - .map(|i| { - shadow_texture.create_view(&wgpu::TextureViewDescriptor { - label: Some(&format!("Shadow Layer {}", i)), - dimension: Some(wgpu::TextureViewDimension::D2), - base_array_layer: i as u32, - array_layer_count: Some(1), - ..Default::default() - }) - }) - .collect(); - - let layout = graphics - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - // light data - 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, - }, - // shadow texture array - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - sample_type: wgpu::TextureSampleType::Depth, - view_dimension: wgpu::TextureViewDimension::D2Array, - }, - count: None, - }, - // shadow sampler - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), - count: None, - }, - ], - label: Some("Light Array Layout"), - }); - - let buffer = graphics.create_uniform(LightArrayUniform::default(), Some("Light Array")); + let buffer = StorageBuffer::new(&graphics.device, "Light Array"); let bind_group = graphics .device .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &layout, + layout: &graphics.layouts.light_array_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&shadow_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&shadow_sampler), + resource: buffer.buffer().as_entire_binding(), }, ], label: Some("Light Array Bind Group"), }); - self.light_array_layout = Some(layout); self.light_array_buffer = Some(buffer); self.light_array_bind_group = Some(bind_group); - self.shadow_texture = Some(shadow_texture); - self.shadow_view = Some(shadow_view); - self.shadow_sampler = Some(shadow_sampler); - log::debug!("Created light array resources"); } @@ -677,8 +452,6 @@ impl LightManager { let mut light_array = LightArrayUniform::default(); let mut light_index = 0; - let mut shadow_map_index = 0; - for (light_component, s_trans, e_trans, light) in world .query::<(&LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>() .iter() @@ -695,23 +468,9 @@ impl LightManager { light.instance_buffer.write(&graphics.device, &graphics.queue, &[instance.to_raw()]); if light_component.enabled && light_index < MAX_LIGHTS { - let mut uniform = *light.uniform(); - - if light_component.cast_shadows - && light_component.light_type != LightType::Point - && shadow_map_index < MAX_LIGHTS - { - uniform.shadow_index = shadow_map_index as i32; - shadow_map_index += 1; - } else { - uniform.shadow_index = -1; - } - - // Keep per-light uniform in sync (used by shadow pass and light cube rendering). - light.uniform.shadow_index = uniform.shadow_index; - graphics - .queue - .write_buffer(light.buffer(), 0, bytemuck::cast_slice(&[light.uniform])); + let uniform = *light.uniform(); + + light.buffer.write(&graphics.queue, &uniform); light_array.lights[light_index] = uniform; light_index += 1; @@ -719,20 +478,13 @@ impl LightManager { } light_array.light_count = light_index as u32; - - if let Some(buffer) = &self.light_array_buffer { - graphics - .queue - .write_buffer(buffer, 0, bytemuck::cast_slice(&[light_array])); + if let Some(buf) = &self.light_array_buffer { + buf.write(&graphics.queue, &light_array); } log_once::debug_once!("LightUniform size = {}", size_of::<LightUniform>()); } - pub fn layout(&self) -> &BindGroupLayout { - self.light_array_layout.as_ref().unwrap() - } - pub fn bind_group(&self) -> &BindGroup { self.light_array_bind_group.as_ref().unwrap() } @@ -741,33 +493,16 @@ impl LightManager { &mut self, graphics: Arc<SharedGraphicsContext>, shader_contents: &str, - camera: &Camera, label: Option<&str>, ) { use crate::shader::Shader; let shader = Shader::new(graphics.clone(), shader_contents, label); - let per_light_layout = graphics - .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("Per-Light Layout"), - }); - + // the light cube rendering let pipeline = Self::create_render_pipeline_for_lighting( graphics, &shader, - vec![camera.layout(), &per_light_layout], label, ); @@ -775,10 +510,10 @@ impl LightManager { log::debug!("Created ECS light render pipeline"); } + // light cube rendering fn create_render_pipeline_for_lighting( graphics: Arc<SharedGraphicsContext>, shader: &Shader, - bind_group_layouts: Vec<&BindGroupLayout>, label: Option<&str>, ) -> RenderPipeline { let render_pipeline_layout = @@ -786,7 +521,7 @@ impl LightManager { .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some(label.unwrap_or("Light Render Pipeline Descriptor")), - bind_group_layouts: bind_group_layouts.as_slice(), + bind_group_layouts: &[&graphics.layouts.camera_bind_group_layout, &graphics.layouts.light_cube_bind_group_layout], push_constant_ranges: &[], }); @@ -1,156 +0,0 @@ -use std::sync::Arc; - -use wgpu::TextureFormat; - -use crate::{graphics::SharedGraphicsContext, shader::Shader}; - -pub struct MipMapGenerator { - pipeline: wgpu::RenderPipeline, - bind_group_layout: wgpu::BindGroupLayout, - sampler: wgpu::Sampler, -} - -impl MipMapGenerator { - pub fn new(graphics: Arc<SharedGraphicsContext>) -> Self { - let shader = Shader::new(graphics.clone(), crate::shader::shader_wesl::MIPMAP_SHADER, Some("mipmap shader")); - - let bind_group_layout = graphics.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("mipmap_bind_group_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - ], - }); - - let pipeline_layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("mipmap_pipeline_layout"), - bind_group_layouts: &[&bind_group_layout], - push_constant_ranges: &[], - }); - - let pipeline = graphics.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("mipmap_pipeline"), - layout: Some(&pipeline_layout), - cache: None, - vertex: wgpu::VertexState { - module: &shader.module, - entry_point: Some("vs_main"), - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader.module, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: TextureFormat::Rgba16Float, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleStrip, - strip_index_format: Some(wgpu::IndexFormat::Uint32), - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - let sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("mipmap_sampler"), - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - - Self { - pipeline, - bind_group_layout, - sampler, - } - } - - /// generates the mipmaps. yeah! - pub fn generate( - &self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - texture: &wgpu::Texture, - ) { - let mip_count = texture.mip_level_count(); - if mip_count < 2 { return; } - - let mut src_view = texture.create_view(&wgpu::TextureViewDescriptor { - base_mip_level: 0, - mip_level_count: Some(1), - ..Default::default() - }); - - for i in 1..mip_count { - let dst_view = texture.create_view(&wgpu::TextureViewDescriptor { - base_mip_level: i, - mip_level_count: Some(1), - ..Default::default() - }); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("mipmap_bind_group"), - layout: &self.bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&self.sampler), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&src_view), - }, - ], - }); - - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("mipmap_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &dst_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, &bind_group, &[]); - pass.draw(0..4, 0..1); - - drop(pass); - - src_view = dst_view; - } - } -} @@ -1,8 +1,10 @@ use crate::asset::AssetRegistry; +use crate::buffer::UniformBuffer; use crate::{ asset::{ASSET_REGISTRY, AssetHandle}, - graphics::{SharedGraphicsContext, Texture}, - utils::{ResourceReference, TextureWrapMode}, + graphics::{SharedGraphicsContext}, + utils::{ResourceReference}, + texture::{Texture, TextureWrapMode} }; use image::GenericImageView; use parking_lot::Mutex; @@ -182,7 +184,7 @@ pub struct Material { pub bind_group: wgpu::BindGroup, pub tint: [f32; 4], pub uv_tiling: [f32; 2], - pub tint_buffer: wgpu::Buffer, + pub tint_buffer: UniformBuffer<MaterialUniform>, pub tint_bind_group: wgpu::BindGroup, pub texture_tag: Option<String>, pub wrap_mode: TextureWrapMode, @@ -214,14 +216,16 @@ impl Material { uv_tiling, _pad: [0.0, 0.0], }; - let tint_buffer = graphics.create_uniform(uniform, Some("material_tint_uniform")); + + let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform"); + tint_buffer.write(&graphics.queue, &uniform); let tint_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.material_tint_bind_layout, + layout: &graphics.layouts.material_tint_bind_layout, entries: &[wgpu::BindGroupEntry { binding: 0, - resource: tint_buffer.as_entire_binding(), + resource: tint_buffer.buffer().as_entire_binding(), }], - label: Some("material_tint_bind_group"), + label: Some("material tint bind group"), }); let bind_group = Self::create_bind_group(&graphics, &diffuse_texture, &normal_texture, &name); @@ -247,7 +251,7 @@ impl Material { name: &str, ) -> BindGroup { graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.texture_bind_layout, + layout: &graphics.layouts.texture_bind_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, @@ -277,9 +281,8 @@ impl Material { uv_tiling: self.uv_tiling, _pad: [0.0, 0.0], }; - graphics - .queue - .write_buffer(&self.tint_buffer, 0, bytemuck::bytes_of(&uniform)); + + self.tint_buffer.write(&graphics.queue, &uniform); } pub fn set_uv_tiling(&mut self, graphics: &SharedGraphicsContext, tiling: [f32; 2]) { @@ -289,9 +292,7 @@ impl Material { uv_tiling: tiling, _pad: [0.0, 0.0], }; - graphics - .queue - .write_buffer(&self.tint_buffer, 0, bytemuck::bytes_of(&uniform)); + self.tint_buffer.write(&graphics.queue, &uniform); } } @@ -593,13 +594,13 @@ impl Model { let start = Instant::now(); let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse { - Texture::from_rgba_buffer(graphics.clone(), &rgba_data, dimensions) + Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba_data, Some(dimensions), None, None, None) } else { (*grey_texture).clone() }; let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal { - Texture::from_rgba_buffer(graphics.clone(), &rgba_data, dimensions) + Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba_data, Some(dimensions), None, None, None) } else { (*flat_normal_texture).clone() }; @@ -1,186 +0,0 @@ -//! A straight plane (and some components). That's it. -//! -//! Inspiration taken from `https://github.com/tirbofish/RedLight/blob/main/src/RedLight/Entities/Plane.cs`, -//! my old game engine made in C sharp, where this is the plane "algorithm". - -use crate::asset::{ASSET_REGISTRY, AssetRegistry}; -use crate::entity::MeshRenderer; -use crate::graphics::{SharedGraphicsContext, Texture}; -use crate::model::{LoadedModel, MODEL_CACHE, Material, Mesh, Model, ModelId, ModelVertex}; -use crate::utils::{ResourceReference, ResourceReferenceType}; -use parking_lot::Mutex; -use std::collections::HashMap; -use std::hash::{DefaultHasher, Hash, Hasher}; -use std::sync::{Arc, LazyLock}; -use wgpu::{AddressMode, util::DeviceExt}; - -/// Creates a plane wrapped in a [`MeshRenderer`]. -pub struct PlaneBuilder { - width: f32, - height: f32, - tiles_x: u32, - tiles_z: u32, -} - -impl Default for PlaneBuilder { - fn default() -> Self { - Self::new() - } -} - -impl PlaneBuilder { - pub fn new() -> Self { - Self { - width: 10.0, - height: 10.0, - tiles_x: 0, - tiles_z: 0, - } - } - - pub fn with_size(mut self, width: f32, height: f32) -> Self { - self.width = width; - self.height = height; - self - } - - pub fn with_tiles(mut self, tiles_x: u32, tiles_z: u32) -> Self { - self.tiles_x = tiles_x; - self.tiles_z = tiles_z; - self - } - - pub async fn build( - self, - graphics: Arc<SharedGraphicsContext>, - texture_bytes: &[u8], - label: Option<&str>, - ) -> anyhow::Result<MeshRenderer> { - self.build_raw( - graphics, - texture_bytes, - label, - &ASSET_REGISTRY, - LazyLock::force(&MODEL_CACHE), - ) - .await - } - - pub async fn build_raw( - mut self, - graphics: Arc<SharedGraphicsContext>, - texture_bytes: &[u8], - label: Option<&str>, - registry: &AssetRegistry, - cache: &Mutex<HashMap<String, Arc<Model>>>, - ) -> anyhow::Result<MeshRenderer> { - let label = if let Some(label) = label { - label.to_string() - } else { - format!( - "{}*{}_tx{}xtz{}_plane", - self.width, self.height, self.tiles_x, self.tiles_z - ) - }; - let mut hasher = DefaultHasher::new(); - if self.tiles_x == 0 && self.tiles_z == 0 { - self.tiles_x = self.width as u32; - self.tiles_z = self.height as u32; - } - let mut vertices = Vec::new(); - let mut indices = Vec::new(); - - for z in 0..=1 { - for x in 0..=1 { - let position = [ - (x as f32 - 0.5) * self.width, - 0.0, - (z as f32 - 0.5) * self.height, - ]; - let normal = [0.0, 1.0, 0.0]; - let tex_coords = [ - x as f32 * self.tiles_x as f32, - z as f32 * self.tiles_z as f32, - ]; - let _ = position.iter().map(|v| (*v as i32).hash(&mut hasher)); - let _ = normal.iter().map(|v| (*v as i32).hash(&mut hasher)); - let _ = tex_coords.iter().map(|v| (*v as i32).hash(&mut hasher)); - - vertices.push(ModelVertex { - position, - tex_coords, - normal, - tangent: [1.0, 0.0, 0.0], - bitangent: [0.0, 0.0, 1.0], - }); - } - } - - indices.extend_from_slice(&[0, 2, 1, 1, 2, 3]); - indices.hash(&mut hasher); - - let hash = hasher.finish(); - - if let Some(cached_model) = { - let cache_guard = cache.lock(); - cache_guard.get(&label).cloned() - } { - log::debug!("Model loaded from cache: {:?}", label); - let handle = LoadedModel::new_raw(registry, cached_model); - return Ok(MeshRenderer::from_handle(handle)); - } - - let vertex_buffer = graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Vertex Buffer", label.clone())), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, - }); - let index_buffer = graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Index Buffer", label)), - contents: bytemuck::cast_slice(&indices), - usage: wgpu::BufferUsages::INDEX, - }); - - let mesh = Mesh { - name: "plane".to_string(), - vertex_buffer, - index_buffer, - num_elements: indices.len() as u32, - material: 0, - }; - - let diffuse_texture = - Texture::new_with_sampler(graphics.clone(), texture_bytes, AddressMode::Repeat); - let normal_texture = (*registry - .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255])) - .clone(); - let material = Material::new_with_tint( - graphics.clone(), - "plane_material", - diffuse_texture, - normal_texture, - [1.0, 1.0, 1.0, 1.0], - Some("plane_material".to_string()), - ); - - let model = Arc::new(Model { - label: label.clone(), - path: ResourceReference::from_reference(ResourceReferenceType::Plane), - meshes: vec![mesh], - materials: vec![material], - id: ModelId(hash), - }); - - { - let mut cache_guard = cache.lock(); - cache_guard.insert(label.clone(), Arc::clone(&model)); - } - - let handle = LoadedModel::new_raw(registry, model); - Ok(MeshRenderer::from_handle(handle)) - } -} @@ -5,15 +5,15 @@ use winit::event_loop::ActiveEventLoop; use winit::window::WindowId; -use crate::{input, WindowData}; +use crate::{WindowData, graphics::{FrameGraphicsContext, SharedGraphicsContext}, input}; use parking_lot::RwLock; -use std::{collections::HashMap, rc::Rc}; +use std::{collections::HashMap, rc::Rc, sync::Arc}; pub trait Scene { - fn load(&mut self, graphics: &mut crate::graphics::RenderContext); - fn physics_update(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext); - fn update(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext); - fn render(&mut self, graphics: &mut crate::graphics::RenderContext); + fn load(&mut self, graphics: Arc<SharedGraphicsContext>); + fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>); + fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>); + fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>); fn exit(&mut self, event_loop: &ActiveEventLoop); /// By far a mess of a trait however it works. /// @@ -22,7 +22,6 @@ pub trait Scene { fn run_command(&mut self) -> SceneCommand { SceneCommand::None } - fn clear_ui(&mut self) {} } #[derive(Clone)] @@ -68,7 +67,6 @@ impl Manager { } } - /// Switches the scene from the current one to another. pub fn switch(&mut self, name: &str) { if self.scenes.contains_key(name) { self.next_scene = Some(name.to_string()); @@ -90,7 +88,7 @@ impl Manager { pub fn update<'a>( &mut self, dt: f32, - graphics: &mut crate::graphics::RenderContext<'a>, + graphics: Arc<SharedGraphicsContext>, event_loop: &ActiveEventLoop, ) -> Vec<SceneCommand> { // transition scene @@ -104,7 +102,7 @@ impl Manager { } if let Some(scene) = self.scenes.get_mut(&next_scene_name) { { - scene.write().load(graphics); + scene.write().load(graphics.clone()); } } self.current_scene = Some(next_scene_name); @@ -115,7 +113,7 @@ impl Manager { && let Some(scene) = self.scenes.get_mut(scene_name) { { - scene.write().update(dt, graphics); + scene.write().update(dt, graphics.clone()); } let command = scene.write().run_command(); match command { @@ -125,7 +123,7 @@ impl Manager { // reload the scene if let Some(scene) = self.scenes.get_mut(current) { scene.write().exit(event_loop); - scene.write().load(graphics); + scene.write().load(graphics.clone()); log::debug!("Reloaded scene: {}", current); } @@ -150,23 +148,23 @@ impl Manager { Vec::new() } - pub fn physics_update<'a>( + pub fn physics_update( &mut self, dt: f32, - graphics: &mut crate::graphics::RenderContext<'a>, + graphics: Arc<SharedGraphicsContext>, ) { if let Some(scene_name) = &self.current_scene && let Some(scene) = self.scenes.get_mut(scene_name) { - scene.write().physics_update(dt, graphics) + scene.write().physics_update(dt, graphics.clone()) } } - pub fn render<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>) { + pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) { if let Some(scene_name) = &self.current_scene && let Some(scene) = self.scenes.get_mut(scene_name) { - scene.write().render(graphics) + scene.write().render(graphics.clone(), frame_ctx) } } @@ -1,16 +1,24 @@ //! Deals with shaders, including WESL shaders + +use std::ops::Deref; use crate::graphics::SharedGraphicsContext; use std::sync::Arc; use wgpu::ShaderModule; -pub use dropbear_shader as shader_wesl; - /// A nice little struct that stored basic information about a WGPU shader. pub struct Shader { pub label: String, pub module: ShaderModule, } +impl Deref for Shader { + type Target = ShaderModule; + + fn deref(&self) -> &Self::Target { + &self.module + } +} + impl Shader { /// Creates a new [`ShaderModule`] from its file contents. pub fn new( @@ -0,0 +1,346 @@ +use std::{fs, path::PathBuf, sync::Arc}; + +use image::{EncodableLayout, GenericImageView}; +use serde::{Deserialize, Serialize}; + +use crate::graphics::SharedGraphicsContext; + + +/// As defined in `shader.wgsl` as +/// ``` +/// @group(0) @binding(0) +/// var t_diffuse: texture_2d<f32>; +/// @group(0) @binding(1) +/// var s_diffuse: sampler; +/// @group(0) @binding(2) +/// var t_normal: texture_2d<f32>; +/// @group(0) @binding(3) +/// var s_normal: sampler; +/// ``` +pub const TEXTURE_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = + wgpu::BindGroupLayoutDescriptor { + entries: &[ + // t_diffuse + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + // s_diffuse + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // t_normal + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + // s_normal + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + label: Some("texture bind group layout"), + }; + +#[derive(Clone)] +/// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture. +pub struct Texture { + pub texture: wgpu::Texture, + pub sampler: wgpu::Sampler, + pub size: wgpu::Extent3d, + pub view: wgpu::TextureView, +} + +impl Texture { + /// Describes the depth format for all Texture related functions in WGPU to use. Makes life easier + pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + + /// Creates a new depth texture. This is an internal function. + pub fn depth_texture( + config: &wgpu::SurfaceConfiguration, + device: &wgpu::Device, + label: Option<&str>, + ) -> Self { + let size = wgpu::Extent3d { + width: config.width.max(1), + height: config.height.max(1), + depth_or_array_layers: 1, + }; + + let desc = wgpu::TextureDescriptor { + label, + size, + mip_level_count: 1, // leave me alone + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: Self::DEPTH_FORMAT, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; + let texture = device.create_texture(&desc); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + compare: Some(wgpu::CompareFunction::LessEqual), + lod_min_clamp: 0.0, + lod_max_clamp: 100.0, + ..Default::default() + }); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + Self { + texture, + sampler, + size, + view + } + } + + /// Creates a viewport texture. + /// + /// This is an internal function. + pub fn viewport( + config: &wgpu::SurfaceConfiguration, + device: &wgpu::Device, + label: Option<&str>, + ) -> Self { + let size = wgpu::Extent3d { + width: config.width.max(1), + height: config.height.max(1), + depth_or_array_layers: 1, + }; + + let desc = wgpu::TextureDescriptor { + label, + size, + mip_level_count: 1, // leave me alone + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba16Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; + + let texture = device.create_texture(&desc); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + Self { + texture, + sampler, + size, + view, + } + } + + /// Loads the texture from a file. + pub async fn from_file( + graphics: Arc<SharedGraphicsContext>, + path: &PathBuf, + ) -> anyhow::Result<Self> { + let data = fs::read(path)?; + Ok(Self::from_bytes(graphics.clone(), &data)) + } + + /// Loads the texture from bytes. + /// + /// If you want more customisability in the texture being generated, you can use [Self::from_bytes_verbose] + pub fn from_bytes(graphics: Arc<SharedGraphicsContext>, bytes: &[u8]) -> Self { + Self::from_bytes_verbose( + &graphics.device, + &graphics.queue, + bytes, + None, + None, + None, + None + ) + } + + /// Loads the texture from bytes, with options for more arguments. + /// + /// Requires more arguments. For a simpler usage, you should use [Self::from_bytes] + pub fn from_bytes_verbose( + device: &wgpu::Device, + queue: &wgpu::Queue, + bytes: &[u8], + dimensions: Option<(u32, u32)>, + texture_descriptor: Option<wgpu::TextureDescriptor>, + view_descriptor: Option<wgpu::TextureViewDescriptor>, + sampler: Option<wgpu::SamplerDescriptor>, + ) -> Self { + let diffuse_image = image::load_from_memory(bytes).unwrap(); + let diffuse_rgba = diffuse_image.to_rgba8(); + + let dimensions = dimensions.unwrap_or_else(|| diffuse_image.dimensions()); + let size = wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth_or_array_layers: 1, + }; + + let desc = texture_descriptor.unwrap_or_else(|| + wgpu::TextureDescriptor { + label: Some("diffuse_texture"), + size: size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + } + ); + + let texture = device.create_texture(&desc); + + let unpadded_bytes_per_row = 4 * size.width; + debug_assert!(unpadded_bytes_per_row.is_power_of_two()); + let padded_bytes_per_row = (unpadded_bytes_per_row + wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1) & !(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1); + debug_assert!(diffuse_rgba.len() >= (unpadded_bytes_per_row * size.height) as usize); + + if padded_bytes_per_row == unpadded_bytes_per_row { + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &diffuse_rgba, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(unpadded_bytes_per_row), + rows_per_image: Some(size.height), + }, + size, + ); + } + + let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize]; + let src_stride = unpadded_bytes_per_row as usize; + let dst_stride = padded_bytes_per_row as usize; + for row in 0..size.height as usize { + let src_start = row * src_stride; + let dst_start = row * dst_stride; + padded[dst_start..dst_start + src_stride] + .copy_from_slice(&diffuse_rgba.as_bytes()[src_start..src_start + src_stride]); + } + + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &padded, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(padded_bytes_per_row), + rows_per_image: Some(size.height), + }, + size, + ); + + let sampler_desc = if let Some(sampler) = sampler { + sampler + } else { + wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + } + }; + + let sampler = device.create_sampler(&sampler_desc); + + let view = texture.create_view(&view_descriptor.unwrap_or_default()); + + Self { + texture, + sampler, + size, + view, + } + } + + pub fn sampler_from_wrap(wrap: TextureWrapMode) -> wgpu::SamplerDescriptor<'static> { + wgpu::SamplerDescriptor { + address_mode_u: wrap.into(), + address_mode_v: wrap.into(), + address_mode_w: wrap.into(), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + } + } +} + +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize +)] +pub enum TextureWrapMode { + Repeat, + Clamp, +} + +impl Default for TextureWrapMode { + fn default() -> Self { + Self::Repeat + } +} + +impl Into<wgpu::AddressMode> for TextureWrapMode { + fn into(self) -> wgpu::AddressMode { + match self { + TextureWrapMode::Repeat => wgpu::AddressMode::Repeat, + TextureWrapMode::Clamp => wgpu::AddressMode::ClampToEdge, + } + } +} + +pub struct DropbearEngineLogo; + +impl DropbearEngineLogo { + /// Note: image size is 256x256 + pub const DROPBEAR_ENGINE_LOGO: &[u8] = include_bytes!("../../../resources/eucalyptus-editor.png"); + + /// Generates the dropbear engine logo in a form that [winit::window::Icon] can accept. + /// + /// Returns (the bytes, width, height) in resp order. + pub fn generate() -> anyhow::Result<(Vec<u8>, u32, u32)> { + let image = image::load_from_memory(Self::DROPBEAR_ENGINE_LOGO)?.into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + Ok((rgba, width, height)) + } +} @@ -115,20 +115,6 @@ pub enum ResourceReferenceType { Cuboid { size_bits: [u32; 3] }, } -#[derive( - Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize -)] -pub enum TextureWrapMode { - Repeat, - Clamp, -} - -impl Default for TextureWrapMode { - fn default() -> Self { - Self::Repeat - } -} - impl Default for ResourceReferenceType { fn default() -> Self { Self::None @@ -1,13 +0,0 @@ -[package] -name = "dropbear-shader" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -readme.workspace = true - -[dependencies] -wesl = { workspace = true } - -[build-dependencies] -wesl = { workspace = true, features = ["package"] } @@ -1,10 +0,0 @@ -# dropbear-shader - -This crate is used for shaders created with the WESL language, such as rendering models and generating -mipmaps. It also provides templates for users to create their own shaders for their own -projects with the WESL language. - -## What is WESL? - -WESL (or WebGPU Extension Shader Language) is a shader language that is fully compatible with WGPU shaders, and -adds extension features such as import statements and rust-like #[cfg] functions. @@ -1,48 +0,0 @@ -use std::fs; -use std::path::Path; -use std::env; - -fn main() { - let shader_dir = Path::new("src/shaders"); - - if shader_dir.exists() { - println!("cargo:rerun-if-changed={}", shader_dir.display()); - - if let Ok(entries) = fs::read_dir(shader_dir) { - for entry in entries.flatten() { - println!("cargo:rerun-if-changed={}", entry.path().display()); - } - } - } - - wesl::PkgBuilder::new("dropbear") - .scan_root("src/shaders") - .expect("failed to scan for dropbear wesl shaders") - .validate() - .map_err(|e| eprintln!("{e}")) - .expect("validation error") - .build_artifact() - .expect("failed to build artifact"); - - wesl::Wesl::new("src/shaders") - .build_artifact(&"package::light".parse().unwrap(), "dropbear_light"); - wesl::Wesl::new("src/shaders") - .build_artifact(&"package::shader".parse().unwrap(), "dropbear_shader"); - wesl::Wesl::new("src/shaders") - .build_artifact(&"package::shadow".parse().unwrap(), "dropbear_shadow"); - wesl::Wesl::new("src/shaders") - .build_artifact(&"package::outline".parse().unwrap(), "dropbear_outline"); - wesl::Wesl::new("src/shaders") - .build_artifact(&"package::collider".parse().unwrap(), "dropbear_collider"); - wesl::Wesl::new("src/shaders") - .build_artifact(&"package::mipmap".parse().unwrap(), "dropbear_mipmap"); - - let out_dir = env::var("OUT_DIR").unwrap(); - let dropbear_path = Path::new(&out_dir).join("dropbear.rs"); - - if let Ok(content) = fs::read_to_string(&dropbear_path) { - let fixed_content = format!("#[allow(dead_code)]\n\n{}", content); - fs::write(&dropbear_path, fixed_content) - .expect("failed to write fixed dropbear.rs"); - } -} @@ -1,10 +0,0 @@ -use wesl::include_wesl; - -wesl::wesl_pkg!(dropbear); - -pub const LIGHT_SHADER: &str = include_wesl!("dropbear_light"); -pub const SHADER_SHADER: &str = include_wesl!("dropbear_shader"); -pub const SHADOW_SHADER: &str = include_wesl!("dropbear_shadow"); -pub const OUTLINE_SHADER: &str = include_wesl!("dropbear_outline"); -pub const COLLIDER_SHADER: &str = include_wesl!("dropbear_collider"); -pub const MIPMAP_SHADER: &str = include_wesl!("dropbear_mipmap"); @@ -1,45 +0,0 @@ -struct CameraUniform { - view_pos: vec4<f32>, - view_proj: mat4x4<f32>, -}; - -@group(0) @binding(0) -var <uniform> camera: CameraUniform; - -struct VertexInput { - @location(0) position: vec3<f32>, - @location(1) model_0: vec4<f32>, - @location(2) model_1: vec4<f32>, - @location(3) model_2: vec4<f32>, - @location(4) model_3: vec4<f32>, - @location(5) color: vec4<f32>, -} - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) color: vec4<f32>, -} - -@vertex -fn vs_main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - - let model = mat4x4<f32>( - input.model_0, - input.model_1, - input.model_2, - input.model_3, - ); - - let world_position = model * vec4<f32>(input.position, 1.0); - output.clip_position = camera.view_proj * world_position; - - output.color = input.color; - - return output; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { - return input.color; -} @@ -1,27 +0,0 @@ -const MAX_LIGHTS: u32 = 8; - -struct CameraUniform { - view_pos: vec4<f32>, - view_proj: mat4x4<f32>, -}; - -struct OutlineUniform { - outline_width: f32, - outline_color: vec4<f32>, -}; - -struct Light { - position: vec4<f32>, - direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type (0, 1, 2) - constant: f32, - lin: f32, - quadratic: f32, - cutoff: f32, -} - -struct LightArray { - _lights: array<Light, config::MAX_LIGHTS>, - light_count: u32, - ambient_strength: f32, -} @@ -1,64 +0,0 @@ -// Shader for rendering the white cube for lighting (or diff depending on colour) - -struct Light { - position: vec4<f32>, - direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type (0, 1, 2) - constant: f32, - lin: f32, - quadratic: f32, - cutoff: f32, - shadow_index: i32, - proj: mat4x4<f32>, -} - -struct CameraUniform { - view_pos: vec4<f32>, - view_proj: mat4x4<f32>, -}; - -@group(0) @binding(0) -var<uniform> camera: CameraUniform; - -@group(1) @binding(0) -var<uniform> light: Light; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, -} - -struct VertexInput { - @location(0) position: vec3<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) color: vec3<f32>, -}; - -@vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - var out: VertexOutput; - out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0); - out.color = light.color.xyz; - return out; -} - -// Fragment shader - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - return vec4<f32>(in.color, 1.0); -} @@ -1,28 +0,0 @@ -/// shader to generate mipmaps -/// -/// source: https://shi-yan.github.io/webgpuunleashed/Basics/mipmapping_and_anisotropic_filtering.html - -var<private> pos : array<vec2<f32>, 4> = array<vec2<f32>, 4>( - vec2<f32>(-1.0, 1.0), vec2<f32>(1.0, 1.0), - vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0)); - -struct VertexOutput { - @builtin(position) position : vec4<f32>, - @location(0) texCoord : vec2<f32>, -}; - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { - var output : VertexOutput; - output.texCoord = pos[vertexIndex] * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5); - output.position = vec4<f32>(pos[vertexIndex], 0.0, 1.0); - return output; -} - -@group(0) @binding(0) var imgSampler : sampler; -@group(0) @binding(1) var img : texture_2d<f32>; - -@fragment -fn fs_main(@location(0) texCoord : vec2<f32>) -> @location(0) vec4<f32> { - return textureSample(img, imgSampler, texCoord); -} @@ -1,65 +0,0 @@ -// outline.wgsl -import package::input::CameraUniform; -import package::input::OutlineUniform; - -@group(1) @binding(0) -var<uniform> camera: CameraUniform; - -@group(0) @binding(0) -var<uniform> outline: OutlineUniform; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, - - @location(9) normal_matrix_0: vec3<f32>, - @location(10) normal_matrix_1: vec3<f32>, - @location(11) normal_matrix_2: vec3<f32>, -} - -struct VertexInput { - @location(0) position: vec3<f32>, - @location(1) tex_coords: vec2<f32>, // unused, but must match vertex layout - @location(2) normal: vec3<f32>, -} - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, -} - -@vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - - let normal_matrix = mat3x3<f32>( - instance.normal_matrix_0, - instance.normal_matrix_1, - instance.normal_matrix_2, - ); - - // Transform normal to world space - let world_normal = normalize(normal_matrix * model.normal); - - // Extrude position in WORLD SPACE - let world_position = (model_matrix * vec4<f32>(model.position, 1.0)).xyz; - let expanded_world_position = world_position + world_normal * outline.outline_width; - - var out: VertexOutput; - out.clip_position = camera.view_proj * vec4<f32>(expanded_world_position, 1.0); - return out; -} - -@fragment -fn fs_main() -> @location(0) vec4<f32> { - return outline.outline_color; // Use the uniform color! -} @@ -1,285 +0,0 @@ -// Main shader for standard objects. - -const MAX_LIGHTS: u32 = 8; - -struct CameraUniform { - view_pos: vec4<f32>, - view_proj: mat4x4<f32>, -}; - -struct Light { - position: vec4<f32>, - direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type (0, 1, 2) - constant: f32, - lin: f32, - quadratic: f32, - cutoff: f32, - shadow_index: i32, - proj: mat4x4<f32>, -} - -struct LightArray { - _lights: array<Light, MAX_LIGHTS>, - light_count: u32, - ambient_strength: f32, -} - -struct MaterialUniform { - // for stuff like tinting - colour: vec4<f32>, - - // scales incoming UVs before sampling - uv_tiling: vec2<f32>, - _pad: vec2<f32>, -} - -@group(0) @binding(0) -var t_diffuse: texture_2d<f32>; -@group(0) @binding(1) -var s_diffuse: sampler; -@group(0) @binding(2) -var t_normal: texture_2d<f32>; -@group(0) @binding(3) -var s_normal: sampler; - -@group(3) @binding(0) -var<uniform> u_material: MaterialUniform; - -@group(1) @binding(0) -var<uniform> camera: CameraUniform; - -@group(2) @binding(0) -var<uniform> light_array: LightArray; -@group(2) @binding(1) -var t_shadow: texture_depth_2d_array; -@group(2) @binding(2) -var s_shadow: sampler_comparison; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, - - @location(9) normal_matrix_0: vec3<f32>, - @location(10) normal_matrix_1: vec3<f32>, - @location(11) normal_matrix_2: vec3<f32>, -}; - -struct VertexInput { - @location(0) position: vec3<f32>, - @location(1) tex_coords: vec2<f32>, - @location(2) normal: vec3<f32>, - @location(3) tangent: vec3<f32>, - @location(4) bitangent: vec3<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) tex_coords: vec2<f32>, - @location(1) world_normal: vec3<f32>, - @location(2) world_position: vec3<f32>, - @location(3) world_tangent: vec3<f32>, - @location(4) world_bitangent: vec3<f32>, -}; - -@vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - let normal_matrix = mat3x3<f32>( - instance.normal_matrix_0, - instance.normal_matrix_1, - instance.normal_matrix_2, - ); - - let world_normal = normalize(normal_matrix * model.normal); - let world_tangent = normalize(normal_matrix * model.tangent); - let world_bitangent = normalize(normal_matrix * model.bitangent); - let world_position = model_matrix * vec4<f32>(model.position, 1.0); - - var out: VertexOutput; - out.clip_position = camera.view_proj * world_position; - out.tex_coords = model.tex_coords; - out.world_normal = world_normal; - out.world_position = world_position.xyz; - out.world_tangent = world_tangent; - out.world_bitangent = world_bitangent; - return out; -} - -fn calculate_shadow(light: Light, world_pos: vec3<f32>, normal: vec3<f32>, light_dir: vec3<f32>) -> f32 { - // If index is -1, light casts no shadow - if (light.shadow_index < 0) { - return 1.0; - } - - let light_space_pos = light.proj * vec4<f32>(world_pos, 1.0); - - let proj_coords = light_space_pos.xyz / light_space_pos.w; - - let flip_correction = vec2<f32>(0.5, -0.5); - let uv = proj_coords.xy * flip_correction + vec2<f32>(0.5, 0.5); - - if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || proj_coords.z < 0.0 || proj_coords.z > 1.0) { - return 1.0; - } - - let current_depth = proj_coords.z - 0.005; - - return textureSampleCompareLevel( - t_shadow, - s_shadow, - uv, - light.shadow_index, - current_depth - ); -} - -fn directional_light( - light: Light, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32>, - world_pos: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(-light.direction.xyz); - - let ambient = light.color.xyz * light_array.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; - - let shadow = calculate_shadow(light, world_pos, world_normal, light_dir); - - return ambient + (shadow * (diffuse + specular)); -} - -fn point_light( - light: Light, - world_pos: vec3<f32>, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); - - let ambient = light.color.xyz * light_array.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; - - let shadow = calculate_shadow(light, world_pos, world_normal, light_dir); - - return (ambient + (shadow * (diffuse + specular))) * attenuation; -} - -fn spot_light( - light: Light, - world_pos: vec3<f32>, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); - let theta = dot(light_dir, normalize(-light.direction.xyz)); - let outer_cutoff = light.direction.w; - - let epsilon = light.cutoff - outer_cutoff; - let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); - - let ambient = light.color.xyz * light_array.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color * intensity; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color * intensity; - - let shadow = calculate_shadow(light, world_pos, world_normal, light_dir); - - return (ambient + (shadow * (diffuse + specular))) * attenuation; -} - -fn apply_normal_map( - world_normal_in: vec3<f32>, - world_tangent_in: vec3<f32>, - world_bitangent_in: vec3<f32>, - normal_sample_rgb: vec3<f32>, -) -> vec3<f32> { - let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0)); - - let n = normalize(world_normal_in); - var t = normalize(world_tangent_in); - t = normalize(t - n * dot(n, t)); - - let b_in = normalize(world_bitangent_in); - let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0); - let b = cross(n, t) * handedness; - - let tbn = mat3x3<f32>(t, b, n); - return normalize(tbn * normal_ts); -} - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - let uv = in.tex_coords * u_material.uv_tiling; - var tex_color = textureSample(t_diffuse, s_diffuse, uv); - var object_normal = textureSample(t_normal, s_normal, uv); - - let base_colour = tex_color * u_material.colour; - - if (base_colour.a < 0.1) { - discard; - } - - let view_dir = normalize(camera.view_pos.xyz - in.world_position); - - let world_normal = apply_normal_map( - in.world_normal, - in.world_tangent, - in.world_bitangent, - object_normal.xyz, - ); - - var final_color = vec3<f32>(0.0); - - for (var i: u32 = 0u; i < light_array.light_count; i = i + 1u) { - let light = light_array._lights[i]; - - let light_type = i32(light.color.w + 0.1); - - if (light_type == 0) { - final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); - } else if (light_type == 1) { - final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } else if (light_type == 2) { - final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } - } - - return vec4<f32>(final_color, base_colour.a); -} @@ -1,45 +0,0 @@ -// Depth-only shader for shadow mapping into a depth texture array. - -struct Light { - position: vec4<f32>, - direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type - constant: f32, - lin: f32, - quadratic: f32, - cutoff: f32, - shadow_index: i32, - proj: mat4x4<f32>, -} - -@group(0) @binding(0) -var<uniform> light: Light; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, -} - -struct VertexInput { - @location(0) position: vec3<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, -}; - -@vertex -fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - - var out: VertexOutput; - out.clip_position = light.proj * model_matrix * vec4<f32>(model.position, 1.0); - return out; -} @@ -7,7 +7,7 @@ repository.workspace = true readme = "README.md" [lib] -crate-type = ["rlib", "dylib"] +crate-type = ["rlib", "cdylib"] [dependencies] dropbear-traits = { path = "../dropbear-traits" } @@ -1,9 +1,9 @@ //! One way command buffers between the scripting module and the editor/runtime. use crossbeam_channel::{Receiver, Sender, unbounded}; -use dropbear_engine::graphics::RenderContext; +use dropbear_engine::graphics::SharedGraphicsContext; use once_cell::sync::Lazy; use parking_lot::RwLock; -use std::sync::{OnceLock}; +use std::sync::{Arc, OnceLock}; use crate::scene::loading::SceneLoadHandle; pub static COMMAND_BUFFER: Lazy<(Box<Sender<CommandBuffer>>, Receiver<CommandBuffer>)> = @@ -48,5 +48,5 @@ pub enum WindowCommand { /// Command buffer that is used for oneway communication between Kotlin to Rust. pub trait CommandBufferPoller { - fn poll(&mut self, graphics: &mut RenderContext); + fn poll(&mut self, graphics: Arc<SharedGraphicsContext>); } @@ -9,6 +9,8 @@ use dropbear_engine::shader::Shader; use dropbear_engine::wgpu::*; use glam::Mat4; +use crate::physics::collider::{ColliderShape, WireframeGeometry}; + pub struct ColliderWireframePipeline { pub pipeline: RenderPipeline, } @@ -16,18 +18,17 @@ pub struct ColliderWireframePipeline { impl ColliderWireframePipeline { pub fn new( graphics: Arc<SharedGraphicsContext>, - camera_bind_group_layout: &BindGroupLayout, ) -> Self { let shader = Shader::new( graphics.clone(), - dropbear_engine::shader::shader_wesl::COLLIDER_SHADER, + include_str!("../../../../../resources/shaders/collider.wgsl"), Some("collider wireframe shader"), ); let pipeline_layout = graphics.device.create_pipeline_layout(&PipelineLayoutDescriptor { label: Some("collider wireframe pipeline layout descriptor"), bind_group_layouts: &[ - camera_bind_group_layout, // @group(0) + &graphics.layouts.camera_bind_group_layout, // @group(0) ], push_constant_ranges: &[], }); @@ -166,4 +167,27 @@ impl ColliderInstanceRaw { attributes: &ATTRIBS, } } +} + +pub fn create_wireframe_geometry( + graphics: Arc<SharedGraphicsContext>, + shape: &ColliderShape, +) -> WireframeGeometry { + match shape { + ColliderShape::Box { half_extents } => { + WireframeGeometry::box_wireframe(graphics, *half_extents) + } + ColliderShape::Sphere { radius } => { + WireframeGeometry::sphere_wireframe(graphics, *radius, 16, 16) + } + ColliderShape::Capsule { half_height, radius } => { + WireframeGeometry::capsule_wireframe(graphics, *half_height, *radius, 16) + } + ColliderShape::Cylinder { half_height, radius } => { + WireframeGeometry::cylinder_wireframe(graphics, *half_height, *radius, 16) + } + ColliderShape::Cone { half_height, radius } => { + WireframeGeometry::cone_wireframe(graphics, *half_height, *radius, 16) + } + } } @@ -10,7 +10,7 @@ use crate::utils::ResolveReference; use dropbear_engine::asset::ASSET_REGISTRY; use dropbear_engine::camera::{Camera, CameraBuilder}; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; -use dropbear_engine::graphics::Texture; +use dropbear_engine::texture::Texture; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light as EngineLight, LightComponent}; use dropbear_engine::model::{LoadedModel, Material, Model, ModelId}; @@ -266,10 +266,14 @@ impl SceneConfig { if let Some(reference) = &custom.diffuse_texture { if let Ok(path) = reference.resolve() { if let Ok(bytes) = std::fs::read(&path) { - let diffuse = Texture::new_with_wrap_mode( - graphics.clone(), - &bytes, - custom.wrap_mode, + let diffuse = Texture::from_bytes_verbose( + &graphics.device, + &graphics.queue, + bytes.as_slice(), + None, + None, + None, + Some(Texture::sampler_from_wrap(custom.wrap_mode)), ); let flat_normal = (*ASSET_REGISTRY .solid_texture_rgba8( @@ -9,7 +9,8 @@ use crate::traits::SerializableComponent; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{MaterialOverride, MeshRenderer, Transform}; use dropbear_engine::lighting::LightComponent; -use dropbear_engine::utils::{ResourceReference, TextureWrapMode}; +use dropbear_engine::texture::TextureWrapMode; +use dropbear_engine::utils::{ResourceReference}; use dropbear_macro::SerializableComponent; use once_cell::sync::Lazy; use parking_lot::RwLock; @@ -10,6 +10,7 @@ readme = "README.md" [dependencies] eucalyptus-core = { path = "../eucalyptus-core", features = ["editor"] } magna-carta = { path = "../magna-carta" } +redback-runtime = { path = "../redback-runtime" } anyhow.workspace = true app_dirs2.workspace = true @@ -1,5 +1,6 @@ //! Used for displaying the Help->About window in the editor. +use dropbear_engine::graphics::FrameGraphicsContext; use egui::{CentralPanel}; use gilrs::{Button, GamepadId}; use winit::dpi::PhysicalPosition; @@ -7,7 +8,6 @@ use winit::event::MouseButton; use winit::event_loop::ActiveEventLoop; use winit::keyboard::KeyCode; use winit::window::{WindowId}; -use dropbear_engine::graphics::RenderContext; use dropbear_engine::input::{Controller, Keyboard, Mouse}; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::input::InputState; @@ -29,14 +29,14 @@ impl AboutWindow { } impl Scene for AboutWindow { - fn load(&mut self, graphics: &mut RenderContext) { - self.window = Some(graphics.shared.window.id()); + fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + self.window = Some(graphics.window.id()); } - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn physics_update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} - fn update(&mut self, _dt: f32, graphics: &mut RenderContext) { - CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { + fn update(&mut self, _dt: f32, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + CentralPanel::default().show(&graphics.get_egui_context(), |ui| { ui.centered_and_justified(|ui| { ui.add_space(8.0); @@ -73,10 +73,10 @@ impl Scene for AboutWindow { }); }); - self.window = Some(graphics.shared.window.id()); + self.window = Some(graphics.window.id()); } - fn render(&mut self, _graphics: &mut RenderContext) { + fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) { } fn exit(&mut self, _event_loop: &ActiveEventLoop) {} @@ -9,7 +9,7 @@ use winit::event::MouseButton; use winit::event_loop::ActiveEventLoop; use winit::keyboard::KeyCode; use winit::window::{WindowId}; -use dropbear_engine::graphics::RenderContext; + use dropbear_engine::input::{Controller, Keyboard, Mouse}; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::input::InputState; @@ -31,21 +31,21 @@ impl DebugWindow { } impl Scene for DebugWindow { - fn load(&mut self, graphics: &mut RenderContext) { - self.window = Some(graphics.shared.window.id()); + fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + self.window = Some(graphics.window.id()); } - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn physics_update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} - fn update(&mut self, _dt: f32, graphics: &mut RenderContext) { - CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { + fn update(&mut self, _dt: f32, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + CentralPanel::default().show(&graphics.get_egui_context(), |ui| { ui.label("Hello Debug Window!"); }); - self.window = Some(graphics.shared.window.id()); + self.window = Some(graphics.window.id()); } - fn render(&mut self, _graphics: &mut RenderContext) { + fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) { } fn exit(&mut self, _event_loop: &ActiveEventLoop) {} @@ -5,6 +5,7 @@ use dropbear_engine::asset::{AssetHandle, ASSET_REGISTRY}; use dropbear_engine::attenuation::ATTENUATION_PRESETS; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; use dropbear_engine::lighting::LightType; +use dropbear_engine::texture::TextureWrapMode; use dropbear_engine::utils::ResourceReferenceType; use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, TextEdit, Ui, UiBuilder}; use eucalyptus_core::camera::CameraType; @@ -1257,21 +1258,21 @@ impl InspectableComponent for MeshRenderer { material_name )) .selected_text(match wrap_mode { - dropbear_engine::utils::TextureWrapMode::Repeat => "Repeat", - dropbear_engine::utils::TextureWrapMode::Clamp => "Clamp", + TextureWrapMode::Repeat => "Repeat", + TextureWrapMode::Clamp => "Clamp", }) .show_ui(ui, |ui| { wrap_changed |= ui .selectable_value( &mut wrap_mode, - dropbear_engine::utils::TextureWrapMode::Repeat, + TextureWrapMode::Repeat, "Repeat", ) .changed(); wrap_changed |= ui .selectable_value( &mut wrap_mode, - dropbear_engine::utils::TextureWrapMode::Clamp, + TextureWrapMode::Clamp, "Clamp", ) .changed(); @@ -1285,7 +1286,7 @@ impl InspectableComponent for MeshRenderer { ); } - if matches!(wrap_mode, dropbear_engine::utils::TextureWrapMode::Repeat) { + if matches!(wrap_mode, TextureWrapMode::Repeat) { ui.label("Repeat"); let mut tiling_changed = ui .add(DragValue::new(&mut uv_tiling[0]).speed(0.05).range(0.01..=10_000.0)) @@ -9,16 +9,15 @@ pub(crate) use crate::editor::dock::*; use crate::build::build; use crate::debug; -use crate::graphics::OutlineShader; use crate::plugin::PluginRegistry; use crate::stats::NerdStats; use crossbeam_channel::{unbounded, Receiver, Sender}; use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::EntityTransform; use dropbear_engine::graphics::InstanceRaw; -use dropbear_engine::mipmap::MipMapGenerator; use dropbear_engine::shader::Shader; -use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{RenderContext, SharedGraphicsContext}, lighting::LightManager, model::{ModelId, MODEL_CACHE}, scene::SceneCommand, DropbearWindowBuilder, WindowData}; +use dropbear_engine::texture::TextureWrapMode; +use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{SharedGraphicsContext}, lighting::LightManager, model::{ModelId, MODEL_CACHE}, scene::SceneCommand, DropbearWindowBuilder, WindowData}; use egui::{self, Context}; use egui_dock::{DockArea, DockState, NodeIndex, Style}; use eucalyptus_core::{register_components, APP_INFO}; @@ -58,7 +57,7 @@ use wgpu::{Color, Extent3d, RenderPipeline}; use winit::window::{CursorGrabMode, WindowAttributes}; use winit::{keyboard::KeyCode, window::Window}; use winit::dpi::PhysicalSize; -use eucalyptus_core::physics::collider::{ColliderShape, ColliderShapeKey, WireframeGeometry}; +use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline; use eucalyptus_core::properties::CustomProperties; @@ -74,15 +73,12 @@ pub struct Editor { pub size: Extent3d, pub render_pipeline: Option<RenderPipeline>, pub instance_buffer_cache: HashMap<ModelId, ResizableBuffer<InstanceRaw>>, - pub outline_pipeline: Option<OutlineShader>, pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>, pub collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>, pub collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>, pub light_manager: LightManager, pub color: Color, - pub mipmap_generator: Option<MipMapGenerator>, - pub active_camera: Arc<Mutex<Option<Entity>>>, pub is_viewport_focused: bool, @@ -231,7 +227,6 @@ impl Editor { show_build_error_window: false, plugin_registry, dock_state_shared: None, - outline_pipeline: None, open_new_scene_window: false, new_scene_name: String::new(), current_scene_name: None, @@ -243,7 +238,6 @@ impl Editor { play_mode_pid: None, play_mode_exit_rx: None, collider_wireframe_pipeline: None, - mipmap_generator: None, instance_buffer_cache: HashMap::new(), collider_wireframe_geometry_cache: HashMap::new(), collider_instance_buffer: None, @@ -607,9 +601,9 @@ impl Editor { Ok(()) } - fn cleanup_scene_resources(&mut self, graphics: &mut RenderContext) { + fn cleanup_scene_resources(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { if let Some(handle) = self.world_load_handle.take() { - graphics.shared.future_queue.cancel(&handle); + graphics.future_queue.cancel(&handle); } self.light_spawn_queue.clear(); @@ -623,7 +617,6 @@ impl Editor { self.active_camera.lock().take(); self.render_pipeline = None; - self.outline_pipeline = None; self.texture_id = None; self.light_manager = LightManager::new(); @@ -633,8 +626,8 @@ impl Editor { } } - fn start_async_scene_load(&mut self, mut scene: SceneConfig, graphics: &mut RenderContext) { - self.cleanup_scene_resources(graphics); + fn start_async_scene_load(&mut self, mut scene: SceneConfig, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + self.cleanup_scene_resources(graphics.clone()); let (progress_sender, progress_receiver) = unbounded::<WorldLoadingStatus>(); @@ -647,12 +640,12 @@ impl Editor { self.is_world_loaded = IsWorldLoadedYet::new(); self.is_world_loaded.mark_scene_loaded(); - let graphics_shared = graphics.shared.clone(); + let graphics_shared = graphics.clone(); let active_camera = self.active_camera.clone(); let scene_name = scene.scene_name.clone(); let component_registry_clone = self.component_registry.clone(); - let handle = graphics.shared.future_queue.push(async move { + let handle = graphics.future_queue.push(async move { let mut temp_world = World::new(); let load_result = scene @@ -1308,69 +1301,38 @@ impl Editor { /// Loads all the wgpu resources such as renderer. /// /// **Note**: To be ran AFTER [`Editor::load_project_config`] - pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: &mut RenderContext<'a>) { - // log::debug!("Contents of viewport shader: \n{:#?}", dropbear_engine::shader::shader_wesl::SHADER_SHADER); + pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { let shader = Shader::new( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::SHADER_SHADER, - Some("viewport_shader"), + graphics.clone(), + include_str!("../../../../resources/shaders/shader.wgsl"), + Some("viewport shader"), ); self.light_manager - .create_light_array_resources(graphics.shared.clone()); - - if let Some(active_camera) = *self.active_camera.lock() { - if let Ok((camera, _)) = self - .world - .query_one::<(&Camera, &CameraComponent)>(active_camera) - .get() - { - let pipeline = graphics.create_render_pipline( - &shader, - vec![ - &graphics.shared.texture_bind_layout.clone(), - camera.layout(), - self.light_manager.layout(), - &graphics.shared.material_tint_bind_layout.clone(), - ], - None, - ); - self.render_pipeline = Some(pipeline); - - // log::debug!("Contents of light shader: \n{:#?}", dropbear_engine::shader::shader_wesl::LIGHT_SHADER); - self.light_manager.create_render_pipeline( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::LIGHT_SHADER, - camera, - Some("Light Pipeline"), - ); - - self.light_manager.create_shadow_pipeline( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::SHADOW_SHADER, - Some("Shadow Pipeline"), - ); - - // log::debug!("Contents of outline shader: \n{:#?}", dropbear_engine::shader::shader_wesl::OUTLINE_SHADER); - let outline_shader = - OutlineShader::init(graphics.shared.clone(), camera.layout()); - self.outline_pipeline = Some(outline_shader); + .create_light_array_resources(graphics.clone()); + + let pipeline = graphics.create_render_pipline( + &shader, + vec![ + &graphics.layouts.texture_bind_layout, + &graphics.layouts.camera_bind_group_layout, + &graphics.layouts.light_array_bind_group_layout, + &graphics.layouts.material_tint_bind_layout, + ], + None, + ); + self.render_pipeline = Some(pipeline); - let collider_pipeline = ColliderWireframePipeline::new(graphics.shared.clone(), camera.layout()); - self.collider_wireframe_pipeline = Some(collider_pipeline); + self.light_manager.create_render_pipeline( + graphics.clone(), + include_str!("../../../../resources/shaders/light.wgsl"), + Some("Light Pipeline"), + ); - self.mipmap_generator = Some(MipMapGenerator::new(graphics.shared.clone())) - } else { - log_once::warn_once!( - "Unable to query Camera and CameraComponent for active camera: {:?}", - active_camera - ); - } - } else { - log_once::warn_once!("No active camera found"); - } + let collider_pipeline = ColliderWireframePipeline::new(graphics.clone()); + self.collider_wireframe_pipeline = Some(collider_pipeline); - self.window = Some(graphics.shared.window.clone()); + self.window = Some(graphics.window.clone()); self.is_world_loaded.mark_rendering_loaded(); } @@ -1548,10 +1510,10 @@ pub enum Signal { UpdateProceduralCuboid(hecs::Entity, [f32; 3]), /// Applies a diffuse texture to a material by loading from a URI/path. - SetMaterialTexture(hecs::Entity, String, String, dropbear_engine::utils::TextureWrapMode), + SetMaterialTexture(hecs::Entity, String, String, TextureWrapMode), /// Changes the sampler wrap mode for a material. - SetMaterialWrapMode(hecs::Entity, String, dropbear_engine::utils::TextureWrapMode), + SetMaterialWrapMode(hecs::Entity, String, TextureWrapMode), /// Sets UV tiling (repeat counts) for a material. SetMaterialUvTiling(hecs::Entity, String, [f32; 2]), @@ -1,21 +1,21 @@ use crossbeam_channel::unbounded; use dropbear_engine::buffer::ResizableBuffer; use glam::DMat4; +use wgpu::util::DeviceExt; use std::collections::HashMap; use super::*; use crate::signal::SignalController; use crate::spawn::PendingSpawnController; use dropbear_engine::asset::{PointerKind, ASSET_REGISTRY}; -use dropbear_engine::graphics::{InstanceRaw, RenderContext}; +use dropbear_engine::graphics::{FrameGraphicsContext, InstanceRaw}; use dropbear_engine::model::MODEL_CACHE; use dropbear_engine::{ entity::{EntityTransform, MeshRenderer, Transform}, lighting::{Light, LightComponent}, - model::{DrawLight, DrawModel, DrawShadow}, + model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}, }; use eucalyptus_core::hierarchy::EntityTransformExt; -use eucalyptus_core::logging; use eucalyptus_core::states::{Label, WorldLoadingStatus}; use log; use parking_lot::Mutex; @@ -23,11 +23,11 @@ use wgpu::Color; use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}; use eucalyptus_core::physics::collider::ColliderGroup; use eucalyptus_core::physics::collider::ColliderShapeKey; -use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; +use eucalyptus_core::physics::collider::shader::{ColliderInstanceRaw, create_wireframe_geometry}; use eucalyptus_core::properties::CustomProperties; impl Scene for Editor { - fn load(&mut self, graphics: &mut RenderContext) { + fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { self.current_scene_name = { let last_opened = { let project = PROJECT.read(); @@ -49,7 +49,7 @@ impl Scene for Editor { self.progress_tx = Some(rx); self.world_receiver = Some(rx2); - let graphics_shared = graphics.shared.clone(); + let graphics_shared = graphics.clone(); let active_camera_clone = self.active_camera.clone(); let project_path_clone = self.project_path.clone(); @@ -58,7 +58,7 @@ impl Scene for Editor { let component_registry = self.component_registry.clone(); - let handle = graphics.shared.future_queue.push(async move { + let handle = graphics.future_queue.push(async move { let mut temp_world = World::new(); if let Err(e) = Self::load_project_config( graphics_shared, @@ -81,13 +81,13 @@ impl Scene for Editor { self.dock_state_shared = Some(dock_state_shared); - self.window = Some(graphics.shared.window.clone()); + self.window = Some(graphics.window.clone()); self.is_world_loaded.mark_scene_loaded(); } - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn physics_update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} - fn update(&mut self, dt: f32, graphics: &mut RenderContext) { + fn update(&mut self, dt: f32, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { if let Some(rx) = &self.play_mode_exit_rx { if rx.try_recv().is_ok() { log::info!("Play mode process has exited, returning to editing mode"); @@ -99,12 +99,11 @@ impl Scene for Editor { } if let Some(request) = self.pending_scene_load.take() { - self.start_async_scene_load(request.scene, graphics); + self.start_async_scene_load(request.scene, graphics.clone()); } - if let Some(mut receiver) = self.world_receiver.take() { - self.show_project_loading_window(&graphics.shared.get_egui_context()); + self.show_project_loading_window(&graphics.get_egui_context()); if let Ok(loaded_world) = receiver.try_recv() { self.world = Box::new(loaded_world); self.is_world_loaded.mark_project_loaded(); @@ -136,8 +135,8 @@ impl Scene for Editor { } match self.check_up( - graphics.shared.clone(), - graphics.shared.future_queue.clone(), + graphics.clone(), + graphics.future_queue.clone(), ) { Ok(_) => {} Err(e) => { @@ -166,7 +165,7 @@ impl Scene for Editor { ) }; - graphics.shared.window.set_title(&title); + graphics.window.set_title(&title); } { @@ -183,7 +182,6 @@ impl Scene for Editor { let mut completed = Vec::new(); for (i, handle) in self.light_spawn_queue.iter().enumerate() { if let Some(l) = graphics - .shared .future_queue .exchange_owned_as::<Light>(handle) { @@ -246,7 +244,7 @@ impl Scene for Editor { } } - let _ = self.run_signal(graphics.shared.clone()); + let _ = self.run_signal(graphics.clone()); if let Some(e) = self.previously_selected_entity @@ -261,7 +259,7 @@ impl Scene for Editor { entity.is_selected = true } - let current_size = graphics.shared.viewport_texture.size; + let current_size = graphics.viewport_texture.size; self.size = current_size; let new_aspect = current_size.width as f64 / current_size.height as f64; @@ -285,7 +283,7 @@ impl Scene for Editor { .iter() { component.update(camera); - camera.update(graphics.shared.clone()); + camera.update(graphics.clone()); } } @@ -330,7 +328,7 @@ impl Scene for Editor { continue; }; - light.update(graphics.shared.as_ref(), light_component, &transform); + light.update(graphics.as_ref(), light_component, &transform); } } } @@ -339,7 +337,7 @@ impl Scene for Editor { let sim_world = self.world.as_ref(); self.light_manager - .update(graphics.shared.clone(), sim_world); + .update(graphics.clone(), sim_world); self.nerd_stats.write().record_stats(dt, sim_world.len() as u32); } @@ -349,312 +347,320 @@ impl Scene for Editor { self.input_state.mouse_delta = None; } - fn render(&mut self, graphics: &mut RenderContext) { - // cornflower blue - let color = Color { + fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) { + self.show_ui(&graphics.get_egui_context()); + eucalyptus_core::logging::render(&graphics.get_egui_context()); + + let cam = { + let c = self.active_camera.lock(); + *c + }; + + let Some(active_camera) = cam else { + return; + }; + log_once::debug_once!("Active camera found: {:?}", active_camera); + + let q = self.world.query_one::<&Camera>(active_camera).get().ok().cloned(); + + let Some(camera) = q else { + return; + }; + log_once::debug_once!("Camera ready"); + log_once::debug_once!("Camera currently being viewed: {}", camera.label); + + let Some(pipeline) = &self.render_pipeline else { + log_once::warn_once!("Render pipeline not ready"); + return; + }; + log_once::debug_once!("Pipeline ready"); + + let clear_color = Color { r: 100.0 / 255.0, g: 149.0 / 255.0, b: 237.0 / 255.0, a: 1.0, }; - self.color = color; - self.size = graphics.shared.viewport_texture.size; - self.texture_id = Some(*graphics.shared.texture_id.clone()); - { - self.show_ui(&graphics.shared.get_egui_context()); + let lights = { + let mut lights = Vec::new(); + let mut query = self.world.query::<(&Light, &LightComponent)>(); + for (light, comp) in query.iter() { + lights.push((light.clone(), comp.clone())); + } + lights + }; + + let renderers = { + let mut renderers = Vec::new(); + let mut query = self.world.query::<&MeshRenderer>(); + for renderer in query.iter() { + renderers.push(renderer.clone()); + } + renderers + }; + + let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new(); + for renderer in &renderers { + model_batches + .entry(renderer.model_id()) + .or_default() + .push(renderer.instance.to_raw()); } - self.window = Some(graphics.shared.window.clone()); - logging::render(&graphics.shared.get_egui_context()); - if let Some(pipeline) = &self.render_pipeline { - log_once::debug_once!("Found render pipeline"); - let (active_camera, world): (Option<Entity>, &World) = - (*self.active_camera.lock(), self.world.as_ref()); + let mut prepared_models = Vec::new(); + for (model_id, instances) in model_batches { + let model_opt = { + let cache = MODEL_CACHE.lock(); + cache.values().find(|model| model.id == model_id).cloned() + }; - if let Some(active_camera) = active_camera { - let cam = { - world.query_one::<&Camera>(active_camera).get().ok().cloned() - }; + let Some(model) = model_opt else { + log_once::error_once!("Missing model {:?} in cache", model_id); + continue; + }; - if let Some(camera) = cam { - let lights = { - let mut lights = Vec::new(); - let mut light_query = world.query::<(&Light, &LightComponent)>(); - for (light, comp) in light_query.iter() { - lights.push((light.clone(), comp.clone())); - } - lights - }; + let instance_buffer = graphics.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("Runtime Instance Buffer"), + contents: bytemuck::cast_slice(&instances), + usage: wgpu::BufferUsages::VERTEX, + }, + ); - let entities = { - let mut entities = Vec::new(); - let mut entity_query = world.query::<&MeshRenderer>(); - for renderer in entity_query.iter() { - entities.push(renderer.clone()); - } - entities - }; + prepared_models.push((model, instance_buffer, instances.len() as u32)); + } - // mipmap gen - // LOL THIS DOESNT WORK FUCK WHY CANT YOU BE LIKE OPENGL AND MAKE MIPMAPPING SIMPLER FML - if let Some(mipmap) = &self.mipmap_generator { - let graphics = graphics.shared.clone(); - let mut encoder = graphics.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("mip map generator command encoder descriptor"), - }); + { + let mut query = self.world.query::<( + &mut LightComponent, + Option<&dropbear_engine::entity::Transform>, + Option<&dropbear_engine::entity::EntityTransform>, + &mut Light, + )>(); + + for (light_component, transform_opt, entity_transform_opt, light) in query.iter() { + let transform = if let Some(entity_transform) = entity_transform_opt { + entity_transform.sync() + } else if let Some(transform) = transform_opt { + *transform + } else { + continue; + }; - for i in ASSET_REGISTRY.iter_material() { - mipmap.generate(&graphics.device, &mut encoder, &i.diffuse_texture.texture); - } + light.update(graphics.as_ref(), light_component, &transform); + } + } - graphics.queue.submit(Some(encoder.finish())); - } + self.light_manager.update(graphics.clone(), &self.world); - let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new(); - for renderer in &entities { - let model_ptr = renderer.model_id(); - let instance_raw = renderer.instance.to_raw(); - model_batches - .entry(model_ptr) - .or_default() - .push(instance_raw); + { + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("light cube render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame_ctx.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(clear_color), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(0.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }); + if let Some(light_pipeline) = &self.light_manager.pipeline { + render_pass.set_pipeline(light_pipeline); + for (light, component) in &lights { + render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); + if component.visible { + render_pass.draw_light_model( + &light.cube_model, + &camera.bind_group, + &light.bind_group + ); } + } + } + } - let model_batches: Vec<(ModelId, Vec<InstanceRaw>)> = - model_batches.into_iter().collect(); - - let mut prepared_models = Vec::new(); - for (model_ptr, instances) in &model_batches { - let model_opt = { - let cache = MODEL_CACHE.lock(); - cache.values().find(|m| m.id == *model_ptr).cloned() - }; - - let Some(model) = model_opt else { - log_once::error_once!("No such MODEL as {:?}", model_ptr); - continue; - }; - - let buffer_handler = self - .instance_buffer_cache - .entry(*model_ptr) - .or_insert_with(|| { - ResizableBuffer::new( - &graphics.shared.device, - instances.len().max(10), - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "Batched Instance Buffer", - ) - }); + for (model, instance_buffer, instance_count) in prepared_models { + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame_ctx.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }); + render_pass.set_pipeline(pipeline); + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.draw_model_instanced( + &model, + 0..instance_count, + &camera.bind_group, + self.light_manager.bind_group(), + ); + } - buffer_handler.write( - &graphics.shared.device, - &graphics.shared.queue, - instances, - ); + { + let show_hitboxes = self + .current_scene_name + .as_ref() + .and_then(|scene_name| { + let scenes = SCENES.read(); + scenes + .iter() + .find(|scene| &scene.scene_name == scene_name) + .map(|scene| scene.settings.show_hitboxes) + }) + .unwrap_or(false); + + if show_hitboxes { + if let Some(collider_pipeline) = &self.collider_wireframe_pipeline { + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame_ctx.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }); + + render_pass.set_pipeline(&collider_pipeline.pipeline); + render_pass.set_bind_group(0, &camera.bind_group, &[]); - prepared_models.push(( - model, - buffer_handler.buffer().clone(), - instances.len() as u32, - )); - } + let mut instances_by_shape: HashMap<ColliderShapeKey, Vec<ColliderInstanceRaw>> = + HashMap::new(); - if let Some(shadow_pipeline) = &self.light_manager.shadow_pipeline { - for (light, component) in &lights { - if !component.enabled || !component.cast_shadows { - continue; - } + let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>(); + for (entity_transform, group) in q.iter() { + for collider in &group.colliders { + let world_tf = entity_transform.sync(); - let shadow_index = light.uniform().shadow_index; - if shadow_index < 0 { - continue; - } + let entity_matrix = DMat4::from_rotation_translation( + world_tf.rotation, + world_tf.position, + ) + .as_mat4(); - let shadow_index = shadow_index as usize; - if shadow_index >= self.light_manager.shadow_target_views.len() { - continue; - } - - let shadow_view = - &self.light_manager.shadow_target_views[shadow_index]; - let mut shadow_pass = graphics - .begin_shadow_pass(shadow_view, Some("Shadow Pass")); - shadow_pass.set_pipeline(shadow_pipeline); - - for (model, instance_buffer, instance_count) in &prepared_models { - shadow_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - shadow_pass.draw_shadow_model_instanced( - model, - 0..*instance_count, - light.bind_group(), - ); - } - } - } + let offset_transform = Transform::new() + .with_offset(collider.translation, collider.rotation); + let offset_matrix = offset_transform.matrix().as_mat4(); + + let final_matrix = entity_matrix * offset_matrix; + + let color = [0.0, 1.0, 0.0, 1.0]; + let instance = ColliderInstanceRaw::from_matrix(final_matrix, color); - { // light cube rendering - let mut render_pass = graphics.clear_colour(color); - if let Some(light_pipeline) = &self.light_manager.pipeline { - render_pass.set_pipeline(light_pipeline); - for (light, _component) in &lights { - render_pass.set_vertex_buffer( - 1, - light.instance_buffer.buffer().slice(..), - ); - if _component.visible { - render_pass.draw_light_model( - &light.cube_model, - camera.bind_group(), - light.bind_group(), - ); - } - } + let key = ColliderShapeKey::from(&collider.shape); + instances_by_shape.entry(key).or_default().push(instance); + + self.collider_wireframe_geometry_cache.entry(key).or_insert_with(|| { + create_wireframe_geometry( + graphics.clone(), + &collider.shape, + ) + }); } } - { // standard model rendering - for (model, instance_buffer, instance_count) in prepared_models { - let mut render_pass = graphics.continue_pass(); - render_pass.set_pipeline(pipeline); - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - - render_pass.draw_model_instanced( - &model, - 0..instance_count, - camera.bind_group(), - self.light_manager.bind_group(), - ); + if !instances_by_shape.is_empty() { + let total_instances: usize = + instances_by_shape.values().map(|v| v.len()).sum(); + let mut all_instances = Vec::with_capacity(total_instances); + let mut draws: Vec<(ColliderShapeKey, usize, usize)> = Vec::new(); + + for (key, instances) in instances_by_shape { + let start = all_instances.len(); + all_instances.extend_from_slice(&instances); + let count = instances.len(); + draws.push((key, start, count)); } - } - { - let show_hitboxes = self - .current_scene_name - .as_ref() - .and_then(|scene_name| { - let scenes = SCENES.read(); - scenes - .iter() - .find(|scene| &scene.scene_name == scene_name) - .map(|scene| scene.settings.show_hitboxes) - }) - .unwrap_or(false); - - if show_hitboxes { - if let Some(collider_pipeline) = &self.collider_wireframe_pipeline { - let mut render_pass = graphics.continue_pass(); - render_pass.set_pipeline(&collider_pipeline.pipeline); - render_pass.set_bind_group(0, camera.bind_group(), &[]); - - let mut instances_by_shape: HashMap<ColliderShapeKey, Vec<ColliderInstanceRaw>> = - HashMap::new(); - - let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>(); - for (entity_transform, group) in q.iter() { - for collider in &group.colliders { - let world_tf = entity_transform.sync(); - - let entity_matrix = DMat4::from_rotation_translation( - world_tf.rotation, - world_tf.position, - ) - .as_mat4(); - - let offset_transform = Transform::new() - .with_offset(collider.translation, collider.rotation); - let offset_matrix = offset_transform.matrix().as_mat4(); - - let final_matrix = entity_matrix * offset_matrix; - - let color = [0.0, 1.0, 0.0, 1.0]; - let instance = ColliderInstanceRaw::from_matrix(final_matrix, color); - - let key = ColliderShapeKey::from(&collider.shape); - instances_by_shape.entry(key).or_default().push(instance); - - self.collider_wireframe_geometry_cache - .entry(key) - .or_insert_with(|| { - Editor::create_wireframe_geometry( - graphics.shared.clone(), - &collider.shape, - ) - }); - } - } - - if !instances_by_shape.is_empty() { - let total_instances: usize = - instances_by_shape.values().map(|v| v.len()).sum(); - let mut all_instances = Vec::with_capacity(total_instances); - let mut draws: Vec<(ColliderShapeKey, usize, usize)> = Vec::new(); - - for (key, instances) in instances_by_shape { - let start = all_instances.len(); - all_instances.extend_from_slice(&instances); - let count = instances.len(); - draws.push((key, start, count)); - } - - let instance_buffer = self.collider_instance_buffer.get_or_insert_with(|| { - ResizableBuffer::new( - &graphics.shared.device, - all_instances.len().max(10), - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "Collider Instance Buffer", - ) - }); - instance_buffer.write( - &graphics.shared.device, - &graphics.shared.queue, - &all_instances, - ); - - for (key, start, count) in draws { - let Some(geometry) = self.collider_wireframe_geometry_cache.get(&key) else { - continue; - }; - - let start_bytes = - (start * std::mem::size_of::<ColliderInstanceRaw>()) as wgpu::BufferAddress; - let end_bytes = - ((start + count) * std::mem::size_of::<ColliderInstanceRaw>()) as wgpu::BufferAddress; - - render_pass.set_vertex_buffer( - 1, - instance_buffer.buffer().slice(start_bytes..end_bytes), - ); - render_pass.set_vertex_buffer( - 0, - geometry.vertex_buffer.slice(..), - ); - render_pass.set_index_buffer( - geometry.index_buffer.slice(..), - wgpu::IndexFormat::Uint16, - ); - render_pass.draw_indexed( - 0..geometry.index_count, - 0, - 0..count as u32, - ); - } - } - } + let instance_buffer = self.collider_instance_buffer.get_or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + all_instances.len().max(10), + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + "Collider Instance Buffer", + ) + }); + instance_buffer.write( + &graphics.device, + &graphics.queue, + &all_instances, + ); + + for (key, start, count) in draws { + let Some(geometry) = self.collider_wireframe_geometry_cache.get(&key) else { + continue; + }; + + let start_bytes = + (start * std::mem::size_of::<ColliderInstanceRaw>()) as wgpu::BufferAddress; + let end_bytes = + ((start + count) * std::mem::size_of::<ColliderInstanceRaw>()) as wgpu::BufferAddress; + + render_pass.set_vertex_buffer( + 1, + instance_buffer.buffer().slice(start_bytes..end_bytes), + ); + render_pass.set_vertex_buffer( + 0, + geometry.vertex_buffer.slice(..), + ); + render_pass.set_index_buffer( + geometry.index_buffer.slice(..), + wgpu::IndexFormat::Uint16, + ); + render_pass.draw_indexed( + 0..geometry.index_count, + 0, + 0..count as u32, + ); } } - } else { - log_once::error_once!("Camera returned None"); } - } else { - log_once::error_once!("No active camera found"); - } - } else { - if self.is_world_loaded.is_fully_loaded() { - log_once::warn_once!("No render pipeline exists"); - } else { - log_once::debug_once!("No render pipeline exists, but world not loaded yet"); } } } @@ -664,29 +670,4 @@ impl Scene for Editor { fn run_command(&mut self) -> SceneCommand { std::mem::replace(&mut self.scene_command, SceneCommand::None) } -} - -impl Editor { - pub fn create_wireframe_geometry( - graphics: Arc<SharedGraphicsContext>, - shape: &ColliderShape, - ) -> WireframeGeometry { - match shape { - ColliderShape::Box { half_extents } => { - WireframeGeometry::box_wireframe(graphics, *half_extents) - } - ColliderShape::Sphere { radius } => { - WireframeGeometry::sphere_wireframe(graphics, *radius, 16, 16) - } - ColliderShape::Capsule { half_height, radius } => { - WireframeGeometry::capsule_wireframe(graphics, *half_height, *radius, 16) - } - ColliderShape::Cylinder { half_height, radius } => { - WireframeGeometry::cylinder_wireframe(graphics, *half_height, *radius, 16) - } - ColliderShape::Cone { half_height, radius } => { - WireframeGeometry::cone_wireframe(graphics, *half_height, *radius, 16) - } - } - } } @@ -13,7 +13,6 @@ use winit::event::MouseButton; use winit::event_loop::ActiveEventLoop; use winit::keyboard::KeyCode; use winit::window::{WindowId}; -use dropbear_engine::graphics::RenderContext; use dropbear_engine::input::{Controller, Keyboard, Mouse}; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::input::InputState; @@ -125,14 +124,14 @@ impl EditorSettingsWindow { } impl Scene for EditorSettingsWindow { - fn load(&mut self, graphics: &mut RenderContext) { - self.window = Some(graphics.shared.window.id()); + fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + self.window = Some(graphics.window.id()); } - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn physics_update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} - fn update(&mut self, _dt: f32, graphics: &mut RenderContext) { - CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { + fn update(&mut self, _dt: f32, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + CentralPanel::default().show(&graphics.get_egui_context(), |ui| { let mut editor = EDITOR_SETTINGS.write(); egui::SidePanel::left("editor_settings_tree_panel") @@ -211,10 +210,10 @@ impl Scene for EditorSettingsWindow { }); }); - self.window = Some(graphics.shared.window.id()); + self.window = Some(graphics.window.id()); } - fn render(&mut self, _graphics: &mut RenderContext) { + fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) { } fn exit(&mut self, _event_loop: &ActiveEventLoop) { @@ -9,7 +9,6 @@ use winit::event::MouseButton; use winit::event_loop::ActiveEventLoop; use winit::keyboard::KeyCode; use winit::window::{WindowId}; -use dropbear_engine::graphics::RenderContext; use dropbear_engine::input::{Controller, Keyboard, Mouse}; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::input::InputState; @@ -45,14 +44,14 @@ impl ProjectSettingsWindow { } impl Scene for ProjectSettingsWindow { - fn load(&mut self, graphics: &mut RenderContext) { - self.window = Some(graphics.shared.window.id()); + fn load(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + self.window = Some(graphics.window.id()); } - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn physics_update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} - fn update(&mut self, _dt: f32, graphics: &mut RenderContext) { - CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { + fn update(&mut self, _dt: f32, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + CentralPanel::default().show(&graphics.get_egui_context(), |ui| { let mut project = PROJECT.write(); egui::SidePanel::left("project_settings_tree_panel") @@ -181,10 +180,10 @@ impl Scene for ProjectSettingsWindow { }); }); - self.window = Some(graphics.shared.window.id()); + self.window = Some(graphics.window.id()); } - fn render(&mut self, _graphics: &mut RenderContext) { + fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) { } fn exit(&mut self, _event_loop: &ActiveEventLoop) { @@ -1,144 +0,0 @@ -//! Extension module for the dropbear graphics that are editor specific. - -use dropbear_engine::colour::Colour; -use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext, Texture}; -use dropbear_engine::model::{ModelVertex, Vertex}; -use dropbear_engine::shader::Shader; -use std::sync::Arc; - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct OutlineUniform { - pub outline_width: f32, - pub outline_color: [f32; 4], - pub _padding: [f32; 3], -} - -impl Default for OutlineUniform { - fn default() -> Self { - Self { - outline_width: 0.02, - outline_color: Colour::ORANGE.to_raw_vec4(), - _padding: [0.0, 0.0, 0.0], - } - } -} - -pub struct OutlineShader { - pub pipeline: wgpu::RenderPipeline, - pub bind_group_layout: wgpu::BindGroupLayout, - pub bind_group: wgpu::BindGroup, - pub uniform_buffer: wgpu::Buffer, -} - -impl OutlineShader { - pub fn init( - graphics: Arc<SharedGraphicsContext>, - camera_layout: &wgpu::BindGroupLayout, - ) -> Self { - let shader = Shader::new( - graphics.clone(), - dropbear_engine::shader::shader_wesl::OUTLINE_SHADER, - Some("outline_shader"), - ); - log::trace!("Created outline shader"); - - let bind_group_layout = - graphics - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Outline Bind Group Layout"), - 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, - }], - }); - log::trace!("Created outline bind group layout"); - - let uniform = OutlineUniform::default(); - let uniform_buffer = graphics.create_uniform(uniform, Some("Outline Uniform")); - - let bind_group = graphics - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Outline Bind Group"), - layout: &bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buffer.as_entire_binding(), - }], - }); - log::trace!("Created outline bind group"); - - let pipeline_layout = - graphics - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Outline Pipeline Layout"), - bind_group_layouts: &[&bind_group_layout, &camera_layout], - push_constant_ranges: &[], - }); - log::trace!("Created outline pipeline layout"); - - let pipeline = graphics - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Outline Pipeline"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shader.module, - entry_point: Some("vs_main"), - buffers: &[ModelVertex::desc(), InstanceRaw::desc()], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader.module, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Front), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: Texture::DEPTH_FORMAT, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::Greater, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }); - log::trace!("Created outline pipeline"); - - log::debug!("Created outline render pipeline"); - Self { - pipeline, - bind_group_layout, - bind_group, - uniform_buffer, - } - } -} @@ -2,14 +2,13 @@ pub mod build; pub mod camera; pub mod debug; pub mod editor; -pub mod graphics; pub mod menu; pub mod plugin; pub mod signal; pub mod spawn; pub mod stats; pub mod utils; -pub mod runtime; pub mod about; pub mod process; pub mod physics; +pub use redback_runtime as runtime; @@ -4,6 +4,7 @@ use anyhow::{bail, Context}; use clap::{Arg, Command}; use dropbear_engine::future::FutureQueue; +use dropbear_engine::texture::DropbearEngineLogo; use eucalyptus_editor::{build, editor, menu}; use parking_lot::RwLock; use std::sync::Arc; @@ -269,7 +270,7 @@ async fn main() -> anyhow::Result<()> { panic!("Unable to initialise Eucalyptus Editor: {}", e) }))); - let img = dropbear_engine::gen_logo()?; + let img = DropbearEngineLogo::generate()?; let window_icon = Icon::from_rgba(img.0, img.1, img.2) .inspect_err(|e| log::warn!("Unable to set logo: {}", e)) @@ -1,5 +1,5 @@ use anyhow::{Context, anyhow}; -use dropbear_engine::{future::{FutureHandle, FutureQueue}, graphics::RenderContext, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand}, DropbearWindowBuilder}; +use dropbear_engine::{DropbearWindowBuilder, future::{FutureHandle, FutureQueue}, graphics::FrameGraphicsContext, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand}}; use egui::{self, FontId, Frame, RichText}; use egui_toast::{ToastOptions, Toasts}; use eucalyptus_core::config::ProjectConfig; @@ -234,19 +234,18 @@ impl MainMenu { } impl Scene for MainMenu { - fn load(&mut self, _graphics: &mut RenderContext) { + fn load(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { log::info!("Loaded main menu scene"); } - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn physics_update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} - fn update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} + fn update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} - fn render(&mut self, graphics: &mut RenderContext) { + fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) { #[allow(clippy::collapsible_if)] if let Some(handle) = self.project_creation_handle.as_ref() { if let Some(result) = graphics - .shared .future_queue .exchange_owned_as::<anyhow::Result<()>>(handle) { @@ -283,10 +282,10 @@ impl Scene for MainMenu { } let screen_size: (f32, f32) = ( - graphics.shared.window.inner_size().width as f32 - 100.0, - graphics.shared.window.inner_size().height as f32 - 100.0, + graphics.window.inner_size().width as f32 - 100.0, + graphics.window.inner_size().height as f32 - 100.0, ); - let egui_ctx = graphics.shared.get_egui_context(); + let egui_ctx = graphics.get_egui_context(); let mut local_open_project = false; let mut local_select_project = false; @@ -437,7 +436,7 @@ impl Scene for MainMenu { .clicked() { log::info!("Creating new project at {:?}", self.project_path); - self.start_project_creation(graphics.shared.future_queue.clone()); + self.start_project_creation(graphics.future_queue.clone()); } }); }); @@ -1,60 +0,0 @@ -use dropbear_engine::{graphics::RenderContext, scene::SceneCommand}; -use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand}; -use winit::window::CursorGrabMode; -use crate::runtime::PlayMode; -use eucalyptus_core::scene::loading::IsSceneLoaded; - -impl CommandBufferPoller for PlayMode { - fn poll(&mut self, graphics: &mut RenderContext) { - while let Ok(cmd) = COMMAND_BUFFER.1.try_recv() { - log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd); - match cmd { - CommandBuffer::WindowCommand(w_cmd) => match w_cmd { - WindowCommand::WindowGrab(lock) => { - if lock { - let window = &graphics.shared.window; - window.set_cursor_visible(false); - if let Err(e) = window.set_cursor_grab(CursorGrabMode::Locked).or_else(|_| { - log_once::warn_once!("Using cursor grab fallback: CursorGrabMode::Confined"); - window.set_cursor_grab(CursorGrabMode::Confined) - }) { - log_once::error_once!("Unable to grab mouse: {}", e); - } - } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) { - log_once::warn_once!("Failed to release cursor: {:?}", e); - } else { - log_once::info_once!("Released cursor"); - } - } - WindowCommand::HideCursor(should_hide) => { - if should_hide { - graphics.shared.window.set_cursor_visible(false); - } else { - graphics.shared.window.set_cursor_visible(true); - } - } - }, - CommandBuffer::Quit => { - self.scene_command = SceneCommand::CloseWindow(graphics.shared.window.id()); - }, - CommandBuffer::SwitchSceneImmediate(scene_name) => { - log::debug!("Immediate scene switch requested: {}", scene_name); - let scene_to_load = IsSceneLoaded::new(scene_name); - self.request_immediate_scene_load(graphics, scene_to_load); - } - CommandBuffer::LoadSceneAsync(handle) => { - log::debug!("Load scene async requested"); - let scene_to_load = IsSceneLoaded::new_with_id(handle.scene_name, handle.id); - self.request_async_scene_load(graphics, scene_to_load); - } - CommandBuffer::SwitchToAsync(handle) => { - if let Some(ref progress) = self.scene_progress { - if progress.requested_scene == handle.scene_name && progress.is_everything_loaded() { - self.switch_to(progress.clone(), graphics); - } - } - } - } - } - } -} @@ -1,78 +0,0 @@ -use gilrs::{Button, GamepadId}; -use winit::dpi::PhysicalPosition; -use winit::event::MouseButton; -use winit::event_loop::ActiveEventLoop; -use winit::keyboard::KeyCode; -use dropbear_engine::input::{Controller, Keyboard, Mouse}; -use crate::runtime::PlayMode; - -impl Keyboard for PlayMode { - fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { - self.input_state.pressed_keys.insert(key); - } - - fn key_up(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { - self.input_state.pressed_keys.remove(&key); - } -} - -impl Mouse for PlayMode { - fn mouse_move(&mut self, position: PhysicalPosition<f64>, delta: Option<(f64, f64)>) { - let delta = if delta.is_none() { - if let Some(last_pos) = self.input_state.last_mouse_pos { - Some((position.x - last_pos.0, position.y - last_pos.1)) - } else { - None - } - } else { - delta - }; - - self.input_state.mouse_delta = delta; - self.input_state.mouse_pos = (position.x, position.y); - self.input_state.last_mouse_pos = Some(<(f64, f64)>::from(position)); - } - - fn mouse_down(&mut self, button: MouseButton) { - self.input_state.mouse_button.insert(button); - } - - fn mouse_up(&mut self, button: MouseButton) { - self.input_state.mouse_button.remove(&button); - } -} - -impl Controller for PlayMode { - fn button_down(&mut self, button: Button, id: GamepadId) { - self.input_state - .pressed_buttons - .entry(id) - .or_default() - .insert(button); - } - - fn button_up(&mut self, button: Button, id: GamepadId) { - if let Some(buttons) = self.input_state.pressed_buttons.get_mut(&id) { - buttons.remove(&button); - } - } - - fn left_stick_changed(&mut self, x: f32, y: f32, id: GamepadId) { - self.input_state.left_stick_position.insert(id, (x, y)); - } - - fn right_stick_changed(&mut self, x: f32, y: f32, id: GamepadId) { - self.input_state.right_stick_position.insert(id, (x, y)); - } - - fn on_connect(&mut self, id: GamepadId) { - self.input_state.connected_gamepads.insert(id); - } - - fn on_disconnect(&mut self, id: GamepadId) { - self.input_state.connected_gamepads.remove(&id); - self.input_state.pressed_buttons.remove(&id); - self.input_state.left_stick_position.remove(&id); - self.input_state.right_stick_position.remove(&id); - } -} @@ -1,482 +0,0 @@ -//! Allows you to a launch play mode as another window. - -use std::sync::Arc; -use crossbeam_channel::{unbounded, Receiver}; -use dropbear_engine::buffer::ResizableBuffer; -use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline; -use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; -use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; -use futures::executor; -use hecs::{Entity, World}; -use wgpu::{RenderPipeline, SurfaceConfiguration}; -use dropbear_engine::camera::Camera; -use dropbear_engine::future::FutureHandle; -use dropbear_engine::graphics::RenderContext; -use dropbear_engine::lighting::LightManager; -use dropbear_engine::scene::SceneCommand; -use dropbear_engine::shader::Shader; -use eucalyptus_core::camera::CameraComponent; -use eucalyptus_core::input::InputState; -use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; -use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT}; -use eucalyptus_core::scene::loading::SCENE_LOADER; -use eucalyptus_core::traits::registry::ComponentRegistry; -use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, PhysicsStatePtr, WorldPtr}; -use eucalyptus_core::command::COMMAND_BUFFER; -use eucalyptus_core::scene::loading::IsSceneLoaded; -use std::collections::HashMap; -use std::path::PathBuf; -use winit::window::Fullscreen; -use eucalyptus_core::physics::PhysicsState; -use eucalyptus_core::rapier3d::prelude::*; -use eucalyptus_core::register_components; - -mod scene; -mod input; -mod command; - -fn find_jvm_library_path() -> PathBuf { - let proj = PROJECT.read(); - let project_path = if !proj.project_path.is_dir() { - proj.project_path - .parent() - .expect("Unable to locate parent of project") - .to_path_buf() - } else { - proj.project_path.clone() - } - .join("../../../../build/libs"); - - let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None; - - for entry in std::fs::read_dir(&project_path).expect("Unable to read directory") { - let entry = entry.expect("Unable to get directory entry"); - let path = entry.path(); - - if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { - if filename.ends_with("-all.jar") { - let metadata = entry.metadata().expect("Unable to get file metadata"); - let modified = metadata.modified().expect("Unable to get file modified time"); - - match latest_jar { - None => latest_jar = Some((path.clone(), modified)), - Some((_, latest_time)) if modified > latest_time => { - latest_jar = Some((path.clone(), modified)); - } - _ => {} - } - } - } - } - - latest_jar - .map(|(path, _)| path) - .expect("No suitable candidate for a JVM targeted play mode session available") -} - -pub struct PlayMode { - scene_command: SceneCommand, - input_state: InputState, - script_manager: ScriptManager, - world: Box<World>, - component_registry: Arc<ComponentRegistry>, - active_camera: Option<Entity>, - - // rendering - render_pipeline: Option<RenderPipeline>, - light_manager: LightManager, - - display_settings: DisplaySettings, - - initial_scene: Option<String>, - current_scene: Option<String>, - world_loading_progress: Option<Receiver<WorldLoadingStatus>>, - world_receiver: Option<tokio::sync::oneshot::Receiver<World>>, - physics_receiver: Option<tokio::sync::oneshot::Receiver<PhysicsState>>, - scene_loading_handle: Option<FutureHandle>, - scene_progress: Option<IsSceneLoaded>, - pending_world: Option<Box<World>>, - pending_camera: Option<Entity>, - pending_physics_state: Option<Box<PhysicsState>>, - pub(crate) scripts_ready: bool, - has_initial_resize_done: bool, - - // physics - physics_pipeline: PhysicsPipeline, - physics_state: Box<PhysicsState>, - collision_event_receiver: Option<std::sync::mpsc::Receiver<CollisionEvent>>, - collision_force_event_receiver: Option<std::sync::mpsc::Receiver<ContactForceEvent>>, - event_collector: ChannelEventCollector, - - collider_wireframe_pipeline: Option<ColliderWireframePipeline>, - collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>, - collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>, -} - -impl PlayMode { - pub fn new(initial_scene: Option<String>) -> anyhow::Result<Self> { - - let mut component_registry = ComponentRegistry::new(); - - register_components(&mut component_registry); - - let (collision_event_sender, ce_r) = std::sync::mpsc::channel::<CollisionEvent>(); - let (contact_force_event_sender, cfe_r) = std::sync::mpsc::channel::<ContactForceEvent>(); - - let event_collector = ChannelEventCollector::new(collision_event_sender, contact_force_event_sender); - - let result = Self { - scene_command: SceneCommand::None, - input_state: InputState::new(), - script_manager: ScriptManager::new()?, - world: Box::new(World::new()), - initial_scene, - current_scene: None, - world_loading_progress: None, - world_receiver: None, - component_registry: Arc::new(component_registry), - scene_loading_handle: None, - scene_progress: None, - pending_world: None, - pending_camera: None, - active_camera: None, - render_pipeline: None, - light_manager: Default::default(), - scripts_ready: false, - has_initial_resize_done: false, - display_settings: DisplaySettings { - window_mode: WindowMode::Windowed, - maintain_aspect_ratio: true, - vsync: true, - }, - physics_pipeline: Default::default(), - physics_state: Box::new(PhysicsState::new()), - pending_physics_state: Default::default(), - physics_receiver: Default::default(), - collider_wireframe_pipeline: None, - collider_wireframe_geometry_cache: HashMap::new(), - collider_instance_buffer: None, - collision_event_receiver: Some(ce_r), - collision_force_event_receiver: Some(cfe_r), - event_collector, - }; - - log::debug!("Created new play mode instance"); - - Ok(result) - } - - pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: &mut RenderContext<'a>) { - let shader = Shader::new( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::SHADER_SHADER, - Some("viewport_shader"), - ); - - self.light_manager - .create_light_array_resources(graphics.shared.clone()); - - let Some(active_camera) = self.active_camera else { - log_once::warn_once!("No active camera found"); - return; - }; - - if let Ok((camera, _component)) = self - .world - .query_one::<(&Camera, &CameraComponent)>(active_camera) - .get() - { - let pipeline = graphics.create_render_pipline( - &shader, - vec![ - &graphics.shared.texture_bind_layout.clone(), - camera.layout(), - self.light_manager.layout(), - &graphics.shared.material_tint_bind_layout.clone(), - ], - None, - ); - self.render_pipeline = Some(pipeline); - - self.light_manager.create_render_pipeline( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::LIGHT_SHADER, - camera, - Some("Light Pipeline"), - ); - - self.light_manager.create_shadow_pipeline( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::SHADOW_SHADER, - Some("Shadow Pipeline"), - ); - - let collider_pipeline = ColliderWireframePipeline::new(graphics.shared.clone(), camera.layout()); - self.collider_wireframe_pipeline = Some(collider_pipeline); - } else { - log_once::warn_once!( - "Unable to query camera, component for active camera: {:?}", - active_camera - ); - } - } - - fn reload_scripts_for_current_world(&mut self) { - let mut entity_tag_map: HashMap<String, Vec<Entity>> = HashMap::new(); - for (entity_id, script) in self.world.query::<(Entity, &Script)>().iter() { - for tag in &script.tags { - entity_tag_map.entry(tag.clone()).or_default().push(entity_id); - } - } - - let target = ScriptTarget::JVM { - library_path: find_jvm_library_path(), - }; - - self.scripts_ready = false; - - if let Err(e) = self - .script_manager - .init_script(None, entity_tag_map.clone(), target.clone()) - { - panic!("Failed to initialise scripts: {}", e); - } else { - log::debug!("Initialised scripts successfully!"); - } - - let world_ptr = self.world.as_mut() as WorldPtr; - let input_ptr = &mut self.input_state as InputStatePtr; - let graphics_ptr = COMMAND_BUFFER.0.as_ref() as CommandBufferPtr; - let physics_ptr = self.physics_state.as_mut() as PhysicsStatePtr; - - if let Err(e) = self - .script_manager - .load_script(world_ptr, input_ptr, graphics_ptr, physics_ptr) - { - panic!("Failed to load scripts: {}", e); - } else { - log::debug!("Loaded scripts successfully!"); - } - - self.scripts_ready = true; - log::debug!("Scripts reloaded successfully!"); - } - - /// Requests an asynchronous scene load, returning immediately and loading the scene in the background. - pub fn request_async_scene_load(&mut self, graphics: &RenderContext, requested_scene: IsSceneLoaded) { - log::debug!("Requested async scene load: {}", requested_scene.requested_scene); - let scene_name = requested_scene.requested_scene.clone(); - self.scene_progress = Some(requested_scene); - - let (tx, rx) = unbounded::<WorldLoadingStatus>(); - let (world_tx, world_rx) = tokio::sync::oneshot::channel::<World>(); - let (physics_tx, physics_rx) = tokio::sync::oneshot::channel::<PhysicsState>(); - - self.world_loading_progress = Some(rx); - self.world_receiver = Some(world_rx); - self.physics_receiver = Some(physics_rx); - - if let Some(ref progress) = self.scene_progress { - if let Some(id) = progress.id { - let mut loader = SCENE_LOADER.lock(); - if let Some(entry) = loader.get_entry_mut(id) { - if entry.status.is_none() { - entry.status = self.world_loading_progress.as_ref().cloned(); - } - } - } - } - - let mut scene_to_load = { - let scenes = SCENES.read(); - let scene = scenes.iter().find(|s| s.scene_name == scene_name).unwrap().clone(); - scene - }; - - let graphics_cloned = graphics.shared.clone(); - let component_registry = self.component_registry.clone(); - - let handle = graphics.shared.future_queue.push(async move { - let mut temp_world = World::new(); - let load_status = scene_to_load.load_into_world( - &mut temp_world, - graphics_cloned, - Some(&component_registry), - Some(tx), - true, - ).await; - match load_status { - Ok(v) => { - if world_tx.send(temp_world).is_err() { - log::warn!("Unable to send world: Receiver has been deallocated. This usually means a new scene load was requested before this one finished."); - }; - - if physics_tx.send(scene_to_load.physics_state.clone()).is_err() { - log::warn!("Unable to send physics state: Receiver has been deallocated"); - } - - v - } - Err(e) => {panic!("Failed to load scene [{}]: {}", scene_to_load.scene_name, e);} - } - }); - - log::debug!("Created future handle for scene loading: {:?}", handle); - - self.scene_loading_handle = Some(handle); - if let Some(ref mut progress) = self.scene_progress { - progress.scene_handle_requested = true; - } - } - - /// Requests an immediate scene load, blocking the current thread until the scene is fully loaded. - pub fn request_immediate_scene_load(&mut self, graphics: &mut RenderContext, requested_scene: IsSceneLoaded) { - let scene_name = requested_scene.requested_scene.clone(); - log::debug!("Immediate scene load requested: {}", scene_name); - - self.world = Box::new(World::new()); - self.physics_state = Box::new(PhysicsState::new()); - self.physics_receiver = None; - self.active_camera = None; - self.render_pipeline = None; - self.current_scene = None; - self.world_loading_progress = None; - self.world_receiver = None; - self.scene_loading_handle = None; - self.scene_progress = None; - - let mut scene_to_load = { - let scenes = SCENES.read(); - scenes.iter() - .find(|s| s.scene_name == scene_name) - .cloned() - .expect(&format!("Scene '{}' not found", scene_name)) - }; - - let graphics_cloned = graphics.shared.clone(); - let component_registry = self.component_registry.clone(); - - let (tx, _rx) = unbounded::<WorldLoadingStatus>(); - - let (loaded_world, camera_entity, physics_state) = executor::block_on(async move { - let mut temp_world = World::new(); - let camera = scene_to_load.load_into_world( - &mut temp_world, - graphics_cloned, - Some(&component_registry), - Some(tx), - true, - ).await; - - match camera { - Ok(cam) => (temp_world, cam, scene_to_load.physics_state), - Err(e) => panic!("Failed to immediately load scene [{}]: {}", scene_to_load.scene_name, e), - } - }); - - self.world = Box::new(loaded_world); - self.physics_state = Box::new(physics_state); - self.active_camera = Some(camera_entity); - self.current_scene = Some(scene_name.clone()); - - let mut progress = requested_scene; - progress.scene_handle_requested = true; - progress.world_loaded = true; - progress.camera_received = true; - self.scene_progress = Some(progress); - - self.load_wgpu_nerdy_stuff(graphics); - - self.reload_scripts_for_current_world(); - - log::debug!("Scene '{}' loaded", scene_name); - } - - /// Switches to a new scene, clearing the current world and preparing to load the new scene. - pub fn switch_to(&mut self, scene_progress: IsSceneLoaded, graphics: &mut RenderContext) { - log::debug!("Switching to new scene requested: {}", scene_progress.requested_scene); - - if scene_progress.is_everything_loaded() { - if let Some(new_world) = self.pending_world.take() { - self.world = new_world; - } - if let Some(physics_state) = self.pending_physics_state.take() { - self.physics_state = physics_state; - } - self.has_initial_resize_done = false; - if let Some(new_camera) = self.pending_camera.take() { - self.active_camera = Some(new_camera); - } - - self.load_wgpu_nerdy_stuff(graphics); - self.reload_scripts_for_current_world(); - - self.current_scene = Some(scene_progress.requested_scene.clone()); - } - } -} - -pub struct DisplaySettings { - pub window_mode: WindowMode, - pub maintain_aspect_ratio: bool, - pub vsync: bool, -} - -impl DisplaySettings { - pub fn update(&mut self, graphics: &RenderContext) { - let window = graphics.shared.window.clone(); - - let is_maximized = window.is_maximized(); - let is_fullscreen = window.fullscreen().is_some(); - - self.window_mode = if is_fullscreen { - WindowMode::BorderlessFullscreen - } else if is_maximized { - WindowMode::Maximized - } else { - WindowMode::Windowed - }; - - match self.window_mode { - WindowMode::Windowed => { - window.set_fullscreen(None); - window.set_maximized(false); - } - WindowMode::Maximized => { - window.set_fullscreen(None); - window.set_maximized(true); - } - WindowMode::Fullscreen | WindowMode::BorderlessFullscreen => { - let monitor = window.current_monitor(); - window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); - window.set_maximized(false); - } - } - - if self.vsync { - let config = SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: graphics.shared.surface_format, - width: graphics.frame.screen_size.0 as u32, - height: graphics.frame.screen_size.1 as u32, - present_mode: if self.vsync { - wgpu::PresentMode::Fifo - } else { - wgpu::PresentMode::Immediate - }, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }; - - graphics.shared.surface.configure(&graphics.shared.device, &config); - } - } -} - -pub enum WindowMode { - Windowed, - Maximized, - Fullscreen, - BorderlessFullscreen, -} @@ -1,766 +0,0 @@ -use std::collections::HashMap; -use egui::{CentralPanel, MenuBar, TopBottomPanel}; -use eucalyptus_core::physics::collider::ColliderGroup; -use eucalyptus_core::physics::collider::ColliderShapeKey; -use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; -use glam::{DMat4, DQuat, DVec3, Quat}; -use hecs::Entity; -use wgpu::Color; -use wgpu::util::DeviceExt; -use winit::event_loop::ActiveEventLoop; -use dropbear_engine::camera::Camera; -use dropbear_engine::buffer::ResizableBuffer; -use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; -use dropbear_engine::graphics::{InstanceRaw, RenderContext}; -use dropbear_engine::lighting::{Light, LightComponent}; -use dropbear_engine::model::{DrawLight, DrawModel, DrawShadow, ModelId, MODEL_CACHE}; -use dropbear_engine::scene::{Scene, SceneCommand}; -use eucalyptus_core::camera::CameraComponent; -use eucalyptus_core::command::CommandBufferPoller; -use eucalyptus_core::hierarchy::{EntityTransformExt, Parent}; -use eucalyptus_core::physics::kcc::KCC; -use eucalyptus_core::rapier3d::prelude::QueryFilter; -use eucalyptus_core::rapier3d::geometry::SharedShape; -use eucalyptus_core::states::{Label, PROJECT}; -use eucalyptus_core::states::SCENES; -use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER}; -use crate::editor::Editor; -use crate::runtime::{PlayMode, WindowMode}; - -impl Scene for PlayMode { - fn load(&mut self, graphics: &mut RenderContext) { - if self.current_scene.is_none() { - let initial_scene = if let Some(s) = &self.initial_scene { - s.clone() - } else { - let proj = PROJECT.read(); - proj.runtime_settings.initial_scene.clone().expect("No initial scene set in project settings") - }; - - log::debug!("Loading initial scene: {}", initial_scene); - - let first_time = IsSceneLoaded::new_first_time(initial_scene); - - self.request_async_scene_load(graphics, first_time); - } - } - - fn physics_update(&mut self, dt: f32, _graphics: &mut RenderContext) { - if self.scripts_ready { - let _ = self.script_manager.physics_update_script(self.world.as_mut(), dt as f64); - } - - for kcc in self.world.query::<&mut KCC>().iter() { - kcc.collisions.clear(); - } - - for (e, l, _) in self.world.query::<(Entity, &Label, &KCC)>().iter() { - log_once::debug_once!("This entity [{:?}, label = {}] has the KCC (KinematicCharacterController) component attached", e, l); - } - - if !self.physics_state.collision_events_to_deal_with.is_empty() { - let entities_with_collisions: Vec<Entity> = self - .physics_state - .collision_events_to_deal_with - .keys() - .copied() - .collect(); - - for entity in entities_with_collisions { - let Some(collisions) = self.physics_state.collision_events_to_deal_with.remove(&entity) else { - continue; - }; - if collisions.is_empty() { - continue; - } - - if let Ok(mut kcc) = self.world.get::<&mut KCC>(entity) { - kcc.collisions = collisions.clone(); - } - - let (label, kcc_controller) = match self.world.query_one::<(&Label, &KCC)>(entity).get() { - Ok(v) => { - (v.0.clone(), v.1.clone()) - } - Err(e) => { - log_once::warn_once!("Unable to query {:?}: {}", entity, e); - continue; - } - }; - - let Some(rigid_body_handle) = self.physics_state.bodies_entity_map.get(&label).copied() else { - continue; - }; - - let Some((_, collider_handle)) = self - .physics_state - .colliders_entity_map - .get(&label) - .and_then(|handles| handles.first()) - .copied() - else { - continue; - }; - - let (character_shape, character_mass): (SharedShape, f32) = { - let Some(collider) = self.physics_state.colliders.get(collider_handle) else { - continue; - }; - - (collider.shared_shape().clone(), collider.mass()) - }; - - let character_mass = if character_mass > 0.0 { character_mass } else { 1.0 }; - - let filter = QueryFilter::default().exclude_rigid_body(rigid_body_handle); - let dispatcher = self.physics_state.narrow_phase.query_dispatcher(); - - let broad_phase = &mut self.physics_state.broad_phase; - let bodies = &mut self.physics_state.bodies; - let colliders = &mut self.physics_state.colliders; - - let mut query_pipeline_mut = broad_phase.as_query_pipeline_mut( - dispatcher, - bodies, - colliders, - filter, - ); - - kcc_controller.controller.solve_character_collision_impulses( - dt, - &mut query_pipeline_mut, - character_shape.as_ref(), - character_mass, - &collisions, - ); - } - } - - let mut entity_label_map = HashMap::new(); - for (entity, label) in self.world.query::<(Entity, &Label)>().iter() { - entity_label_map.insert(entity, label.clone()); - } - - self.physics_state.step(entity_label_map, &mut self.physics_pipeline, &(), &self.event_collector); - - if self.scripts_ready { - if let (Some(ce_r), Some(cfe_r)) = (&self.collision_event_receiver, &self.collision_force_event_receiver) { - // both are not crucial, so no need to panic - while let Ok(event) = ce_r.try_recv() { - log_once::debug_once!("Collision event received"); - if let Some(evt) = eucalyptus_core::types::CollisionEvent::from_rapier3d(&self.physics_state, event) { - if let Err(err) = self.script_manager.collision_event_script(self.world.as_mut(), &evt) { - log::error!("Script collision event error: {}", err); - } - } - } - - while let Ok(event) = cfe_r.try_recv() { - log_once::debug_once!("Contact force event received"); - if let Some(evt) = eucalyptus_core::types::ContactForceEvent::from_rapier3d(&self.physics_state, event) { - if let Err(err) = self.script_manager.contact_force_event_script(self.world.as_mut(), &evt) { - log::error!("Script contact force event error: {}", err); - } - } - } - } - } - - let mut sync_updates = Vec::new(); - - for (entity, label, _) in self.world.query::<(Entity, &Label, &EntityTransform)>().iter() { - if let Some(handle) = self.physics_state.bodies_entity_map.get(label) { - if let Some(body) = self.physics_state.bodies.get(*handle) { - let p = body.translation(); - let r = body.rotation(); - - sync_updates.push(( - entity, - DVec3::new(p.x as f64, p.y as f64, p.z as f64), - Quat::from(r.clone()).as_dquat() - )); - } - } - } - - for (entity, new_world_pos, new_world_rot) in sync_updates { - - let parent_world = if let Ok(parent_comp) = self.world.get::<&Parent>(entity) { - let parent_entity = parent_comp.parent(); - if let Ok(p_transform) = self.world.get::<&EntityTransform>(parent_entity) { - Some(p_transform.propagate(&self.world, parent_entity)) - } else { - None - } - } else { - None - }; - - if let Ok(mut entity_transform) = self.world.get::<&mut EntityTransform>(entity) { - if let Some(p_world) = parent_world { - let inv_p_rot = p_world.rotation.inverse(); - - let relative_pos = new_world_pos - p_world.position; - let new_local_pos = (inv_p_rot * relative_pos) / p_world.scale; - let new_local_rot = inv_p_rot * new_world_rot; - - let base = entity_transform.world_mut(); - base.position = new_local_pos; - base.rotation = new_local_rot; - - let offset = entity_transform.local_mut(); - offset.position = DVec3::ZERO; - offset.rotation = DQuat::IDENTITY; - } else { - let base = entity_transform.world_mut(); - base.position = new_world_pos; - base.rotation = new_world_rot; - - let offset = entity_transform.local_mut(); - offset.position = DVec3::ZERO; - offset.rotation = DQuat::IDENTITY; - } - } - } - } - - fn update(&mut self, dt: f32, graphics: &mut RenderContext) { - graphics.shared.future_queue.poll(); - self.poll(graphics); - - { - if let Some(fps) = PROJECT.read().runtime_settings.target_fps.get() { - log_once::debug_once!("setting new fps for play mode session: {}", fps); - if matches!(self.scene_command, SceneCommand::None) { - self.scene_command = SceneCommand::SetFPS(*fps); - } - } - } - - if let Some(ref progress) = self.scene_progress { - if !progress.scene_handle_requested && self.world_receiver.is_none() && self.scene_loading_handle.is_none() { - log::debug!("Starting async load for scene: {}", progress.requested_scene); - let scene_to_load = IsSceneLoaded::new(progress.requested_scene.clone()); - self.request_async_scene_load(graphics, scene_to_load); - } - } - - if let Some(mut receiver) = self.world_receiver.take() { - if let Ok(loaded_world) = receiver.try_recv() { - self.pending_world = Some(Box::new(loaded_world)); - log::debug!("World received"); - if let Some(ref mut progress) = self.scene_progress { - progress.world_loaded = true; - - if progress.camera_received { - if let Some(id) = progress.id { - let mut loader = SCENE_LOADER.lock(); - if let Some(entry) = loader.get_entry_mut(id) { - entry.result = SceneLoadResult::Success; - } - } - } - } - } else { - self.world_receiver = Some(receiver); - } - } - - if let Some(mut receiver) = self.physics_receiver.take() { - if let Ok(loaded_physics) = receiver.try_recv() { - self.pending_physics_state = Some(Box::new(loaded_physics)); - log::debug!("PhysicsState received"); - } else { - self.physics_receiver = Some(receiver); - } - } - - if let Some(handle) = self.scene_loading_handle.take() { - if let Some(cam) = graphics.shared.future_queue.exchange_owned_as::<Entity>(&handle) { - self.pending_camera = Some(cam); - log::debug!("Camera entity received: {:?}", cam); - if let Some(ref mut progress) = self.scene_progress { - progress.camera_received = true; - - if progress.world_loaded { - if let Some(id) = progress.id { - let mut loader = SCENE_LOADER.lock(); - if let Some(entry) = loader.get_entry_mut(id) { - entry.result = SceneLoadResult::Success; - } - } - } - } - } else { - self.scene_loading_handle = Some(handle) - } - } - - if let Some(ref progress) = self.scene_progress { - if progress.is_everything_loaded() { - if self.current_scene.as_ref() != Some(&progress.requested_scene) { - self.switch_to(progress.clone(), graphics); - } - } - } - - if self.scripts_ready { - if let Err(e) = self.script_manager.update_script(self.world.as_mut(), dt as f64) { - panic!("Script update error: {}", e); - } - } - - { - let mut query = self.world.query::<(&mut MeshRenderer, &Transform)>(); - for (renderer, transform) in query.iter() { - renderer.update(transform); - } - } - - { - let mut updates = Vec::new(); - for (entity, transform) in self.world.query::<(Entity, &EntityTransform)>().iter() { - let final_transform = transform.propagate(&self.world, entity); - updates.push((entity, final_transform)); - } - - for (entity, final_transform) in updates { - if let Ok(mut renderer) = self.world.get::<&mut MeshRenderer>(entity) { - renderer.update(&final_transform); - } - } - } - - { - let mut light_query = self - .world - .query::<(&mut LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>(); - - for (light_comp, transform_opt, entity_transform_opt, light) in light_query.iter() { - let transform = if let Some(entity_transform) = entity_transform_opt { - entity_transform.sync() - } else if let Some(transform) = transform_opt { - *transform - } else { - continue; - }; - - light.update(graphics.shared.as_ref(), light_comp, &transform); - } - } - - { - for (camera, component) in self - .world - .query::<(&mut Camera, &mut CameraComponent)>() - .iter() - { - component.update(camera); - camera.update(graphics.shared.clone()); - } - } - - self.light_manager - .update(graphics.shared.clone(), &self.world); - - TopBottomPanel::top("menu_bar").show(&graphics.shared.get_egui_context(), |ui| { - MenuBar::new().ui(ui, |ui| { - ui.menu_button("Window", |ui| { - ui.menu_button("Window Mode", |ui| { - let is_windowed = matches!(self.display_settings.window_mode, WindowMode::Windowed); - if ui.selectable_label(is_windowed, "Windowed").clicked() { - self.display_settings.window_mode = WindowMode::Windowed; - ui.close(); - } - - let is_maximized = matches!(self.display_settings.window_mode, WindowMode::Maximized); - if ui.selectable_label(is_maximized, "Maximized").clicked() { - self.display_settings.window_mode = WindowMode::Maximized; - ui.close(); - } - - let is_fullscreen = matches!(self.display_settings.window_mode, WindowMode::Fullscreen); - if ui.selectable_label(is_fullscreen, "Fullscreen").clicked() { - self.display_settings.window_mode = WindowMode::Fullscreen; - ui.close(); - } - - let is_borderless = matches!(self.display_settings.window_mode, WindowMode::BorderlessFullscreen); - if ui.selectable_label(is_borderless, "Borderless Fullscreen").clicked() { - self.display_settings.window_mode = WindowMode::BorderlessFullscreen; - ui.close(); - } - }); - - ui.separator(); - - ui.checkbox(&mut self.display_settings.maintain_aspect_ratio, "Maintain aspect ratio"); - ui.checkbox(&mut self.display_settings.vsync, "VSync").clicked(); - }); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.group(|ui| { - ui.add_enabled_ui(true, |ui| { - if ui.button("⏹").clicked() { - log::debug!("Menu button Stop button pressed"); - self.scene_command = SceneCommand::CloseWindow(graphics.shared.window.id()); - } - }); - - ui.add_enabled_ui(false, |ui| { - if ui.button("▶").clicked() { - log::debug!("how tf can you press this???"); - } - }); - }); - }); - }); - }); - - CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { - if let Some(p) = &self.scene_progress { - if !p.is_everything_loaded() && p.is_first_scene { - // todo: change from label to "splashscreen" - ui.centered_and_justified(|ui| { - ui.label("Loading scene..."); - }); - return; - } - } - - let texture_id = *graphics.shared.texture_id; - - let available_size = ui.available_size(); - let available_rect = ui.available_rect_before_wrap(); - - if let Some(active_camera) = self.active_camera { - if let Ok(cam) = self.world.query_one_mut::<&mut Camera>(active_camera) { - if !self.has_initial_resize_done { - cam.aspect = (available_size.x / available_size.y) as f64; - - self.has_initial_resize_done = true; - } - - if !self.display_settings.maintain_aspect_ratio { - cam.aspect = (available_size.x / available_size.y) as f64; - } - cam.update_view_proj(); - cam.update(graphics.shared.clone()); - - let (display_width, display_height) = if self.display_settings.maintain_aspect_ratio { - let width = available_size.x; - let height = width / cam.aspect as f32; - (width, height) - } else { - (available_size.x, available_size.y) - }; - - let center_x = available_rect.center().x; - let center_y = available_rect.center().y; - - let image_rect = egui::Rect::from_center_size( - egui::pos2(center_x, center_y), - egui::vec2(display_width, display_height), - ); - - ui.allocate_exact_size(available_size, egui::Sense::hover()); - - ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| { - ui.add(egui::Image::new(egui::load::SizedTexture { - id: texture_id, - size: egui::vec2(display_width, display_height), - })); - }); - } else { - log::warn!("No such camera exists in the world"); - } - } else { - log::warn!("No active camera available"); - } - }); - - self.input_state.mouse_delta = None; - } - - fn render(&mut self, graphics: &mut RenderContext) { - let Some(active_camera) = self.active_camera else { - return; - }; - log_once::debug_once!("Active camera found: {:?}", active_camera); - - let q = self.world.query_one::<&Camera>(active_camera).get().ok().cloned(); - - let Some(camera) = q else { - return; - }; - log_once::debug_once!("Camera ready"); - log_once::debug_once!("Camera currently being viewed: {}", camera.label); - - let Some(pipeline) = &self.render_pipeline else { - log_once::warn_once!("Render pipeline not ready"); - return; - }; - log_once::debug_once!("Pipeline ready"); - - let clear_color = Color { - r: 100.0 / 255.0, - g: 149.0 / 255.0, - b: 237.0 / 255.0, - a: 1.0, - }; - - let lights = { - let mut lights = Vec::new(); - let mut query = self.world.query::<(&Light, &LightComponent)>(); - for (light, comp) in query.iter() { - lights.push((light.clone(), comp.clone())); - } - lights - }; - - let renderers = { - let mut renderers = Vec::new(); - let mut query = self.world.query::<&MeshRenderer>(); - for renderer in query.iter() { - renderers.push(renderer.clone()); - } - renderers - }; - - let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new(); - for renderer in &renderers { - model_batches - .entry(renderer.model_id()) - .or_default() - .push(renderer.instance.to_raw()); - } - - let mut prepared_models = Vec::new(); - for (model_id, instances) in model_batches { - let model_opt = { - let cache = MODEL_CACHE.lock(); - cache.values().find(|model| model.id == model_id).cloned() - }; - - let Some(model) = model_opt else { - log_once::error_once!("Missing model {:?} in cache", model_id); - continue; - }; - - let instance_buffer = graphics.shared.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("Runtime Instance Buffer"), - contents: bytemuck::cast_slice(&instances), - usage: wgpu::BufferUsages::VERTEX, - }, - ); - - prepared_models.push((model, instance_buffer, instances.len() as u32)); - } - - { - let mut query = self.world.query::<( - &mut LightComponent, - Option<&dropbear_engine::entity::Transform>, - Option<&dropbear_engine::entity::EntityTransform>, - &mut Light, - )>(); - - for (light_component, transform_opt, entity_transform_opt, light) in query.iter() { - let transform = if let Some(entity_transform) = entity_transform_opt { - entity_transform.sync() - } else if let Some(transform) = transform_opt { - *transform - } else { - continue; - }; - - light.update(graphics.shared.as_ref(), light_component, &transform); - } - } - - self.light_manager.update(graphics.shared.clone(), &self.world); - - if let Some(shadow_pipeline) = &self.light_manager.shadow_pipeline { - for (light, component) in &lights { - if !component.enabled || !component.cast_shadows { - continue; - } - - let shadow_index = light.uniform().shadow_index; - if shadow_index < 0 { - continue; - } - - let shadow_index = shadow_index as usize; - if shadow_index >= self.light_manager.shadow_target_views.len() { - continue; - } - - let shadow_view = &self.light_manager.shadow_target_views[shadow_index]; - let mut shadow_pass = - graphics.begin_shadow_pass(shadow_view, Some("Shadow Pass")); - shadow_pass.set_pipeline(shadow_pipeline); - - for (model, instance_buffer, instance_count) in &prepared_models { - shadow_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - shadow_pass.draw_shadow_model_instanced( - model, - 0..*instance_count, - light.bind_group(), - ); - } - } - } - - { - let mut render_pass = graphics.clear_colour(clear_color); - if let Some(light_pipeline) = &self.light_manager.pipeline { - render_pass.set_pipeline(light_pipeline); - for (light, component) in &lights { - render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); - if component.visible { - render_pass.draw_light_model( - &light.cube_model, - camera.bind_group(), - light.bind_group(), - ); - } - } - } - } - - for (model, instance_buffer, instance_count) in prepared_models { - let mut render_pass = graphics.continue_pass(); - render_pass.set_pipeline(pipeline); - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - render_pass.draw_model_instanced( - &model, - 0..instance_count, - camera.bind_group(), - self.light_manager.bind_group(), - ); - } - - { - let show_hitboxes = self - .current_scene - .as_ref() - .and_then(|scene_name| { - let scenes = SCENES.read(); - scenes - .iter() - .find(|scene| &scene.scene_name == scene_name) - .map(|scene| scene.settings.show_hitboxes) - }) - .unwrap_or(false); - - if show_hitboxes { - if let Some(collider_pipeline) = &self.collider_wireframe_pipeline { - let mut render_pass = graphics.continue_pass(); - render_pass.set_pipeline(&collider_pipeline.pipeline); - render_pass.set_bind_group(0, camera.bind_group(), &[]); - - let mut instances_by_shape: HashMap<ColliderShapeKey, Vec<ColliderInstanceRaw>> = - HashMap::new(); - - let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>(); - for (entity_transform, group) in q.iter() { - for collider in &group.colliders { - let world_tf = entity_transform.sync(); - - let entity_matrix = DMat4::from_rotation_translation( - world_tf.rotation, - world_tf.position, - ) - .as_mat4(); - - let offset_transform = Transform::new() - .with_offset(collider.translation, collider.rotation); - let offset_matrix = offset_transform.matrix().as_mat4(); - - let final_matrix = entity_matrix * offset_matrix; - - let color = [0.0, 1.0, 0.0, 1.0]; - let instance = ColliderInstanceRaw::from_matrix(final_matrix, color); - - let key = ColliderShapeKey::from(&collider.shape); - instances_by_shape.entry(key).or_default().push(instance); - - self.collider_wireframe_geometry_cache.entry(key).or_insert_with(|| { - Editor::create_wireframe_geometry( - graphics.shared.clone(), - &collider.shape, - ) - }); - } - } - - if !instances_by_shape.is_empty() { - let total_instances: usize = - instances_by_shape.values().map(|v| v.len()).sum(); - let mut all_instances = Vec::with_capacity(total_instances); - let mut draws: Vec<(ColliderShapeKey, usize, usize)> = Vec::new(); - - for (key, instances) in instances_by_shape { - let start = all_instances.len(); - all_instances.extend_from_slice(&instances); - let count = instances.len(); - draws.push((key, start, count)); - } - - let instance_buffer = self.collider_instance_buffer.get_or_insert_with(|| { - ResizableBuffer::new( - &graphics.shared.device, - all_instances.len().max(10), - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "Collider Instance Buffer", - ) - }); - instance_buffer.write( - &graphics.shared.device, - &graphics.shared.queue, - &all_instances, - ); - - for (key, start, count) in draws { - let Some(geometry) = self.collider_wireframe_geometry_cache.get(&key) else { - continue; - }; - - let start_bytes = - (start * std::mem::size_of::<ColliderInstanceRaw>()) as wgpu::BufferAddress; - let end_bytes = - ((start + count) * std::mem::size_of::<ColliderInstanceRaw>()) as wgpu::BufferAddress; - - render_pass.set_vertex_buffer( - 1, - instance_buffer.buffer().slice(start_bytes..end_bytes), - ); - render_pass.set_vertex_buffer( - 0, - geometry.vertex_buffer.slice(..), - ); - render_pass.set_index_buffer( - geometry.index_buffer.slice(..), - wgpu::IndexFormat::Uint16, - ); - render_pass.draw_indexed( - 0..geometry.index_count, - 0, - 0..count as u32, - ); - } - } - } - } - } - } - - fn exit(&mut self, _event_loop: &ActiveEventLoop) {} - - fn run_command(&mut self) -> SceneCommand { - std::mem::replace(&mut self.scene_command, SceneCommand::None) - } -} - @@ -1,12 +1,12 @@ use crate::editor::{Editor, EditorState, Signal}; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{MeshRenderer, Transform}; -use dropbear_engine::graphics::{SharedGraphicsContext, Texture}; +use dropbear_engine::graphics::{SharedGraphicsContext}; use dropbear_engine::lighting::{Light as EngineLight, LightComponent}; use dropbear_engine::model::{LoadedModel, Material, Model, ModelId, MODEL_CACHE}; use dropbear_engine::procedural::ProcedurallyGeneratedObject; +use dropbear_engine::texture::{Texture, TextureWrapMode}; use dropbear_engine::utils::{relative_path_from_euca, EUCA_SCHEME, ResourceReference, ResourceReferenceType}; -use dropbear_engine::utils::TextureWrapMode; use egui::Align2; use eucalyptus_core::camera::{CameraComponent, CameraType}; use eucalyptus_core::scripting::{build_jvm, BuildStatus}; @@ -800,10 +800,14 @@ impl SignalController for Editor { let path = resolve_editor_path(&uri); if let Ok(bytes) = std::fs::read(&path) { - let diffuse = Texture::new_with_wrap_mode( - graphics.clone(), - &bytes, - wrap_mode, + let diffuse = Texture::from_bytes_verbose( + &graphics.device, + &graphics.queue, + &bytes, + None, + None, + None, + Some(Texture::sampler_from_wrap(wrap_mode)) ); let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY .solid_texture_rgba8( @@ -852,7 +856,15 @@ impl SignalController for Editor { } }; - let diffuse = Texture::new_with_wrap_mode(graphics.clone(), &bytes, *wrap_mode); + let diffuse = Texture::from_bytes_verbose( + &graphics.device, + &graphics.queue, + &bytes, + None, + None, + None, + Some(Texture::sampler_from_wrap(wrap_mode.clone())) + ); let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255])) .clone(); @@ -899,10 +911,14 @@ impl SignalController for Editor { let path = resolve_editor_path(&uri); if let Ok(bytes) = std::fs::read(&path) { - let diffuse = Texture::new_with_wrap_mode( - graphics.clone(), - &bytes, - *wrap_mode, + let diffuse = Texture::from_bytes_verbose( + &graphics.device, + &graphics.queue, + &bytes, + None, + None, + None, + Some(Texture::sampler_from_wrap(wrap_mode.clone())) ); material.diffuse_texture = diffuse; material.bind_group = Material::create_bind_group( @@ -3,9 +3,10 @@ use dropbear_engine::asset::ASSET_REGISTRY; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; use dropbear_engine::future::FutureQueue; -use dropbear_engine::graphics::{SharedGraphicsContext, Texture}; +use dropbear_engine::graphics::{SharedGraphicsContext}; use dropbear_engine::lighting::{Light, LightComponent}; use dropbear_engine::model::{LoadedModel, Material, Model, ModelId}; +use dropbear_engine::texture::Texture; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::scene::SceneEntity; @@ -392,10 +393,14 @@ async fn load_renderer_from_serialized( if let Ok(path) = reference.resolve() { match std::fs::read(&path) { Ok(bytes) => { - let diffuse = Texture::new_with_wrap_mode( - graphics.clone(), - &bytes, - custom.wrap_mode, + let diffuse = Texture::from_bytes_verbose( + &graphics.device, + &graphics.queue, + &bytes, + None, + None, + None, + Some(Texture::sampler_from_wrap(custom.wrap_mode)) ); let flat_normal = (*ASSET_REGISTRY .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255])) @@ -1,11 +1,12 @@ use std::{collections::VecDeque, time::Instant}; use dropbear_engine::WGPU_BACKEND; +use dropbear_engine::graphics::FrameGraphicsContext; use egui::{Color32, Context, RichText, Ui}; use egui_plot::{Legend, Line, Plot, PlotPoints}; use dropbear_engine::scene::Scene; use dropbear_engine::input::{Keyboard, Mouse, Controller}; -use dropbear_engine::graphics::RenderContext; + use winit::event_loop::ActiveEventLoop; use winit::keyboard::KeyCode; use winit::event::MouseButton; @@ -330,13 +331,13 @@ impl NerdStats { } impl Scene for NerdStats { - fn load(&mut self, _graphics: &mut RenderContext) {} - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {} - fn update(&mut self, dt: f32, _graphics: &mut RenderContext) { + fn load(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} + fn physics_update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {} + fn update(&mut self, dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { self.record_stats(dt, self.entity_count); } - fn render(&mut self, graphics: &mut RenderContext) { - self.show_window(&graphics.shared.get_egui_context()); + fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) { + self.show_window(&graphics.get_egui_context()); } fn exit(&mut self, _event_loop: &ActiveEventLoop) {} } @@ -7,8 +7,8 @@ repository.workspace = true readme = "README.md" [dependencies] -dropbear-engine = { path = "../crates/dropbear-engine" } -eucalyptus-core = { path = "../crates/eucalyptus-core", features = ["runtime"], default-features = false } +dropbear-engine = { path = "../dropbear-engine" } +eucalyptus-core = { path = "../eucalyptus-core", features = ["runtime"], default-features = false } anyhow.workspace = true log.workspace = true @@ -28,13 +28,10 @@ serde.workspace = true crossbeam-channel.workspace = true gilrs.workspace = true futures.workspace = true +glam.workspace = true +wgpu.workspace = true +egui.workspace = true [features] -default = ["eucalyptus-core/runtime"] - -# enable this to allow for JVM support -jvm = ["eucalyptus-core/jvm"] - -# enable this to enable JVM support and allow for debugging through sockets. -# WARN: ENSURE THIS IS NOT ENABLED ON DEBUG -jvm_debug = ["eucalyptus-core/jvm_debug"] +debug = [] +jvm = [] @@ -1,25 +1,29 @@ -use dropbear_engine::graphics::RenderContext; +use std::sync::Arc; + +use dropbear_engine::{scene::SceneCommand}; use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand}; use winit::window::CursorGrabMode; -use crate::scene::IsSceneLoaded; +use crate::PlayMode; +use eucalyptus_core::scene::loading::IsSceneLoaded; +use dropbear_engine::graphics::SharedGraphicsContext; -impl CommandBufferPoller for Runtime { - fn poll(&mut self, graphics: &mut RenderContext) { +impl CommandBufferPoller for PlayMode { + fn poll(&mut self, graphics: Arc<SharedGraphicsContext>) { while let Ok(cmd) = COMMAND_BUFFER.1.try_recv() { log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd); match cmd { CommandBuffer::WindowCommand(w_cmd) => match w_cmd { WindowCommand::WindowGrab(lock) => { if lock { - if let Err(e) = graphics.shared.window - .set_cursor_grab(CursorGrabMode::Confined) - .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked)) - { - log_once::warn_once!("Failed to grab cursor: {:?}", e); - } else { - log_once::info_once!("Grabbed cursor"); + let window = &graphics.window; + window.set_cursor_visible(false); + if let Err(e) = window.set_cursor_grab(CursorGrabMode::Locked).or_else(|_| { + log_once::warn_once!("Using cursor grab fallback: CursorGrabMode::Confined"); + window.set_cursor_grab(CursorGrabMode::Confined) + }) { + log_once::error_once!("Unable to grab mouse: {}", e); } - } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) { + } else if let Err(e) = graphics.clone().window.set_cursor_grab(CursorGrabMode::None) { log_once::warn_once!("Failed to release cursor: {:?}", e); } else { log_once::info_once!("Released cursor"); @@ -27,29 +31,29 @@ impl CommandBufferPoller for Runtime { } WindowCommand::HideCursor(should_hide) => { if should_hide { - graphics.shared.window.set_cursor_visible(false); + graphics.window.set_cursor_visible(false); } else { - graphics.shared.window.set_cursor_visible(true); + graphics.window.set_cursor_visible(true); } } }, CommandBuffer::Quit => { - self.scene_command = dropbear_engine::scene::SceneCommand::Quit; + self.scene_command = SceneCommand::CloseWindow(graphics.window.id()); }, CommandBuffer::SwitchSceneImmediate(scene_name) => { log::debug!("Immediate scene switch requested: {}", scene_name); let scene_to_load = IsSceneLoaded::new(scene_name); - self.request_immediate_scene_load(graphics, scene_to_load); + self.request_immediate_scene_load(graphics.clone(), scene_to_load); } CommandBuffer::LoadSceneAsync(handle) => { log::debug!("Load scene async requested"); let scene_to_load = IsSceneLoaded::new_with_id(handle.scene_name, handle.id); - self.request_async_scene_load(graphics, scene_to_load); + self.request_async_scene_load(graphics.clone(), scene_to_load); } CommandBuffer::SwitchToAsync(handle) => { if let Some(ref progress) = self.scene_progress { if progress.requested_scene == handle.scene_name && progress.is_everything_loaded() { - self.switch_to(progress.clone(), graphics); + self.switch_to(progress.clone(), graphics.clone()); } } } @@ -1,81 +0,0 @@ -use std::any::TypeId; -use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{MeshRenderer, Transform}; -use dropbear_engine::lighting::{Light, LightComponent}; -use eucalyptus_core::camera::CameraComponent; -use eucalyptus_core::states::{Label, CustomProperties, Script}; - -impl Runtime { - #[allow(dead_code)] - pub fn display_all_entities(&self) { - log::debug!("===================="); - log::info!("world total items: {}", self.world.len()); - log::info!("typeid of Label: {:?}", TypeId::of::<Label>()); - log::info!("typeid of MeshRenderer: {:?}", TypeId::of::<MeshRenderer>()); - log::info!("typeid of Transform: {:?}", TypeId::of::<Transform>()); - log::info!( - "typeid of ModelProperties: {:?}", - TypeId::of::<CustomProperties>() - ); - log::info!("typeid of Camera: {:?}", TypeId::of::<Camera>()); - log::info!( - "typeid of CameraComponent: {:?}", - TypeId::of::<CameraComponent>() - ); - log::info!("typeid of Script: {:?}", TypeId::of::<Script>()); - log::info!("typeid of Light: {:?}", TypeId::of::<Light>()); - log::info!( - "typeid of LightComponent: {:?}", - TypeId::of::<LightComponent>() - ); - for i in self.world.iter() { - log::info!("entity id: {:?}", i.entity().id()); - log::info!("entity bytes: {:?}", i.entity().to_bits().get()); - log::info!( - "components [{}]: ", - i.component_types().collect::<Vec<_>>().len() - ); - let mut comp_builder = String::new(); - for j in i.component_types() { - comp_builder.push_str(format!("{:?} ", j).as_str()); - if TypeId::of::<Label>() == j { - log::info!(" |- Label"); - } - - if TypeId::of::<MeshRenderer>() == j { - log::info!(" |- MeshRenderer"); - } - - if TypeId::of::<Transform>() == j { - log::info!(" |- Transform"); - } - - if TypeId::of::<CustomProperties>() == j { - log::info!(" |- ModelProperties"); - } - - if TypeId::of::<Camera>() == j { - log::info!(" |- Camera"); - } - - if TypeId::of::<CameraComponent>() == j { - log::info!(" |- CameraComponent"); - } - - if TypeId::of::<Script>() == j { - log::info!(" |- Script"); - } - - if TypeId::of::<Light>() == j { - log::info!(" |- Light"); - } - - if TypeId::of::<LightComponent>() == j { - log::info!(" |- LightComponent"); - } - log::info!("----------") - } - log::info!("components (typeid) [{}]: ", comp_builder); - } - } -} @@ -4,9 +4,9 @@ use winit::event::MouseButton; use winit::event_loop::ActiveEventLoop; use winit::keyboard::KeyCode; use dropbear_engine::input::{Controller, Keyboard, Mouse}; -use crate::Runtime; +use crate::PlayMode; -impl Keyboard for Runtime { +impl Keyboard for PlayMode { fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { self.input_state.pressed_keys.insert(key); } @@ -16,7 +16,7 @@ impl Keyboard for Runtime { } } -impl Mouse for Runtime { +impl Mouse for PlayMode { fn mouse_move(&mut self, position: PhysicalPosition<f64>, delta: Option<(f64, f64)>) { let delta = if delta.is_none() { if let Some(last_pos) = self.input_state.last_mouse_pos { @@ -42,7 +42,7 @@ impl Mouse for Runtime { } } -impl Controller for Runtime { +impl Controller for PlayMode { fn button_down(&mut self, button: Button, id: GamepadId) { self.input_state .pressed_buttons @@ -2,61 +2,124 @@ use std::sync::Arc; use crossbeam_channel::{unbounded, Receiver}; +use dropbear_engine::buffer::ResizableBuffer; +use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline; +use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; +use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; use futures::executor; use hecs::{Entity, World}; -use wgpu::RenderPipeline; -use dropbear_engine::camera::Camera; +use wgpu::{RenderPipeline}; use dropbear_engine::future::FutureHandle; -use dropbear_engine::graphics::RenderContext; +use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::LightManager; use dropbear_engine::scene::SceneCommand; use dropbear_engine::shader::Shader; -use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::input::InputState; use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT}; -use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER}; +use eucalyptus_core::scene::loading::SCENE_LOADER; use eucalyptus_core::traits::registry::ComponentRegistry; -use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr}; +use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, PhysicsStatePtr, WorldPtr}; use eucalyptus_core::command::COMMAND_BUFFER; +use eucalyptus_core::scene::loading::IsSceneLoaded; use std::collections::HashMap; use std::path::PathBuf; -use dropbear_engine::wgpu::RenderPipeline; +use eucalyptus_core::physics::PhysicsState; +use eucalyptus_core::rapier3d::prelude::*; +use eucalyptus_core::register_components; -mod input; mod scene; -mod debug; -mod utils; +mod input; mod command; -pub struct Runtime { +fn find_jvm_library_path() -> PathBuf { + let proj = PROJECT.read(); + let project_path = if !proj.project_path.is_dir() { + proj.project_path + .parent() + .expect("Unable to locate parent of project") + .to_path_buf() + } else { + proj.project_path.clone() + } + .join("../../../../build/libs"); + + let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None; + + for entry in std::fs::read_dir(&project_path).expect("Unable to read directory") { + let entry = entry.expect("Unable to get directory entry"); + let path = entry.path(); + + if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { + if filename.ends_with("-all.jar") { + let metadata = entry.metadata().expect("Unable to get file metadata"); + let modified = metadata.modified().expect("Unable to get file modified time"); + + match latest_jar { + None => latest_jar = Some((path.clone(), modified)), + Some((_, latest_time)) if modified > latest_time => { + latest_jar = Some((path.clone(), modified)); + } + _ => {} + } + } + } + } + + latest_jar + .map(|(path, _)| path) + .expect("No suitable candidate for a JVM targeted play mode session available") +} + +pub struct PlayMode { scene_command: SceneCommand, input_state: InputState, script_manager: ScriptManager, world: Box<World>, component_registry: Arc<ComponentRegistry>, active_camera: Option<Entity>, - + // rendering render_pipeline: Option<RenderPipeline>, light_manager: LightManager, - display_settings: DisplaySettings, - initial_scene: Option<String>, current_scene: Option<String>, world_loading_progress: Option<Receiver<WorldLoadingStatus>>, world_receiver: Option<tokio::sync::oneshot::Receiver<World>>, + physics_receiver: Option<tokio::sync::oneshot::Receiver<PhysicsState>>, scene_loading_handle: Option<FutureHandle>, scene_progress: Option<IsSceneLoaded>, pending_world: Option<Box<World>>, pending_camera: Option<Entity>, + pending_physics_state: Option<Box<PhysicsState>>, pub(crate) scripts_ready: bool, has_initial_resize_done: bool, + + // physics + physics_pipeline: PhysicsPipeline, + physics_state: Box<PhysicsState>, + collision_event_receiver: Option<std::sync::mpsc::Receiver<CollisionEvent>>, + collision_force_event_receiver: Option<std::sync::mpsc::Receiver<ContactForceEvent>>, + event_collector: ChannelEventCollector, + + collider_wireframe_pipeline: Option<ColliderWireframePipeline>, + collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>, + collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>, } -impl Runtime { +impl PlayMode { pub fn new(initial_scene: Option<String>) -> anyhow::Result<Self> { + + let mut component_registry = ComponentRegistry::new(); + + register_components(&mut component_registry); + + let (collision_event_sender, ce_r) = std::sync::mpsc::channel::<CollisionEvent>(); + let (contact_force_event_sender, cfe_r) = std::sync::mpsc::channel::<ContactForceEvent>(); + + let event_collector = ChannelEventCollector::new(collision_event_sender, contact_force_event_sender); + let result = Self { scene_command: SceneCommand::None, input_state: InputState::new(), @@ -66,7 +129,7 @@ impl Runtime { current_scene: None, world_loading_progress: None, world_receiver: None, - component_registry: Arc::new(Default::default()), + component_registry: Arc::new(component_registry), scene_loading_handle: None, scene_progress: None, pending_world: None, @@ -76,11 +139,16 @@ impl Runtime { light_manager: Default::default(), scripts_ready: false, has_initial_resize_done: false, - display_settings: DisplaySettings { - window_mode: WindowMode::Windowed, - maintain_aspect_ratio: true, - vsync: true, - }, + physics_pipeline: Default::default(), + physics_state: Box::new(PhysicsState::new()), + pending_physics_state: Default::default(), + physics_receiver: Default::default(), + collider_wireframe_pipeline: None, + collider_wireframe_geometry_cache: HashMap::new(), + collider_instance_buffer: None, + collision_event_receiver: Some(ce_r), + collision_force_event_receiver: Some(cfe_r), + event_collector, }; log::debug!("Created new play mode instance"); @@ -88,66 +156,41 @@ impl Runtime { Ok(result) } - pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: &mut RenderContext<'a>) { + pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { let shader = Shader::new( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::SHADER_SHADER, - Some("viewport_shader"), + graphics.clone(), + include_str!("../../../resources/shaders/shader.wgsl"), + Some("viewport shader"), ); self.light_manager - .create_light_array_resources(graphics.shared.clone()); - - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self - .world - .query_one::<(&Camera, &CameraComponent)>(active_camera) - { - if let Some((camera, _component)) = q.get() { - let pipeline = graphics.create_render_pipline( - &shader, - vec![ - &graphics.shared.texture_bind_layout.clone(), - camera.layout(), - self.light_manager.layout(), - &graphics.shared.material_tint_bind_layout.clone(), - ], - None, - ); - self.render_pipeline = Some(pipeline); - - self.light_manager.create_render_pipeline( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::LIGHT_SHADER, - camera, - Some("Light Pipeline"), - ); - - self.light_manager.create_shadow_pipeline( - graphics.shared.clone(), - dropbear_engine::shader::shader_wesl::SHADOW_SHADER, - Some("Shadow Pipeline"), - ); - } else { - log_once::warn_once!( - "Unable to fetch the query result of camera: {:?}", - active_camera - ) - } - } else { - log_once::warn_once!( - "Unable to query camera, component for active camera: {:?}", - active_camera - ); - } - } else { - log_once::warn_once!("No active camera found"); - } + .create_light_array_resources(graphics.clone()); + + let pipeline = graphics.create_render_pipline( + &shader, + vec![ + &graphics.layouts.texture_bind_layout, + &graphics.layouts.camera_bind_group_layout, + &graphics.layouts.light_array_bind_group_layout, + &graphics.layouts.material_tint_bind_layout, + ], + None, + ); + self.render_pipeline = Some(pipeline); + + self.light_manager.create_render_pipeline( + graphics.clone(), + include_str!("../../../resources/shaders/light.wgsl"), + Some("Light Pipeline"), + ); + + let collider_pipeline = ColliderWireframePipeline::new(graphics.clone()); + self.collider_wireframe_pipeline = Some(collider_pipeline); } fn reload_scripts_for_current_world(&mut self) { let mut entity_tag_map: HashMap<String, Vec<Entity>> = HashMap::new(); - for (entity_id, script) in self.world.query::<&Script>().iter() { + for (entity_id, script) in self.world.query::<(Entity, &Script)>().iter() { for tag in &script.tags { entity_tag_map.entry(tag.clone()).or_default().push(entity_id); } @@ -163,38 +206,42 @@ impl Runtime { .script_manager .init_script(None, entity_tag_map.clone(), target.clone()) { - log::error!("Failed to initialise scripts: {}", e); - return; + panic!("Failed to initialise scripts: {}", e); + } else { + log::debug!("Initialised scripts successfully!"); } let world_ptr = self.world.as_mut() as WorldPtr; let input_ptr = &mut self.input_state as InputStatePtr; let graphics_ptr = COMMAND_BUFFER.0.as_ref() as CommandBufferPtr; - + let physics_ptr = self.physics_state.as_mut() as PhysicsStatePtr; + if let Err(e) = self .script_manager - .load_script(world_ptr, input_ptr, graphics_ptr) + .load_script(world_ptr, input_ptr, graphics_ptr, physics_ptr) { - log::error!("Failed to load scripts: {}", e); - return; + panic!("Failed to load scripts: {}", e); + } else { + log::debug!("Loaded scripts successfully!"); } self.scripts_ready = true; - log::debug!("Scripts initialised successfully"); + log::debug!("Scripts reloaded successfully!"); } /// Requests an asynchronous scene load, returning immediately and loading the scene in the background. - pub fn request_async_scene_load(&mut self, graphics: &RenderContext, requested_scene: IsSceneLoaded) { + pub fn request_async_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) { log::debug!("Requested async scene load: {}", requested_scene.requested_scene); let scene_name = requested_scene.requested_scene.clone(); self.scene_progress = Some(requested_scene); let (tx, rx) = unbounded::<WorldLoadingStatus>(); let (world_tx, world_rx) = tokio::sync::oneshot::channel::<World>(); + let (physics_tx, physics_rx) = tokio::sync::oneshot::channel::<PhysicsState>(); self.world_loading_progress = Some(rx); self.world_receiver = Some(world_rx); - + self.physics_receiver = Some(physics_rx); if let Some(ref progress) = self.scene_progress { if let Some(id) = progress.id { @@ -207,16 +254,16 @@ impl Runtime { } } - let scene_to_load = { + let mut scene_to_load = { let scenes = SCENES.read(); let scene = scenes.iter().find(|s| s.scene_name == scene_name).unwrap().clone(); scene }; - let graphics_cloned = graphics.shared.clone(); + let graphics_cloned = graphics.clone(); let component_registry = self.component_registry.clone(); - let handle = graphics.shared.future_queue.push(async move { + let handle = graphics.future_queue.push(async move { let mut temp_world = World::new(); let load_status = scene_to_load.load_into_world( &mut temp_world, @@ -230,6 +277,11 @@ impl Runtime { if world_tx.send(temp_world).is_err() { log::warn!("Unable to send world: Receiver has been deallocated. This usually means a new scene load was requested before this one finished."); }; + + if physics_tx.send(scene_to_load.physics_state.clone()).is_err() { + log::warn!("Unable to send physics state: Receiver has been deallocated"); + } + v } Err(e) => {panic!("Failed to load scene [{}]: {}", scene_to_load.scene_name, e);} @@ -245,11 +297,13 @@ impl Runtime { } /// Requests an immediate scene load, blocking the current thread until the scene is fully loaded. - pub fn request_immediate_scene_load(&mut self, graphics: &mut RenderContext, requested_scene: IsSceneLoaded) { + pub fn request_immediate_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) { let scene_name = requested_scene.requested_scene.clone(); log::debug!("Immediate scene load requested: {}", scene_name); self.world = Box::new(World::new()); + self.physics_state = Box::new(PhysicsState::new()); + self.physics_receiver = None; self.active_camera = None; self.render_pipeline = None; self.current_scene = None; @@ -258,7 +312,7 @@ impl Runtime { self.scene_loading_handle = None; self.scene_progress = None; - let scene_to_load = { + let mut scene_to_load = { let scenes = SCENES.read(); scenes.iter() .find(|s| s.scene_name == scene_name) @@ -266,12 +320,12 @@ impl Runtime { .expect(&format!("Scene '{}' not found", scene_name)) }; - let graphics_cloned = graphics.shared.clone(); + let graphics_cloned = graphics.clone(); let component_registry = self.component_registry.clone(); let (tx, _rx) = unbounded::<WorldLoadingStatus>(); - let (loaded_world, camera_entity) = executor::block_on(async move { + let (loaded_world, camera_entity, physics_state) = executor::block_on(async move { let mut temp_world = World::new(); let camera = scene_to_load.load_into_world( &mut temp_world, @@ -282,12 +336,13 @@ impl Runtime { ).await; match camera { - Ok(cam) => (temp_world, cam), + Ok(cam) => (temp_world, cam, scene_to_load.physics_state), Err(e) => panic!("Failed to immediately load scene [{}]: {}", scene_to_load.scene_name, e), } }); self.world = Box::new(loaded_world); + self.physics_state = Box::new(physics_state); self.active_camera = Some(camera_entity); self.current_scene = Some(scene_name.clone()); @@ -305,13 +360,17 @@ impl Runtime { } /// Switches to a new scene, clearing the current world and preparing to load the new scene. - pub fn switch_to(&mut self, scene_progress: IsSceneLoaded, graphics: &mut RenderContext) { + pub fn switch_to(&mut self, scene_progress: IsSceneLoaded, graphics: Arc<SharedGraphicsContext>) { log::debug!("Switching to new scene requested: {}", scene_progress.requested_scene); if scene_progress.is_everything_loaded() { if let Some(new_world) = self.pending_world.take() { self.world = new_world; } + if let Some(physics_state) = self.pending_physics_state.take() { + self.physics_state = physics_state; + } + self.has_initial_resize_done = false; if let Some(new_camera) = self.pending_camera.take() { self.active_camera = Some(new_camera); } @@ -322,17 +381,4 @@ impl Runtime { self.current_scene = Some(scene_progress.requested_scene.clone()); } } -} - -pub struct DisplaySettings { - pub window_mode: WindowMode, - pub maintain_aspect_ratio: bool, - pub vsync: bool, -} - -pub enum WindowMode { - Windowed, - Maximized, - Fullscreen, - BorderlessFullscreen, } @@ -1,203 +0,0 @@ -use crate::scene::RuntimeScene; -use app_dirs2::AppInfo; -use dropbear_engine::future::FutureQueue; -use eucalyptus_core::runtime::RuntimeProjectConfig; -use parking_lot::RwLock; -use std::env::current_exe; -use std::fs; -use std::rc::Rc; -use std::sync::Arc; -use ron::ser::PrettyConfig; -use winit::window::WindowAttributes; -use dropbear_engine::{DropbearAppBuilder, DropbearWindowBuilder}; -use serde::{Deserialize, Serialize}; - -#[tokio::main] -async fn main() { - #[cfg(not(target_os = "android"))] - { - use chrono::offset::Local; - use colored::Colorize; - use env_logger::Builder; - use log::LevelFilter; - use parking_lot::Mutex; - use std::fs::OpenOptions; - - let log_dir = - app_dirs2::app_root(app_dirs2::AppDataType::UserData, &eucalyptus_core::APP_INFO) - .expect("Failed to get app data directory") - .join("logs"); - fs::create_dir_all(&log_dir).expect("Failed to create log dir"); - - let datetime_str = Local::now().format("%Y-%m-%d_%H-%M-%S"); - let log_filename = format!("{}.{}.log", "redback-runtime", datetime_str); - let log_path = log_dir.join(log_filename); - - let file = OpenOptions::new() - .create(true) - .append(true) - .open(&log_path) - .expect("Failed to open log file"); - let file = Mutex::new(file); - - Builder::new() - .format(move |buf, record| { - use std::io::Write; - - let ts = Local::now().format("%Y-%m-%dT%H:%M:%S"); - - let colored_level = match record.level() { - log::Level::Error => record.level().to_string().red().bold(), - log::Level::Warn => record.level().to_string().yellow().bold(), - log::Level::Info => record.level().to_string().green().bold(), - log::Level::Debug => record.level().to_string().blue().bold(), - log::Level::Trace => record.level().to_string().cyan().bold(), - }; - - let colored_timestamp = ts.to_string().bright_black(); - - let file_info = format!( - "{}:{}", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0) - ) - .bright_black(); - - let console_line = format!( - "{} {} [{}] - {}\n", - file_info, - colored_timestamp, - colored_level, - record.args() - ); - - let file_line = format!( - "{}:{} {} [{}] - {}\n", - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - ts, - record.level(), - record.args() - ); - - write!(buf, "{}", console_line).unwrap(); - - let mut fh = file.lock(); - let _ = fh.write_all(file_line.as_bytes()); - - Ok(()) - }) - .filter(Some("dropbear_engine"), LevelFilter::Trace) - .filter( - Some("redback-runtime".replace('-', "_").as_str()), - LevelFilter::Debug, - ) - .filter(Some("eucalyptus_core"), LevelFilter::Debug) - .filter(Some("dropbear_traits"), LevelFilter::Debug) - .init(); - log::info!("Initialised logger"); - } - - dropbear_engine::panic::set_hook(); - log::debug!("Set panic hook"); - - let window_config_file = current_exe().unwrap() - .parent() - .ok_or(anyhow::anyhow!( - "Unable to get parent of current executable" - )).unwrap() - .join("config.eucfg"); - log::debug!("Fetched window config file path: {}", window_config_file.display()); - - log::debug!("Reading from window config file"); - let value = fs::read(&window_config_file); - - let config = match value { - Ok(val) => { - log::debug!("Config file exists, reading contents"); - let config = ron::de::from_bytes::<ConfigFile>(val.as_slice()).unwrap(); - log::debug!("File converted to ConfigFile"); - config - } - Err(e) => { - log::warn!("Unable to read config: {}", e); - log::warn!("Creating new config file to overwrite old one"); - let cfg = ConfigFile { - jvm_args: None, - max_fps: 60, - target_resolution: WindowModes::Windowed(1920, 1080), - }; - let vec = ron::ser::to_string_pretty(&cfg, PrettyConfig::default()).unwrap(); - if let Err(e) = fs::write(&window_config_file, vec) { - log::warn!("Unable to write, still running game: {}", e); - } - cfg - } - }; - - let path = current_exe().unwrap() - .parent() - .ok_or(anyhow::anyhow!( - "Unable to locate parent folder for current executable" - )).unwrap() - .join("data.eupak"); - log::debug!("scene config (potential) file path: {}", path.display()); - - let scene_config = fs::read(&path).unwrap(); - log::debug!("Located scene config file: [{}] ({} bytes)", path.display(), scene_config.len()); - - let (scene_config, _): (RuntimeProjectConfig, usize) = - bincode::decode_from_slice(scene_config.as_slice(), bincode::config::standard()).unwrap(); - log::debug!("Converted scene config file to RuntimeProjectConfig"); - - let runtime_scene = Rc::new(RwLock::new(RuntimeScene::new(scene_config.clone(), config.clone()).unwrap())); - let future_queue = Arc::new(FutureQueue::new()); - - let authors = scene_config.authors.developer.clone(); - let project_name = scene_config.project_name.clone(); - - let name = Box::leak(project_name.into_boxed_str()); - let author = Box::leak(authors.into_boxed_str()); - - log::debug!("Loading {} by {}", name, author); - - let attributes = WindowAttributes::default(); - - match config.target_resolution { - WindowModes::Windowed(_, _) => {} - WindowModes::Maximised => {} - WindowModes::Fullscreen => {} - } - - let window = DropbearWindowBuilder::new() - .with_attributes(attributes) - .add_scene_with_input(runtime_scene, "runtime_scene") - .set_initial_scene("runtime_scene") - .build(); - - log::debug!("Running dropbear app"); - - DropbearAppBuilder::new() - .add_window(window) - .max_fps(config.max_fps) - .app_data(AppInfo { - name, - author, - }) - .with_future_queue(future_queue) - .run().await.unwrap(); -} - -#[derive(Debug, Clone, Deserialize, Serialize, bincode::Encode, bincode::Decode)] -pub struct ConfigFile { - pub jvm_args: Option<String>, - pub max_fps: u32, - pub target_resolution: WindowModes -} - -#[derive(Debug, Clone, Deserialize, Serialize, bincode::Encode, bincode::Decode)] -pub enum WindowModes { - Windowed(u32, u32), - Maximised, - Fullscreen, -} @@ -1,26 +1,36 @@ +use std::sync::Arc; + use std::collections::HashMap; -use hecs::{Entity}; +use eucalyptus_core::egui::{CentralPanel, MenuBar, TopBottomPanel}; +use eucalyptus_core::physics::collider::ColliderGroup; +use eucalyptus_core::physics::collider::ColliderShapeKey; +use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; +use glam::{DMat4, DQuat, DVec3, Quat}; +use hecs::Entity; use wgpu::Color; use wgpu::util::DeviceExt; use winit::event_loop::ActiveEventLoop; use dropbear_engine::camera::Camera; +use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; -use dropbear_engine::graphics::{InstanceRaw, RenderContext}; +use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext, FrameGraphicsContext}; use dropbear_engine::lighting::{Light, LightComponent}; -use dropbear_engine::model::{DrawLight, DrawModel, DrawShadow, ModelId, MODEL_CACHE}; +use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE}; use dropbear_engine::scene::{Scene, SceneCommand}; -use dropbear_engine::wgpu; use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::command::CommandBufferPoller; -use eucalyptus_core::egui; -use eucalyptus_core::egui::{CentralPanel, MenuBar, TopBottomPanel}; -use eucalyptus_core::hierarchy::EntityTransformExt; -use eucalyptus_core::states::PROJECT; -use eucalyptus_core::scene::loading::{SCENE_LOADER, SceneLoadResult, IsSceneLoaded}; -use crate::Runtime; - -impl Scene for Runtime { - fn load(&mut self, graphics: &mut RenderContext) { +use eucalyptus_core::hierarchy::{EntityTransformExt, Parent}; +use eucalyptus_core::physics::kcc::KCC; +use eucalyptus_core::rapier3d::prelude::QueryFilter; +use eucalyptus_core::rapier3d::geometry::SharedShape; +use eucalyptus_core::states::{Label, PROJECT}; +use eucalyptus_core::states::SCENES; +use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER}; +use crate::{PlayMode}; +use eucalyptus_core::physics::collider::shader::create_wireframe_geometry; + +impl Scene for PlayMode { + fn load(&mut self, graphics: Arc<SharedGraphicsContext>) { if self.current_scene.is_none() { let initial_scene = if let Some(s) = &self.initial_scene { s.clone() @@ -37,15 +47,203 @@ impl Scene for Runtime { } } - fn update(&mut self, dt: f32, graphics: &mut RenderContext) { - graphics.shared.future_queue.poll(); - self.poll(graphics); + fn physics_update(&mut self, dt: f32, _graphics: Arc<SharedGraphicsContext>) { + if self.scripts_ready { + let _ = self.script_manager.physics_update_script(self.world.as_mut(), dt as f64); + } + + for kcc in self.world.query::<&mut KCC>().iter() { + kcc.collisions.clear(); + } + + for (e, l, _) in self.world.query::<(Entity, &Label, &KCC)>().iter() { + log_once::debug_once!("This entity [{:?}, label = {}] has the KCC (KinematicCharacterController) component attached", e, l); + } + + if !self.physics_state.collision_events_to_deal_with.is_empty() { + let entities_with_collisions: Vec<Entity> = self + .physics_state + .collision_events_to_deal_with + .keys() + .copied() + .collect(); + + for entity in entities_with_collisions { + let Some(collisions) = self.physics_state.collision_events_to_deal_with.remove(&entity) else { + continue; + }; + if collisions.is_empty() { + continue; + } + + if let Ok(mut kcc) = self.world.get::<&mut KCC>(entity) { + kcc.collisions = collisions.clone(); + } + + let (label, kcc_controller) = match self.world.query_one::<(&Label, &KCC)>(entity).get() { + Ok(v) => { + (v.0.clone(), v.1.clone()) + } + Err(e) => { + log_once::warn_once!("Unable to query {:?}: {}", entity, e); + continue; + } + }; + + let Some(rigid_body_handle) = self.physics_state.bodies_entity_map.get(&label).copied() else { + continue; + }; + + let Some((_, collider_handle)) = self + .physics_state + .colliders_entity_map + .get(&label) + .and_then(|handles| handles.first()) + .copied() + else { + continue; + }; + + let (character_shape, character_mass): (SharedShape, f32) = { + let Some(collider) = self.physics_state.colliders.get(collider_handle) else { + continue; + }; + + (collider.shared_shape().clone(), collider.mass()) + }; + + let character_mass = if character_mass > 0.0 { character_mass } else { 1.0 }; + + let filter = QueryFilter::default().exclude_rigid_body(rigid_body_handle); + let dispatcher = self.physics_state.narrow_phase.query_dispatcher(); + + let broad_phase = &mut self.physics_state.broad_phase; + let bodies = &mut self.physics_state.bodies; + let colliders = &mut self.physics_state.colliders; + + let mut query_pipeline_mut = broad_phase.as_query_pipeline_mut( + dispatcher, + bodies, + colliders, + filter, + ); + + kcc_controller.controller.solve_character_collision_impulses( + dt, + &mut query_pipeline_mut, + character_shape.as_ref(), + character_mass, + &collisions, + ); + } + } + + let mut entity_label_map = HashMap::new(); + for (entity, label) in self.world.query::<(Entity, &Label)>().iter() { + entity_label_map.insert(entity, label.clone()); + } + + self.physics_state.step(entity_label_map, &mut self.physics_pipeline, &(), &self.event_collector); + + if self.scripts_ready { + if let (Some(ce_r), Some(cfe_r)) = (&self.collision_event_receiver, &self.collision_force_event_receiver) { + // both are not crucial, so no need to panic + while let Ok(event) = ce_r.try_recv() { + log_once::debug_once!("Collision event received"); + if let Some(evt) = eucalyptus_core::types::CollisionEvent::from_rapier3d(&self.physics_state, event) { + if let Err(err) = self.script_manager.collision_event_script(self.world.as_mut(), &evt) { + log::error!("Script collision event error: {}", err); + } + } + } + + while let Ok(event) = cfe_r.try_recv() { + log_once::debug_once!("Contact force event received"); + if let Some(evt) = eucalyptus_core::types::ContactForceEvent::from_rapier3d(&self.physics_state, event) { + if let Err(err) = self.script_manager.contact_force_event_script(self.world.as_mut(), &evt) { + log::error!("Script contact force event error: {}", err); + } + } + } + } + } + + let mut sync_updates = Vec::new(); + + for (entity, label, _) in self.world.query::<(Entity, &Label, &EntityTransform)>().iter() { + if let Some(handle) = self.physics_state.bodies_entity_map.get(label) { + if let Some(body) = self.physics_state.bodies.get(*handle) { + let p = body.translation(); + let r = body.rotation(); + + sync_updates.push(( + entity, + DVec3::new(p.x as f64, p.y as f64, p.z as f64), + Quat::from(r.clone()).as_dquat() + )); + } + } + } + + for (entity, new_world_pos, new_world_rot) in sync_updates { + + let parent_world = if let Ok(parent_comp) = self.world.get::<&Parent>(entity) { + let parent_entity = parent_comp.parent(); + if let Ok(p_transform) = self.world.get::<&EntityTransform>(parent_entity) { + Some(p_transform.propagate(&self.world, parent_entity)) + } else { + None + } + } else { + None + }; + + if let Ok(mut entity_transform) = self.world.get::<&mut EntityTransform>(entity) { + if let Some(p_world) = parent_world { + let inv_p_rot = p_world.rotation.inverse(); + + let relative_pos = new_world_pos - p_world.position; + let new_local_pos = (inv_p_rot * relative_pos) / p_world.scale; + let new_local_rot = inv_p_rot * new_world_rot; + + let base = entity_transform.world_mut(); + base.position = new_local_pos; + base.rotation = new_local_rot; + + let offset = entity_transform.local_mut(); + offset.position = DVec3::ZERO; + offset.rotation = DQuat::IDENTITY; + } else { + let base = entity_transform.world_mut(); + base.position = new_world_pos; + base.rotation = new_world_rot; + + let offset = entity_transform.local_mut(); + offset.position = DVec3::ZERO; + offset.rotation = DQuat::IDENTITY; + } + } + } + } + + fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>) { + graphics.future_queue.poll(); + self.poll(graphics.clone()); + + { + if let Some(fps) = PROJECT.read().runtime_settings.target_fps.get() { + log_once::debug_once!("setting new fps for play mode session: {}", fps); + if matches!(self.scene_command, SceneCommand::None) { + self.scene_command = SceneCommand::SetFPS(*fps); + } + } + } if let Some(ref progress) = self.scene_progress { if !progress.scene_handle_requested && self.world_receiver.is_none() && self.scene_loading_handle.is_none() { log::debug!("Starting async load for scene: {}", progress.requested_scene); let scene_to_load = IsSceneLoaded::new(progress.requested_scene.clone()); - self.request_async_scene_load(graphics, scene_to_load); + self.request_async_scene_load(graphics.clone(), scene_to_load); } } @@ -70,8 +268,17 @@ impl Scene for Runtime { } } + if let Some(mut receiver) = self.physics_receiver.take() { + if let Ok(loaded_physics) = receiver.try_recv() { + self.pending_physics_state = Some(Box::new(loaded_physics)); + log::debug!("PhysicsState received"); + } else { + self.physics_receiver = Some(receiver); + } + } + if let Some(handle) = self.scene_loading_handle.take() { - if let Some(cam) = graphics.shared.future_queue.exchange_owned_as::<Entity>(&handle) { + if let Some(cam) = graphics.future_queue.exchange_owned_as::<Entity>(&handle) { self.pending_camera = Some(cam); log::debug!("Camera entity received: {:?}", cam); if let Some(ref mut progress) = self.scene_progress { @@ -94,27 +301,27 @@ impl Scene for Runtime { if let Some(ref progress) = self.scene_progress { if progress.is_everything_loaded() { if self.current_scene.as_ref() != Some(&progress.requested_scene) { - self.switch_to(progress.clone(), graphics); + self.switch_to(progress.clone(), graphics.clone()); } } } if self.scripts_ready { - if let Err(e) = self.script_manager.update_script(self.world.as_mut(), dt) { + if let Err(e) = self.script_manager.update_script(self.world.as_mut(), dt as f64) { panic!("Script update error: {}", e); } } { let mut query = self.world.query::<(&mut MeshRenderer, &Transform)>(); - for (_entity, (renderer, transform)) in query.iter() { + for (renderer, transform) in query.iter() { renderer.update(transform); } } { let mut updates = Vec::new(); - for (entity, transform) in self.world.query::<&EntityTransform>().iter() { + for (entity, transform) in self.world.query::<(Entity, &EntityTransform)>().iter() { let final_transform = transform.propagate(&self.world, entity); updates.push((entity, final_transform)); } @@ -131,7 +338,7 @@ impl Scene for Runtime { .world .query::<(&mut LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>(); - for (_, (light_comp, transform_opt, entity_transform_opt, light)) in light_query.iter() { + for (light_comp, transform_opt, entity_transform_opt, light) in light_query.iter() { let transform = if let Some(entity_transform) = entity_transform_opt { entity_transform.sync() } else if let Some(transform) = transform_opt { @@ -140,25 +347,46 @@ impl Scene for Runtime { continue; }; - light.update(graphics.shared.as_ref(), light_comp, &transform); + light.update(graphics.as_ref(), light_comp, &transform); } } { - for (_entity_id, (camera, component)) in self + for (camera, component) in self .world .query::<(&mut Camera, &mut CameraComponent)>() .iter() { component.update(camera); - camera.update(graphics.shared.clone()); + camera.update(graphics.clone()); } } self.light_manager - .update(graphics.shared.clone(), &self.world); + .update(graphics.clone(), &self.world); + + TopBottomPanel::top("menu_bar").show(&graphics.get_egui_context(), |ui| { + MenuBar::new().ui(ui, |ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.group(|ui| { + ui.add_enabled_ui(true, |ui| { + if ui.button("⏹").clicked() { + log::debug!("Menu button Stop button pressed"); + self.scene_command = SceneCommand::CloseWindow(graphics.window.id()); + } + }); - CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { + ui.add_enabled_ui(false, |ui| { + if ui.button("▶").clicked() { + log::debug!("how tf can you press this???"); + } + }); + }); + }); + }); + }); + + CentralPanel::default().show(&graphics.get_egui_context(), |ui| { if let Some(p) = &self.scene_progress { if !p.is_everything_loaded() && p.is_first_scene { // todo: change from label to "splashscreen" @@ -169,7 +397,7 @@ impl Scene for Runtime { } } - let texture_id = *graphics.shared.texture_id; + let texture_id = *graphics.texture_id; let available_size = ui.available_size(); let available_rect = ui.available_rect_before_wrap(); @@ -182,19 +410,20 @@ impl Scene for Runtime { self.has_initial_resize_done = true; } - if !self.display_settings.maintain_aspect_ratio { - cam.aspect = (available_size.x / available_size.y) as f64; - } + // if !self.display_settings.maintain_aspect_ratio { + // cam.aspect = (available_size.x / available_size.y) as f64; + // } cam.update_view_proj(); - cam.update(graphics.shared.clone()); - - let (display_width, display_height) = if self.display_settings.maintain_aspect_ratio { - let width = available_size.x; - let height = width / cam.aspect as f32; - (width, height) - } else { - (available_size.x, available_size.y) - }; + cam.update(graphics.clone()); + + let (display_width, display_height) = (available_size.x, available_size.y); + // if self.display_settings.maintain_aspect_ratio { + // let width = available_size.x; + // let height = width / cam.aspect as f32; + // (width, height) + // } else { + + // }; let center_x = available_rect.center().x; let center_y = available_rect.center().y; @@ -223,25 +452,13 @@ impl Scene for Runtime { self.input_state.mouse_delta = None; } - fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) { - if self.scripts_ready { - if let Err(e) = self.script_manager.physics_update_script(self.world.as_mut(), _dt) { - panic!("Script physics update error: {}", e); - } - } - } - - fn render(&mut self, graphics: &mut RenderContext) { + fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) { let Some(active_camera) = self.active_camera else { return; }; log_once::debug_once!("Active camera found: {:?}", active_camera); - let q = if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) { - query.get().cloned() - } else { - None - }; + let q = self.world.query_one::<&Camera>(active_camera).get().ok().cloned(); let Some(camera) = q else { return; @@ -265,36 +482,27 @@ impl Scene for Runtime { let lights = { let mut lights = Vec::new(); let mut query = self.world.query::<(&Light, &LightComponent)>(); - for (_, (light, comp)) in query.iter() { + for (light, comp) in query.iter() { lights.push((light.clone(), comp.clone())); } lights }; - let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new(); - { - let mut query = self.world.query::<( - &mut MeshRenderer, - Option<&dropbear_engine::entity::Transform>, - Option<&dropbear_engine::entity::EntityTransform>, - )>(); - - for (_, (renderer, transform_opt, entity_transform_opt)) in query.iter() { - let transform = if let Some(entity_transform) = entity_transform_opt { - entity_transform.sync() - } else if let Some(transform) = transform_opt { - *transform - } else { - continue; - }; - - renderer.update(&transform); - - model_batches - .entry(renderer.model_id()) - .or_default() - .push(renderer.instance.to_raw()); + let renderers = { + let mut renderers = Vec::new(); + let mut query = self.world.query::<&MeshRenderer>(); + for renderer in query.iter() { + renderers.push(renderer.clone()); } + renderers + }; + + let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new(); + for renderer in &renderers { + model_batches + .entry(renderer.model_id()) + .or_default() + .push(renderer.instance.to_raw()); } let mut prepared_models = Vec::new(); @@ -309,7 +517,7 @@ impl Scene for Runtime { continue; }; - let instance_buffer = graphics.shared.device.create_buffer_init( + let instance_buffer = graphics.device.create_buffer_init( &wgpu::util::BufferInitDescriptor { label: Some("Runtime Instance Buffer"), contents: bytemuck::cast_slice(&instances), @@ -328,7 +536,7 @@ impl Scene for Runtime { &mut Light, )>(); - for (_, (light_component, transform_opt, entity_transform_opt, light)) in query.iter() { + for (light_component, transform_opt, entity_transform_opt, light) in query.iter() { let transform = if let Some(entity_transform) = entity_transform_opt { entity_transform.sync() } else if let Some(transform) = transform_opt { @@ -337,74 +545,221 @@ impl Scene for Runtime { continue; }; - light.update(graphics.shared.as_ref(), light_component, &transform); + light.update(graphics.as_ref(), light_component, &transform); } } - self.light_manager.update(graphics.shared.clone(), &self.world); - - if let Some(shadow_pipeline) = &self.light_manager.shadow_pipeline { - for (light, component) in &lights { - if !component.enabled || !component.cast_shadows { - continue; - } - - let shadow_index = light.uniform().shadow_index; - if shadow_index < 0 { - continue; - } - - let shadow_index = shadow_index as usize; - if shadow_index >= self.light_manager.shadow_target_views.len() { - continue; - } - - let shadow_view = &self.light_manager.shadow_target_views[shadow_index]; - let mut shadow_pass = - graphics.begin_shadow_pass(shadow_view, Some("Shadow Pass")); - shadow_pass.set_pipeline(shadow_pipeline); - - for (model, instance_buffer, instance_count) in &prepared_models { - shadow_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - shadow_pass.draw_shadow_model_instanced( - model, - 0..*instance_count, - light.bind_group(), - ); - } - } - } + self.light_manager.update(graphics.clone(), &self.world); { - let mut render_pass = graphics.clear_colour(clear_color); + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("light cube render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame_ctx.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(clear_color), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(0.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }); if let Some(light_pipeline) = &self.light_manager.pipeline { render_pass.set_pipeline(light_pipeline); for (light, component) in &lights { - if let Some(buffer) = &light.instance_buffer { - render_pass.set_vertex_buffer(1, buffer.slice(..)); - if component.visible { - render_pass.draw_light_model( - &light.cube_model, - camera.bind_group(), - light.bind_group(), - ); - } + render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); + if component.visible { + render_pass.draw_light_model( + &light.cube_model, + &camera.bind_group, + &light.bind_group + ); } } } } for (model, instance_buffer, instance_count) in prepared_models { - let mut render_pass = graphics.continue_pass(); + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame_ctx.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }); render_pass.set_pipeline(pipeline); render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); render_pass.draw_model_instanced( &model, 0..instance_count, - camera.bind_group(), + &camera.bind_group, self.light_manager.bind_group(), ); } + + { + let show_hitboxes = self + .current_scene + .as_ref() + .and_then(|scene_name| { + let scenes = SCENES.read(); + scenes + .iter() + .find(|scene| &scene.scene_name == scene_name) + .map(|scene| scene.settings.show_hitboxes) + }) + .unwrap_or(false); + + if show_hitboxes { + if let Some(collider_pipeline) = &self.collider_wireframe_pipeline { + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame_ctx.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }); + + render_pass.set_pipeline(&collider_pipeline.pipeline); + render_pass.set_bind_group(0, &camera.bind_group, &[]); + + let mut instances_by_shape: HashMap<ColliderShapeKey, Vec<ColliderInstanceRaw>> = + HashMap::new(); + + let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>(); + for (entity_transform, group) in q.iter() { + for collider in &group.colliders { + let world_tf = entity_transform.sync(); + + let entity_matrix = DMat4::from_rotation_translation( + world_tf.rotation, + world_tf.position, + ) + .as_mat4(); + + let offset_transform = Transform::new() + .with_offset(collider.translation, collider.rotation); + let offset_matrix = offset_transform.matrix().as_mat4(); + + let final_matrix = entity_matrix * offset_matrix; + + let color = [0.0, 1.0, 0.0, 1.0]; + let instance = ColliderInstanceRaw::from_matrix(final_matrix, color); + + let key = ColliderShapeKey::from(&collider.shape); + instances_by_shape.entry(key).or_default().push(instance); + + self.collider_wireframe_geometry_cache.entry(key).or_insert_with(|| { + create_wireframe_geometry( + graphics.clone(), + &collider.shape, + ) + }); + } + } + + if !instances_by_shape.is_empty() { + let total_instances: usize = + instances_by_shape.values().map(|v| v.len()).sum(); + let mut all_instances = Vec::with_capacity(total_instances); + let mut draws: Vec<(ColliderShapeKey, usize, usize)> = Vec::new(); + + for (key, instances) in instances_by_shape { + let start = all_instances.len(); + all_instances.extend_from_slice(&instances); + let count = instances.len(); + draws.push((key, start, count)); + } + + let instance_buffer = self.collider_instance_buffer.get_or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + all_instances.len().max(10), + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + "Collider Instance Buffer", + ) + }); + instance_buffer.write( + &graphics.device, + &graphics.queue, + &all_instances, + ); + + for (key, start, count) in draws { + let Some(geometry) = self.collider_wireframe_geometry_cache.get(&key) else { + continue; + }; + + let start_bytes = + (start * std::mem::size_of::<ColliderInstanceRaw>()) as wgpu::BufferAddress; + let end_bytes = + ((start + count) * std::mem::size_of::<ColliderInstanceRaw>()) as wgpu::BufferAddress; + + render_pass.set_vertex_buffer( + 1, + instance_buffer.buffer().slice(start_bytes..end_bytes), + ); + render_pass.set_vertex_buffer( + 0, + geometry.vertex_buffer.slice(..), + ); + render_pass.set_index_buffer( + geometry.index_buffer.slice(..), + wgpu::IndexFormat::Uint16, + ); + render_pass.draw_indexed( + 0..geometry.index_count, + 0, + 0..count as u32, + ); + } + } + } + } + } } fn exit(&mut self, _event_loop: &ActiveEventLoop) {} @@ -412,4 +767,5 @@ impl Scene for Runtime { fn run_command(&mut self) -> SceneCommand { std::mem::replace(&mut self.scene_command, SceneCommand::None) } -} +} + @@ -0,0 +1,45 @@ +struct CameraUniform { + view_pos: vec4<f32>, + view_proj: mat4x4<f32>, +}; + +@group(0) @binding(0) +var <uniform> camera: CameraUniform; + +struct VertexInput { + @location(0) position: vec3<f32>, + @location(1) model_0: vec4<f32>, + @location(2) model_1: vec4<f32>, + @location(3) model_2: vec4<f32>, + @location(4) model_3: vec4<f32>, + @location(5) color: vec4<f32>, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) color: vec4<f32>, +} + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + let model = mat4x4<f32>( + input.model_0, + input.model_1, + input.model_2, + input.model_3, + ); + + let world_position = model * vec4<f32>(input.position, 1.0); + output.clip_position = camera.view_proj * world_position; + + output.color = input.color; + + return output; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { + return input.color; +} @@ -0,0 +1,62 @@ +// Shader for rendering the white cube for lighting (or diff depending on colour) + +struct Light { + position: vec4<f32>, + direction: vec4<f32>, // x, y, z, outer_cutoff_angle + color: vec4<f32>, // r, g, b, light_type (0, 1, 2) + constant: f32, + lin: f32, + quadratic: f32, + cutoff: f32, +} + +struct CameraUniform { + view_pos: vec4<f32>, + view_proj: mat4x4<f32>, +}; + +@group(0) @binding(0) +var<uniform> camera: CameraUniform; + +@group(1) @binding(0) +var<uniform> light: Light; + +struct InstanceInput { + @location(5) model_matrix_0: vec4<f32>, + @location(6) model_matrix_1: vec4<f32>, + @location(7) model_matrix_2: vec4<f32>, + @location(8) model_matrix_3: vec4<f32>, +} + +struct VertexInput { + @location(0) position: vec3<f32>, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) color: vec3<f32>, +}; + +@vertex +fn vs_main( + model: VertexInput, + instance: InstanceInput, +) -> VertexOutput { + let model_matrix = mat4x4<f32>( + instance.model_matrix_0, + instance.model_matrix_1, + instance.model_matrix_2, + instance.model_matrix_3, + ); + var out: VertexOutput; + out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0); + out.color = light.color.xyz; + return out; +} + +// Fragment shader + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + return vec4<f32>(in.color, 1.0); +} @@ -0,0 +1,245 @@ +// Main shader for standard objects. + +const MAX_LIGHTS: u32 = 8; + +struct CameraUniform { + view_pos: vec4<f32>, + view_proj: mat4x4<f32>, +}; + +struct Light { + position: vec4<f32>, + direction: vec4<f32>, // x, y, z, outer_cutoff_angle + color: vec4<f32>, // r, g, b, light_type (0, 1, 2) + constant: f32, + lin: f32, + quadratic: f32, + cutoff: f32, +} + +struct LightArray { + _lights: array<Light, MAX_LIGHTS>, + light_count: u32, + ambient_strength: f32, +} + +struct MaterialUniform { + // for stuff like tinting + colour: vec4<f32>, + + // scales incoming UVs before sampling + uv_tiling: vec2<f32>, + _pad: vec2<f32>, +} + +@group(0) @binding(0) +var t_diffuse: texture_2d<f32>; +@group(0) @binding(1) +var s_diffuse: sampler; +@group(0) @binding(2) +var t_normal: texture_2d<f32>; +@group(0) @binding(3) +var s_normal: sampler; + +@group(3) @binding(0) +var<uniform> u_material: MaterialUniform; + +@group(1) @binding(0) +var<uniform> camera: CameraUniform; + +@group(2) @binding(0) +var<storage, read> light_array: LightArray; + +struct InstanceInput { + @location(5) model_matrix_0: vec4<f32>, + @location(6) model_matrix_1: vec4<f32>, + @location(7) model_matrix_2: vec4<f32>, + @location(8) model_matrix_3: vec4<f32>, + + @location(9) normal_matrix_0: vec3<f32>, + @location(10) normal_matrix_1: vec3<f32>, + @location(11) normal_matrix_2: vec3<f32>, +}; + +struct VertexInput { + @location(0) position: vec3<f32>, + @location(1) tex_coords: vec2<f32>, + @location(2) normal: vec3<f32>, + @location(3) tangent: vec3<f32>, + @location(4) bitangent: vec3<f32>, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) tex_coords: vec2<f32>, + @location(1) world_normal: vec3<f32>, + @location(2) world_position: vec3<f32>, + @location(3) world_tangent: vec3<f32>, + @location(4) world_bitangent: vec3<f32>, +}; + +@vertex +fn vs_main( + model: VertexInput, + instance: InstanceInput, +) -> VertexOutput { + let model_matrix = mat4x4<f32>( + instance.model_matrix_0, + instance.model_matrix_1, + instance.model_matrix_2, + instance.model_matrix_3, + ); + let normal_matrix = mat3x3<f32>( + instance.normal_matrix_0, + instance.normal_matrix_1, + instance.normal_matrix_2, + ); + + let world_normal = normalize(normal_matrix * model.normal); + let world_tangent = normalize(normal_matrix * model.tangent); + let world_bitangent = normalize(normal_matrix * model.bitangent); + let world_position = model_matrix * vec4<f32>(model.position, 1.0); + + var out: VertexOutput; + out.clip_position = camera.view_proj * world_position; + out.tex_coords = model.tex_coords; + out.world_normal = world_normal; + out.world_position = world_position.xyz; + out.world_tangent = world_tangent; + out.world_bitangent = world_bitangent; + return out; +} + +fn directional_light( + light: Light, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32>, + world_pos: vec3<f32> +) -> vec3<f32> { + let light_dir = normalize(-light.direction.xyz); + + let ambient = light.color.xyz * light_array.ambient_strength * tex_color; + + let diff = max(dot(world_normal, light_dir), 0.0); + let diffuse = light.color.xyz * diff * tex_color; + + let reflect_dir = reflect(-light_dir, world_normal); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + let specular = light.color.xyz * spec * tex_color; + + return ambient + diffuse + specular; +} + +fn point_light( + light: Light, + world_pos: vec3<f32>, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32> +) -> vec3<f32> { + let light_dir = normalize(light.position.xyz - world_pos); + + let distance = length(light.position.xyz - world_pos); + let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); + + let ambient = light.color.xyz * light_array.ambient_strength * tex_color; + + let diff = max(dot(world_normal, light_dir), 0.0); + let diffuse = light.color.xyz * diff * tex_color; + + let reflect_dir = reflect(-light_dir, world_normal); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + let specular = light.color.xyz * spec * tex_color; + + return (ambient + diffuse + specular) * attenuation; +} + +fn spot_light( + light: Light, + world_pos: vec3<f32>, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32> +) -> vec3<f32> { + let light_dir = normalize(light.position.xyz - world_pos); + let theta = dot(light_dir, normalize(-light.direction.xyz)); + let outer_cutoff = light.direction.w; + + let epsilon = light.cutoff - outer_cutoff; + let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); + + let distance = length(light.position.xyz - world_pos); + let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); + + let ambient = light.color.xyz * light_array.ambient_strength * tex_color; + + let diff = max(dot(world_normal, light_dir), 0.0); + let diffuse = light.color.xyz * diff * tex_color * intensity; + + let reflect_dir = reflect(-light_dir, world_normal); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + let specular = light.color.xyz * spec * tex_color * intensity; + + return (ambient + diffuse + specular) * attenuation; +} + +fn apply_normal_map( + world_normal_in: vec3<f32>, + world_tangent_in: vec3<f32>, + world_bitangent_in: vec3<f32>, + normal_sample_rgb: vec3<f32>, +) -> vec3<f32> { + let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0)); + + let n = normalize(world_normal_in); + var t = normalize(world_tangent_in); + t = normalize(t - n * dot(n, t)); + + let b_in = normalize(world_bitangent_in); + let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0); + let b = cross(n, t) * handedness; + + let tbn = mat3x3<f32>(t, b, n); + return normalize(tbn * normal_ts); +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + let uv = in.tex_coords * u_material.uv_tiling; + var tex_color = textureSample(t_diffuse, s_diffuse, uv); + var object_normal = textureSample(t_normal, s_normal, uv); + + let base_colour = tex_color * u_material.colour; + + if (base_colour.a < 0.1) { + discard; + } + + let view_dir = normalize(camera.view_pos.xyz - in.world_position); + + let world_normal = apply_normal_map( + in.world_normal, + in.world_tangent, + in.world_bitangent, + object_normal.xyz, + ); + + var final_color = vec3<f32>(0.0); + + for (var i: u32 = 0u; i < light_array.light_count; i = i + 1u) { + let light = light_array._lights[i]; + + let light_type = i32(light.color.w + 0.1); + + if (light_type == 0) { + final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); + } else if (light_type == 1) { + final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); + } else if (light_type == 2) { + final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); + } + } + + return vec4<f32>(final_color, base_colour.a); +}