tirbofish/dropbear · commit
65bc7fccb862c06a9e38cd495931fbe59b46f290
feature: added support for lighting as a storage resource. fix: dealt with some weird "../../../" corruption or bug. wip: redback-runtime runner (euca-runner) bin Unverified
@@ -2,8 +2,6 @@ use std::marker::PhantomData; -use wgpu::BufferUsages; - #[repr(C)] #[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)] pub struct Vertex { @@ -54,7 +52,7 @@ impl<T: bytemuck::Pod> UniformBuffer<T> { let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size, - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -78,6 +76,7 @@ impl<T: bytemuck::Pod> UniformBuffer<T> { } } +/// A wrapper to a [wgpu::Buffer] that stores #[derive(Debug, Clone)] pub struct StorageBuffer<T> { buffer: wgpu::Buffer, @@ -95,7 +94,7 @@ impl<T: bytemuck::Pod> StorageBuffer<T> { let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size, - usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -143,13 +142,6 @@ impl<T: bytemuck::Pod> ResizableBuffer<T> { } } - pub fn uniform( - device: &wgpu::Device, - label: &str, - ) -> Self { - Self::new(device, 1, BufferUsages::UNIFORM | BufferUsages::COPY_DST, label) - } - pub fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[T]) { if data.is_empty() { return; @@ -1,10 +1,7 @@ -use crate::shader::Shader; -use crate::texture::Texture; use crate::{BindGroupLayouts, texture}; use crate::{ State, egui_renderer::EguiRenderer, - model::{self, Vertex}, }; use dropbear_future_queue::FutureQueue; use egui::{Context, TextureId}; @@ -34,6 +31,7 @@ pub struct SharedGraphicsContext { pub egui_renderer: Arc<Mutex<EguiRenderer>>, pub texture_id: Arc<TextureId>, pub future_queue: Arc<FutureQueue>, + pub supports_storage: bool, } impl SharedGraphicsContext { @@ -74,71 +72,9 @@ impl SharedGraphicsContext { texture_id: state.texture_id.clone(), surface: state.surface.clone(), surface_format: state.surface_format, + supports_storage: state.supports_storage, } } - - pub fn create_render_pipline( - &self, - shader: &Shader, - bind_group_layouts: Vec<&BindGroupLayout>, - label: Option<&str>, - ) -> RenderPipeline { - let render_pipeline_layout = - self.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.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: 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::Ccw, - 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 new render pipeline"); - render_pipeline - } } #[derive(Default, Clone)] @@ -176,6 +112,19 @@ impl Instance { } } +/// 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 { @@ -189,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, @@ -16,6 +16,7 @@ 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; @@ -39,7 +40,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 +52,7 @@ use winit::{ use crate::camera::Camera; use crate::graphics::{FrameGraphicsContext, SharedGraphicsContext}; -use crate::lighting::{Light, LightManager}; +use crate::lighting::{Light}; use crate::texture::Texture; use crate::{egui_renderer::EguiRenderer}; @@ -63,6 +64,7 @@ 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, @@ -76,6 +78,7 @@ 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, @@ -96,7 +99,7 @@ pub struct State { } impl State { - /// As defined in `shader.wgsl` as + /// As defined in `shaders.wgsl` as /// ``` /// @group(3) @binding(0) /// var<uniform> u_material: MaterialUniform; @@ -146,6 +149,14 @@ impl State { }) .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(); @@ -218,12 +229,6 @@ Hardware: let viewport_texture = Texture::viewport(&config, &device, Some("viewport texture")); - let texture_bind_group_layout = - device.create_bind_group_layout(&texture::TEXTURE_BIND_GROUP_LAYOUT); - - let material_tint_bind_group_layout = - device.create_bind_group_layout(&Self::MATERIAL_BIND_GROUP_LAYOUT); - let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new( &device, config.format, @@ -238,11 +243,80 @@ Hardware: .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); - let light_array_bind_group_layout = device.create_bind_group_layout(&LightManager::LIGHT_ARRAY_BIND_GROUP_LAYOUT); + + // shaders/light.wgsl - @group(1) let light_cube_bind_group_layout = device - .create_bind_group_layout(&LightManager::LIGHT_CUBE_BIND_GROUP_LAYOUT); + .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(&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::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, + }, + ], + label: Some("Light Array Layout"), + } + ); + + // shaders/shader.wgsl - @group(3) + let material_tint_bind_group_layout = + 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: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + } + ], + }); let result = Self { surface: Arc::new(surface), @@ -261,6 +335,7 @@ Hardware: 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, @@ -268,6 +343,7 @@ Hardware: light_array_bind_group_layout, light_cube_bind_group_layout, }), + supports_storage: supports_storage_resources, }; Ok(result) @@ -1,20 +1,19 @@ use crate::attenuation::{Attenuation, RANGE_50}; -use crate::buffer::{ResizableBuffer, StorageBuffer, UniformBuffer}; +use crate::buffer::{ResizableBuffer, UniformBuffer}; use crate::graphics::SharedGraphicsContext; -use crate::shader::Shader; -use crate::texture::Texture; +use crate::pipelines::light_cube::InstanceInput; use crate::{ - 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, Buffer, BufferAddress, CompareFunction, DepthBiasState, RenderPipeline, StencilState, VertexBufferLayout}; +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)] @@ -245,12 +244,13 @@ pub struct Light { pub label: String, pub buffer: UniformBuffer<LightUniform>, pub bind_group: BindGroup, - pub instance_buffer: ResizableBuffer<InstanceRaw>, + 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), @@ -313,11 +313,7 @@ impl Light { 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, @@ -326,7 +322,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); @@ -370,292 +366,4 @@ impl Light { pub fn label(&self) -> &str { &self.label } -} - -#[derive(Clone)] -pub struct LightManager { - pub pipeline: Option<RenderPipeline>, - light_array_buffer: Option<StorageBuffer<LightArrayUniform>>, - light_array_bind_group: Option<BindGroup>, -} - -impl Default for LightManager { - fn default() -> Self { - Self::new() - } -} - -impl LightManager { - pub const LIGHT_ARRAY_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = - wgpu::BindGroupLayoutDescriptor { - entries: &[ - // light data - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT), - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - ], - label: Some("Light Array Layout"), - }; - pub const LIGHT_CUBE_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = - wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX.union(wgpu::ShaderStages::FRAGMENT), - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: Some("Per-Light Layout"), - }; - - pub fn new() -> Self { - log::info!("Initialised lighting"); - Self { - pipeline: None, - light_array_buffer: None, - light_array_bind_group: None, - } - } - - pub fn create_light_array_resources(&mut self, graphics: Arc<SharedGraphicsContext>) { - let buffer = StorageBuffer::new(&graphics.device, "Light Array"); - - let bind_group = graphics - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.layouts.light_array_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: buffer.buffer().as_entire_binding(), - }, - ], - label: Some("Light Array Bind Group"), - }); - - self.light_array_buffer = Some(buffer); - self.light_array_bind_group = Some(bind_group); - - 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; - - 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 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.light_array_buffer { - buf.write(&graphics.queue, &light_array); - } - - log_once::debug_once!("LightUniform size = {}", size_of::<LightUniform>()); - } - - 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, - label: Option<&str>, - ) { - use crate::shader::Shader; - - let shader = Shader::new(graphics.clone(), shader_contents, label); - - // the light cube rendering - let pipeline = Self::create_render_pipeline_for_lighting( - graphics, - &shader, - label, - ); - - self.pipeline = Some(pipeline); - log::debug!("Created ECS light render pipeline"); - } - - // light cube rendering - fn create_render_pipeline_for_lighting( - graphics: Arc<SharedGraphicsContext>, - shader: &Shader, - 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: &[&graphics.layouts.camera_bind_group_layout, &graphics.layouts.light_cube_bind_group_layout], - 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: 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::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, - }, - ], - } - } -} +} @@ -537,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(); @@ -550,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(); @@ -566,7 +588,7 @@ impl Model { ); let dimensions = image.dimensions(); - (rgba.into_raw(), dimensions) + Some((rgba.into_raw(), dimensions)) }); log::trace!( @@ -1091,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::Ccw, + 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::Ccw, + 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,14 +1,26 @@ -//! 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; -/// 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 { @@ -33,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 { @@ -41,6 +53,7 @@ impl Shader { None => "shader".into(), }, module, + content: shader_file_contents.to_string(), } } } @@ -1,12 +1,12 @@ use std::{fs, path::PathBuf, sync::Arc}; -use image::{EncodableLayout, GenericImageView}; +use image::GenericImageView; use serde::{Deserialize, Serialize}; use crate::graphics::SharedGraphicsContext; -/// As defined in `shader.wgsl` as +/// As defined in `shaders.wgsl` as /// ``` /// @group(0) @binding(0) /// var t_diffuse: texture_2d<f32>; @@ -193,10 +193,39 @@ impl Texture { view_descriptor: Option<wgpu::TextureViewDescriptor>, sampler: Option<wgpu::SamplerDescriptor>, ) -> Self { - let diffuse_image = image::load_from_memory(bytes).unwrap(); - let diffuse_rgba = diffuse_image.to_rgba8(); - - let dimensions = dimensions.unwrap_or_else(|| diffuse_image.dimensions()); + let (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, @@ -219,7 +248,6 @@ impl Texture { let texture = device.create_texture(&desc); let unpadded_bytes_per_row = 4 * size.width; - debug_assert!(unpadded_bytes_per_row.is_power_of_two()); let padded_bytes_per_row = (unpadded_bytes_per_row + wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1) & !(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1); debug_assert!(diffuse_rgba.len() >= (unpadded_bytes_per_row * size.height) as usize); @@ -239,34 +267,34 @@ impl Texture { }, 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]); + } - let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize]; - let src_stride = unpadded_bytes_per_row as usize; - let dst_stride = padded_bytes_per_row as usize; - for row in 0..size.height as usize { - let src_start = row * src_stride; - let dst_start = row * dst_stride; - padded[dst_start..dst_start + src_stride] - .copy_from_slice(&diffuse_rgba.as_bytes()[src_start..src_start + src_stride]); + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &padded, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(padded_bytes_per_row), + rows_per_image: Some(size.height), + }, + size, + ); } - 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 { @@ -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, +} @@ -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,7 +130,7 @@ 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()); @@ -156,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)?; @@ -343,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(()) @@ -370,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))?; @@ -396,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()), @@ -404,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(()) @@ -413,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))?; @@ -459,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,6 +3,7 @@ use std::mem::size_of; use std::sync::Arc; +use dropbear_engine::pipelines::DropbearShaderPipeline; use dropbear_engine::{entity::Transform, texture::Texture}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::shader::Shader; @@ -12,17 +13,17 @@ 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>, - ) -> Self { +impl DropbearShaderPipeline for ColliderWireframePipeline { + fn new(graphics: Arc<SharedGraphicsContext>) -> Self { let shader = Shader::new( graphics.clone(), - include_str!("../../../../../resources/shaders/collider.wgsl"), - Some("collider wireframe shader"), + include_str!("shaders/collider.wgsl"), + Some("collider wireframe shaders"), ); let pipeline_layout = graphics.device.create_pipeline_layout(&PipelineLayoutDescriptor { @@ -90,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 } } @@ -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; +} @@ -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())); @@ -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); } @@ -16,7 +16,6 @@ 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}; @@ -138,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", @@ -280,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: {}).", @@ -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()); } @@ -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 { @@ -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(); @@ -15,9 +15,9 @@ use crossbeam_channel::{unbounded, Receiver, Sender}; use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::EntityTransform; use dropbear_engine::graphics::InstanceRaw; -use dropbear_engine::shader::Shader; +use dropbear_engine::pipelines::light_cube::LightCubePipeline; use dropbear_engine::texture::TextureWrapMode; -use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{SharedGraphicsContext}, lighting::LightManager, model::{ModelId, MODEL_CACHE}, scene::SceneCommand, DropbearWindowBuilder, WindowData}; +use 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}; @@ -53,10 +53,13 @@ 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 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; @@ -71,14 +74,17 @@ 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 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, + // 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>>>, pub is_viewport_focused: bool, @@ -186,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, @@ -208,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(), @@ -616,9 +623,10 @@ impl Editor { self.previously_selected_entity = None; self.active_camera.lock().take(); - self.render_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(); @@ -1042,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, @@ -1302,38 +1319,12 @@ impl Editor { /// /// **Note**: To be ran AFTER [`Editor::load_project_config`] pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) { - let shader = Shader::new( - graphics.clone(), - include_str!("../../../../resources/shaders/shader.wgsl"), - Some("viewport shader"), - ); - - self.light_manager - .create_light_array_resources(graphics.clone()); - - let pipeline = graphics.create_render_pipline( - &shader, - vec![ - &graphics.layouts.texture_bind_layout, - &graphics.layouts.camera_bind_group_layout, - &graphics.layouts.light_array_bind_group_layout, - &graphics.layouts.material_tint_bind_layout, - ], - None, - ); - self.render_pipeline = Some(pipeline); - - self.light_manager.create_render_pipeline( - graphics.clone(), - include_str!("../../../../resources/shaders/light.wgsl"), - Some("Light Pipeline"), - ); - - let collider_pipeline = ColliderWireframePipeline::new(graphics.clone()); - self.collider_wireframe_pipeline = Some(collider_pipeline); + 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(); } @@ -11,7 +11,7 @@ use dropbear_engine::graphics::{FrameGraphicsContext, InstanceRaw}; use dropbear_engine::model::MODEL_CACHE; use dropbear_engine::{ entity::{EntityTransform, MeshRenderer, Transform}, - lighting::{Light, LightComponent}, + lighting::{Light, LightComponent, MAX_LIGHTS}, model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}, }; @@ -333,13 +333,12 @@ impl Scene for Editor { } } - { - let sim_world = self.world.as_ref(); - - self.light_manager - .update(graphics.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(); @@ -348,6 +347,10 @@ impl Scene for Editor { } 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, @@ -356,9 +359,6 @@ impl Scene for Editor { a: 1.0, }; self.color = clear_color; - self.size = graphics.viewport_texture.size; - self.texture_id = Some(*graphics.texture_id.clone()); - self.window = Some(graphics.window.clone()); eucalyptus_core::logging::render(&graphics.get_egui_context()); let cam = { @@ -379,7 +379,7 @@ impl Scene for Editor { 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_render_pipeline else { log_once::warn_once!("Render pipeline not ready"); return; }; @@ -394,6 +394,16 @@ impl Scene for Editor { lights }; + 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 renderers = { let mut renderers = Vec::new(); let mut query = self.world.query::<&MeshRenderer>(); @@ -455,8 +465,6 @@ impl Scene for Editor { } } - self.light_manager.update(graphics.clone(), &self.world); - { let mut render_pass = frame_ctx.encoder .begin_render_pass(&wgpu::RenderPassDescriptor { @@ -481,8 +489,8 @@ impl Scene for Editor { occlusion_query_set: None, timestamp_writes: None, }); - if let Some(light_pipeline) = &self.light_manager.pipeline { - render_pass.set_pipeline(light_pipeline); + 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 { @@ -495,39 +503,47 @@ impl Scene for Editor { } } } - - for (model, instance_buffer, instance_count) in prepared_models { - let mut render_pass = frame_ctx.encoder - .begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("model render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &frame_ctx.view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, + 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: &frame_ctx.view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - }); - render_pass.set_pipeline(pipeline); - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - render_pass.draw_model_instanced( - &model, - 0..instance_count, - &camera.bind_group, - self.light_manager.bind_group(), - ); + 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(), + ); + } } { @@ -6,6 +6,7 @@ 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 @@ -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); } } @@ -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(); @@ -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..."), ]; @@ -3,17 +3,18 @@ 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::future::FutureHandle; use dropbear_engine::graphics::SharedGraphicsContext; -use dropbear_engine::lighting::LightManager; use dropbear_engine::scene::SceneCommand; -use dropbear_engine::shader::Shader; use eucalyptus_core::input::InputState; use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT}; @@ -42,7 +43,7 @@ fn find_jvm_library_path() -> PathBuf { } else { proj.project_path.clone() } - .join("../../../../build/libs"); + .join("build/libs"); let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None; @@ -80,8 +81,10 @@ pub struct PlayMode { active_camera: Option<Entity>, // rendering - render_pipeline: Option<RenderPipeline>, - light_manager: LightManager, + 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>, @@ -103,7 +106,6 @@ pub struct PlayMode { 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>>, } @@ -135,8 +137,9 @@ impl PlayMode { 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, physics_pipeline: Default::default(), @@ -157,35 +160,10 @@ impl PlayMode { } pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { - let shader = Shader::new( - graphics.clone(), - include_str!("../../../resources/shaders/shader.wgsl"), - Some("viewport shader"), - ); - - self.light_manager - .create_light_array_resources(graphics.clone()); - - let pipeline = graphics.create_render_pipline( - &shader, - vec![ - &graphics.layouts.texture_bind_layout, - &graphics.layouts.camera_bind_group_layout, - &graphics.layouts.light_array_bind_group_layout, - &graphics.layouts.material_tint_bind_layout, - ], - None, - ); - self.render_pipeline = Some(pipeline); - - self.light_manager.create_render_pipeline( - graphics.clone(), - include_str!("../../../resources/shaders/light.wgsl"), - Some("Light Pipeline"), - ); - - let collider_pipeline = ColliderWireframePipeline::new(graphics.clone()); - self.collider_wireframe_pipeline = Some(collider_pipeline); + 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) { @@ -305,7 +283,8 @@ impl PlayMode { 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; @@ -1,6 +1,7 @@ use std::sync::Arc; use std::collections::HashMap; +use dropbear_engine::pipelines::DropbearShaderPipeline; use eucalyptus_core::egui::{CentralPanel, MenuBar, TopBottomPanel}; use eucalyptus_core::physics::collider::ColliderGroup; use eucalyptus_core::physics::collider::ColliderShapeKey; @@ -15,6 +16,7 @@ use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext, FrameGraphicsContext}; use dropbear_engine::lighting::{Light, LightComponent}; +use dropbear_engine::lighting::MAX_LIGHTS; use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE}; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::camera::CameraComponent; @@ -362,9 +364,11 @@ impl Scene for PlayMode { } } - self.light_manager - .update(graphics.clone(), &self.world); - + if let Some(l) = &mut self.light_cube_pipeline { + l.update(graphics.clone(), &self.world); + } + + #[cfg(feature = "debug")] TopBottomPanel::top("menu_bar").show(&graphics.get_egui_context(), |ui| { MenuBar::new().ui(ui, |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { @@ -466,7 +470,7 @@ impl Scene for PlayMode { 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; }; @@ -488,6 +492,16 @@ impl Scene for PlayMode { lights }; + 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 renderers = { let mut renderers = Vec::new(); let mut query = self.world.query::<&MeshRenderer>(); @@ -549,8 +563,6 @@ impl Scene for PlayMode { } } - self.light_manager.update(graphics.clone(), &self.world); - { let mut render_pass = frame_ctx.encoder .begin_render_pass(&wgpu::RenderPassDescriptor { @@ -575,8 +587,8 @@ impl Scene for PlayMode { occlusion_query_set: None, timestamp_writes: None, }); - if let Some(light_pipeline) = &self.light_manager.pipeline { - render_pass.set_pipeline(light_pipeline); + 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 { @@ -591,6 +603,18 @@ impl Scene for PlayMode { } for (model, instance_buffer, instance_count) in prepared_models { + 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"), @@ -614,13 +638,14 @@ impl Scene for PlayMode { occlusion_query_set: None, timestamp_writes: None, }); - render_pass.set_pipeline(pipeline); + 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(), + light_bind_group, ); } @@ -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,62 +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, -} - -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,245 +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, -} - -struct LightArray { - _lights: array<Light, MAX_LIGHTS>, - light_count: u32, - ambient_strength: f32, -} - -struct MaterialUniform { - // for stuff like tinting - colour: vec4<f32>, - - // scales incoming UVs before sampling - uv_tiling: vec2<f32>, - _pad: vec2<f32>, -} - -@group(0) @binding(0) -var t_diffuse: texture_2d<f32>; -@group(0) @binding(1) -var s_diffuse: sampler; -@group(0) @binding(2) -var t_normal: texture_2d<f32>; -@group(0) @binding(3) -var s_normal: sampler; - -@group(3) @binding(0) -var<uniform> u_material: MaterialUniform; - -@group(1) @binding(0) -var<uniform> camera: CameraUniform; - -@group(2) @binding(0) -var<storage, read> light_array: LightArray; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, - - @location(9) normal_matrix_0: vec3<f32>, - @location(10) normal_matrix_1: vec3<f32>, - @location(11) normal_matrix_2: vec3<f32>, -}; - -struct VertexInput { - @location(0) position: vec3<f32>, - @location(1) tex_coords: vec2<f32>, - @location(2) normal: vec3<f32>, - @location(3) tangent: vec3<f32>, - @location(4) bitangent: vec3<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) tex_coords: vec2<f32>, - @location(1) world_normal: vec3<f32>, - @location(2) world_position: vec3<f32>, - @location(3) world_tangent: vec3<f32>, - @location(4) world_bitangent: vec3<f32>, -}; - -@vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - let normal_matrix = mat3x3<f32>( - instance.normal_matrix_0, - instance.normal_matrix_1, - instance.normal_matrix_2, - ); - - let world_normal = normalize(normal_matrix * model.normal); - let world_tangent = normalize(normal_matrix * model.tangent); - let world_bitangent = normalize(normal_matrix * model.bitangent); - let world_position = model_matrix * vec4<f32>(model.position, 1.0); - - var out: VertexOutput; - out.clip_position = camera.view_proj * world_position; - out.tex_coords = model.tex_coords; - out.world_normal = world_normal; - out.world_position = world_position.xyz; - out.world_tangent = world_tangent; - out.world_bitangent = world_bitangent; - return out; -} - -fn directional_light( - light: Light, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32>, - world_pos: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(-light.direction.xyz); - - let ambient = light.color.xyz * light_array.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; - - return ambient + diffuse + specular; -} - -fn point_light( - light: Light, - world_pos: vec3<f32>, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); - - let ambient = light.color.xyz * light_array.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; - - return (ambient + diffuse + specular) * attenuation; -} - -fn spot_light( - light: Light, - world_pos: vec3<f32>, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); - let theta = dot(light_dir, normalize(-light.direction.xyz)); - let outer_cutoff = light.direction.w; - - let epsilon = light.cutoff - outer_cutoff; - let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); - - let ambient = light.color.xyz * light_array.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color * intensity; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color * intensity; - - return (ambient + diffuse + specular) * attenuation; -} - -fn apply_normal_map( - world_normal_in: vec3<f32>, - world_tangent_in: vec3<f32>, - world_bitangent_in: vec3<f32>, - normal_sample_rgb: vec3<f32>, -) -> vec3<f32> { - let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0)); - - let n = normalize(world_normal_in); - var t = normalize(world_tangent_in); - t = normalize(t - n * dot(n, t)); - - let b_in = normalize(world_bitangent_in); - let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0); - let b = cross(n, t) * handedness; - - let tbn = mat3x3<f32>(t, b, n); - return normalize(tbn * normal_ts); -} - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - let uv = in.tex_coords * u_material.uv_tiling; - var tex_color = textureSample(t_diffuse, s_diffuse, uv); - var object_normal = textureSample(t_normal, s_normal, uv); - - let base_colour = tex_color * u_material.colour; - - if (base_colour.a < 0.1) { - discard; - } - - let view_dir = normalize(camera.view_pos.xyz - in.world_position); - - let world_normal = apply_normal_map( - in.world_normal, - in.world_tangent, - in.world_bitangent, - object_normal.xyz, - ); - - var final_color = vec3<f32>(0.0); - - for (var i: u32 = 0u; i < light_array.light_count; i = i + 1u) { - let light = light_array._lights[i]; - - let light_type = i32(light.color.w + 0.1); - - if (light_type == 0) { - final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); - } else if (light_type == 1) { - final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } else if (light_type == 2) { - final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } - } - - return vec4<f32>(final_color, base_colour.a); -}