tirbofish/dropbear · commit
0f3aade9411d597578d6c29027f6f5434ca905e1
refactor: refactoring the render() functions to make it more organised, plan on optimising later.
Signature present but could not be verified.
Unverified
@@ -6,6 +6,9 @@ use glam::Mat4; use std::collections::HashMap; use std::sync::Arc; +pub const MAX_MORPH_WEIGHTS: usize = 4096; +pub const MAX_SKINNING_MATRICES: usize = 256; + #[repr(C)] #[derive(Copy, Clone, Default, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct MorphTargetInfo { @@ -573,5 +576,3 @@ impl AnimationComponent { } } } - -pub const MAX_MORPH_WEIGHTS: usize = 4096; @@ -1,6 +1,7 @@ //! Vertices and different buffers used for wgpu use std::marker::PhantomData; +use bytemuck::NoUninit; #[derive(Debug, Clone)] pub struct ResizableBuffer<T> { @@ -11,7 +12,7 @@ pub struct ResizableBuffer<T> { _marker: PhantomData<T>, } -impl<T: bytemuck::Pod> ResizableBuffer<T> { +impl<T: NoUninit> ResizableBuffer<T> { pub fn new( device: &wgpu::Device, initial_capacity: usize, @@ -85,7 +86,7 @@ pub struct UniformBuffer<T> { _marker: PhantomData<T>, } -impl<T: bytemuck::Pod> UniformBuffer<T> { +impl<T: NoUninit> UniformBuffer<T> { pub fn new(device: &wgpu::Device, label: &str) -> Self { let size = (std::mem::size_of::<T>() as wgpu::BufferAddress).max(16); let buffer = device.create_buffer(&wgpu::BufferDescriptor { @@ -117,7 +118,6 @@ impl<T: bytemuck::Pod> UniformBuffer<T> { } } -/// A wrapper to a [wgpu::Buffer] that stores #[derive(Debug, Clone)] pub struct StorageBuffer<T> { buffer: wgpu::Buffer, @@ -125,7 +125,7 @@ pub struct StorageBuffer<T> { _marker: PhantomData<T>, } -impl<T: bytemuck::Pod> StorageBuffer<T> { +impl<T: NoUninit> StorageBuffer<T> { pub fn new_read_only(device: &wgpu::Device, label: &str) -> Self { Self::new(device, label, true) } @@ -176,3 +176,27 @@ impl<T: bytemuck::Pod> StorageBuffer<T> { &self.label } } + +impl<T: NoUninit> StorageBuffer<T> { + pub fn new_slice(device: &wgpu::Device, label: &str, count: usize, read_only: bool) -> Self { + let usage = if read_only { + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST + } else { + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC + }; + + let size = ((std::mem::size_of::<T>() * count) as wgpu::BufferAddress).max(16); + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage, + mapped_at_creation: false, + }); + + Self { buffer, label: label.to_string(), _marker: PhantomData } + } + + pub fn write_slice(&self, queue: &wgpu::Queue, values: &[T]) { + queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(values)); + } +} @@ -0,0 +1,73 @@ +use std::sync::Arc; +use glam::{Mat4}; +use wgpu::util::DeviceExt; +use dropbear_utils::Dirty; +use crate::animation::{MorphTargetInfo, MAX_MORPH_WEIGHTS, MAX_SKINNING_MATRICES}; +use crate::buffer::{StorageBuffer, UniformBuffer}; +use crate::graphics::SharedGraphicsContext; + +pub struct AnimationDefaults { + pub skinning_buffer: StorageBuffer<Mat4>, + pub morph_deltas_buffer: StorageBuffer<f32>, + pub morph_weights_buffer: StorageBuffer<f32>, + pub morph_info_buffer: UniformBuffer<MorphTargetInfo>, + pub animation_bind_group: wgpu::BindGroup, +} + +impl AnimationDefaults { + pub fn new(graphics: Arc<SharedGraphicsContext>) -> Self { + let identity = vec![Mat4::IDENTITY; MAX_SKINNING_MATRICES]; + let skinning_buffer = StorageBuffer::new_slice( + &graphics.device, "editor default skinning buffer", MAX_SKINNING_MATRICES, true + ); + skinning_buffer.write_slice(&graphics.queue, &identity); + + // mapped to default value + let morph_deltas_buffer: StorageBuffer<f32> = StorageBuffer::new_read_only( + &graphics.device, "editor default morph deltas buffer" + ); + + let morph_weights_buffer: StorageBuffer<f32> = StorageBuffer::new_slice( + &graphics.device, "editor default skinning buffer", MAX_MORPH_WEIGHTS, true + ); + + let morph_info = MorphTargetInfo::default(); + let morph_info_buffer = UniformBuffer::new( + &graphics.device, "editor default morph info buffer" + ); + morph_info_buffer.write(&graphics.queue, &morph_info); + + let animation_bind_group = graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("editor default animation bind group"), + layout: &graphics.layouts.animation_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: skinning_buffer.buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: morph_deltas_buffer.buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: morph_weights_buffer.buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: morph_info_buffer.buffer().as_entire_binding(), + }, + ], + }, + ); + + Self { + skinning_buffer, + morph_deltas_buffer, + morph_weights_buffer, + morph_info_buffer, + animation_bind_group, + } + } +} @@ -6,6 +6,7 @@ pub mod globals; pub mod hdr; pub mod light_cube; pub mod shader; +pub mod animation; pub use globals::{Globals, GlobalsUniform}; @@ -153,76 +153,6 @@ impl MainRenderPipeline { self.per_frame.as_ref().unwrap() // safe as its guaranteed to always have some content } - pub fn per_material_bind_group( - &mut self, - graphics: Arc<SharedGraphicsContext>, - material_uniform_buffer: &wgpu::Buffer, - diffuse_texture: &Texture, - normal_texture: &Texture, - emissive_texture: &Texture, - metallic_texture: &Texture, - occlusion_texture: &Texture, - ) -> &wgpu::BindGroup { - if self.per_material.is_none() { - let bind_group = graphics - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("per material bind group"), - layout: &graphics.layouts.material_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: material_uniform_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&diffuse_texture.view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::TextureView(&normal_texture.view), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: wgpu::BindingResource::Sampler(&normal_texture.sampler), - }, - wgpu::BindGroupEntry { - binding: 5, - resource: wgpu::BindingResource::TextureView(&emissive_texture.view), - }, - wgpu::BindGroupEntry { - binding: 6, - resource: wgpu::BindingResource::Sampler(&emissive_texture.sampler), - }, - wgpu::BindGroupEntry { - binding: 7, - resource: wgpu::BindingResource::TextureView(&metallic_texture.view), - }, - wgpu::BindGroupEntry { - binding: 8, - resource: wgpu::BindingResource::Sampler(&metallic_texture.sampler), - }, - wgpu::BindGroupEntry { - binding: 9, - resource: wgpu::BindingResource::TextureView(&occlusion_texture.view), - }, - wgpu::BindGroupEntry { - binding: 10, - resource: wgpu::BindingResource::Sampler(&occlusion_texture.sampler), - }, - ], - }); - - self.per_material = Some(bind_group); - } - - self.per_material.as_ref().unwrap() - } - pub fn animation_bind_group( &self, graphics: Arc<SharedGraphicsContext>, @@ -231,6 +161,8 @@ impl MainRenderPipeline { morph_weights_buffer: &wgpu::Buffer, morph_info_buffer: &wgpu::Buffer, ) -> wgpu::BindGroup { + + graphics .device .create_bind_group(&wgpu::BindGroupDescriptor { @@ -52,6 +52,7 @@ rkyv.workspace = true bitflags.workspace = true uuid.workspace = true splines.workspace = true +puffin.workspace = true [features] default = [] @@ -30,6 +30,7 @@ pub mod types; pub mod ui; pub mod utils; pub mod uuid; +pub mod rendering; pub use dropbear_macro as macros; @@ -0,0 +1,641 @@ +use std::collections::HashMap; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::Arc; +use hecs::{Entity, World}; +use glam::{Mat4, Quat, Vec3}; +use dropbear_engine::animation::{AnimationComponent, MorphTargetInfo}; +use dropbear_engine::asset::{Handle, ASSET_REGISTRY}; +use dropbear_engine::billboarding::BillboardPipeline; +use dropbear_engine::buffer::ResizableBuffer; +use dropbear_engine::camera::Camera; +use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; +use dropbear_engine::graphics::{CommandEncoder, InstanceRaw, SharedGraphicsContext}; +use dropbear_engine::lighting::Light; +use dropbear_engine::model::{DrawLight, DrawModel, Material, Mesh, Model}; +use dropbear_engine::pipelines::DropbearShaderPipeline; +use dropbear_engine::pipelines::animation::AnimationDefaults; +use dropbear_engine::pipelines::hdr::HdrPipeline; +use dropbear_engine::pipelines::light_cube::LightCubePipeline; +use dropbear_engine::pipelines::shader::MainRenderPipeline; +use dropbear_engine::sky::SkyPipeline; +use kino_ui::KinoState; +use crate::billboard::BillboardComponent; +use crate::debug::DebugDrawExt; +use crate::entity_status::EntityStatus; +use crate::hierarchy::EntityTransformExt; +use crate::physics::collider::ColliderGroup; +use crate::states::SCENES; + +pub struct RenderInstance { + pub entity: Entity, + pub instance: InstanceRaw, + pub animation: Option<AnimationBuffers>, +} + +pub struct AnimationBuffers { + pub skinning: wgpu::Buffer, + pub morph_weights: wgpu::Buffer, + pub morph_info: wgpu::Buffer, + pub weight_count: u32, +} + +pub struct ModelBatch { + pub model_id: u64, + pub instances: Vec<RenderInstance>, +} + +pub struct PreparedModel { + pub model: Arc<Model>, + pub handle_id: u64, + pub instance_count: u32, + pub entity: Option<Entity>, +} + +/// Just common rendering functions that are shared between redback-runtime and eucalyptus-editor. +pub struct RendererCommon; + +impl RendererCommon { + pub fn clear_viewport(graphics: &SharedGraphicsContext, encoder: &mut CommandEncoder, hdr: &HdrPipeline) { + puffin::profile_scope!("Clearing viewport"); + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("viewport clear pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.render_view(), + depth_slice: None, + resolve_target: hdr.resolve_target(), + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 100.0 / 255.0, + g: 149.0 / 255.0, + b: 237.0 / 255.0, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(0.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + } + + pub fn collect_lights(world: &World) -> (Vec<Light>, u32) { + puffin::profile_scope!("Locating lights"); + let mut lights = Vec::new(); + let mut enabled = 0u32; + for light in world.query::<&Light>().iter() { + if light.component.enabled { enabled += 1; } + lights.push(light.clone()); + } + (lights, enabled) + } + + pub fn locate_renderers( + world: &World, + batches: &mut HashMap<u64, ModelBatch>, + graphics: Arc<SharedGraphicsContext>, + default_skinning_buffer: &Option<wgpu::Buffer>, + ) { + puffin::profile_scope!("finding all renderers"); + + let mut query = world.query::<(Entity, &MeshRenderer, Option<&mut AnimationComponent>)>(); + + for (entity, renderer, animation) in query.iter() { + if let Ok(status) = world.get::<&EntityStatus>(entity) { + if status.hidden || status.disabled { continue; } + } + + let handle = renderer.model(); + if handle.is_null() { continue; } + + let instance_raw = renderer.instance.to_raw(); + + let animation_buffers = Self::resolve_animation_buffers( + graphics.clone(), + default_skinning_buffer, + animation + ); + + batches + .entry(handle.id) + .or_insert_with(|| ModelBatch { model_id: handle.id, instances: Vec::new() }) + .instances + .push(RenderInstance { + entity, + instance: instance_raw, + animation: animation_buffers, + }); + } + } + + fn resolve_animation_buffers( + graphics: Arc<SharedGraphicsContext>, + default_skinning_buffer: &Option<wgpu::Buffer>, + animation: Option<&mut AnimationComponent>, + ) -> Option<AnimationBuffers> { + let anim = animation?; + + let has_skinning = !anim.skinning_matrices.is_empty(); + let has_morph = !anim.morph_weights.is_empty(); + + if !has_skinning && !has_morph { + return None; + } + + anim.prepare_gpu_resources(graphics.clone()); + + let skinning = anim + .skinning_buffer + .as_ref() + .and_then(|b| Some(b.buffer().clone())) + .or_else(|| default_skinning_buffer.clone())?; + + let morph_weights = anim.morph_weights_buffer + .as_ref() + .map(|b| b.buffer().clone())?; + + let morph_info = anim.morph_info_buffer + .as_ref() + .map(|b| b.buffer().clone())?; + + Some(AnimationBuffers { + skinning, + morph_weights, + morph_info, + weight_count: anim.morph_weight_count, + }) + } + + pub fn prepare_models( + graphics: &SharedGraphicsContext, + batches: &HashMap<u64, ModelBatch>, + instance_buffer_cache: &mut HashMap<u64, ResizableBuffer<InstanceRaw>>, + ) -> (Vec<PreparedModel>, HashMap<u64, Arc<Model>>) { + puffin::profile_scope!("preparing models"); + let registry = ASSET_REGISTRY.read(); + let mut model_cache = HashMap::new(); + let mut prepared = Vec::new(); + + for (handle_id, batch) in batches { + let static_instances: Vec<_> = batch.instances.iter() + .filter(|i| i.animation.is_none()) + .collect(); + if static_instances.is_empty() { continue; } + + let Some(model) = registry.get_model(Handle::new(*handle_id)) else { + log_once::error_once!("Missing model handle {} in registry", handle_id); + continue; + }; + + let instances: Vec<InstanceRaw> = static_instances.iter() + .map(|i| i.instance) + .collect(); + let entity = static_instances.first().map(|i| i.entity); + + let instance_buffer = instance_buffer_cache + .entry(*handle_id) + .or_insert_with(|| ResizableBuffer::new( + &graphics.device, + instances.len().max(1), + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + "Runtime Instance Buffer", + )); + instance_buffer.write(&graphics.device, &graphics.queue, &instances); + + model_cache.insert(*handle_id, model.clone()); + prepared.push(PreparedModel { + model, + handle_id: *handle_id, + instance_count: instances.len() as u32, + entity, + }); + } + + // also cache models needed for animated instances + for batch in batches.values() { + for inst in &batch.instances { + if inst.animation.is_some() && !model_cache.contains_key(&batch.model_id) { + if let Some(model) = registry.get_model(Handle::new(batch.model_id)) { + model_cache.insert(batch.model_id, model); + } + } + } + } + + (prepared, model_cache) + } + + pub fn render_light_cubes( + graphics: &Arc<SharedGraphicsContext>, + encoder: &mut CommandEncoder, + hdr: &HdrPipeline, + lights: &[Light], + camera: &Camera, + + light_cube_pipeline: Option<&LightCubePipeline>, + ) { + let (Some(light_pipeline), Some(l)) = (&light_cube_pipeline, lights.first()) else { return }; + let Some(model) = ASSET_REGISTRY.read().get_model(l.cube_model) else { + log_once::error_once!("Missing light cube model handle in registry"); + return; + }; + + puffin::profile_scope!("light cube pass"); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("light cube render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.render_view(), + depth_slice: None, + resolve_target: hdr.resolve_target(), + ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + pass.set_pipeline(light_pipeline.pipeline()); + for light in lights { + puffin::profile_scope!("rendering light", &light.label); + pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); + if !light.component.visible { continue; } + pass.draw_light_model(&model, &camera.bind_group, &light.bind_group); + } + } + + pub fn render_models( + graphics: &Arc<SharedGraphicsContext>, + encoder: &mut CommandEncoder, + hdr: &HdrPipeline, + world: &World, + batches: &HashMap<u64, ModelBatch>, + model_cache: &HashMap<u64, Arc<Model>>, + per_frame_bind_group: &wgpu::BindGroup, + environment_bind_group: &wgpu::BindGroup, + pipeline: &MainRenderPipeline, + animation_defaults: &AnimationDefaults, + instance_buffer_cache: &HashMap<u64, ResizableBuffer<InstanceRaw>>, + animated_instance_buffers: &mut HashMap<Entity, ResizableBuffer<InstanceRaw>>, + animated_bind_group_cache: &mut HashMap<Entity, (u64, wgpu::BindGroup)>, + static_bind_group_cache: &mut HashMap<u64, wgpu::BindGroup>, + last_morph_info_per_mesh: &mut HashMap<u32, MorphTargetInfo>, + ) { + puffin::profile_scope!("model render pass"); + + for (_, batch) in batches { + let Some(model) = model_cache.get(&batch.model_id) else { continue }; + + let static_count = batch.instances.iter().filter(|i| i.animation.is_none()).count() as u32; + if static_count > 0 { + let Some(first) = batch.instances.iter().find(|i| i.animation.is_none()) else { continue }; + let Ok(renderer) = world.get::<&MeshRenderer>(first.entity) else { continue }; + + if let Some(deltas) = model.morph_deltas_buffer.as_ref() { + if !static_bind_group_cache.contains_key(&batch.model_id) { + let bg = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("static model animation bind group"), + layout: &graphics.layouts.animation_layout, + entries: &[ + wgpu::BindGroupEntry { binding: 0, resource: animation_defaults.skinning_buffer.buffer().as_entire_binding() }, + wgpu::BindGroupEntry { binding: 1, resource: deltas.as_entire_binding() }, + wgpu::BindGroupEntry { binding: 2, resource: animation_defaults.morph_weights_buffer.buffer().as_entire_binding() }, + wgpu::BindGroupEntry { binding: 3, resource: animation_defaults.morph_info_buffer.buffer().as_entire_binding() }, + ], + }); + static_bind_group_cache.insert(batch.model_id, bg); + } + } + + let animation_bg: &wgpu::BindGroup = if model.morph_deltas_buffer.is_some() { + static_bind_group_cache.get(&batch.model_id).unwrap() + } else { + &animation_defaults.animation_bind_group + }; + + let Some(instance_buffer) = instance_buffer_cache.get(&batch.model_id) else { continue }; + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.render_view(), + depth_slice: None, + resolve_target: hdr.resolve_target(), + ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + pass.set_pipeline(pipeline.pipeline()); + pass.set_vertex_buffer(1, instance_buffer.slice(static_count as usize)); + + for mesh in &model.meshes { + let mut weights = mesh.morph_default_weights.clone(); + let target_count = mesh.morph_target_count as usize; + if weights.len() < target_count { weights.resize(target_count, 0.0); } + if weights.is_empty() { weights.push(0.0); } + + graphics.queue.write_buffer( + animation_defaults.morph_weights_buffer.buffer(), 0, + bytemuck::cast_slice(&weights), + ); + + let info = MorphTargetInfo { + num_vertices: mesh.morph_vertex_count, + num_targets: mesh.morph_target_count, + base_offset: mesh.morph_deltas_offset, + weight_offset: 0, + uses_morph: if mesh.morph_target_count > 0 && !weights.is_empty() { 1 } else { 0 }, + _padding: Default::default(), + }; + + let cache_key = mesh.morph_deltas_offset; + let needs_write = last_morph_info_per_mesh.get(&cache_key).map_or(true, |prev| { + prev.num_vertices != info.num_vertices + || prev.num_targets != info.num_targets + || prev.base_offset != info.base_offset + || prev.uses_morph != info.uses_morph + }); + if needs_write { + graphics.queue.write_buffer( + animation_defaults.morph_info_buffer.buffer(), 0, + bytemuck::bytes_of(&info), + ); + last_morph_info_per_mesh.insert(cache_key, info); + } + + let material = Self::resolve_material(model, mesh, &renderer); + pass.draw_mesh_instanced(mesh, material, 0..static_count, per_frame_bind_group, animation_bg, environment_bind_group); + } + } + + for inst in batch.instances.iter().filter(|i| i.animation.is_some()) { + puffin::profile_scope!("rendering animated model", format!("{:?}", inst.entity)); + let anim = inst.animation.as_ref().unwrap(); + + { + let buf = animated_instance_buffers.entry(inst.entity).or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, 1, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + "animated instance buffer", + ) + }); + buf.write(&graphics.device, &graphics.queue, &[inst.instance]); + } + + let Ok(renderer) = world.get::<&MeshRenderer>(inst.entity) else { continue }; + + let morph_deltas = model.morph_deltas_buffer.as_ref() + .map(|b| b as &wgpu::Buffer) + .unwrap_or_else(|| animation_defaults.morph_deltas_buffer.buffer()); + + let mut hasher = DefaultHasher::new(); + anim.skinning.hash(&mut hasher); + let stamp = hasher.finish(); + + { + let cached = animated_bind_group_cache.get(&inst.entity); + if cached.map_or(true, |(s, _)| *s != stamp) { + let bg = pipeline.animation_bind_group( + graphics.clone(), + &anim.skinning, + morph_deltas, + &anim.morph_weights, + &anim.morph_info, + ); + animated_bind_group_cache.insert(inst.entity, (stamp, bg)); + } + } + + let animation_bg = &animated_bind_group_cache[&inst.entity].1; + let instance_buffer = &animated_instance_buffers[&inst.entity]; + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("animated model render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.render_view(), + depth_slice: None, + resolve_target: hdr.resolve_target(), + ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + pass.set_pipeline(pipeline.pipeline()); + pass.set_vertex_buffer(1, instance_buffer.slice(1)); + + for mesh in &model.meshes { + let mesh_target_count = mesh.morph_target_count.min(anim.weight_count); + let info = MorphTargetInfo { + num_vertices: mesh.morph_vertex_count, + num_targets: mesh_target_count, + base_offset: mesh.morph_deltas_offset, + weight_offset: 0, + uses_morph: if mesh_target_count > 0 { 1 } else { 0 }, + _padding: Default::default(), + }; + graphics.queue.write_buffer(&anim.morph_info, 0, bytemuck::bytes_of(&info)); + + let material = Self::resolve_material(model, mesh, &renderer); + pass.draw_mesh_instanced(mesh, material, 0..1, per_frame_bind_group, animation_bg, environment_bind_group); + } + } + } + } + + pub fn render_sky( + graphics: &SharedGraphicsContext, + encoder: &mut CommandEncoder, + hdr: &HdrPipeline, + sky: &SkyPipeline, + ) { + puffin::profile_scope!("sky render pass"); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("sky render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.render_view(), + depth_slice: None, + resolve_target: hdr.resolve_target(), + 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, + }), + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + pass.set_pipeline(&sky.pipeline); + pass.set_bind_group(0, &sky.camera_bind_group, &[]); + pass.set_bind_group(1, &sky.environment_bind_group, &[]); + pass.draw(0..3, 0..1); + } + + pub fn render_collider_debug( + graphics: &SharedGraphicsContext, + world: &World, + current_scene_name: Option<&str>, + ) { + let show_hitboxes = current_scene_name + .and_then(|scene_name| { + let scenes = SCENES.read(); + scenes.iter() + .find(|s| s.scene_name == scene_name) + .map(|s| s.settings.show_hitboxes) + }) + .unwrap_or(false); + + if !show_hitboxes { return; } + + puffin::profile_scope!("collider debug draw"); + if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() { + let colour = [0.0, 1.0, 0.0, 1.0]; + let to_draw: Vec<_> = { + let mut q = world.query::<(Entity, &ColliderGroup)>(); + q.iter().map(|(e, cg)| (e, cg.colliders.clone())).collect() + }; + for (entity, colliders) in to_draw { + let Ok(et) = world.get::<&EntityTransform>(entity) else { continue }; + let world_tf = et.propagate(world, entity); + drop(et); + for collider in &colliders { + let entity_matrix = world_tf.matrix().as_mat4(); + let offset_transform = Transform::new().with_offset(collider.translation, collider.rotation); + let offset_matrix = offset_transform.matrix().as_mat4(); + let final_matrix = entity_matrix * offset_matrix; + let (scale, rotation, translation) = final_matrix.to_scale_rotation_translation(); + debug_draw.draw_collider(&collider.shape, translation, scale, rotation, colour); + } + } + } + } + + pub fn render_billboards( + graphics: &Arc<SharedGraphicsContext>, + encoder: &mut CommandEncoder, + hdr: &HdrPipeline, + camera: &Camera, + world: &World, + kino: Option<&mut KinoState>, + billboard_pipeline: Option<&BillboardPipeline>, + ) { + puffin::profile_scope!("rendering billboard targets"); + + let mut kino_views: HashMap<u64, wgpu::TextureView> = HashMap::new(); + + if let Some(kino) = kino { + let mut kino_encoder = CommandEncoder::new(graphics.clone(), Some("kino billboard encoder")); + kino.render_billboard_targets(&graphics.device, &graphics.queue, &mut kino_encoder); + if let Err(e) = kino_encoder.submit() { + log_once::error_once!("Unable to submit billboard kino pass: {}", e); + } + kino_views.extend(kino.billboard_render_target_views()); + } + + let Some(billboard_pipeline) = billboard_pipeline else { return }; + + let camera_position = camera.position().as_vec3(); + let camera_projection = Mat4::from_cols_array_2d(&camera.uniform.view_proj); + + let single_fallback_view = if kino_views.len() == 1 { + kino_views.values().next().cloned() + } else { + None + }; + + let mut billboards: Vec<(Mat4, wgpu::TextureView)> = Vec::new(); + let mut query = world.query::<(Entity, &BillboardComponent, Option<&EntityTransform>)>(); + + for (entity, billboard, entity_transform) in query.iter() { + puffin::profile_scope!("rendering billboard", format!("{:?}", entity)); + if !billboard.enabled { continue; } + + let entity_id = entity.to_bits().get(); + let texture_view = kino_views.get(&entity_id).cloned() + .or_else(|| single_fallback_view.clone()); + let Some(texture_view) = texture_view else { continue }; + + let position = entity_transform + .map(|t| t.sync().position.as_vec3()) + .unwrap_or(Vec3::ZERO) + + billboard.offset; + let scale = Vec3::new(billboard.world_size.x, billboard.world_size.y, 1.0); + + let rotation = if let Some(r) = billboard.rotation { + r + } else { + let to_camera = (camera_position - position).normalize_or_zero(); + if to_camera.length_squared() > 0.0 { + let mut world_up = Vec3::Y; + if to_camera.dot(world_up).abs() > 0.999 { world_up = Vec3::X; } + let right = world_up.cross(to_camera).normalize_or_zero(); + let up = to_camera.cross(right).normalize_or_zero(); + Quat::from_mat3(&glam::Mat3::from_cols(right, up, to_camera)) + } else { + Quat::IDENTITY + } + }; + + billboards.push((Mat4::from_scale_rotation_translation(scale, rotation, position), texture_view)); + } + + if billboards.is_empty() { return; } + + puffin::profile_scope!("billboard render pass"); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("billboard render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.render_view(), + depth_slice: None, + resolve_target: hdr.resolve_target(), + 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, + }), + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + + for (transform, texture_view) in billboards { + billboard_pipeline.draw(graphics.clone(), &mut pass, transform, camera_projection, &texture_view); + } + } + + fn resolve_material<'a>(model: &'a Model, mesh: &Mesh, renderer: &'a MeshRenderer) -> &'a Material { + let material = &model.materials[mesh.material]; + renderer.material_snapshot.get(&material.name).unwrap_or_else(|| { + log_once::warn_once!("Unable to locate MeshRenderer's material_snapshot for that specific material"); + material + }) + } +} @@ -73,6 +73,8 @@ use wgpu::{Color, Extent3d}; use winit::dpi::PhysicalSize; use winit::window::{CursorGrabMode, WindowAttributes}; use winit::{keyboard::KeyCode, window::Window}; +use dropbear_engine::model::Animation; +use dropbear_engine::pipelines::animation::AnimationDefaults; pub struct Editor { pub dt: f32, @@ -84,7 +86,6 @@ pub struct Editor { pub texture_id: Option<egui::TextureId>, pub size: Extent3d, pub instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>, - pub animated_instance_buffers: HashMap<Entity, ResizableBuffer<InstanceRaw>>, pub color: Color, pub ui_editor: UiEditor, @@ -99,22 +100,10 @@ pub struct Editor { pub sky_pipeline: Option<SkyPipeline>, pub billboard_pipeline: Option<BillboardPipeline>, pub kino: Option<KinoState>, - pub(crate) default_skinning_buffer: Option<wgpu::Buffer>, - pub(crate) default_morph_deltas_buffer: Option<wgpu::Buffer>, - pub(crate) default_morph_weights_buffer: Option<wgpu::Buffer>, - pub(crate) default_morph_info_buffer: Option<wgpu::Buffer>, - pub(crate) default_animation_bind_group: Option<wgpu::BindGroup>, - pub(crate) static_batches: HashMap<u64, Vec<(Entity, InstanceRaw)>>, - pub(crate) animated_instances: Vec<( - Entity, - u64, - InstanceRaw, - wgpu::Buffer, - wgpu::Buffer, - wgpu::Buffer, - u32, - )>, + pub animation_pipeline: Option<AnimationDefaults>, + pub(crate) animated_instance_buffers: HashMap<Entity, ResizableBuffer<InstanceRaw>>, pub(crate) animated_bind_group_cache: HashMap<Entity, (u64, wgpu::BindGroup)>, + pub(crate) static_bind_group_cache: HashMap<u64, wgpu::BindGroup>, pub(crate) last_morph_info_per_mesh: HashMap<u32, MorphTargetInfo>, // key = morph_deltas_offset pub active_camera: Arc<Mutex<Option<Entity>>>, @@ -326,19 +315,14 @@ impl Editor { asset_clipboard: None, pending_aa_reload: None, instance_buffer_cache: HashMap::new(), - animated_instance_buffers: HashMap::new(), mipmapper: None, sky_pipeline: None, billboard_pipeline: None, kino: None, - default_skinning_buffer: None, - default_morph_deltas_buffer: None, - default_morph_weights_buffer: None, - default_morph_info_buffer: None, - default_animation_bind_group: None, - static_batches: Default::default(), - animated_instances: vec![], + animation_pipeline: None, + animated_instance_buffers: Default::default(), animated_bind_group_cache: Default::default(), + static_bind_group_cache: Default::default(), dt: 60.0, ui_editor_dock_state: DockState::new(vec![]), current_page: EditorTabVisibility::GameEditor, @@ -1664,11 +1648,7 @@ impl Editor { KinoWinitWindowing::new(graphics.window.clone(), None), )); - self.default_skinning_buffer = None; - self.default_morph_deltas_buffer = None; - self.default_morph_weights_buffer = None; - self.default_morph_info_buffer = None; - self.default_animation_bind_group = None; + self.animation_pipeline = None; self.texture_id = Some((*graphics.texture_id).clone()); self.window = Some(graphics.window.clone()); @@ -1679,92 +1659,7 @@ impl Editor { let active_camera = self.active_camera.lock().clone(); if let Some(camera_entity) = active_camera { if let Ok(camera) = self.world.query_one::<&Camera>(camera_entity).get() { - let max_skinning_matrices = 256usize; - let identity = vec![Mat4::IDENTITY; max_skinning_matrices]; - let skinning_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("editor default skinning buffer"), - contents: bytemuck::cast_slice(&identity), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); - - let morph_deltas_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("editor default morph deltas buffer"), - contents: bytemuck::cast_slice(&[0.0f32]), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); - - let morph_weights = vec![0.0f32; MAX_MORPH_WEIGHTS]; - let morph_weights_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("editor default morph weights buffer"), - contents: bytemuck::cast_slice(&morph_weights), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); - - let morph_info = dropbear_engine::animation::MorphTargetInfo::default(); - let morph_info_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("editor default morph info buffer"), - contents: bytemuck::bytes_of(&morph_info), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - - self.default_skinning_buffer = Some(skinning_buffer); - self.default_morph_deltas_buffer = Some(morph_deltas_buffer); - self.default_morph_weights_buffer = Some(morph_weights_buffer); - self.default_morph_info_buffer = Some(morph_info_buffer); - - let skinning_buffer = self - .default_skinning_buffer - .as_ref() - .expect("Default skinning buffer missing"); - let morph_deltas_buffer = self - .default_morph_deltas_buffer - .as_ref() - .expect("Default morph deltas buffer missing"); - let morph_weights_buffer = self - .default_morph_weights_buffer - .as_ref() - .expect("Default morph weights buffer missing"); - let morph_info_buffer = self - .default_morph_info_buffer - .as_ref() - .expect("Default morph info buffer missing"); - - self.default_animation_bind_group = Some(graphics.device.create_bind_group( - &wgpu::BindGroupDescriptor { - label: Some("editor default animation bind group"), - layout: &graphics.layouts.animation_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: skinning_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: morph_deltas_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: morph_weights_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: morph_info_buffer.as_entire_binding(), - }, - ], - }, - )); + self.animation_pipeline = Some(AnimationDefaults::new(graphics.clone())); if let Some(main_pipeline) = self.main_render_pipeline.as_mut() { if let (Some(globals), Some(light_pipeline)) = ( @@ -2,22 +2,15 @@ use super::*; use crate::signal::SignalController; use crate::spawn::PendingSpawnController; use crossbeam_channel::unbounded; -use dropbear_engine::animation::{AnimationComponent, MorphTargetInfo}; -use dropbear_engine::asset::{ASSET_REGISTRY, Handle}; -use dropbear_engine::buffer::ResizableBuffer; -use dropbear_engine::graphics::{CommandEncoder, InstanceRaw}; +use dropbear_engine::graphics::CommandEncoder; use dropbear_engine::{ entity::{EntityTransform, MeshRenderer, Transform}, lighting::Light, - model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}, }; use egui::UiBuilder; use eucalyptus_core::billboard::BillboardComponent; use eucalyptus_core::component::KotlinComponentDecl; -use eucalyptus_core::entity_status::EntityStatus; -use eucalyptus_core::hierarchy::EntityTransformExt; -use eucalyptus_core::physics::collider::{ColliderGroup}; use eucalyptus_core::properties::CustomProperties; use eucalyptus_core::states::{Label, SCENES, WorldLoadingStatus}; use eucalyptus_core::transform::OnRails; @@ -28,7 +21,6 @@ use log; use magna_carta::ScriptManifest; use parking_lot::Mutex; use std::collections::HashMap; -use std::hash::{DefaultHasher, Hash, Hasher}; use std::sync::Arc; use std::{ fs, @@ -36,7 +28,7 @@ use std::{ }; use winit::event::{MouseScrollDelta, TouchPhase}; use winit::{event::WindowEvent, event_loop::ActiveEventLoop, keyboard::KeyCode}; -use eucalyptus_core::debug::DebugDrawExt; +use eucalyptus_core::rendering::RendererCommon; impl Scene for Editor { fn load(&mut self, graphics: Arc<SharedGraphicsContext>) { @@ -456,285 +448,41 @@ impl Scene for Editor { self.input_state.mouse_delta = None; } - fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { + fn render(&mut self, graphics: Arc<SharedGraphicsContext>) { self.editor_specific_render(&graphics); let hdr = graphics.hdr.read(); - let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder")); - let active_camera = { self.active_camera.lock().as_ref().cloned() }; - let Some(active_camera) = active_camera else { - return; - }; + let Some(active_camera) = self.active_camera.lock().as_ref().cloned() else { return }; log_once::debug_once!("Active camera found: {:?}", active_camera); + let Some(camera) = self.world.query_one::<&Camera>(active_camera).get().ok().cloned() else { return }; + log_once::debug_once!("Camera ready: {}", camera.label); - let q = self - .world - .query_one::<&Camera>(active_camera) - .get() - .ok() - .cloned(); - - let Some(camera) = q else { - return; - }; - - log_once::debug_once!("Camera ready"); - log_once::debug_once!("Camera currently being viewed: {}", camera.label); - - // clear viewport render pass - { - puffin::profile_scope!("Clearing viewport"); - let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("viewport clear pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 100.0 / 255.0, - g: 149.0 / 255.0, - b: 237.0 / 255.0, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(0.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - } + RendererCommon::clear_viewport(&graphics, &mut encoder, &hdr); - if let Some(light_pipeline) = &mut self.light_cube_pipeline { - light_pipeline.update(graphics.clone(), &self.world); + if let Some(p) = &mut self.light_cube_pipeline { + p.update(graphics.clone(), &self.world); } + let (lights, enabled_light_count) = RendererCommon::collect_lights(&self.world); - let (lights, enabled_light_count) = { - puffin::profile_scope!("Locating lights"); - let mut lights = Vec::new(); - let mut enabled = 0u32; - for light in self.world.query::<&Light>().iter() { - if light.component.enabled { - enabled += 1; - } - lights.push(light.clone()); - } - (lights, enabled) - }; - - if let Some(globals) = &mut self.shader_globals { - puffin::profile_scope!("Fetching globals"); - globals.set_num_lights(enabled_light_count); + { + let Some(globals) = &mut self.shader_globals else { return }; + globals.data.num_lights = enabled_light_count; if let Some(scene_name) = &self.current_scene_name { let scenes = SCENES.read(); if let Some(scene) = scenes.iter().find(|s| s.scene_name == *scene_name) { - globals.set_ambient_strength(scene.settings.ambient_strength); + globals.data.ambient_strength = scene.settings.ambient_strength; } } globals.write(&graphics.queue); } - self.static_batches.clear(); - self.animated_instances.clear(); - { - puffin::profile_scope!("finding all renderers and animation components"); - let mut query = self - .world - .query::<(Entity, &MeshRenderer, Option<&mut AnimationComponent>)>(); - - for (entity, renderer, animation) in query.iter() { - puffin::profile_scope!(format!("locating {:?}", entity)); - let world_ptr = &*self.world as *const hecs::World; - let world_ref = unsafe { &*world_ptr }; - if let Ok(status) = world_ref.get::<&EntityStatus>(entity) { - if status.hidden || status.disabled { - continue; - } - } - let handle = renderer.model(); - if handle.is_null() { - continue; - } - - let instance = renderer.instance.to_raw(); - - if let Some(animation) = animation { - let has_skinning = !animation.skinning_matrices.is_empty(); - let has_morph = !animation.morph_weights.is_empty(); - - if !has_skinning && !has_morph { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - } - - animation.prepare_gpu_resources(graphics.clone()); - - let skinning_buffer = match animation - .skinning_buffer - .as_ref() - .map(|b| b.buffer().clone()) - { - Some(buf) => buf, - None if !has_skinning => { - let Some(default) = self.default_skinning_buffer.as_ref() else { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - }; - default.clone() - } - None => { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - } - }; - - let Some(morph_weights_buffer) = animation - .morph_weights_buffer - .as_ref() - .map(|b| b.buffer().clone()) - else { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - }; - - let Some(morph_info_buffer) = animation - .morph_info_buffer - .as_ref() - .map(|b| b.buffer().clone()) - else { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - }; - - self.animated_instances.push(( - entity, - handle.id, - instance, - skinning_buffer, - morph_weights_buffer, - morph_info_buffer, - animation.morph_weight_count, - )); - } else { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - } - } - } - - let registry = ASSET_REGISTRY.read(); - - let mut model_cache: HashMap<u64, _> = HashMap::new(); - let mut prepared_models = Vec::new(); - for (handle, batched_instances) in &self.static_batches { - puffin::profile_scope!("preparing models"); - let Some(model) = registry.get_model(Handle::new(*handle)) else { - log_once::error_once!("Missing model handle {} in registry", handle); - continue; - }; - - let entity = batched_instances.first().map(|(e, _)| *e); - let instances: Vec<InstanceRaw> = - batched_instances.iter().map(|(_, inst)| *inst).collect(); - - let instance_buffer = self - .instance_buffer_cache - .entry(*handle) - .or_insert_with(|| { - ResizableBuffer::new( - &graphics.device, - instances.len().max(1), - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "Runtime Instance Buffer", - ) - }); - instance_buffer.write(&graphics.device, &graphics.queue, &instances); - - model_cache.insert(*handle, model.clone()); - prepared_models.push((model, *handle, instances.len() as u32, entity)); - } - - for (_, handle, ..) in &self.animated_instances { - if !model_cache.contains_key(handle) { - if let Some(model) = registry.get_model(Handle::new(*handle)) { - model_cache.insert(*handle, model); - } - } - } + let mut batches = HashMap::new(); + let default_skinning = self.animation_pipeline.as_ref().map(|p| p.skinning_buffer.buffer().clone()); + RendererCommon::locate_renderers(&self.world, &mut batches, graphics.clone(), &default_skinning); - // light cube rendering - if let Some(light_pipeline) = &self.light_cube_pipeline { - if let Some(l) = lights.first() - && let Some(model) = registry.get_model(l.cube_model) - { - { - puffin::profile_scope!("light cube pass"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("light cube render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - render_pass.set_pipeline(light_pipeline.pipeline()); - for light in &lights { - puffin::profile_scope!("rendering light", &light.label); - render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); - if !light.component.visible { - continue; - } - render_pass.draw_light_model(&model, &camera.bind_group, &light.bind_group); - } - } - } else { - log_once::error_once!("Missing light cube model handle in registry",); - } - } + let (_, model_cache) = RendererCommon::prepare_models(&graphics, &batches, &mut self.instance_buffer_cache); if self.last_active_camera_for_per_frame != Some(active_camera) { self.last_active_camera_for_per_frame = Some(active_camera); @@ -753,525 +501,62 @@ impl Scene for Editor { } } - let sky = self - .sky_pipeline - .as_ref() - .expect("Sky pipeline must be initialised before rendering models"); + let sky = self.sky_pipeline.as_ref().expect("Sky pipeline must be initialised"); let environment_bind_group = &sky.environment_bind_group; - let Some(pipeline) = self.main_render_pipeline.as_mut() else { + let Some(pipeline) = self.main_render_pipeline.as_ref() else { log_once::warn_once!("Render pipeline not ready"); return; }; - log_once::debug_once!("Pipeline ready"); - - // static models - if let Some(_) = &self.light_cube_pipeline { - puffin::profile_scope!("model render pass"); - - let default_skinning_buffer = self - .default_skinning_buffer - .as_ref() - .expect("Default skinning buffer not initialised"); - let default_morph_weights_buffer = self - .default_morph_weights_buffer - .as_ref() - .expect("Default morph weights buffer not initialised"); - let default_morph_info_buffer = self - .default_morph_info_buffer - .as_ref() - .expect("Default morph info buffer not initialised"); - let per_frame_bind_group = pipeline - .per_frame - .as_ref() - .expect("Per-frame bind group not initialised") - .clone(); - - for (model, handle, instance_count, entity) in prepared_models { - let Some(entity) = entity else { continue }; - let Ok(renderer) = self.world.get::<&MeshRenderer>(entity) else { - continue; - }; - - let morph_deltas_buffer = model - .morph_deltas_buffer - .as_ref() - .or(self.default_morph_deltas_buffer.as_ref()); - let Some(morph_deltas_buffer) = morph_deltas_buffer else { - log_once::error_once!("Missing morph deltas buffer for model {}", handle); - continue; - }; - - let animation_bind_group = pipeline.animation_bind_group( - graphics.clone(), - default_skinning_buffer, - morph_deltas_buffer, - default_morph_weights_buffer, - default_morph_info_buffer, - ); - - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("model render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - render_pass.set_pipeline(pipeline.pipeline()); - let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) else { - continue; - }; - render_pass.set_vertex_buffer(1, instance_buffer.slice(instance_count as usize)); - - for mesh in &model.meshes { - let mut weights = mesh.morph_default_weights.clone(); - let target_count = mesh.morph_target_count as usize; - if weights.len() < target_count { - weights.resize(target_count, 0.0); - } - if weights.is_empty() { - weights.push(0.0); - } - - graphics.queue.write_buffer( - default_morph_weights_buffer, - 0, - bytemuck::cast_slice(&weights), - ); - - let info = MorphTargetInfo { - num_vertices: mesh.morph_vertex_count, - num_targets: mesh.morph_target_count, - base_offset: mesh.morph_deltas_offset, - weight_offset: 0, - uses_morph: if mesh.morph_target_count > 0 && !weights.is_empty() { - 1 - } else { - 0 - }, - _padding: Default::default(), - }; - - let cache_key = mesh.morph_deltas_offset; - let needs_write = - self.last_morph_info_per_mesh - .get(&cache_key) - .map_or(true, |prev| { - prev.num_vertices != info.num_vertices - || prev.num_targets != info.num_targets - || prev.base_offset != info.base_offset - || prev.uses_morph != info.uses_morph - }); - - if needs_write { - graphics.queue.write_buffer( - default_morph_info_buffer, - 0, - bytemuck::bytes_of(&info), - ); - self.last_morph_info_per_mesh.insert(cache_key, info); - } - - let material = &model.materials[mesh.material]; - let material = if let Some(mat) = renderer.material_snapshot.get(&material.name) - { - mat - } else { - log_once::warn_once!( - "Unable to locate MeshRenderer's material_snapshot for that specific material" - ); - material - }; - - render_pass.draw_mesh_instanced( - mesh, - material, - 0..instance_count, - &per_frame_bind_group, - &animation_bind_group, - environment_bind_group, - ); - } - } - } - - // animated models - if let Some(_) = &self.light_cube_pipeline { - puffin::profile_scope!("animated model render pass"); - - let per_frame_bind_group = pipeline - .per_frame - .as_ref() - .expect("Per-frame bind group not initialised") - .clone(); - - for (entity, _, instance, _, _, _, _) in &self.animated_instances { - let instance_buffer = self - .animated_instance_buffers - .entry(*entity) - .or_insert_with(|| { - ResizableBuffer::new( - &graphics.device, - 1, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "animated instance buffer", - ) - }); - instance_buffer.write(&graphics.device, &graphics.queue, &[*instance]); - } - - for ( - entity, - handle, - _, - skinning_buffer, - morph_weights_buffer, - morph_info_buffer, - morph_weight_count, - ) in &self.animated_instances - { - let Ok(renderer) = self.world.get::<&MeshRenderer>(*entity) else { - continue; - }; - puffin::profile_scope!("rendering animated model", format!("{:?}", entity)); - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("animated model render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - render_pass.set_pipeline(pipeline.pipeline()); - - let Some(model) = model_cache.get(handle) else { - log_once::error_once!("Missing model handle {} in registry", handle); - continue; - }; - - let morph_deltas_buffer = model - .morph_deltas_buffer - .as_ref() - .or(self.default_morph_deltas_buffer.as_ref()); - let Some(morph_deltas_buffer) = morph_deltas_buffer else { - log_once::error_once!("Missing morph deltas buffer for model {}", handle); - continue; - }; - - let mut hasher = DefaultHasher::new(); - skinning_buffer.hash(&mut hasher); - let bind_group_stamp = hasher.finish(); - let animation_bind_group = { - let cached = self.animated_bind_group_cache.get(entity); - if cached.map_or(true, |(stamp, _)| *stamp != bind_group_stamp) { - let bg = pipeline.animation_bind_group( - graphics.clone(), - skinning_buffer, - morph_deltas_buffer, - morph_weights_buffer, - morph_info_buffer, - ); - self.animated_bind_group_cache - .insert(*entity, (bind_group_stamp, bg)); - } - &self.animated_bind_group_cache[entity].1 - }; - - let Some(instance_buffer) = self.animated_instance_buffers.get(entity) else { - continue; - }; - render_pass.set_vertex_buffer(1, instance_buffer.slice(1)); - - for mesh in &model.meshes { - let mesh_target_count = mesh.morph_target_count.min(*morph_weight_count); - - let info = MorphTargetInfo { - num_vertices: mesh.morph_vertex_count, - num_targets: mesh_target_count, - base_offset: mesh.morph_deltas_offset, - weight_offset: 0, - uses_morph: if mesh_target_count > 0 { 1 } else { 0 }, - _padding: Default::default(), - }; - - graphics.queue.write_buffer( - morph_info_buffer, - 0, - bytemuck::bytes_of(&info), - ); - - let material = &model.materials[mesh.material]; - let material = if let Some(mat) = - renderer.material_snapshot.get(&material.name) - { - mat - } else { - log_once::warn_once!( - "Unable to locate MeshRenderer's material_snapshot for that specific material" - ); - material - }; - - render_pass.draw_mesh_instanced( - mesh, - material, - 0..1, - &per_frame_bind_group, - animation_bind_group, - environment_bind_group, - ); - } - } - } - } - - // skybox rendering - if let Some(sky) = &self.sky_pipeline { - puffin::profile_scope!("sky render pass"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("sky render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - 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, - }), - timestamp_writes: None, - occlusion_query_set: None, - multiview_mask: None, - }); - - render_pass.set_pipeline(&sky.pipeline); - render_pass.set_bind_group(0, &sky.camera_bind_group, &[]); - render_pass.set_bind_group(1, &sky.environment_bind_group, &[]); - render_pass.draw(0..3, 0..1); - } - - // collider debug draw - { - let show_hitboxes = self - .current_scene_name - .as_ref() - .and_then(|scene_name| { - let scenes = SCENES.read(); - scenes - .iter() - .find(|scene| &scene.scene_name == scene_name) - .map(|scene| scene.settings.show_hitboxes) - }) - .unwrap_or(false); - - if show_hitboxes { - puffin::profile_scope!("collider debug draw"); - if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() { - let colour = [0.0, 1.0, 0.0, 1.0]; // green - let to_draw: Vec<_> = { - let mut q = self.world.query::<(Entity, &ColliderGroup)>(); - q.iter().map(|(e, cg)| (e, cg.colliders.clone())).collect() - }; - for (entity, colliders) in to_draw { - let Ok(et) = self.world.get::<&EntityTransform>(entity) else { - continue; - }; - let world_tf = et.propagate(&self.world, entity); - drop(et); - for collider in &colliders { - let entity_matrix = world_tf.matrix().as_mat4(); - let offset_transform = Transform::new() - .with_offset(collider.translation, collider.rotation); - let offset_matrix = offset_transform.matrix().as_mat4(); - let final_matrix = entity_matrix * offset_matrix; - let (scale, rotation, translation) = - final_matrix.to_scale_rotation_translation(); - - debug_draw.draw_collider(&collider.shape, translation, scale, rotation, colour); - } - } - } - } - } - - // kino billboard renderer - { - puffin::profile_scope!("rendering billboard targets"); - if let Some(kino) = &mut self.kino { - let mut kino_encoder = - CommandEncoder::new(graphics.clone(), Some("kino billboard encoder")); - kino.render_billboard_targets(&graphics.device, &graphics.queue, &mut kino_encoder); - - if let Err(e) = kino_encoder.submit() { - log_once::error_once!("Unable to submit billboard kino pass: {}", e); - } - } - - if let Some(billboard_pipeline) = &self.billboard_pipeline { - let camera_position = camera.position().as_vec3(); - let camera_projection = Mat4::from_cols_array_2d(&camera.uniform.view_proj); - - let mut kino_views = HashMap::<u64, wgpu::TextureView>::new(); - if let Some(kino) = &mut self.kino { - kino_views.extend(kino.billboard_render_target_views()); - } - - let single_fallback_view = if kino_views.len() == 1 { - kino_views.values().next().cloned() - } else { - None - }; - - let mut billboards: Vec<(Mat4, wgpu::TextureView)> = Vec::new(); - let mut query = self - .world - .query::<(Entity, &BillboardComponent, Option<&EntityTransform>)>(); - - for (entity, billboard, entity_transform) in query.iter() { - puffin::profile_scope!("rendering billboard", format!("{:?}", entity)); - if !billboard.enabled { - continue; - } - - let entity_id = entity.to_bits().get(); - let texture_view = kino_views - .get(&entity_id) - .cloned() - .or_else(|| single_fallback_view.clone()); - - let Some(texture_view) = texture_view else { - continue; - }; - - let position = entity_transform - .map(|transform| transform.sync().position.as_vec3()) - .unwrap_or(glam::Vec3::ZERO) - + billboard.offset; - let world_size = billboard.world_size; - let scale = glam::Vec3::new(world_size.x, world_size.y, 1.0); + let Some(animation_defaults) = self.animation_pipeline.as_ref() else { + log_once::warn_once!("Animation pipeline not ready"); + return; + }; + let per_frame_bind_group = pipeline.per_frame.as_ref() + .expect("Per-frame bind group not initialised") + .clone(); + + RendererCommon::render_light_cubes(&graphics, &mut encoder, &hdr, &lights, &camera, self.light_cube_pipeline.as_ref()); + + RendererCommon::render_models( + &graphics, &mut encoder, &hdr, + &self.world, &batches, &model_cache, + &per_frame_bind_group, environment_bind_group, + pipeline, animation_defaults, + &self.instance_buffer_cache, + &mut self.animated_instance_buffers, + &mut self.animated_bind_group_cache, + &mut self.static_bind_group_cache, + &mut self.last_morph_info_per_mesh, + ); - let rotation = if let Some(rotation) = billboard.rotation { - rotation - } else { - let to_camera = (camera_position - position).normalize_or_zero(); - if to_camera.length_squared() > 0.0 { - let mut world_up = glam::Vec3::Y; - if to_camera.dot(world_up).abs() > 0.999 { - world_up = glam::Vec3::X; - } - - let right = world_up.cross(to_camera).normalize_or_zero(); - let up = to_camera.cross(right).normalize_or_zero(); - let basis = glam::Mat3::from_cols(right, up, to_camera); - glam::Quat::from_mat3(&basis) - } else { - glam::Quat::IDENTITY - } - }; + RendererCommon::render_sky(&graphics, &mut encoder, &hdr, sky); - let transform = - Mat4::from_scale_rotation_translation(scale, rotation, position); - billboards.push((transform, texture_view)); - } + RendererCommon::render_collider_debug( + &graphics, + &self.world, + self.current_scene_name.as_deref(), + ); - if !billboards.is_empty() { - puffin::profile_scope!("billboard render pass"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("editor billboard render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - 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, - }), - timestamp_writes: None, - occlusion_query_set: None, - multiview_mask: None, - }); - - for (transform, texture_view) in billboards { - billboard_pipeline.draw( - graphics.clone(), - &mut render_pass, - transform, - camera_projection, - &texture_view, - ); - } - } - } - } + RendererCommon::render_billboards( + &graphics, &mut encoder, &hdr, &camera, + &self.world, + self.kino.as_mut(), + self.billboard_pipeline.as_ref(), + ); - // debug draw flush if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() { let view_proj = Mat4::from_cols_array_2d(&camera.uniform.view_proj); debug_draw.flush(graphics.clone(), &mut encoder, view_proj); } hdr.process(&mut encoder, &graphics.viewport_texture.view); + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } - if let Err(e) = encoder.submit() { - log_once::error_once!("{}", e); - } - - // kino hud renderer - if let Some(kino) = &mut self.kino { + { + let Some(kino) = &mut self.kino else { return }; let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder")); kino.render(&graphics.device, &graphics.queue, &mut encoder, hdr.view()); - if let Err(e) = encoder.submit() { log_once::error_once!("Unable to submit kino: {}", e); } @@ -9,6 +9,7 @@ use dropbear_engine::future::{FutureHandle, FutureQueue}; use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext}; use dropbear_engine::pipelines::DropbearShaderPipeline; use dropbear_engine::pipelines::GlobalsUniform; +use dropbear_engine::pipelines::animation::AnimationDefaults; use dropbear_engine::pipelines::light_cube::LightCubePipeline; use dropbear_engine::pipelines::shader::MainRenderPipeline; use dropbear_engine::scene::SceneCommand; @@ -27,7 +28,6 @@ use eucalyptus_core::scene::loading::{SCENE_LOADER, SceneLoadResult}; use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; use eucalyptus_core::states::{SCENES, Script, WorldLoadingStatus}; use futures::executor; -use glam::Mat4; use hecs::{Entity, World}; use kino_ui::KinoState; use kino_ui::rendering::KinoWGPURenderer; @@ -37,15 +37,12 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use wgpu::SurfaceConfiguration; -use wgpu::util::DeviceExt; use winit::window::Fullscreen; mod command; mod input; mod scene; -const MAX_MORPH_WEIGHTS: usize = 4096; - #[cfg(feature = "debug")] fn find_jvm_library_path() -> PathBuf { let proj = eucalyptus_core::states::PROJECT.read(); @@ -137,23 +134,10 @@ pub struct PlayMode { instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>, animated_instance_buffers: HashMap<Entity, ResizableBuffer<InstanceRaw>>, sky_pipeline: Option<SkyPipeline>, - default_skinning_buffer: Option<wgpu::Buffer>, - default_morph_deltas_buffer: Option<wgpu::Buffer>, - default_morph_weights_buffer: Option<wgpu::Buffer>, - default_morph_info_buffer: Option<wgpu::Buffer>, - default_animation_bind_group: Option<wgpu::BindGroup>, + animation_pipeline: Option<AnimationDefaults>, billboard_pipeline: Option<BillboardPipeline>, - pub(crate) static_batches: HashMap<u64, Vec<(Entity, InstanceRaw)>>, - pub(crate) animated_instances: Vec<( - Entity, - u64, - InstanceRaw, - wgpu::Buffer, - wgpu::Buffer, - wgpu::Buffer, - u32, - )>, pub(crate) animated_bind_group_cache: HashMap<Entity, (u64, wgpu::BindGroup)>, + pub(crate) static_bind_group_cache: HashMap<u64, wgpu::BindGroup>, pub(crate) last_morph_info_per_mesh: HashMap<u32, MorphTargetInfo>, last_active_camera_for_per_frame: Option<Entity>, @@ -238,15 +222,10 @@ impl PlayMode { }, kino: None, sky_pipeline: None, - default_skinning_buffer: None, - default_morph_deltas_buffer: None, - default_morph_weights_buffer: None, - default_morph_info_buffer: None, - default_animation_bind_group: None, + animation_pipeline: None, billboard_pipeline: None, - static_batches: Default::default(), - animated_instances: vec![], animated_bind_group_cache: Default::default(), + static_bind_group_cache: Default::default(), last_morph_info_per_mesh: Default::default(), last_active_camera_for_per_frame: None, }; @@ -266,11 +245,8 @@ impl PlayMode { self.shader_globals = None; self.kino = None; self.sky_pipeline = None; - self.default_skinning_buffer = None; - self.default_morph_deltas_buffer = None; - self.default_morph_weights_buffer = None; - self.default_morph_info_buffer = None; - self.default_animation_bind_group = None; + self.animation_pipeline = None; + self.static_bind_group_cache.clear(); self.load_wgpu_nerdy_stuff(graphics, sky_texture); } @@ -289,95 +265,8 @@ impl PlayMode { let mut pending_sky_pipeline = None; - if self.default_skinning_buffer.is_none() { - let max_skinning_matrices = 256usize; - let identity = vec![Mat4::IDENTITY; max_skinning_matrices]; - let skinning_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("runtime default skinning buffer"), - contents: bytemuck::cast_slice(&identity), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); - - let morph_deltas_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("runtime default morph deltas buffer"), - contents: bytemuck::cast_slice(&[0.0f32]), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); - - let morph_weights = vec![0.0f32; MAX_MORPH_WEIGHTS]; - let morph_weights_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("runtime default morph weights buffer"), - contents: bytemuck::cast_slice(&morph_weights), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); - - let morph_info = dropbear_engine::animation::MorphTargetInfo::default(); - let morph_info_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("runtime default morph info buffer"), - contents: bytemuck::bytes_of(&morph_info), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - - self.default_skinning_buffer = Some(skinning_buffer); - self.default_morph_deltas_buffer = Some(morph_deltas_buffer); - self.default_morph_weights_buffer = Some(morph_weights_buffer); - self.default_morph_info_buffer = Some(morph_info_buffer); - } - - if self.default_animation_bind_group.is_none() { - let skinning_buffer = self - .default_skinning_buffer - .as_ref() - .expect("Default skinning buffer missing"); - let morph_deltas_buffer = self - .default_morph_deltas_buffer - .as_ref() - .expect("Default morph deltas buffer missing"); - let morph_weights_buffer = self - .default_morph_weights_buffer - .as_ref() - .expect("Default morph weights buffer missing"); - let morph_info_buffer = self - .default_morph_info_buffer - .as_ref() - .expect("Default morph info buffer missing"); - - self.default_animation_bind_group = Some(graphics.device.create_bind_group( - &wgpu::BindGroupDescriptor { - label: Some("runtime default animation bind group"), - layout: &graphics.layouts.animation_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: skinning_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: morph_deltas_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: morph_weights_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: morph_info_buffer.as_entire_binding(), - }, - ], - }, - )); + if self.animation_pipeline.is_none() { + self.animation_pipeline = Some(AnimationDefaults::new(graphics.clone())); } self.kino = Some(kino_ui::KinoState::new( @@ -1,36 +1,29 @@ use std::sync::Arc; use crate::PlayMode; -use dropbear_engine::animation::{AnimationComponent, MorphTargetInfo}; -use dropbear_engine::asset::{ASSET_REGISTRY, Handle}; -use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; +use dropbear_engine::entity::EntityTransform; use dropbear_engine::graphics::CommandEncoder; -use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext}; -use dropbear_engine::lighting::Light; -use dropbear_engine::model::{DrawLight, DrawModel}; -use dropbear_engine::pipelines::DropbearShaderPipeline; +use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::billboard::BillboardComponent; use eucalyptus_core::command::CommandBufferPoller; use eucalyptus_core::egui::CentralPanel; use eucalyptus_core::entity_status::EntityStatus; use eucalyptus_core::hierarchy::{EntityTransformExt, Parent}; -use eucalyptus_core::physics::collider::{ColliderGroup, ColliderShape}; use eucalyptus_core::physics::kcc::KCC; use eucalyptus_core::rapier3d::geometry::SharedShape; use eucalyptus_core::rapier3d::prelude::QueryFilter; +use eucalyptus_core::rendering::RendererCommon; use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResult}; use eucalyptus_core::states::SCENES; use eucalyptus_core::states::{Label, PROJECT}; use eucalyptus_core::ui::HUDComponent; -use glam::{DVec3, Mat4, Quat, Vec2, Vec3}; +use glam::{DVec3, Mat4, Quat, Vec2}; use hecs::Entity; use kino_ui::WidgetTree; use kino_ui::rendering::KinoRenderTargetId; use std::collections::HashMap; -use std::hash::{DefaultHasher, Hash, Hasher}; use winit::event::WindowEvent; use winit::event_loop::ActiveEventLoop; @@ -608,80 +601,21 @@ impl Scene for PlayMode { fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { let hdr = graphics.hdr.read(); - let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder")); - let active_camera = { self.active_camera.as_ref().cloned() }; - let Some(active_camera) = active_camera else { - return; - }; + let Some(active_camera) = self.active_camera.as_ref().cloned() else { return }; log_once::debug_once!("Active camera found: {:?}", active_camera); + let Some(camera) = self.world.query_one::<&Camera>(active_camera).get().ok().cloned() else { return }; + log_once::debug_once!("Camera ready: {}", camera.label); - let q = self - .world - .query_one::<&Camera>(active_camera) - .get() - .ok() - .cloned(); - - let Some(camera) = q else { - return; - }; - log_once::debug_once!("Camera ready"); - log_once::debug_once!("Camera currently being viewed: {}", camera.label); - - // clear viewport render pass - { - puffin::profile_scope!("Clearing viewport"); - let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("viewport clear pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 100.0 / 255.0, - g: 149.0 / 255.0, - b: 237.0 / 255.0, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(0.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - } + RendererCommon::clear_viewport(&graphics, &mut encoder, &hdr); if let Some(light_pipeline) = &mut self.light_cube_pipeline { light_pipeline.update(graphics.clone(), &self.world); } - - let (lights, enabled_light_count) = { - puffin::profile_scope!("Locating lights"); - let mut lights = Vec::new(); - let mut enabled = 0u32; - for light in self.world.query::<&Light>().iter() { - if light.component.enabled { - enabled += 1; - } - lights.push(light.clone()); - } - (lights, enabled) - }; + let (lights, enabled_light_count) = RendererCommon::collect_lights(&self.world); if let Some(globals) = &mut self.shader_globals { - puffin::profile_scope!("Fetching globals"); globals.set_num_lights(enabled_light_count); if let Some(scene_name) = &self.current_scene { let scenes = SCENES.read(); @@ -692,197 +626,11 @@ impl Scene for PlayMode { globals.write(&graphics.queue); } - self.static_batches.clear(); - self.animated_instances.clear(); - { - puffin::profile_scope!("finding all renderers and animation components"); - let mut query = self - .world - .query::<(Entity, &MeshRenderer, Option<&mut AnimationComponent>)>(); - - for (entity, renderer, animation) in query.iter() { - puffin::profile_scope!(format!("locating {:?}", entity)); - // skip entities that are hidden or disabled - let world_ptr = &*self.world as *const hecs::World; - let world_ref = unsafe { &*world_ptr }; - if let Ok(status) = world_ref.get::<&EntityStatus>(entity) { - if status.hidden || status.disabled { - continue; - } - } - let handle = renderer.model(); - if handle.is_null() { - continue; - } - - let instance = renderer.instance.to_raw(); - - if let Some(animation) = animation { - let has_skinning = !animation.skinning_matrices.is_empty(); - let has_morph = !animation.morph_weights.is_empty(); - - if !has_skinning && !has_morph { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - } - - animation.prepare_gpu_resources(graphics.clone()); - - let skinning_buffer = match animation - .skinning_buffer - .as_ref() - .map(|b| b.buffer().clone()) - { - Some(buf) => buf, - None if !has_skinning => { - let Some(default) = self.default_skinning_buffer.as_ref() else { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - }; - default.clone() - } - None => { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - } - }; - - let Some(morph_weights_buffer) = animation - .morph_weights_buffer - .as_ref() - .map(|b| b.buffer().clone()) - else { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - }; - - let Some(morph_info_buffer) = animation - .morph_info_buffer - .as_ref() - .map(|b| b.buffer().clone()) - else { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - continue; - }; - - self.animated_instances.push(( - entity, - handle.id, - instance, - skinning_buffer, - morph_weights_buffer, - morph_info_buffer, - animation.morph_weight_count, - )); - } else { - self.static_batches - .entry(handle.id) - .or_default() - .push((entity, instance)); - } - } - } - - let registry = ASSET_REGISTRY.read(); + let mut batches = HashMap::new(); + let default_skinning = self.animation_pipeline.as_ref().map(|p| p.skinning_buffer.buffer().clone()); + RendererCommon::locate_renderers(&self.world, &mut batches, graphics.clone(), &default_skinning); - let mut model_cache: HashMap<u64, _> = HashMap::new(); - let mut prepared_models = Vec::new(); - for (handle, batched_instances) in &self.static_batches { - puffin::profile_scope!("preparing models"); - let Some(model) = registry.get_model(Handle::new(*handle)) else { - log_once::error_once!("Missing model handle {} in registry", handle); - continue; - }; - - let entity = batched_instances.first().map(|(e, _)| *e); - let instances: Vec<InstanceRaw> = - batched_instances.iter().map(|(_, inst)| *inst).collect(); - - let instance_buffer = self - .instance_buffer_cache - .entry(*handle) - .or_insert_with(|| { - ResizableBuffer::new( - &graphics.device, - instances.len().max(1), - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "Runtime Instance Buffer", - ) - }); - instance_buffer.write(&graphics.device, &graphics.queue, &instances); - - model_cache.insert(*handle, model.clone()); - prepared_models.push((model, *handle, instances.len() as u32, entity)); - } - - for (_, handle, ..) in &self.animated_instances { - if !model_cache.contains_key(handle) { - if let Some(model) = registry.get_model(Handle::new(*handle)) { - model_cache.insert(*handle, model); - } - } - } - - // light cube rendering - if let Some(light_pipeline) = &self.light_cube_pipeline { - if let Some(l) = lights.first() - && let Some(model) = registry.get_model(l.cube_model) - { - { - puffin::profile_scope!("light cube pass"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("light cube render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - render_pass.set_pipeline(light_pipeline.pipeline()); - for light in &lights { - puffin::profile_scope!("rendering light", &light.label); - render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); - if !light.component.visible { - continue; - } - render_pass.draw_light_model(&model, &camera.bind_group, &light.bind_group); - } - } - } else { - log_once::error_once!("Missing light cube model handle in registry",); - } - } + let (_, model_cache) = RendererCommon::prepare_models(&graphics, &batches, &mut self.instance_buffer_cache); if self.last_active_camera_for_per_frame != Some(active_camera) { self.last_active_camera_for_per_frame = Some(active_camera); @@ -901,581 +649,61 @@ impl Scene for PlayMode { } } - let sky = self - .sky_pipeline - .as_ref() - .expect("Sky pipeline must be initialised before rendering models"); + let sky = self.sky_pipeline.as_ref().expect("Sky pipeline must be initialised before rendering models"); let environment_bind_group = &sky.environment_bind_group; - let Some(pipeline) = self.main_pipeline.as_mut() else { + let Some(pipeline) = self.main_pipeline.as_ref() else { log_once::warn_once!("Render pipeline not ready"); return; }; - log_once::debug_once!("Pipeline ready"); - - // static models - if let Some(_) = &self.light_cube_pipeline { - puffin::profile_scope!("model render pass"); - - let default_skinning_buffer = self - .default_skinning_buffer - .as_ref() - .expect("Default skinning buffer not initialised"); - let default_morph_weights_buffer = self - .default_morph_weights_buffer - .as_ref() - .expect("Default morph weights buffer not initialised"); - let default_morph_info_buffer = self - .default_morph_info_buffer - .as_ref() - .expect("Default morph info buffer not initialised"); - let per_frame_bind_group = pipeline - .per_frame - .as_ref() - .expect("Per-frame bind group not initialised") - .clone(); - - for (model, handle, instance_count, entity) in prepared_models { - let Some(entity) = entity else { continue }; - let Ok(renderer) = self.world.get::<&MeshRenderer>(entity) else { - continue; - }; - - let morph_deltas_buffer = model - .morph_deltas_buffer - .as_ref() - .or(self.default_morph_deltas_buffer.as_ref()); - let Some(morph_deltas_buffer) = morph_deltas_buffer else { - log_once::error_once!("Missing morph deltas buffer for model {}", handle); - continue; - }; - - let animation_bind_group = pipeline.animation_bind_group( - graphics.clone(), - default_skinning_buffer, - morph_deltas_buffer, - default_morph_weights_buffer, - default_morph_info_buffer, - ); - - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("model render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - render_pass.set_pipeline(pipeline.pipeline()); - let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) else { - continue; - }; - render_pass.set_vertex_buffer(1, instance_buffer.slice(instance_count as usize)); - - for mesh in &model.meshes { - let mut weights = mesh.morph_default_weights.clone(); - let target_count = mesh.morph_target_count as usize; - if weights.len() < target_count { - weights.resize(target_count, 0.0); - } - if weights.is_empty() { - weights.push(0.0); - } - - graphics.queue.write_buffer( - default_morph_weights_buffer, - 0, - bytemuck::cast_slice(&weights), - ); - - let info = MorphTargetInfo { - num_vertices: mesh.morph_vertex_count, - num_targets: mesh.morph_target_count, - base_offset: mesh.morph_deltas_offset, - weight_offset: 0, - uses_morph: if mesh.morph_target_count > 0 && !weights.is_empty() { - 1 - } else { - 0 - }, - _padding: Default::default(), - }; - - let cache_key = mesh.morph_deltas_offset; - let needs_write = - self.last_morph_info_per_mesh - .get(&cache_key) - .map_or(true, |prev| { - prev.num_vertices != info.num_vertices - || prev.num_targets != info.num_targets - || prev.base_offset != info.base_offset - || prev.uses_morph != info.uses_morph - }); - - if needs_write { - graphics.queue.write_buffer( - default_morph_info_buffer, - 0, - bytemuck::bytes_of(&info), - ); - self.last_morph_info_per_mesh.insert(cache_key, info); - } - - let material = &model.materials[mesh.material]; - let material = if let Some(mat) = renderer.material_snapshot.get(&material.name) - { - mat - } else { - log_once::warn_once!( - "Unable to locate MeshRenderer's material_snapshot for that specific material" - ); - material - }; - - render_pass.draw_mesh_instanced( - mesh, - material, - 0..instance_count, - &per_frame_bind_group, - &animation_bind_group, - environment_bind_group, - ); - } - } - } - - // animated models - if let Some(_) = &self.light_cube_pipeline { - puffin::profile_scope!("animated model render pass"); - - let per_frame_bind_group = pipeline - .per_frame - .as_ref() - .expect("Per-frame bind group not initialised") - .clone(); - - for (entity, _, instance, _, _, _, _) in &self.animated_instances { - let instance_buffer = self - .animated_instance_buffers - .entry(*entity) - .or_insert_with(|| { - ResizableBuffer::new( - &graphics.device, - 1, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "animated instance buffer", - ) - }); - instance_buffer.write(&graphics.device, &graphics.queue, &[*instance]); - } - - for ( - entity, - handle, - _, - skinning_buffer, - morph_weights_buffer, - morph_info_buffer, - morph_weight_count, - ) in &self.animated_instances - { - let Ok(renderer) = self.world.get::<&MeshRenderer>(*entity) else { - continue; - }; - puffin::profile_scope!("rendering animated model", format!("{:?}", entity)); - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("animated model render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - render_pass.set_pipeline(pipeline.pipeline()); - - let Some(model) = model_cache.get(handle) else { - log_once::error_once!("Missing model handle {} in registry", handle); - continue; - }; - - let morph_deltas_buffer = model - .morph_deltas_buffer - .as_ref() - .or(self.default_morph_deltas_buffer.as_ref()); - let Some(morph_deltas_buffer) = morph_deltas_buffer else { - log_once::error_once!("Missing morph deltas buffer for model {}", handle); - continue; - }; - - let mut hasher = DefaultHasher::new(); - skinning_buffer.hash(&mut hasher); - let bind_group_stamp = hasher.finish(); - let animation_bind_group = { - let cached = self.animated_bind_group_cache.get(entity); - if cached.map_or(true, |(stamp, _)| *stamp != bind_group_stamp) { - let bg = pipeline.animation_bind_group( - graphics.clone(), - skinning_buffer, - morph_deltas_buffer, - morph_weights_buffer, - morph_info_buffer, - ); - self.animated_bind_group_cache - .insert(*entity, (bind_group_stamp, bg)); - } - &self.animated_bind_group_cache[entity].1 - }; - - let Some(instance_buffer) = self.animated_instance_buffers.get(entity) else { - continue; - }; - render_pass.set_vertex_buffer(1, instance_buffer.slice(1)); - - for mesh in &model.meshes { - let mesh_target_count = mesh.morph_target_count.min(*morph_weight_count); - - let info = MorphTargetInfo { - num_vertices: mesh.morph_vertex_count, - num_targets: mesh_target_count, - base_offset: mesh.morph_deltas_offset, - weight_offset: 0, - uses_morph: if mesh_target_count > 0 { 1 } else { 0 }, - _padding: Default::default(), - }; - - graphics.queue.write_buffer( - morph_info_buffer, - 0, - bytemuck::bytes_of(&info), - ); - - let material = &model.materials[mesh.material]; - let material = if let Some(mat) = - renderer.material_snapshot.get(&material.name) - { - mat - } else { - log_once::warn_once!( - "Unable to locate MeshRenderer's material_snapshot for that specific material" - ); - material - }; - - render_pass.draw_mesh_instanced( - mesh, - material, - 0..1, - &per_frame_bind_group, - animation_bind_group, - environment_bind_group, - ); - } - } - } - } - - // skybox rendering - if let Some(sky) = &self.sky_pipeline { - puffin::profile_scope!("sky render pass"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("sky render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - 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, - }), - timestamp_writes: None, - occlusion_query_set: None, - multiview_mask: None, - }); - - render_pass.set_pipeline(&sky.pipeline); - render_pass.set_bind_group(0, &sky.camera_bind_group, &[]); - render_pass.set_bind_group(1, &sky.environment_bind_group, &[]); - render_pass.draw(0..3, 0..1); - } - - // collider debug draw - { - let show_hitboxes = self - .current_scene - .as_ref() - .and_then(|scene_name| { - let scenes = SCENES.read(); - scenes - .iter() - .find(|scene| &scene.scene_name == scene_name) - .map(|scene| scene.settings.show_hitboxes) - }) - .unwrap_or(false); - - if show_hitboxes { - puffin::profile_scope!("collider debug draw"); - if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() { - let colour = [0.0, 1.0, 0.0, 1.0]; - let to_draw: Vec<_> = { - let mut q = self.world.query::<(Entity, &ColliderGroup)>(); - q.iter().map(|(e, cg)| (e, cg.colliders.clone())).collect() - }; - for (entity, colliders) in to_draw { - let Ok(et) = self.world.get::<&EntityTransform>(entity) else { - continue; - }; - let world_tf = et.propagate(&self.world, entity); - drop(et); - for collider in &colliders { - let entity_matrix = world_tf.matrix().as_mat4(); - let offset_transform = Transform::new() - .with_offset(collider.translation, collider.rotation); - let offset_matrix = offset_transform.matrix().as_mat4(); - let final_matrix = entity_matrix * offset_matrix; - let (scale, rotation, translation) = - final_matrix.to_scale_rotation_translation(); - - match &collider.shape { - ColliderShape::Box { half_extents } => { - let he = Vec3::new( - half_extents.x as f32 * scale.x, - half_extents.y as f32 * scale.y, - half_extents.z as f32 * scale.z, - ); - debug_draw.draw_obb(translation, he, rotation, colour); - } - ColliderShape::Sphere { radius } => { - let r = radius * scale.x.max(scale.y).max(scale.z); - debug_draw.draw_sphere(translation, r, colour); - } - ColliderShape::Capsule { - half_height, - radius, - } => { - let axis = rotation * Vec3::Y; - let top = translation + axis * (half_height * scale.y); - let bottom = translation - axis * (half_height * scale.y); - debug_draw.draw_capsule( - bottom, - top, - radius * scale.x.max(scale.z), - colour, - ); - } - ColliderShape::Cylinder { - half_height, - radius, - } => { - let axis = rotation * Vec3::Y; - debug_draw.draw_cylinder( - translation, - half_height * scale.y, - radius * scale.x.max(scale.z), - axis, - colour, - ); - } - ColliderShape::Cone { - half_height, - radius, - } => { - let axis = rotation * Vec3::Y; - let apex = translation + axis * (half_height * scale.y); - let height = 2.0 * half_height * scale.y; - let r = radius * scale.x.max(scale.z); - debug_draw.draw_cone( - apex, - -(rotation * Vec3::Y), - (r / height).atan(), - height, - colour, - ); - } - } - } - } - } - } - } - - // kino billboard renderer - { - puffin::profile_scope!("rendering billboard targets"); - if let Some(kino) = &mut self.kino { - let mut kino_encoder = - CommandEncoder::new(graphics.clone(), Some("kino billboard encoder")); - kino.render_billboard_targets(&graphics.device, &graphics.queue, &mut kino_encoder); - - if let Err(e) = kino_encoder.submit() { - log_once::error_once!("Unable to submit billboard kino pass: {}", e); - } - } - - if let Some(billboard_pipeline) = &self.billboard_pipeline { - let camera_position = camera.position().as_vec3(); - let camera_projection = Mat4::from_cols_array_2d(&camera.uniform.view_proj); - - let mut kino_views = HashMap::<u64, wgpu::TextureView>::new(); - if let Some(kino) = &mut self.kino { - kino_views.extend(kino.billboard_render_target_views()); - } - - let single_fallback_view = if kino_views.len() == 1 { - kino_views.values().next().cloned() - } else { - None - }; - - let mut billboards: Vec<(Mat4, wgpu::TextureView)> = Vec::new(); - let mut query = self - .world - .query::<(Entity, &BillboardComponent, Option<&EntityTransform>)>(); - - for (entity, billboard, entity_transform) in query.iter() { - puffin::profile_scope!("rendering billboard", format!("{:?}", entity)); - if !billboard.enabled { - continue; - } - - let entity_id = entity.to_bits().get(); - let texture_view = kino_views - .get(&entity_id) - .cloned() - .or_else(|| single_fallback_view.clone()); - - let Some(texture_view) = texture_view else { - continue; - }; - - let position = entity_transform - .map(|transform| transform.sync().position.as_vec3()) - .unwrap_or(glam::Vec3::ZERO) - + billboard.offset; - let world_size = billboard.world_size; - let scale = glam::Vec3::new(world_size.x, world_size.y, 1.0); - - let rotation = if let Some(rotation) = billboard.rotation { - rotation - } else { - let to_camera = (camera_position - position).normalize_or_zero(); - if to_camera.length_squared() > 0.0 { - let mut world_up = glam::Vec3::Y; - if to_camera.dot(world_up).abs() > 0.999 { - world_up = glam::Vec3::X; - } - - let right = world_up.cross(to_camera).normalize_or_zero(); - let up = to_camera.cross(right).normalize_or_zero(); - let basis = glam::Mat3::from_cols(right, up, to_camera); - glam::Quat::from_mat3(&basis) - } else { - glam::Quat::IDENTITY - } - }; + let Some(animation_defaults) = self.animation_pipeline.as_ref() else { + log_once::warn_once!("Animation pipeline not ready"); + return; + }; + let per_frame_bind_group = pipeline.per_frame.as_ref() + .expect("Per-frame bind group not initialised") + .clone(); + + RendererCommon::render_light_cubes(&graphics, &mut encoder, &hdr, &lights, &camera, self.light_cube_pipeline.as_ref()); + + RendererCommon::render_models( + &graphics, &mut encoder, &hdr, + &self.world, &batches, &model_cache, + &per_frame_bind_group, environment_bind_group, + pipeline, animation_defaults, + &self.instance_buffer_cache, + &mut self.animated_instance_buffers, + &mut self.animated_bind_group_cache, + &mut self.static_bind_group_cache, + &mut self.last_morph_info_per_mesh, + ); - let transform = - Mat4::from_scale_rotation_translation(scale, rotation, position); - billboards.push((transform, texture_view)); - } + RendererCommon::render_sky(&graphics, &mut encoder, &hdr, sky); - if !billboards.is_empty() { - puffin::profile_scope!("billboard render pass"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("editor billboard render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - 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, - }), - timestamp_writes: None, - occlusion_query_set: None, - multiview_mask: None, - }); + RendererCommon::render_collider_debug( + &graphics, + &self.world, + self.current_scene.as_deref(), + ); - for (transform, texture_view) in billboards { - billboard_pipeline.draw( - graphics.clone(), - &mut render_pass, - transform, - camera_projection, - &texture_view, - ); - } - } - } - } + RendererCommon::render_billboards( + &graphics, &mut encoder, &hdr, &camera, + &self.world, + self.kino.as_mut(), + self.billboard_pipeline.as_ref(), + ); - // debug draw flush if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() { let view_proj = Mat4::from_cols_array_2d(&camera.uniform.view_proj); debug_draw.flush(graphics.clone(), &mut encoder, view_proj); } hdr.process(&mut encoder, &graphics.viewport_texture.view); + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } - if let Err(e) = encoder.submit() { - log_once::error_once!("{}", e); - } - - // kino hud renderer if let Some(kino) = &mut self.kino { let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder")); kino.render(&graphics.device, &graphics.queue, &mut encoder, hdr.view()); - if let Err(e) = encoder.submit() { log_once::error_once!("Unable to submit kino: {}", e); }