tirbofish/dropbear · diff
Merge pull request #85 from tirbofish/engine-refactor
refactor: rendering refactor
jarvis, run github actions
Signature present but could not be verified.
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] @@ -80,6 +72,7 @@ semver = { version = "1.0", features = ["serde"] } rapier3d = { version = "0.32", features = [ "simd-stable", "serde-serialize" ] } cbindgen = { version = "0.29.2" } postcard = { version = "1.1"} +pollster = "0.4" [workspace.dependencies.image] version = "0.25" @@ -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" } @@ -43,6 +42,7 @@ ron.workspace = true dashmap.workspace = true typetag.workspace = true postcard.workspace = true +pollster.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -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) => { @@ -30,7 +30,7 @@ impl Vertex { } } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct ResizableBuffer<T> { buffer: wgpu::Buffer, capacity: usize, @@ -39,6 +39,85 @@ 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: wgpu::BufferUsages::UNIFORM | wgpu::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 + } +} + +/// A wrapper to a [wgpu::Buffer] that stores +#[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: wgpu::BufferUsages::STORAGE | wgpu::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, @@ -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,26 +1,21 @@ -use crate::shader::Shader; +use crate::{BindGroupLayouts, texture}; use crate::{ State, egui_renderer::EguiRenderer, - model::{self, Vertex}, }; 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 +24,19 @@ 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), + pub supports_storage: bool, } 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,621 +48,33 @@ impl SharedGraphicsContext { count: None, }], label: Some("model_uniform_bind_group_layout"), - }) - } -} - -impl<'a> RenderContext<'a> { - pub fn from_state( - state: &'a mut State, - view: &'a TextureView, - encoder: &'a mut CommandEncoder, - ) -> 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, - }, - } - } - - pub fn create_render_pipline( - &self, - shader: &Shader, - bind_group_layouts: Vec<&BindGroupLayout>, - label: Option<&str>, - ) -> RenderPipeline { - let render_pipeline_layout = - self.shared - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(label.unwrap_or("Render Pipeline Descriptor")), - bind_group_layouts: bind_group_layouts.as_slice(), - push_constant_ranges: &[], - }); - - let render_pipeline = - self.shared - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(label.unwrap_or("Render Pipeline")), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader.module, - entry_point: Some("vs_main"), - buffers: &[model::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::Back), // todo: change for improved performance - cull_mode: None, - 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: CompareFunction::Greater, - stencil: StencilState::default(), - bias: DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }); - 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() + pub fn get_egui_context(&self) -> Context { + self.egui_renderer.lock().context().clone() } } -#[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, +impl SharedGraphicsContext { + pub(crate) fn from_state( + state: &State, ) -> 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, + 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, + supports_storage: state.supports_storage, } } - - /// 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 +82,6 @@ pub struct Instance { pub position: DVec3, pub rotation: DQuat, pub scale: DVec3, - - buffer: Option<Buffer>, } impl Instance { @@ -709,7 +90,6 @@ impl Instance { position, rotation, scale, - buffer: None, } } @@ -722,21 +102,29 @@ 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, } } } +/// Maps to +/// ```wgsl +/// 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>, +/// }; +/// ``` #[repr(C)] #[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)] pub struct InstanceRaw { @@ -750,38 +138,46 @@ impl InstanceRaw { array_stride: size_of::<InstanceRaw>() as BufferAddress, step_mode: wgpu::VertexStepMode::Instance, attributes: &[ - // model + // model_matrix_0 wgpu::VertexAttribute { offset: 0, shader_location: 5, format: wgpu::VertexFormat::Float32x4, }, + // model_matrix_1 wgpu::VertexAttribute { offset: size_of::<[f32; 4]>() as wgpu::BufferAddress, shader_location: 6, format: wgpu::VertexFormat::Float32x4, }, + // model_matrix_2 wgpu::VertexAttribute { offset: size_of::<[f32; 8]>() as wgpu::BufferAddress, shader_location: 7, format: wgpu::VertexFormat::Float32x4, }, + // model_matrix_3 wgpu::VertexAttribute { offset: size_of::<[f32; 12]>() as wgpu::BufferAddress, shader_location: 8, format: wgpu::VertexFormat::Float32x4, }, - // normal + + // normal_matrix_0 wgpu::VertexAttribute { offset: size_of::<[f32; 16]>() as wgpu::BufferAddress, shader_location: 9, format: wgpu::VertexFormat::Float32x3, }, + + // normal_matrix_1 wgpu::VertexAttribute { offset: size_of::<[f32; 19]>() as wgpu::BufferAddress, shader_location: 10, format: wgpu::VertexFormat::Float32x3, }, + + // normal_matrix_2 wgpu::VertexAttribute { offset: size_of::<[f32; 22]>() as wgpu::BufferAddress, shader_location: 11, @@ -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,12 @@ pub mod resources; pub mod scene; pub mod shader; pub mod utils; +pub mod texture; +pub mod pipelines; 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; @@ -31,7 +30,6 @@ use dropbear_future_queue::FutureQueue; use egui::TextureId; use egui_wgpu::ScreenDescriptor; use env_logger::Builder; -use futures::executor::block_on; use gilrs::{Gilrs, GilrsBuilder}; use log::LevelFilter; use parking_lot::{Mutex, RwLock}; @@ -41,7 +39,7 @@ use std::sync::OnceLock; use std::{fs, sync::Arc, time::{Duration, Instant}}; use std::collections::HashMap; use std::rc::Rc; -use wgpu::{BindGroupLayout, Device, ExperimentalFeatures, Instance, Queue, Surface, SurfaceConfiguration, SurfaceError, TextureFormat}; +use wgpu::{BindGroupLayout, BindGroupLayoutEntry, BindingType, BufferBindingType, Device, ExperimentalFeatures, Instance, Queue, ShaderStages, Surface, SurfaceConfiguration, SurfaceError, TextureFormat}; use winit::event::{DeviceEvent, DeviceId}; use winit::{ application::ApplicationHandler, @@ -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}; +use crate::texture::Texture; +use crate::{egui_renderer::EguiRenderer}; pub use dropbear_future_queue as future; pub use gilrs; @@ -60,11 +62,22 @@ pub use winit; use winit::window::{WindowAttributes, WindowId}; use crate::scene::Scene; +pub struct BindGroupLayouts { + pub shader_globals_bind_group_layout: BindGroupLayout, + 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 pub window: Arc<Window>, pub instance: Arc<Instance>, + pub supports_storage: bool, pub surface: Arc<Surface<'static>>, pub surface_format: TextureFormat, @@ -72,10 +85,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 +97,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 `shaders.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(); @@ -117,17 +137,30 @@ impl State { }) .await?; + let limits = wgpu::Limits { + max_bind_groups: 8, + ..Default::default() + }; + let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { label: Some(format!("{} graphics device", title).as_str()), required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::default(), + required_limits: limits, experimental_features: unsafe { ExperimentalFeatures::enabled() }, memory_hints: Default::default(), trace: wgpu::Trace::Off, }) .await?; + let supports_storage_resources = adapter + .get_downlevel_capabilities() + .flags + .contains(wgpu::DownlevelFlags::VERTEX_STORAGE) + && device.limits().max_storage_buffers_per_shader_stage > 0; + + log::debug!("graphics device {} support storage resources", if !supports_storage_resources { "does not" } else { "does" }); + if WGPU_BACKEND.get().is_none() { let info = adapter.get_info(); let os_info = os_info::get(); @@ -174,16 +207,16 @@ Hardware: let surface_caps = surface.get_capabilities(&adapter); - let surface_format = surface_caps - .formats - .iter() - .find(|f| f.is_srgb()) - .copied() - .unwrap_or(TextureFormat::Rgba16Float); + // let surface_format = surface_caps + // .formats + // .iter() + // .find(|f| f.is_srgb()) + // .copied() + // .unwrap_or(TextureFormat::Rgba16Float); let config = SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: surface_format, + format: Texture::TEXTURE_FORMAT, width: initial_width, height: initial_height, present_mode: surface_caps.present_modes[0], @@ -196,89 +229,107 @@ 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 mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new( + &device, + config.format, + None, + 1, + &window, + ))); + let texture_id = Arc::get_mut(&mut egui_renderer) + .unwrap() + .lock() + .renderer() + .register_native_texture(&device, &viewport_texture.view, wgpu::FilterMode::Linear); + + // shaders/shader.wgsl - @group(1) + let camera_bind_group_layout = device.create_bind_group_layout(&Camera::CAMERA_BIND_GROUP_LAYOUT); + + // shader/light.wgsl - @group(1) + let light_bind_group_layout = device.create_bind_group_layout(&Light::LIGHT_BIND_GROUP_LAYOUT); + + // shaders/light.wgsl - @group(1) + let light_cube_bind_group_layout = device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + // @binding(0) + 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("Per-Light Layout"), + }); + + // shaders/shader.wgsl - @group(0) let texture_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + device.create_bind_group_layout(&texture::TEXTURE_BIND_GROUP_LAYOUT); + + // shaders/shader.wgsl - @group(2) + let light_array_bind_group_layout = device.create_bind_group_layout( + &wgpu::BindGroupLayoutDescriptor { entries: &[ + // @binding(0) 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, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: if supports_storage_resources { + // s_light_array + wgpu::BufferBindingType::Storage { read_only: true } + } else { + // u_light_array + wgpu::BufferBindingType::Uniform + }, + has_dynamic_offset: false, + min_binding_size: None, }, 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"), - }); + label: Some("Light Array Layout"), + } + ); + // shaders/shader.wgsl - @group(3) let material_tint_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { + device.create_bind_group_layout(&Self::MATERIAL_BIND_GROUP_LAYOUT); + + // shaders/shader.wgsl - @group(4) + let shader_globals_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("shader.wgsl globals bind group layout"), + entries: &[ + BindGroupLayoutEntry { + // @binding(0) binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, - }], - label: Some("material_tint_bind_group_layout"), - }); - - let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new( - &device, - config.format, - None, - 1, - &window, - ))); - - let texture_id = Arc::get_mut(&mut egui_renderer) - .unwrap() - .lock() - .renderer() - .register_native_texture(&device, &viewport_texture.view, wgpu::FilterMode::Linear); + } + ], + }); let result = Self { surface: Arc::new(surface), - surface_format, + surface_format: Texture::TEXTURE_FORMAT, device: Arc::new(device), queue: Arc::new(queue), 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 +338,16 @@ Hardware: instance, physics_accumulator: Duration::ZERO, scene_manager: scene::Manager::new(), + layouts: Arc::new(BindGroupLayouts { + shader_globals_bind_group_layout, + 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, + }), + supports_storage: supports_storage_resources, }; Ok(result) @@ -302,9 +363,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 +383,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 +433,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 +447,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 +458,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 +509,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 +794,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>, } @@ -782,12 +844,14 @@ impl App { let window_id = window.id(); - let mut win_state = block_on(State::new(window, self.instance.clone(), self.future_queue.clone()))?; + let mut win_state = pollster::block_on(State::new(window, self.instance.clone(), self.future_queue.clone()))?; 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 +864,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 +885,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 +940,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, }; @@ -896,6 +960,8 @@ impl ApplicationHandler for App { match event { WindowEvent::Resized(size) => { state.resize(size.width, size.height); + + *graphics = Arc::new(graphics::SharedGraphicsContext::from_state(state)); } WindowEvent::RedrawRequested => { self.future_queue.poll(); @@ -907,7 +973,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 +999,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 +1049,7 @@ impl ApplicationHandler for App { } } - for state in self.windows.values() { + for (state, _) in self.windows.values() { state.window.request_redraw(); } @@ -1034,7 +1100,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,20 +1,19 @@ use crate::attenuation::{Attenuation, RANGE_50}; -use crate::buffer::ResizableBuffer; +use crate::buffer::{ResizableBuffer, UniformBuffer}; use crate::graphics::SharedGraphicsContext; -use crate::shader::Shader; +use crate::pipelines::light_cube::InstanceInput; use crate::{ - camera::Camera, - entity::{EntityTransform, Transform}, - model::{self, Model, Vertex}, + entity::Transform, + model::Model, }; use dropbear_macro::SerializableComponent; use dropbear_traits::SerializableComponent; -use glam::{DMat4, DQuat, DVec3}; +use glam::{DMat4, 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}; -pub const MAX_LIGHTS: usize = 8; +pub const MAX_LIGHTS: usize = 10; #[repr(C)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] @@ -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,28 @@ 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 instance_buffer: ResizableBuffer<InstanceRaw>, + pub buffer: UniformBuffer<LightUniform>, + pub bind_group: BindGroup, + pub instance_buffer: ResizableBuffer<InstanceInput>, } impl Light { + pub const LIGHT_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = + wgpu::BindGroupLayoutDescriptor { + // @binding(0) + 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, @@ -265,7 +271,10 @@ impl Light { label: Option<&str>, ) -> Self { let forward = DVec3::new(0.0, 0.0, -1.0); - let direction = transform.rotation * forward; + let mut direction = transform.rotation * forward; + if matches!(light.light_type, LightType::Directional) { + direction = -direction; + } let uniform = LightUniform { position: dvec3_to_uniform_array(transform.position), @@ -278,9 +287,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,40 +303,20 @@ 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, }); - let instance = Instance::new( - transform.position, - transform.rotation, - DVec3::new(0.25, 0.25, 0.25), - ); + let instance: InstanceInput = DMat4::from_scale_rotation_translation(transform.scale, transform.rotation, transform.position).into(); let mut instance_buffer = ResizableBuffer::new( &graphics.device, @@ -339,7 +325,7 @@ impl Light { "Light Instance Buffer" ); - instance_buffer.write(&graphics.device, &graphics.queue, &[instance.to_raw()]); + instance_buffer.write(&graphics.device, &graphics.queue, &[instance]); log::debug!("Created new light [{}]", label_str); @@ -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, } } @@ -358,7 +343,10 @@ impl Light { self.uniform.position = dvec3_to_uniform_array(transform.position); let forward = DVec3::new(0.0, 0.0, -1.0); - let direction = transform.rotation * forward; + let mut direction = transform.rotation * forward; + if matches!(light.light_type, LightType::Directional) { + direction = -direction; + } self.uniform.direction = dvec3_direction_to_uniform_array(direction, light.outer_cutoff_angle); @@ -370,48 +358,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,501 +372,4 @@ 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_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 { - fn default() -> Self { - Self::new() - } -} - -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 { - 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("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, - }, - multiview: None, - cache: None, - }); - - self.shadow_pipeline = Some(pipeline); - log::debug!("Created shadow render pipeline"); - } - - 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 bind_group = graphics - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &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), - }, - ], - 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"); - } - - pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, world: &hecs::World) { - 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() - { - let instance = if let Some(transform) = e_trans { - let sync_transform = transform.sync(); - Instance::from_matrix(sync_transform.matrix()) - } else if let Some(transform) = s_trans { - Instance::from_matrix(transform.matrix()) - } else { - panic!("Unable to locate either a \"Transform\" or an \"EntityTransform\" component for the light {}", light.label); - }; - - 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])); - - light_array.lights[light_index] = uniform; - light_index += 1; - } - } - - 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])); - } - - 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() - } - - pub fn create_render_pipeline( - &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"), - }); - - let pipeline = Self::create_render_pipeline_for_lighting( - graphics, - &shader, - vec![camera.layout(), &per_light_layout], - label, - ); - - self.pipeline = Some(pipeline); - log::debug!("Created ECS light render pipeline"); - } - - fn create_render_pipeline_for_lighting( - graphics: Arc<SharedGraphicsContext>, - shader: &Shader, - bind_group_layouts: Vec<&BindGroupLayout>, - label: Option<&str>, - ) -> RenderPipeline { - let render_pipeline_layout = - graphics - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(label.unwrap_or("Light Render Pipeline Descriptor")), - bind_group_layouts: bind_group_layouts.as_slice(), - push_constant_ranges: &[], - }); - - graphics - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Render Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader.module, - entry_point: Some("vs_main"), - buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()], - compilation_options: Default::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 { - alpha: wgpu::BlendComponent::REPLACE, - color: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE - polygon_mode: wgpu::PolygonMode::Fill, - // Requires Features::DEPTH_CLIP_CONTROL - unclipped_depth: false, - // Requires Features::CONSERVATIVE_RASTERIZATION - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: crate::Texture::DEPTH_FORMAT, - depth_write_enabled: true, - depth_compare: CompareFunction::Greater, - stencil: StencilState::default(), - bias: DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }) - } -} - -#[derive(Default)] -pub struct Instance { - pub position: DVec3, - pub rotation: DQuat, - pub scale: DVec3, - - buffer: Option<Buffer>, -} - -impl Instance { - pub fn new(position: DVec3, rotation: DQuat, scale: DVec3) -> Self { - Self { - position, - rotation, - scale, - buffer: None, - } - } - - pub fn to_raw(&self) -> InstanceRaw { - let model_matrix = - DMat4::from_scale_rotation_translation(self.scale, self.rotation, self.position); - InstanceRaw { - model: model_matrix.as_mat4().to_cols_array_2d(), - } - } - - 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, - } - } -} - -#[repr(C)] -#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)] -pub struct InstanceRaw { - model: [[f32; 4]; 4], -} - -impl InstanceRaw { - fn desc() -> VertexBufferLayout<'static> { - VertexBufferLayout { - array_stride: size_of::<InstanceRaw>() as BufferAddress, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &[ - // model - wgpu::VertexAttribute { - offset: 0, - shader_location: 5, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: size_of::<[f32; 4]>() as wgpu::BufferAddress, - shader_location: 6, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: size_of::<[f32; 8]>() as wgpu::BufferAddress, - shader_location: 7, - format: wgpu::VertexFormat::Float32x4, - }, - wgpu::VertexAttribute { - offset: size_of::<[f32; 12]>() as wgpu::BufferAddress, - shader_location: 8, - format: wgpu::VertexFormat::Float32x4, - }, - ], - } - } -} +} @@ -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); } } @@ -536,9 +537,20 @@ impl Model { .map(|(material_name, diffuse_bytes, normal_bytes, tint)| { let material_start = Instant::now(); - let processed_diffuse = diffuse_bytes.as_ref().map(|bytes| { + let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| { let load_start = Instant::now(); - let image = image::load_from_memory(bytes).unwrap(); + let image = match image::load_from_memory(bytes) { + Ok(image) => image, + Err(err) => { + log::warn!( + "Failed to decode diffuse texture for material '{}': {} ({} bytes)", + material_name, + err, + bytes.len() + ); + return None; + } + }; log::trace!("Loading diffuse image to memory: {:?}", load_start.elapsed()); let rgba_start = Instant::now(); @@ -549,12 +561,23 @@ impl Model { ); let dimensions = image.dimensions(); - (rgba.into_raw(), dimensions) + Some((rgba.into_raw(), dimensions)) }); - let processed_normal = normal_bytes.as_ref().map(|bytes| { + let processed_normal = normal_bytes.as_ref().and_then(|bytes| { let load_start = Instant::now(); - let image = image::load_from_memory(bytes).unwrap(); + let image = match image::load_from_memory(bytes) { + Ok(image) => image, + Err(err) => { + log::warn!( + "Failed to decode normal texture for material '{}': {} ({} bytes)", + material_name, + err, + bytes.len() + ); + return None; + } + }; log::trace!("Loading normal image to memory: {:?}", load_start.elapsed()); let rgba_start = Instant::now(); @@ -565,7 +588,7 @@ impl Model { ); let dimensions = image.dimensions(); - (rgba.into_raw(), dimensions) + Some((rgba.into_raw(), dimensions)) }); log::trace!( @@ -593,13 +616,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() }; @@ -1090,6 +1113,16 @@ pub trait Vertex { fn desc() -> VertexBufferLayout<'static>; } +/// Maps to +/// ```wgsl +/// 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>, +/// }; +/// ``` #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, serde::Serialize, serde::Deserialize)] pub struct ModelVertex { @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use crate::buffer::UniformBuffer; +use crate::graphics::SharedGraphicsContext; + +/// Mirrors `Globals` in `pipelines/shaders/shader.wgsl` (`@group(4) @binding(0)`). +/// +/// Note: the `_padding` ensures the uniform is 16 bytes, which matches WGSL +/// uniform layout expectations. +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Globals { + pub num_lights: u32, + pub ambient_strength: f32, + pub _padding: [u32; 2], +} + +impl Default for Globals { + fn default() -> Self { + Self { + num_lights: 0, + ambient_strength: 0.1, + _padding: [0; 2], + } + } +} + +/// Owns the globals uniform buffer and its bind group. +#[derive(Debug, Clone)] +pub struct GlobalsUniform { + pub data: Globals, + pub buffer: UniformBuffer<Globals>, + pub bind_group: wgpu::BindGroup, +} + +impl GlobalsUniform { + pub fn new(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self { + let label = label.unwrap_or("shader globals"); + + let buffer = UniformBuffer::new(&graphics.device, label); + + let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.layouts.shader_globals_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.buffer().as_entire_binding(), + }], + label: Some(label), + }); + + let data = Globals::default(); + buffer.write(&graphics.queue, &data); + + Self { + data, + buffer, + bind_group, + } + } + + pub fn write(&mut self, queue: &wgpu::Queue) { + self.buffer.write(queue, &self.data); + } + + pub fn set_num_lights(&mut self, num_lights: u32) { + self.data.num_lights = num_lights; + } + + pub fn set_ambient_strength(&mut self, ambient_strength: f32) { + self.data.ambient_strength = ambient_strength; + } +} @@ -0,0 +1,292 @@ +use std::sync::Arc; +use std::mem::size_of; +use glam::DMat4; +use wgpu::{BufferAddress, VertexAttribute, VertexFormat}; +use crate::buffer::{StorageBuffer, UniformBuffer}; +use crate::entity::{EntityTransform, Transform}; +use crate::graphics::SharedGraphicsContext; +use crate::lighting::{Light, LightArrayUniform, LightComponent, MAX_LIGHTS}; +use crate::model::Vertex; +use crate::pipelines::DropbearShaderPipeline; +use crate::shader::Shader; +use crate::texture::Texture; + +pub struct LightCubePipeline { + shader: Shader, + pipeline_layout: wgpu::PipelineLayout, + pipeline: wgpu::RenderPipeline, + storage_buffer: Option<StorageBuffer<LightArrayUniform>>, + uniform_buffer: Option<UniformBuffer<LightArrayUniform>>, + /// Bind group, defined in `shaders/shader.wgsl` as @group(2). + /// + /// This can either be a storage buffer or a uniform buffer depending on if the backend + /// supports storage resources. + light_bind_group: wgpu::BindGroup, +} + +impl DropbearShaderPipeline for LightCubePipeline { + fn new(graphics: Arc<SharedGraphicsContext>) -> Self { + let shader = Shader::new( + graphics.clone(), + include_str!("shaders/light.wgsl"), + Some("light cube shader"), + ); + + let pipeline_layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("light cube pipeline layout"), + bind_group_layouts: &[ + &graphics.layouts.camera_bind_group_layout, + &graphics.layouts.light_cube_bind_group_layout, + ], + push_constant_ranges: &[], + }); + + let pipeline = graphics.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("light cube pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[ + // model + VertexInput::desc(), + // instance + InstanceInput::desc(), + ], + }, + fragment: Some(wgpu::FragmentState { + module: &shader.module, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: Texture::TEXTURE_FORMAT, + blend: Some(wgpu::BlendState { + alpha: wgpu::BlendComponent::REPLACE, + color: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Cw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: crate::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, + }); + + let mut storage_buffer = None; + let mut uniform_buffer = None; + + if graphics.supports_storage { + storage_buffer = Some(StorageBuffer::new( + &graphics.device, + "light cube pipeline storage buffer", + )); + } else { + uniform_buffer = Some(UniformBuffer::new( + &graphics.device, + "light cube pipeline uniform buffer", + )); + } + + let light_buffer: &wgpu::Buffer = if let Some(buf) = &storage_buffer { + buf.buffer() + } else if let Some(buf) = &uniform_buffer { + buf.buffer() + } else { + panic!("Either a storage buffer or a uniform buffer should have been created"); + }; + + let light_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.layouts.light_array_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: light_buffer.as_entire_binding(), + }], + label: Some("light array bind group"), + }); + + Self { + shader, + pipeline_layout, + pipeline, + storage_buffer, + uniform_buffer, + light_bind_group, + } + } + + fn shader(&self) -> &Shader { + &self.shader + } + + fn pipeline_layout(&self) -> &wgpu::PipelineLayout { + &self.pipeline_layout + } + + fn pipeline(&self) -> &wgpu::RenderPipeline { + &self.pipeline + } +} + +impl LightCubePipeline { + pub fn bind_group(&self) -> &wgpu::BindGroup { + &self.light_bind_group + } + + pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, world: &hecs::World) { + debug_assert!( + self.storage_buffer.is_some() ^ self.uniform_buffer.is_some(), + "Expected exactly one of storage_buffer/uniform_buffer to exist" + ); + + let mut light_array = LightArrayUniform::default(); + + let mut light_index: usize = 0; + + for (light_component, s_trans, e_trans, light) in world + .query::<(&LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>() + .iter() + { + let instance: InstanceInput = if let Some(transform) = e_trans { + let sync_transform = transform.sync(); + sync_transform.matrix().into() + } else if let Some(transform) = s_trans { + transform.matrix().into() + } else { + panic!("Unable to locate either a \"Transform\" or an \"EntityTransform\" component for the light {}", light.label); + }; + + light.instance_buffer.write(&graphics.device, &graphics.queue, &[instance]); + + if light_component.enabled && light_index < MAX_LIGHTS { + let uniform = *light.uniform(); + + light.buffer.write(&graphics.queue, &uniform); + + light_array.lights[light_index] = uniform; + light_index += 1; + } + } + + light_array.light_count = light_index as u32; + + if let Some(buf) = &self.storage_buffer { + buf.write(&graphics.queue, &light_array); + } else if let Some(buf) = &self.uniform_buffer { + buf.write(&graphics.queue, &light_array); + } else { + panic!("Either a storage buffer or a uniform buffer should have been created"); + } + } + + pub fn buffer(&self) -> &wgpu::Buffer { + if let Some(s) = &self.storage_buffer { + s.buffer() + } else if let Some(u) = &self.uniform_buffer { + u.buffer() + } else { + panic!("Either a storage buffer or a uniform buffer should have been created"); + } + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct VertexInput { + position: [f32; 3], +} + +impl Vertex for VertexInput { + fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: size_of::<VertexInput>() as BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + // position + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + } + ], + } + } +} + +/// As mapped in `shaders/light.wgsl` as +/// ```wgsl +/// 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>, +/// } +/// ``` +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct InstanceInput { + pub model_matrix: [[f32; 4]; 4], +} + +impl Vertex for InstanceInput { + fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: size_of::<InstanceInput>() as BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + // model_matrix_0 + wgpu::VertexAttribute { + offset: 0, + shader_location: 5, + format: wgpu::VertexFormat::Float32x4, + }, + // model_matrix_1 + wgpu::VertexAttribute { + offset: size_of::<[f32; 4]>() as wgpu::BufferAddress, + shader_location: 6, + format: wgpu::VertexFormat::Float32x4, + }, + // model_matrix_2 + wgpu::VertexAttribute { + offset: size_of::<[f32; 8]>() as wgpu::BufferAddress, + shader_location: 7, + format: wgpu::VertexFormat::Float32x4, + }, + // model_matrix_3 + wgpu::VertexAttribute { + offset: size_of::<[f32; 12]>() as wgpu::BufferAddress, + shader_location: 8, + format: wgpu::VertexFormat::Float32x4, + }, + ], + } + } +} + +impl Into<InstanceInput> for DMat4 { + fn into(self) -> InstanceInput { + InstanceInput { + model_matrix: self.as_mat4().to_cols_array_2d(), + } + } +} @@ -0,0 +1,23 @@ +use std::sync::Arc; +use crate::graphics::SharedGraphicsContext; +use crate::shader::Shader; + +pub mod shader; +pub mod light_cube; +pub mod globals; + +pub use globals::{Globals, GlobalsUniform}; + +/// A helper in defining a pipelines required information, as well as getters. +/// +/// This contains the bare minimum for any pipeline. +pub trait DropbearShaderPipeline { + /// Creates a new instance of a pipeline. + fn new(graphics: Arc<SharedGraphicsContext>) -> Self; + /// Fetches the shader property + fn shader(&self) -> &Shader; + /// Fetches the pipeline layout + fn pipeline_layout(&self) -> &wgpu::PipelineLayout; + /// Fetches the pipeline + fn pipeline(&self) -> &wgpu::RenderPipeline; +} @@ -0,0 +1,111 @@ +use std::sync::Arc; +use wgpu::{CompareFunction, DepthBiasState, StencilState}; +use crate::graphics::{InstanceRaw, SharedGraphicsContext}; +use crate::model; +use crate::model::Vertex; +use crate::pipelines::DropbearShaderPipeline; +use crate::shader::Shader; +use crate::texture::Texture; + +/// As defined in `shaders/shader.wgsl` +pub struct MainRenderPipeline { + shader: Shader, + pipeline_layout: wgpu::PipelineLayout, + pipeline: wgpu::RenderPipeline, +} + +impl DropbearShaderPipeline for MainRenderPipeline { + fn new(graphics: Arc<SharedGraphicsContext>) -> Self { + let shader = Shader::new( + graphics.clone(), + include_str!("shaders/shader.wgsl"), + Some("viewport shaders"), + ); + + let bind_group_layouts = vec![ + &graphics.layouts.texture_bind_layout, // @group(0) + &graphics.layouts.camera_bind_group_layout, // @group(1) + &graphics.layouts.light_array_bind_group_layout, // @group(2) + &graphics.layouts.material_tint_bind_layout, // @group(3) + &graphics.layouts.shader_globals_bind_group_layout, // @group(4) + ]; + + let pipeline_layout = + graphics.device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("main render pipeline layout"), + bind_group_layouts: bind_group_layouts.as_slice(), + push_constant_ranges: &[], + }); + + let pipeline = + graphics.device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("main render pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader.module, + entry_point: Some("vs_main"), + buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader.module, + entry_point: if graphics.supports_storage { + Some("s_fs_main") + } else { + Some("u_fs_main") + }, + targets: &[Some(wgpu::ColorTargetState { + format: Texture::TEXTURE_FORMAT, + 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::Cw, + cull_mode: Some(wgpu::Face::Back), + 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: CompareFunction::Greater, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + log::debug!("Created main render pipeline"); + + Self { + shader, + pipeline_layout, + pipeline, + } + } + + fn shader(&self) -> &Shader { + &self.shader + } + + fn pipeline_layout(&self) -> &wgpu::PipelineLayout { + &self.pipeline_layout + } + + fn pipeline(&self) -> &wgpu::RenderPipeline { + &self.pipeline + } +} @@ -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 shaders + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + return vec4<f32>(in.color, 1.0); +} @@ -0,0 +1,290 @@ +// Main shaders for standard objects. + +struct Globals { + num_lights: u32, + ambient_strength: f32, +} + +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 MaterialUniform { + // for stuff like tinting + colour: vec4<f32>, + + // scales incoming UVs before sampling + uv_tiling: vec2<f32>, +} + +const c_max_lights: u32 = 10u; + +@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(1) @binding(0) +var<uniform> u_camera: CameraUniform; + +@group(2) @binding(0) +var<storage, read> s_light_array: array<Light>; +@group(2) @binding(0) +var<uniform> u_light_array: array<Light, 10>; // when storage is not available + +@group(3) @binding(0) +var<uniform> u_material: MaterialUniform; + +@group(4) @binding(0) +var<uniform> u_globals: Globals; + +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 = u_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 * u_globals.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 * u_globals.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 * u_globals.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); +} + +// when storage is supported +@fragment +fn s_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(u_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 = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) { + let light = s_light_array[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); +} + +// when storage is NOT supported +@fragment +fn u_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(u_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 = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) { + let light = u_light_array[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,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,14 +1,34 @@ -//! Deals with shaders, including WESL shaders +//! Deals with shaders, primarily around the [Shader] struct. + +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. +/// A nice little struct that stored basic information about a WGPU shaders. pub struct Shader { + /// The label of the shader. + /// + /// If it is not set in [Shader::new], the default is "shader". pub label: String, + + /// The compiled content of the WGSL shader. + /// + /// When [Shader] is dereferenced (such as that in `&shader`), it will automatically reference + /// this module. pub module: ShaderModule, + + /// The content of the shader as a readable string content, in the case you need to look + /// at the original source. + pub content: String, +} + +impl Deref for Shader { + type Target = ShaderModule; + + fn deref(&self) -> &Self::Target { + &self.module + } } impl Shader { @@ -25,7 +45,7 @@ impl Shader { source: wgpu::ShaderSource::Wgsl(shader_file_contents.into()), }); - log::debug!("Created new shader under the label: {:?}", label); + log::debug!("Created new shaders under the label: {:?}", label); Self { label: match label { @@ -33,6 +53,7 @@ impl Shader { None => "shader".into(), }, module, + content: shader_file_contents.to_string(), } } } @@ -0,0 +1,375 @@ +use std::{fs, path::PathBuf, sync::Arc}; + +use image::GenericImageView; +use serde::{Deserialize, Serialize}; + +use crate::graphics::SharedGraphicsContext; + + +/// As defined in `shaders.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; + pub const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; + + /// 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: Texture::TEXTURE_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::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_rgba, dimensions) = match image::load_from_memory(bytes) { + Ok(image) => { + let rgba = image.to_rgba8().into_raw(); + let dims = dimensions.unwrap_or_else(|| image.dimensions()); + (rgba, dims) + } + Err(err) => { + if let Some(dims) = dimensions { + let expected_len = (dims.0 as usize) + .saturating_mul(dims.1 as usize) + .saturating_mul(4); + if bytes.len() == expected_len { + (bytes.to_vec(), dims) + } else { + log::error!( + "Texture decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.", + err, + expected_len, + dims.0, + dims.1, + bytes.len() + ); + (vec![255, 0, 255, 255], (1, 1)) + } + } else { + log::error!( + "Texture decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.", + err + ); + (vec![255, 0, 255, 255], (1, 1)) + } + } + }; + 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: Texture::TEXTURE_FORMAT, + 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; + 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, + ); + } else { + 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[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; -} @@ -0,0 +1,31 @@ +[package] +name = "euca-runner" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +eucalyptus-core = { path = "../eucalyptus-core" } +redback-runtime = { path = "../redback-runtime", default-features = false } +dropbear-engine = { path = "../dropbear-engine" } + +tokio.workspace = true +anyhow.workspace = true +postcard.workspace = true +log.workspace = true +parking_lot.workspace = true +app_dirs2.workspace = true +chrono.workspace = true +env_logger.workspace = true +colored.workspace = true +ron.workspace = true +hecs.workspace = true +bytemuck.workspace = true +log-once.workspace = true +winit.workspace = true +serde.workspace = true +crossbeam-channel.workspace = true +gilrs.workspace = true +futures.workspace = true @@ -0,0 +1,5 @@ +# euca-runner + +Runs any app made with the eucalyptus editor. + +Bascially redback-runtime without the debug feature. @@ -0,0 +1,121 @@ +use app_dirs2::AppInfo; +use dropbear_engine::future::FutureQueue; +use eucalyptus_core::runtime::RuntimeProjectConfig; +use parking_lot::RwLock; +use redback_runtime::PlayMode; +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() { + env_logger::init(); + + 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 = postcard::from_bytes(&scene_config).unwrap(); + + log::debug!("Converted scene config file to RuntimeProjectConfig"); + + let runtime_scene = Rc::new(RwLock::new(PlayMode::new(None).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)] +pub struct ConfigFile { + pub jvm_args: Option<String>, + pub max_fps: u32, + pub target_resolution: WindowModes +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum WindowModes { + Windowed(u32, u32), + Maximised, + Fullscreen, +} @@ -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>); } @@ -101,9 +101,10 @@ impl ProjectConfig { /// # Parameters /// * path - The root config **file** for the project pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> { - let ron_str = fs::read_to_string(path.as_ref())?; + let path = path.as_ref().to_path_buf(); + let ron_str = fs::read_to_string(&path)?; let mut config: ProjectConfig = ron::de::from_str(ron_str.as_str())?; - config.project_path = path.as_ref().parent().unwrap().to_path_buf(); + config.project_path = path.parent().unwrap().to_path_buf(); log::info!("Loaded project!"); log::debug!("Loaded config info"); log::debug!("Updating with new content"); @@ -129,9 +130,10 @@ impl ProjectConfig { if io_err.kind() == std::io::ErrorKind::NotFound { log::warn!("resources.eucc not found, creating default."); let default = ResourceConfig { - path: project_root.join("../../../resources"), + path: project_root.join("resources"), nodes: vec![], }; + log::debug!("Writing to {}", default.path.display()); default.write_to(&project_root)?; { let mut cfg = RESOURCES.write(); @@ -155,7 +157,7 @@ impl ProjectConfig { if io_err.kind() == std::io::ErrorKind::NotFound { log::warn!("source.eucc not found, creating default."); let default = SourceConfig { - path: project_root.join("../../../src"), + path: project_root.join("src"), nodes: vec![], }; default.write_to(&project_root)?; @@ -342,14 +344,14 @@ impl ResourceConfig { /// # Parameters /// - path: The root **folder** of the project pub fn write_to(&self, path: impl AsRef<Path>) -> anyhow::Result<()> { - let resource_dir = path.as_ref().join("../../../resources"); + let resource_dir = path.as_ref().join("resources"); let updated_config = ResourceConfig { path: resource_dir.clone(), nodes: collect_nodes(&resource_dir, path.as_ref(), vec!["thumbnails"].as_slice()), }; let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - let config_path = path.as_ref().join("../../../resources").join("resources.eucc"); + let config_path = path.as_ref().join("resources").join("resources.eucc"); fs::create_dir_all(config_path.parent().unwrap())?; fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; Ok(()) @@ -369,7 +371,7 @@ impl ResourceConfig { /// # Parameters /// - path: The location to the **resources.eucc** file pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> { - let config_path = path.as_ref().join("../../../resources").join("resources.eucc"); + let config_path = path.as_ref().join("resources").join("resources.eucc"); let ron_str = fs::read_to_string(&config_path)?; let config: ResourceConfig = ron::de::from_str(&ron_str) .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; @@ -395,7 +397,7 @@ impl SourceConfig { /// # Parameters /// - path: The root **folder** of the project pub fn write_to(&self, path: impl AsRef<Path>) -> anyhow::Result<()> { - let resource_dir = path.as_ref().join("../../../src"); + let resource_dir = path.as_ref().join("src"); let updated_config = SourceConfig { path: resource_dir.clone(), nodes: collect_nodes(&resource_dir, path.as_ref(), vec!["scripts"].as_slice()), @@ -403,7 +405,7 @@ impl SourceConfig { let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) .map_err(|e| anyhow::anyhow!("RON serialisation error: {}", e))?; - let config_path = path.as_ref().join("../../../src").join("source.eucc"); + let config_path = path.as_ref().join("src").join("source.eucc"); fs::create_dir_all(config_path.parent().unwrap())?; fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; Ok(()) @@ -412,7 +414,7 @@ impl SourceConfig { /// # Parameters /// - path: The location to the **source.eucc** file pub fn read_from(path: impl AsRef<Path>) -> anyhow::Result<Self> { - let config_path = path.as_ref().join("../../../src").join("source.eucc"); + let config_path = path.as_ref().join("src").join("source.eucc"); let ron_str = fs::read_to_string(&config_path)?; let config: SourceConfig = ron::de::from_str(&ron_str) .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; @@ -458,7 +460,7 @@ fn collect_nodes( ResourceType::Model } else if parent_folder.contains("texture") { ResourceType::Texture - } else if parent_folder.contains("shader") { + } else if parent_folder.contains("shaders") { ResourceType::Shader } else if entry_path .extension() @@ -3,31 +3,33 @@ use std::mem::size_of; use std::sync::Arc; -use dropbear_engine::entity::Transform; +use dropbear_engine::pipelines::DropbearShaderPipeline; +use dropbear_engine::{entity::Transform, texture::Texture}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::shader::Shader; use dropbear_engine::wgpu::*; use glam::Mat4; +use crate::physics::collider::{ColliderShape, WireframeGeometry}; + pub struct ColliderWireframePipeline { + pub shader: Shader, + pub pipeline_layout: PipelineLayout, pub pipeline: RenderPipeline, } -impl ColliderWireframePipeline { - pub fn new( - graphics: Arc<SharedGraphicsContext>, - camera_bind_group_layout: &BindGroupLayout, - ) -> Self { +impl DropbearShaderPipeline for ColliderWireframePipeline { + fn new(graphics: Arc<SharedGraphicsContext>) -> Self { let shader = Shader::new( graphics.clone(), - dropbear_engine::shader::shader_wesl::COLLIDER_SHADER, - Some("collider wireframe shader"), + include_str!("shaders/collider.wgsl"), + Some("collider wireframe shaders"), ); 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: &[], }); @@ -58,7 +60,7 @@ impl ColliderWireframePipeline { module: &shader.module, entry_point: Some("fs_main"), targets: &[Some(ColorTargetState { - format: TextureFormat::Rgba16Float, + format: Texture::TEXTURE_FORMAT, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], @@ -74,7 +76,7 @@ impl ColliderWireframePipeline { conservative: false, }, depth_stencil: Some(DepthStencilState { - format: TextureFormat::Depth32Float, + format: Texture::DEPTH_FORMAT, depth_write_enabled: false, depth_compare: CompareFunction::Always, stencil: StencilState::default(), @@ -89,7 +91,23 @@ impl ColliderWireframePipeline { cache: None, }); - Self { pipeline } + Self { + shader, + pipeline_layout, + pipeline, + } + } + + fn shader(&self) -> &Shader { + &self.shader + } + + fn pipeline_layout(&self) -> &PipelineLayout { + &self.pipeline_layout + } + + fn pipeline(&self) -> &RenderPipeline { + &self.pipeline } } @@ -166,4 +184,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) + } + } } @@ -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; +} @@ -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( @@ -675,14 +675,14 @@ impl Drop for ScriptManager { fn get_gradle_command(project_root: impl AsRef<Path>) -> String { let project_root = project_root.as_ref().to_owned(); if cfg!(target_os = "windows") { - let gradlew = project_root.join("../../../gradlew.bat"); + let gradlew = project_root.join("gradlew.bat"); if gradlew.exists() { gradlew.to_string_lossy().to_string() } else { "gradle.bat".to_string() } } else { - let gradlew = project_root.join("../../../gradlew"); + let gradlew = project_root.join("gradlew"); if gradlew.exists() { "./gradlew".to_string() } else { @@ -707,14 +707,14 @@ pub async fn build_jvm( let _ = status_sender.send(BuildStatus::Started); let _ = status_sender.send(BuildStatus::Building(format!("Building manifest at {}", project_root.join("build/magna-carta/jvmMain/RunnableRegistry.kt").display()))); - if let Err(e) = magna_carta::parse(project_root.join("../../../src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) { + if let Err(e) = magna_carta::parse(project_root.join("src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) { let _ = status_sender.send(BuildStatus::Failed(format!("Failed to build manifest: {}", e))); return Err(e); } let _ = status_sender.send(BuildStatus::Building(String::from("Successfully built manifest!"))); if !(project_root.join("build.gradle").exists() - || project_root.join("../../../build.gradle.kts").exists()) + || project_root.join("build.gradle.kts").exists()) { let err = format!("No Gradle build script found in: {:?}", project_root); let _ = status_sender.send(BuildStatus::Failed(err.clone())); @@ -723,7 +723,7 @@ pub async fn build_jvm( let gradle_cmd = get_gradle_command(project_root); - let _ = status_sender.send(BuildStatus::Building(format!("Running: {}", gradle_cmd))); + let _ = status_sender.send(BuildStatus::Building(format!("Running: {} fatJar", gradle_cmd))); let mut child = Command::new(&gradle_cmd) .current_dir(project_root) @@ -766,7 +766,7 @@ pub async fn build_jvm( return Err(anyhow::anyhow!(err_msg)); } - let libs_dir = project_root.join("../../../build").join("libs"); + let libs_dir = project_root.join("build").join("libs"); if !libs_dir.exists() { let err = "Build succeeded but 'build/libs' directory is missing".to_string(); let _ = status_sender.send(BuildStatus::Failed(err.clone())); @@ -870,7 +870,7 @@ pub async fn build_native( } let _ = status_sender.send(BuildStatus::Building(format!("Building manifest at {}", project_root.join("build/magna-carta/jvmMain/RunnableRegistry.kt").display()))); - if let Err(e) = magna_carta::parse(project_root.join("../../../src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) { + if let Err(e) = magna_carta::parse(project_root.join("src"), Target::Jvm, project_root.join("build/magna-carta/jvmMain")) { let _ = status_sender.send(BuildStatus::Failed(format!("Failed to build manifest: {}", e))); return Err(e); } @@ -9,13 +9,13 @@ 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; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; -use std::collections::HashMap; use std::fmt; use std::fmt::{Display, Formatter}; use std::ops::{Deref, DerefMut}; @@ -137,7 +137,7 @@ impl Display for ResourceType { ResourceType::Unknown => "unknown", ResourceType::Model => "model", ResourceType::Texture => "texture", - ResourceType::Shader => "shader", + ResourceType::Shader => "shaders", ResourceType::Thumbnail => "thumbnail", ResourceType::Script => "script", ResourceType::Config => "eucalyptus project config", @@ -279,14 +279,6 @@ pub enum WorldLoadingStatus { Completed, } -#[derive(Clone, Serialize, Deserialize)] -pub struct RuntimeData { - pub project_config: ProjectConfig, - pub source_config: SourceConfig, - pub scene_data: Vec<SceneConfig>, - pub scripts: HashMap<String, String>, -} - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, SerializableComponent)] pub struct Label(String); @@ -274,7 +274,7 @@ impl ResolveReference for ResourceReference { }; if !project_path.as_os_str().is_empty() { - let root = project_path.join("../../../resources"); + let root = project_path.join("resources"); return resolve_resource_from_root(relative, &root); } } @@ -294,7 +294,7 @@ fn runtime_resources_dir() -> anyhow::Result<PathBuf> { let dir = current_exe .parent() .ok_or_else(|| anyhow::anyhow!("Unable to locate parent folder of runtime executable"))?; - let resources_dir = dir.join("../../../resources"); + let resources_dir = dir.join("resources"); if !resources_dir.exists() { anyhow::bail!( "Runtime resources directory is missing at '{}'. Ensure the packaged build includes a 'resources' folder next to the executable (current exe: {}).", @@ -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", features = ["debug"]} 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) {} @@ -82,8 +82,8 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> { log::debug!("Exported scene config to {:?}", eupak_path); // copy resources - let resources_src = project_root.join("../../../resources"); - let resources_dst = build_dir.join("../../../resources"); + let resources_src = project_root.join("resources"); + let resources_dst = build_dir.join("resources"); if resources_src.exists() { copy_dir_recursive(&resources_src, &resources_dst)?; log::debug!("Copied resources to {:?}", resources_dst); @@ -212,8 +212,8 @@ pub async fn package( } tokio_fs::copy(&data_src, package_dir.join("data.eupak")).await?; - let resources_src = build_dir.join("../../../resources"); - let resources_dst = package_dir.join("../../../resources"); + let resources_src = build_dir.join("resources"); + let resources_dst = package_dir.join("resources"); if resources_src.exists() && !resources_dst.exists() { let src = resources_src.clone(); let dst = resources_dst.clone(); @@ -231,7 +231,7 @@ pub async fn package( let magna_output_dir = project_root.join("build/magna-carta/nativeLibMain"); tokio_fs::create_dir_all(&magna_output_dir).await?; - magna_carta::parse(project_root.join("../../../src"), Target::Native, &magna_output_dir)?; + magna_carta::parse(project_root.join("src"), Target::Native, &magna_output_dir)?; // let magna_status = Command::new("magna-carta") // .arg("--input") @@ -600,7 +600,7 @@ async fn run_gradle_build(project_root: &Path) -> anyhow::Result<()> { #[cfg(not(windows))] { - let script = project_root.join("../../../gradlew"); + let script = project_root.join("gradlew"); if !script.exists() { bail!("Gradle wrapper not found at {}", script.display()); } @@ -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)) @@ -1149,7 +1149,7 @@ impl<'a> EditorTabViewer<'a> { ) { let label = "euca://resources"; builder.node(Self::dir_node_labeled(label, "resources")); - let resources_root = project_root.join("../../../../resources"); + let resources_root = project_root.join("resources"); if resources_root.exists() { Self::walk_resource_directory(cfg, builder, &resources_root, &resources_root); } else { @@ -1167,9 +1167,9 @@ impl<'a> EditorTabViewer<'a> { let entries = match Self::sorted_entries(current_path) { Ok(entries) => entries, Err(err) => { - log::warn!( + log_once::warn_once!( "Unable to enumerate resources at '{}': {}", - current_path.display(), + current_path.parent().unwrap_or(current_path).display(), err ); return; @@ -1219,7 +1219,7 @@ impl<'a> EditorTabViewer<'a> { ) { let label = "euca://scripts"; builder.node(Self::dir_node_labeled(label, "scripts")); - let scripts_root = project_root.join("../../../../src"); + let scripts_root = project_root.join("src"); if !scripts_root.exists() { Self::walk_resource_directory(cfg, builder, &scripts_root, &scripts_root); builder.close_dir(); @@ -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::pipelines::light_cube::LightCubePipeline; +use dropbear_engine::texture::TextureWrapMode; +use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{SharedGraphicsContext}, 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}; @@ -54,11 +53,14 @@ use std::rc::Rc; use log::debug; use tokio::sync::oneshot; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation}; -use wgpu::{Color, Extent3d, RenderPipeline}; +use wgpu::{Color, Extent3d}; use winit::window::{CursorGrabMode, WindowAttributes}; use winit::{keyboard::KeyCode, window::Window}; use winit::dpi::PhysicalSize; -use eucalyptus_core::physics::collider::{ColliderShape, ColliderShapeKey, WireframeGeometry}; +use dropbear_engine::pipelines::DropbearShaderPipeline; +use dropbear_engine::pipelines::shader::MainRenderPipeline; +use dropbear_engine::pipelines::GlobalsUniform; +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; @@ -72,16 +74,16 @@ pub struct Editor { pub dock_state: DockState<EditorTab>, pub texture_id: Option<egui::TextureId>, 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>, + // rendering + pub light_cube_pipeline: Option<LightCubePipeline>, + pub main_render_pipeline: Option<MainRenderPipeline>, + pub shader_globals: Option<GlobalsUniform>, + pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>, pub active_camera: Arc<Mutex<Option<Entity>>>, @@ -190,7 +192,8 @@ impl Editor { dock_state, texture_id: None, size: Extent3d::default(), - render_pipeline: None, + main_render_pipeline: None, + shader_globals: None, color: Color::default(), is_viewport_focused: false, // is_cursor_locked: false, @@ -212,7 +215,7 @@ impl Editor { gizmo_orientation: GizmoOrientation::Global, play_mode_backup: None, input_state: Box::new(InputState::new()), - light_manager: LightManager::new(), + light_cube_pipeline: None, active_camera: Arc::new(Mutex::new(None)), progress_tx: None, is_world_loaded: IsWorldLoadedYet::new(), @@ -231,7 +234,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 +245,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 +608,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(); @@ -622,10 +623,10 @@ impl Editor { self.previously_selected_entity = None; self.active_camera.lock().take(); - self.render_pipeline = None; - self.outline_pipeline = None; + self.main_render_pipeline = None; + self.shader_globals = None; self.texture_id = None; - self.light_manager = LightManager::new(); + self.light_cube_pipeline = None; { let mut cache = MODEL_CACHE.lock(); @@ -633,8 +634,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 +648,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 @@ -1049,13 +1050,22 @@ impl Editor { let editor_ptr = self as *mut Editor; + let Some(view) = self.texture_id else { + egui::CentralPanel::default().show(ctx, |ui| { + ui.centered_and_justified(|ui| { + ui.label("Viewport is still initialising..."); + }); + }); + return; + }; + egui::CentralPanel::default().show(ctx, |ui| { DockArea::new(&mut self.dock_state) .style(Style::from_egui(ui.style().as_ref())) .show_inside( ui, &mut EditorTabViewer { - view: self.texture_id.unwrap(), + view, gizmo: &mut self.gizmo, tex_size: self.size, world: &mut self.world, @@ -1308,69 +1318,14 @@ 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); - 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()); - - 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); - - let collider_pipeline = ColliderWireframePipeline::new(graphics.shared.clone(), camera.layout()); - self.collider_wireframe_pipeline = Some(collider_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"); - } - - self.window = Some(graphics.shared.window.clone()); + pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { + self.main_render_pipeline = Some(MainRenderPipeline::new(graphics.clone())); + self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone())); + self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("editor shader globals"))); + self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone())); + + self.texture_id = Some((*graphics.texture_id).clone()); + self.window = Some(graphics.window.clone()); self.is_world_loaded.mark_rendering_loaded(); } @@ -1548,10 +1503,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}, + lighting::{Light, LightComponent, MAX_LIGHTS}, + 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,18 +328,17 @@ impl Scene for Editor { continue; }; - light.update(graphics.shared.as_ref(), light_component, &transform); + light.update(graphics.as_ref(), light_component, &transform); } } } - { - let sim_world = self.world.as_ref(); - - self.light_manager - .update(graphics.shared.clone(), sim_world); + if let Some(l) = &mut self.light_cube_pipeline { + l.update(graphics.clone(), &self.world); + } - self.nerd_stats.write().record_stats(dt, sim_world.len() as u32); + { + self.nerd_stats.write().record_stats(dt, self.world.len() as u32); } self.input_state.window = self.window.clone(); @@ -349,312 +346,358 @@ 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.size = graphics.viewport_texture.size; + self.texture_id = Some(*graphics.texture_id.clone()); + self.window = Some(graphics.window.clone()); + + self.show_ui(&graphics.get_egui_context()); + let clear_color = Color { r: 100.0 / 255.0, g: 149.0 / 255.0, b: 237.0 / 255.0, a: 1.0, }; + self.color = clear_color; + eucalyptus_core::logging::render(&graphics.get_egui_context()); - 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 _ = frame_ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("editor surface clear 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: None, + occlusion_query_set: None, + timestamp_writes: None, + }); } - 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 cam = { + let c = self.active_camera.lock(); + *c + }; - if let Some(active_camera) = active_camera { - let cam = { - world.query_one::<&Camera>(active_camera).get().ok().cloned() - }; + let Some(active_camera) = cam else { + return; + }; + log_once::debug_once!("Active camera found: {:?}", active_camera); - 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 q = self.world.query_one::<&Camera>(active_camera).get().ok().cloned(); - 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 - }; + let Some(camera) = q else { + return; + }; + log_once::debug_once!("Camera ready"); + log_once::debug_once!("Camera currently being viewed: {}", camera.label); - // 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 Some(pipeline) = &self.main_render_pipeline else { + log_once::warn_once!("Render pipeline not ready"); + return; + }; + log_once::debug_once!("Pipeline ready"); - for i in ASSET_REGISTRY.iter_material() { - mipmap.generate(&graphics.device, &mut encoder, &i.diffuse_texture.texture); - } + 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 + }; - graphics.queue.submit(Some(encoder.finish())); - } + if let Some(globals) = &mut self.shader_globals { + let enabled_count = lights + .iter() + .filter(|(_, comp)| comp.enabled) + .take(MAX_LIGHTS) + .count() as u32; + globals.set_num_lights(enabled_count); + globals.write(&graphics.queue); + } - 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 renderers = { + let mut renderers = Vec::new(); + let mut query = self.world.query::<&MeshRenderer>(); + for renderer in query.iter() { + renderers.push(renderer.clone()); + } + renderers + }; - 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", - ) - }); + 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()); + } - buffer_handler.write( - &graphics.shared.device, - &graphics.shared.queue, - instances, - ); + 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() + }; - prepared_models.push(( - model, - buffer_handler.buffer().clone(), - instances.len() as u32, - )); - } + let Some(model) = model_opt else { + log_once::error_once!("Missing model {:?} in cache", model_id); + continue; + }; - if let Some(shadow_pipeline) = &self.light_manager.shadow_pipeline { - for (light, component) in &lights { - if !component.enabled || !component.cast_shadows { - continue; - } + 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 shadow_index = light.uniform().shadow_index; - if shadow_index < 0 { - continue; - } + prepared_models.push((model, instance_buffer, instances.len() as u32)); + } - 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 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.as_ref(), light_component, &transform); + } + } + + { + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("light cube render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &graphics.viewport_texture.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_cube_pipeline { + render_pass.set_pipeline(light_pipeline.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 + ); } + } + } + } + if let Some(lcp) = &self.light_cube_pipeline { + for (model, instance_buffer, instance_count) in prepared_models { + let globals_bind_group = &self + .shader_globals + .as_ref() + .expect("Shader globals not initialised") + .bind_group; + + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &graphics.viewport_texture.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.pipeline()); + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.set_bind_group(4, globals_bind_group, &[]); + render_pass.draw_model_instanced( + &model, + 0..instance_count, + &camera.bind_group, + lcp.bind_group(), + ); + } + } + + { + 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: &graphics.viewport_texture.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, &[]); - { // 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 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, + ) + }); } } - { // 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 +707,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,11 +1,12 @@ 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; use eucalyptus_core::states::PROJECT; use git2::Repository; use log::{self, debug}; +use log_once::debug_once; use rfd::FileDialog; use std::sync::Arc; use std::{fs, path::PathBuf}; @@ -143,7 +144,7 @@ impl MainMenu { fs::write(&dest_script_path, content)?; - let build_gradle_path = project_root.join("../../../build.gradle.kts"); + let build_gradle_path = project_root.join("build.gradle.kts"); let gradle_content = fs::read_to_string(&build_gradle_path)?; let updated_gradle_content = gradle_content @@ -234,19 +235,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 +283,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; @@ -363,6 +363,7 @@ impl Scene for MainMenu { .add_filter("Eucalyptus Configuration Files", &["eucp"]) .pick_file() { + log::debug!("Reading from {}", path.display()); match ProjectConfig::read_from(&path) { Ok(config) => { log::info!("Loaded project: {:?}", path); @@ -437,7 +438,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()); } }); }); @@ -510,26 +511,26 @@ impl Mouse for MainMenu { impl Controller for MainMenu { fn button_down(&mut self, button: gilrs::Button, id: gilrs::GamepadId) { - debug!("Controller button {:?} pressed! [{}]", button, id); + debug_once!("Controller button {:?} pressed! [{}]", button, id); } fn button_up(&mut self, button: gilrs::Button, id: gilrs::GamepadId) { - debug!("Controller button {:?} released! [{}]", button, id); + debug_once!("Controller button {:?} released! [{}]", button, id); } fn left_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) { - debug!("Left stick changed: x = {} | y = {} | id = {}", x, y, id); + debug_once!("Left stick changed: x = {} | y = {} | id = {}", x, y, id); } fn right_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) { - debug!("Right stick changed: x = {} | y = {} | id = {}", x, y, id); + debug_once!("Right stick changed: x = {} | y = {} | id = {}", x, y, id); } fn on_connect(&mut self, id: gilrs::GamepadId) { - debug!("Controller connected [{}]", id); + debug_once!("Controller connected [{}]", id); } fn on_disconnect(&mut self, id: gilrs::GamepadId) { - debug!("Controller disconnected [{}]", id); + debug_once!("Controller disconnected [{}]", id); } } @@ -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}; @@ -26,7 +26,7 @@ fn resolve_editor_path(uri: &str) -> PathBuf { let relative = relative_path_from_euca(uri) .unwrap_or_else(|_| uri.trim_start_matches(EUCA_SCHEME)); let project_path = PROJECT.read().project_path.clone(); - project_path.join("../../../resources").join(relative) + project_path.join("resources").join(relative) } else { PathBuf::from(uri) } @@ -198,7 +198,7 @@ impl SignalController for Editor { let cfg = PROJECT.read(); cfg.project_path.clone() }; - let libs_dir = project_root.join("../../../build").join("libs"); + let libs_dir = project_root.join("build").join("libs"); if !libs_dir.exists() { let err = "Build succeeded but 'build/libs' directory is missing".to_string(); @@ -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) {} } @@ -113,7 +113,7 @@ pub fn start_project_creation( ("git", 0.1, "Creating a git folder..."), ("src", 0.2, "Creating src folder..."), ("resources/models", 0.4, "Creating models folder..."), - ("resources/shaders", 0.6, "Creating shader folder..."), + ("resources/shaders", 0.6, "Creating shaders folder..."), ("resources/textures", 0.8, "Creating textures folder..."), ("src2", 0.9, "Creating project config file..."), ]; @@ -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,126 @@ use std::sync::Arc; use crossbeam_channel::{unbounded, Receiver}; +use dropbear_engine::buffer::ResizableBuffer; +use dropbear_engine::pipelines::DropbearShaderPipeline; +use dropbear_engine::pipelines::GlobalsUniform; +use dropbear_engine::pipelines::light_cube::LightCubePipeline; +use dropbear_engine::pipelines::shader::MainRenderPipeline; +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 dropbear_engine::future::FutureHandle; -use dropbear_engine::graphics::RenderContext; -use dropbear_engine::lighting::LightManager; +use dropbear_engine::graphics::SharedGraphicsContext; 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, + light_cube_pipeline: Option<LightCubePipeline>, + main_pipeline: Option<MainRenderPipeline>, + shader_globals: Option<GlobalsUniform>, + collider_wireframe_pipeline: Option<ColliderWireframePipeline>, 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_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,21 +131,27 @@ 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, pending_camera: None, active_camera: None, - render_pipeline: None, - light_manager: Default::default(), + main_pipeline: None, + light_cube_pipeline: None, + shader_globals: None, 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 +159,16 @@ impl Runtime { 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()); - - 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"); - } + pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { + self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone())); + self.main_pipeline = Some(MainRenderPipeline::new(graphics.clone())); + self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals"))); + self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone())); } 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 +184,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 +232,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 +255,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,20 +275,23 @@ 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.main_pipeline = None; + self.light_cube_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 scene_to_load = { + let mut scene_to_load = { let scenes = SCENES.read(); scenes.iter() .find(|s| s.scene_name == scene_name) @@ -266,12 +299,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 +315,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 +339,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 +360,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,38 @@ +use std::sync::Arc; + use std::collections::HashMap; -use hecs::{Entity}; +use dropbear_engine::pipelines::DropbearShaderPipeline; +use eucalyptus_core::egui::CentralPanel; +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::lighting::MAX_LIGHTS; +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 +49,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 +270,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 +303,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 +340,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 +349,48 @@ 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); + if let Some(l) = &mut self.light_cube_pipeline { + l.update(graphics.clone(), &self.world); + } + + #[cfg(feature = "debug")] + egui::TopBottomPanel::top("menu_bar").show(&graphics.get_egui_context(), |ui| { + egui::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()); + } + }); + + 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| { + 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 +401,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 +414,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 +456,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; @@ -249,7 +470,7 @@ impl Scene for Runtime { log_once::debug_once!("Camera ready"); log_once::debug_once!("Camera currently being viewed: {}", camera.label); - let Some(pipeline) = &self.render_pipeline else { + let Some(pipeline) = &self.main_pipeline else { log_once::warn_once!("Render pipeline not ready"); return; }; @@ -265,36 +486,37 @@ 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); + if let Some(globals) = &mut self.shader_globals { + let enabled_count = lights + .iter() + .filter(|(_, comp)| comp.enabled) + .take(MAX_LIGHTS) + .count() as u32; + globals.set_num_lights(enabled_count); + globals.write(&graphics.queue); + } - 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 +531,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 +550,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 +559,250 @@ 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(), - ); - } - } + { + let _ = frame_ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("runtime surface clear 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: None, + occlusion_query_set: None, + timestamp_writes: None, + }); } { - let mut render_pass = graphics.clear_colour(clear_color); - if let Some(light_pipeline) = &self.light_manager.pipeline { - render_pass.set_pipeline(light_pipeline); + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("light cube render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &graphics.viewport_texture.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_cube_pipeline { + render_pass.set_pipeline(light_pipeline.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(); - render_pass.set_pipeline(pipeline); + let light_bind_group = self + .light_cube_pipeline + .as_ref() + .expect("Light cube pipeline not initialised") + .bind_group(); + + let globals_bind_group = &self + .shader_globals + .as_ref() + .expect("Shader globals not initialised") + .bind_group; + + let mut render_pass = frame_ctx.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &graphics.viewport_texture.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.pipeline()); render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.set_bind_group(4, globals_bind_group, &[]); render_pass.draw_model_instanced( &model, 0..instance_count, - camera.bind_group(), - self.light_manager.bind_group(), + &camera.bind_group, + light_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: &graphics.viewport_texture.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 +810,5 @@ impl Scene for Runtime { fn run_command(&mut self) -> SceneCommand { std::mem::replace(&mut self.scene_command, SceneCommand::None) } -} +} +