tirbofish/dropbear · commit
f4471a782884cbe562b5f7af4e9e7489b19299f5
refactor: main shader, bind groups and textures.
jarvis, run github actions
Signature present but could not be verified.
Unverified
@@ -86,10 +86,12 @@ impl AnimationComponent { self.available_animations = model.animations.iter().map(|v| v.name.clone()).collect::<Vec<_>>(); let Some(anim_idx) = self.active_animation_index else { + self.reset_to_bind_pose(model); return; }; if anim_idx >= model.animations.len() { + self.reset_to_bind_pose(model); return; } @@ -108,6 +110,7 @@ impl AnimationComponent { self.speed = settings.speed; self.looping = settings.looping; self.is_playing = settings.is_playing; + self.reset_to_bind_pose(model); return; } let animation = &model.animations[anim_idx]; @@ -129,6 +132,11 @@ impl AnimationComponent { self.looping = settings.looping; self.is_playing = settings.is_playing; + if !settings.is_playing { + self.reset_to_bind_pose(model); + return; + } + for channel in &animation.channels { let count = channel.times.len(); if count == 0 { continue; } @@ -275,6 +283,11 @@ impl AnimationComponent { self.update_matrices(model); } + fn reset_to_bind_pose(&mut self, model: &Model) { + self.local_pose.clear(); + self.update_matrices(model); + } + fn apply_single_keyframe( channel: &crate::model::AnimationChannel, index: usize, @@ -26,8 +26,6 @@ impl<T> PartialEq for Handle<T> { } self.id == other.id } - - } impl<T> Copy for Handle<T> {} @@ -98,6 +96,7 @@ impl AssetRegistry { animations: vec![], nodes: vec![], }); + result } @@ -187,13 +186,11 @@ impl AssetRegistry { } pub fn grey_texture(&mut self, graphics: Arc<SharedGraphicsContext>) -> Handle<Texture> { - let grey_handle = Handle::new(Self::hash_contents("Solid texture [128, 128, 128, 255]")); - - if self.contains_hash(grey_handle.id) { - return grey_handle; - } - - self.solid_texture_rgba8(graphics, [128, 128, 128, 255]) + self.solid_texture_rgba8_with_format( + graphics, + [128, 128, 128, 255], + Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), + ) } pub fn solid_texture_rgba8( @@ -201,21 +198,43 @@ impl AssetRegistry { graphics: Arc<SharedGraphicsContext>, rgba: [u8; 4], ) -> Handle<Texture> { - let handle = Handle::new(Self::hash_bytes(&rgba)); + self.solid_texture_rgba8_with_format( + graphics, + rgba, + Texture::TEXTURE_FORMAT, + ) + } + + pub fn solid_texture_rgba8_with_format( + &mut self, + graphics: Arc<SharedGraphicsContext>, + rgba: [u8; 4], + format: wgpu::TextureFormat, + ) -> Handle<Texture> { + let format_tag = format!("{:?}", format); + let handle = Handle::new(Self::hash_contents((rgba, format_tag.as_str()))); if self.contains_hash(handle.id) { return handle; } - let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]); + let label = format!( + "Solid texture [{}, {}, {}, {}] {}", + rgba[0], + rgba[1], + rgba[2], + rgba[3], + format_tag, + ); - let texture = Texture::from_bytes_verbose_mipmapped( + let texture = Texture::from_bytes_verbose_mipmapped_with_format( graphics, &rgba, Some((1, 1)), None, None, - Some(label.as_str()) + format, + Some(label.as_str()), ); self.add_texture_with_label(label, texture) @@ -194,6 +194,10 @@ impl Camera { yaw * pitch } + pub fn buffer(&self) -> &wgpu::Buffer { + self.buffer.buffer() + } + pub fn forward(&self) -> DVec3 { (self.target - self.eye).normalize() } @@ -75,11 +75,13 @@ use crate::pipelines::hdr::HdrPipeline; use crate::scene::Scene; pub struct BindGroupLayouts { + pub scene_globals_bind_group_layout: BindGroupLayout, pub shader_globals_bind_group_layout: BindGroupLayout, pub material_bind_layout: BindGroupLayout, pub camera_bind_group_layout: BindGroupLayout, pub light_bind_group_layout: BindGroupLayout, pub light_array_bind_group_layout: BindGroupLayout, + pub scene_light_skin_bind_group_layout: BindGroupLayout, pub light_cube_bind_group_layout: BindGroupLayout, pub environment_bind_group_layout: BindGroupLayout, pub skinning_bind_group_layout: BindGroupLayout, @@ -273,14 +275,54 @@ Hardware: label: Some("Per-Light Layout"), }); - // shaders/shader.wgsl - @group(2) + // shaders/shader.wgsl - @group(0) + let scene_globals_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("scene globals bind group layout"), + entries: &[ + // u_globals + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // u_camera + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + + // shaders/shader.wgsl - @group(1) let material_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("material_bind_layout"), entries: &[ - // t_diffuse + // u_material wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // t_diffuse + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, @@ -290,14 +332,14 @@ Hardware: }, // s_diffuse wgpu::BindGroupLayoutEntry { - binding: 1, + binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, // t_normal wgpu::BindGroupLayoutEntry { - binding: 2, + binding: 3, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, @@ -308,26 +350,69 @@ Hardware: }, // s_normal wgpu::BindGroupLayoutEntry { - binding: 3, + binding: 4, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, - // u_material + // t_emissive wgpu::BindGroupLayoutEntry { - binding: 4, + binding: 5, visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, count: None, }, + // s_emissive + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // t_metallic + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + // s_metallic + wgpu::BindGroupLayoutEntry { + binding: 8, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // t_occlusion + wgpu::BindGroupLayoutEntry { + binding: 9, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + // s_occlusion + wgpu::BindGroupLayoutEntry { + binding: 10, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, ], }); - - // shaders/shader.wgsl - @group(1) + + // shaders/light.wgsl - @group(1) let light_array_bind_group_layout = device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { entries: &[ @@ -347,7 +432,36 @@ Hardware: } ); - // shaders/shader.wgsl - @group(3) + // shaders/shader.wgsl - @group(2) + let scene_light_skin_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("scene light+skinning bind group layout"), + entries: &[ + // s_light_array + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // s_skinning + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + + // shaders/shader.wgsl - legacy globals layout let shader_globals_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("shader.wgsl globals bind group layout"), entries: &[ @@ -442,11 +556,13 @@ Hardware: physics_accumulator: Duration::ZERO, scene_manager: scene::Manager::new(), layouts: Arc::new(BindGroupLayouts { + scene_globals_bind_group_layout, shader_globals_bind_group_layout, material_bind_layout, camera_bind_group_layout, light_bind_group_layout, light_array_bind_group_layout, + scene_light_skin_bind_group_layout, light_cube_bind_group_layout, environment_bind_group_layout, skinning_bind_group_layout, @@ -61,6 +61,7 @@ pub struct Material { pub tint_buffer: UniformBuffer<MaterialUniform>, pub texture_tag: Option<String>, pub wrap_mode: TextureWrapMode, + pub has_normal_texture: bool, } #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Default)] @@ -158,6 +159,13 @@ impl Material { name: impl Into<String>, diffuse_texture: Texture, normal_texture: Texture, + emissive_texture: Option<Texture>, + metallic_roughness_texture: Option<Texture>, + occlusion_texture: Option<Texture>, + emissive_texture_bound: Texture, + metallic_roughness_texture_bound: Texture, + occlusion_texture_bound: Texture, + has_normal_texture: bool, tint: [f32; 4], texture_tag: Option<String>, ) -> Self { @@ -175,13 +183,26 @@ impl Material { occlusion_strength: 1.0, alpha_cutoff: 0.5, uv_tiling, - _pad: 0.0, + has_normal_texture: has_normal_texture as u32, + has_emissive_texture: emissive_texture.is_some() as u32, + has_metallic_texture: metallic_roughness_texture.is_some() as u32, + has_occlusion_texture: occlusion_texture.is_some() as u32, + pad: 0, }; let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform"); tint_buffer.write(&graphics.queue, &uniform); - let bind_group = Self::create_bind_group(&graphics, &diffuse_texture, &normal_texture, &tint_buffer, &name); + let bind_group = Self::create_bind_group( + &graphics, + &diffuse_texture, + &normal_texture, + &emissive_texture_bound, + &metallic_roughness_texture_bound, + &occlusion_texture_bound, + &tint_buffer, + &name, + ); Self { name, @@ -201,9 +222,10 @@ impl Material { tint_buffer, texture_tag, wrap_mode: TextureWrapMode::Repeat, - emissive_texture: None, - metallic_roughness_texture: None, - occlusion_texture: None, + emissive_texture, + metallic_roughness_texture, + occlusion_texture, + has_normal_texture, } } @@ -211,6 +233,9 @@ impl Material { graphics: &SharedGraphicsContext, diffuse: &Texture, normal: &Texture, + emissive: &Texture, + metallic_roughness: &Texture, + occlusion: &Texture, uniform_buffer: &UniformBuffer<MaterialUniform>, name: &str, ) -> BindGroup { @@ -221,23 +246,47 @@ impl Material { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureView(&diffuse.view), + resource: uniform_buffer.buffer().as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse.sampler), + resource: wgpu::BindingResource::TextureView(&diffuse.view), }, wgpu::BindGroupEntry { binding: 2, - resource: wgpu::BindingResource::TextureView(&normal.view), + resource: wgpu::BindingResource::Sampler(&diffuse.sampler), }, wgpu::BindGroupEntry { binding: 3, - resource: wgpu::BindingResource::Sampler(&normal.sampler), + resource: wgpu::BindingResource::TextureView(&normal.view), }, wgpu::BindGroupEntry { binding: 4, - resource: uniform_buffer.buffer().as_entire_binding(), + resource: wgpu::BindingResource::Sampler(&normal.sampler), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::TextureView(&emissive.view), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::Sampler(&emissive.sampler), + }, + wgpu::BindGroupEntry { + binding: 7, + resource: wgpu::BindingResource::TextureView(&metallic_roughness.view), + }, + wgpu::BindGroupEntry { + binding: 8, + resource: wgpu::BindingResource::Sampler(&metallic_roughness.sampler), + }, + wgpu::BindGroupEntry { + binding: 9, + resource: wgpu::BindingResource::TextureView(&occlusion.view), + }, + wgpu::BindGroupEntry { + binding: 10, + resource: wgpu::BindingResource::Sampler(&occlusion.sampler), }, ], }) @@ -254,7 +303,11 @@ impl Material { occlusion_strength: self.occlusion_strength, alpha_cutoff: self.alpha_cutoff.unwrap_or(0.5), uv_tiling: self.uv_tiling, - _pad: 0.0, + has_normal_texture: self.has_normal_texture as u32, + has_emissive_texture: self.emissive_texture.is_some() as u32, + has_metallic_texture: self.metallic_roughness_texture.is_some() as u32, + has_occlusion_texture: self.occlusion_texture.is_some() as u32, + pad: 0, }; self.tint_buffer.write(&graphics.queue, &uniform); @@ -932,9 +985,21 @@ impl Model { let mut materials = Vec::new(); - let grey_texture = registry.grey_texture(graphics.clone()); - let flat_normal_texture = - registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]); + let white_srgb_texture = registry.solid_texture_rgba8_with_format( + graphics.clone(), + [255, 255, 255, 255], + Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), + ); + let white_linear_texture = registry.solid_texture_rgba8_with_format( + graphics.clone(), + [255, 255, 255, 255], + Texture::TEXTURE_FORMAT_BASE, + ); + let flat_normal_texture = registry.solid_texture_rgba8_with_format( + graphics.clone(), + [128, 128, 255, 255], + Texture::TEXTURE_FORMAT_BASE, + ); for processed in processed_textures { puffin::profile_scope!("creating material"); @@ -952,28 +1017,31 @@ impl Model { let occlusion_sampler = processed.occlusion_sampler; let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse { - Texture::from_bytes_verbose_mipmapped( + Texture::from_bytes_verbose_mipmapped_with_format( graphics.clone(), &rgba_data, Some(dimensions), None, diffuse_sampler.clone(), - Some(material_name.as_str()) + Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), + Some(material_name.as_str()), ) - } else if let Some(grey) = registry.get_texture(grey_texture) { - (*grey).clone() + } else if let Some(white) = registry.get_texture(white_srgb_texture) { + (*white).clone() } else { anyhow::bail!("Unable to find processed diffuse or fetch fallback texture for model {:?}", label); }; + let has_normal_texture = processed_normal.is_some(); let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal { - Texture::from_bytes_verbose_mipmapped( + Texture::from_bytes_verbose_mipmapped_with_format( graphics.clone(), &rgba_data, Some(dimensions), None, normal_sampler.clone(), - Some(material_name.as_str()) + Texture::TEXTURE_FORMAT_BASE, + Some(material_name.as_str()), ) } else if let Some(tex) = registry.get_texture(flat_normal_texture) { (*tex).clone() @@ -982,36 +1050,51 @@ impl Model { }; let emissive_texture = processed_emissive.map(|(rgba_data, dimensions)| { - Texture::from_bytes_verbose_mipmapped( + Texture::from_bytes_verbose_mipmapped_with_format( graphics.clone(), &rgba_data, Some(dimensions), None, emissive_sampler.clone(), - Some(material_name.as_str()) + Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), + Some(material_name.as_str()), + ) + }); + let metallic_roughness_texture = processed_metallic_roughness.map(|(rgba_data, dimensions)| { + Texture::from_bytes_verbose_mipmapped_with_format( + graphics.clone(), + &rgba_data, + Some(dimensions), + None, + metallic_roughness_sampler.clone(), + Texture::TEXTURE_FORMAT_BASE, + Some(material_name.as_str()), ) }); - let metallic_roughness_texture = - processed_metallic_roughness.map(|(rgba_data, dimensions)| { - Texture::from_bytes_verbose_mipmapped( - graphics.clone(), - &rgba_data, - Some(dimensions), - None, - metallic_roughness_sampler.clone(), - Some(material_name.as_str()) - ) - }); let occlusion_texture = processed_occlusion.map(|(rgba_data, dimensions)| { - Texture::from_bytes_verbose_mipmapped( + Texture::from_bytes_verbose_mipmapped_with_format( graphics.clone(), &rgba_data, Some(dimensions), None, occlusion_sampler.clone(), - Some(material_name.as_str()) + Texture::TEXTURE_FORMAT_BASE, + Some(material_name.as_str()), ) }); + + let emissive_texture_bound = emissive_texture + .clone() + .or_else(|| registry.get_texture(white_srgb_texture).cloned()) + .ok_or_else(|| anyhow::anyhow!("Unable to resolve emissive fallback texture for model {:?}", label))?; + let metallic_roughness_texture_bound = metallic_roughness_texture + .clone() + .or_else(|| registry.get_texture(white_linear_texture).cloned()) + .ok_or_else(|| anyhow::anyhow!("Unable to resolve metallic fallback texture for model {:?}", label))?; + let occlusion_texture_bound = occlusion_texture + .clone() + .or_else(|| registry.get_texture(white_linear_texture).cloned()) + .ok_or_else(|| anyhow::anyhow!("Unable to resolve occlusion fallback texture for model {:?}", label))?; let texture_tag = Some(material_name.clone()); let mut material = Material::new( @@ -1019,6 +1102,13 @@ impl Model { material_name, diffuse_texture, normal_texture, + emissive_texture.clone(), + metallic_roughness_texture.clone(), + occlusion_texture.clone(), + emissive_texture_bound, + metallic_roughness_texture_bound, + occlusion_texture_bound, + has_normal_texture, processed.tint, texture_tag, ); @@ -1144,35 +1234,31 @@ pub trait DrawModel<'a> { &mut self, mesh: &'a Mesh, material: &'a Material, - camera_bind_group: &'a wgpu::BindGroup, - light_bind_group: &'a wgpu::BindGroup, - skin_bind_group: Option<&'a wgpu::BindGroup>, + globals_camera_bind_group: &'a wgpu::BindGroup, + light_skin_bind_group: &'a wgpu::BindGroup, ); fn draw_mesh_instanced( &mut self, mesh: &'a Mesh, material: &'a Material, instances: Range<u32>, - camera_bind_group: &'a wgpu::BindGroup, - light_bind_group: &'a wgpu::BindGroup, - skin_bind_group: Option<&'a wgpu::BindGroup>, + globals_camera_bind_group: &'a wgpu::BindGroup, + light_skin_bind_group: &'a wgpu::BindGroup, ); #[allow(unused)] fn draw_model( &mut self, model: &'a Model, - camera_bind_group: &'a wgpu::BindGroup, - light_bind_group: &'a wgpu::BindGroup, - skin_bind_group: Option<&'a wgpu::BindGroup>, + globals_camera_bind_group: &'a wgpu::BindGroup, + light_skin_bind_group: &'a wgpu::BindGroup, ); fn draw_model_instanced( &mut self, model: &'a Model, instances: Range<u32>, - camera_bind_group: &'a wgpu::BindGroup, - light_bind_group: &'a wgpu::BindGroup, - skin_bind_group: Option<&'a wgpu::BindGroup>, + globals_camera_bind_group: &'a wgpu::BindGroup, + light_skin_bind_group: &'a wgpu::BindGroup, ); } @@ -1184,11 +1270,10 @@ where &mut self, mesh: &'b Mesh, material: &'b Material, - camera_bind_group: &'b wgpu::BindGroup, - light_bind_group: &'a wgpu::BindGroup, - skin_bind_group: Option<&'a wgpu::BindGroup>, + globals_camera_bind_group: &'b wgpu::BindGroup, + light_skin_bind_group: &'a wgpu::BindGroup, ) { - self.draw_mesh_instanced(mesh, material, 0..1, camera_bind_group, light_bind_group, skin_bind_group); + self.draw_mesh_instanced(mesh, material, 0..1, globals_camera_bind_group, light_skin_bind_group); } fn draw_mesh_instanced( @@ -1196,19 +1281,14 @@ where mesh: &'b Mesh, material: &'b Material, instances: Range<u32>, - camera_bind_group: &'b wgpu::BindGroup, - light_bind_group: &'a wgpu::BindGroup, - skin_bind_group: Option<&'a wgpu::BindGroup>, + globals_camera_bind_group: &'b wgpu::BindGroup, + light_skin_bind_group: &'a wgpu::BindGroup, ) { self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - self.set_bind_group(0, &material.bind_group, &[]); - self.set_bind_group(1, camera_bind_group, &[]); - self.set_bind_group(2, light_bind_group, &[]); - - if let Some(skin_bg) = skin_bind_group { - self.set_bind_group(4, skin_bg, &[]); - } + self.set_bind_group(0, globals_camera_bind_group, &[]); + self.set_bind_group(1, &material.bind_group, &[]); + self.set_bind_group(2, light_skin_bind_group, &[]); self.draw_indexed(0..mesh.num_elements, 0, instances); } @@ -1216,20 +1296,18 @@ where fn draw_model( &mut self, model: &'b Model, - camera_bind_group: &'b wgpu::BindGroup, - light_bind_group: &'a wgpu::BindGroup, - skin_bind_group: Option<&'a wgpu::BindGroup>, + globals_camera_bind_group: &'b wgpu::BindGroup, + light_skin_bind_group: &'a wgpu::BindGroup, ) { - self.draw_model_instanced(model, 0..1, camera_bind_group, light_bind_group, skin_bind_group); + self.draw_model_instanced(model, 0..1, globals_camera_bind_group, light_skin_bind_group); } fn draw_model_instanced( &mut self, model: &'b Model, instances: Range<u32>, - camera_bind_group: &'b wgpu::BindGroup, - light_bind_group: &'a wgpu::BindGroup, - skin_bind_group: Option<&'a wgpu::BindGroup>, + globals_camera_bind_group: &'b wgpu::BindGroup, + light_skin_bind_group: &'a wgpu::BindGroup, ) { for mesh in &model.meshes { let material = &model.materials[mesh.material]; @@ -1237,9 +1315,8 @@ where mesh, material, instances.clone(), - camera_bind_group, - light_bind_group, - skin_bind_group, + globals_camera_bind_group, + light_skin_bind_group, ); } } @@ -1462,5 +1539,9 @@ pub struct MaterialUniform { pub occlusion_strength: f32, pub alpha_cutoff: f32, pub uv_tiling: [f32; 2], - pub _pad: f32, + pub has_normal_texture: u32, + pub has_emissive_texture: u32, + pub has_metallic_texture: u32, + pub has_occlusion_texture: u32, + pub pad: u32, } @@ -130,6 +130,13 @@ impl LightCubePipeline { &self.light_bind_group } + pub fn light_buffer(&self) -> &wgpu::Buffer { + self.storage_buffer + .as_ref() + .expect("Light cube storage buffer missing") + .buffer() + } + pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, world: &hecs::World) { let mut light_array = LightArrayUniform::default(); @@ -23,11 +23,9 @@ impl DropbearShaderPipeline for MainRenderPipeline { ); let bind_group_layouts = vec![ - &graphics.layouts.material_bind_layout, // @group(0) - &graphics.layouts.camera_bind_group_layout, // @group(1) - &graphics.layouts.light_array_bind_group_layout, // @group(2) - &graphics.layouts.shader_globals_bind_group_layout, // @group(3) - &graphics.layouts.skinning_bind_group_layout, // @group(4) + &graphics.layouts.scene_globals_bind_group_layout, // @group(0) + &graphics.layouts.material_bind_layout, // @group(1) + &graphics.layouts.scene_light_skin_bind_group_layout, // @group(2) ]; let pipeline_layout = @@ -4,6 +4,7 @@ use crate::asset::{AssetRegistry, Handle}; use crate::graphics::SharedGraphicsContext; use crate::model::{Material, Mesh, Model}; +use crate::texture::Texture; use crate::utils::ResourceReference; use crate::model::ModelVertex; use std::hash::{DefaultHasher, Hasher}; @@ -85,22 +86,45 @@ impl ProcedurallyGeneratedObject { }; let material = material.unwrap_or_else(|| { - let grey_handle = _rguard.grey_texture(graphics.clone()); - let flat_normal_handle = - _rguard.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]); - let grey = _rguard - .get_texture(grey_handle) - .expect("Grey texture handle missing") - .clone(); + let flat_normal_handle = _rguard.solid_texture_rgba8_with_format( + graphics.clone(), + [128, 128, 255, 255], + Texture::TEXTURE_FORMAT_BASE, + ); + let white_srgb_handle = _rguard.solid_texture_rgba8_with_format( + graphics.clone(), + [255, 255, 255, 255], + Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), + ); + let white_linear_handle = _rguard.solid_texture_rgba8_with_format( + graphics.clone(), + [255, 255, 255, 255], + Texture::TEXTURE_FORMAT_BASE, + ); let flat_normal = _rguard .get_texture(flat_normal_handle) .expect("Flat normal texture handle missing") .clone(); + let white_srgb = _rguard + .get_texture(white_srgb_handle) + .expect("White SRGB texture handle missing") + .clone(); + let white_linear = _rguard + .get_texture(white_linear_handle) + .expect("White linear texture handle missing") + .clone(); Material::new( graphics.clone(), "procedural_material", - grey, + white_srgb.clone(), flat_normal, + None, + None, + None, + white_srgb, + white_linear.clone(), + white_linear, + false, [1.0, 1.0, 1.0, 1.0], Some("procedural_material".to_string()), ) @@ -12,6 +12,7 @@ struct CameraUniform { inv_proj: mat4x4<f32>, inv_view: mat4x4<f32>, } + struct Light { position: vec4<f32>, direction: vec4<f32>, // x, y, z, outer_cutoff_angle @@ -32,29 +33,44 @@ struct MaterialUniform { occlusion_strength: f32, alpha_cutoff: f32, uv_tiling: vec2<f32>, + + has_normal_texture: u32, // 0,1 bool + has_emissive_texture: u32, // 0,1 bool + has_metallic_texture: u32, // 0,1 bool + has_occlusion_texture: u32, // 0,1 bool } @group(0) @binding(0) -var t_diffuse: texture_2d<f32>; +var<uniform> u_globals: Globals; @group(0) @binding(1) +var<uniform> u_camera: CameraUniform; + +@group(1) @binding(0) +var<uniform> u_material: MaterialUniform; +@group(1) @binding(1) +var t_diffuse: texture_2d<f32>; +@group(1) @binding(2) var s_diffuse: sampler; -@group(0) @binding(2) +@group(1) @binding(3) var t_normal: texture_2d<f32>; -@group(0) @binding(3) +@group(1) @binding(4) var s_normal: sampler; -@group(0) @binding(4) -var<uniform> u_material: MaterialUniform; - -@group(1) @binding(0) -var<uniform> u_camera: CameraUniform; +@group(1) @binding(5) +var t_emissive: texture_2d<f32>; +@group(1) @binding(6) +var s_emissive: sampler; +@group(1) @binding(7) +var t_metallic: texture_2d<f32>; +@group(1) @binding(8) +var s_metallic: sampler; +@group(1) @binding(9) +var t_occlusion: texture_2d<f32>; +@group(1) @binding(10) +var s_occlusion: sampler; @group(2) @binding(0) var<storage, read> s_light_array: array<Light>; - -@group(3) @binding(0) -var<uniform> u_globals: Globals; - -@group(4) @binding(0) +@group(2) @binding(1) var<storage, read> s_skinning: array<mat4x4<f32>>; struct InstanceInput { @@ -234,41 +250,68 @@ fn apply_normal_map( @fragment fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { let uv = in.tex_coords * u_material.uv_tiling; - var tex_color = textureSample(t_diffuse, s_diffuse, uv); - var object_normal = textureSample(t_normal, s_normal, uv); + let tex_color = textureSample(t_diffuse, s_diffuse, uv); let base_colour = tex_color * u_material.base_colour; if (base_colour.a < u_material.alpha_cutoff) { discard; } - let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); + var world_normal: vec3<f32>; + if (u_material.has_normal_texture != 0u) { + let object_normal = textureSample(t_normal, s_normal, uv).xyz; + let scaled_normal = normalize((object_normal * 2.0 - 1.0) * vec3<f32>(u_material.normal_scale, u_material.normal_scale, 1.0)); + world_normal = apply_normal_map( + in.world_normal, + in.world_tangent, + in.world_bitangent, + scaled_normal, + ); + } else { + world_normal = normalize(in.world_normal); + } - let world_normal = apply_normal_map( - in.world_normal, - in.world_tangent, - in.world_bitangent, - object_normal.xyz, - ); + var metallic = u_material.metallic; + var roughness = u_material.roughness; + if (u_material.has_metallic_texture != 0u) { + let mr = textureSample(t_metallic, s_metallic, uv); + metallic *= mr.b; + roughness *= mr.g; + } + + var occlusion = 1.0; + if (u_material.has_occlusion_texture != 0u) { + let occ = textureSample(t_occlusion, s_occlusion, uv).r; + occlusion = 1.0 + u_material.occlusion_strength * (occ - 1.0); + } + + var emissive = u_material.emissive * u_material.emissive_strength; + if (u_material.has_emissive_texture != 0u) { + let emissive_tex = textureSample(t_emissive, s_emissive, uv).rgb; + emissive *= emissive_tex; + } + + let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); + let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.rgb * occlusion; - let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz; var final_color = ambient; - for(var i = 0u; i < u_globals.num_lights; i += 1u) { + for (var i = 0u; i < u_globals.num_lights; i += 1u) { let light = s_light_array[i]; - let light_type = i32(light.color.w + 0.1); - if (light_type == 0) { - final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); + final_color += directional_light(light, world_normal, view_dir, base_colour.rgb, in.world_position); } else if (light_type == 1) { - final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); + final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.rgb); } else if (light_type == 2) { - final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); + final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.rgb); } } - return vec4<f32>(final_color, base_colour.a); -} + final_color *= occlusion; + final_color += emissive; + + return vec4<f32>(final_color, base_colour.a); +} @@ -75,6 +75,7 @@ pub struct Texture { impl Texture { /// Describes the depth format for all Texture related functions in WGPU to use. Makes life easier pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + pub const TEXTURE_FORMAT_BASE: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; pub const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; pub fn create_2d_texture( @@ -92,6 +93,7 @@ impl Texture { height, depth_or_array_layers: 1, }; + Self::create_texture( device, label, @@ -270,7 +272,7 @@ impl Texture { label: Option<&str>, ) -> Self { puffin::profile_function!(label.unwrap_or("")); - let texture = Self::from_bytes_verbose( + let texture = Self::from_bytes_verbose_with_format( graphics.clone(), bytes, dimensions, @@ -290,6 +292,37 @@ impl Texture { texture } + /// Loads the texture from bytes and generates mipmaps on the GPU using the specified format. + pub fn from_bytes_verbose_mipmapped_with_format( + graphics: Arc<SharedGraphicsContext>, + bytes: &[u8], + dimensions: Option<(u32, u32)>, + view_descriptor: Option<wgpu::TextureViewDescriptor>, + sampler: Option<wgpu::SamplerDescriptor>, + format: wgpu::TextureFormat, + label: Option<&str>, + ) -> Self { + puffin::profile_function!(label.unwrap_or("")); + let texture = Self::from_bytes_verbose_with_format( + graphics.clone(), + bytes, + dimensions, + Some(format), + view_descriptor, + sampler, + label, + ); + + if let Err(err) = graphics + .mipmapper + .compute_mipmaps(&graphics.device, &graphics.queue, &texture) + { + log_once::warn_once!("Failed to generate mipmaps: {}", err); + } + + texture + } + /// Loads the texture from bytes, with options for more arguments. /// /// Requires more arguments. For a simpler usage, you should use [Self::from_bytes] @@ -302,6 +335,26 @@ impl Texture { sampler: Option<wgpu::SamplerDescriptor>, label: Option<&str>, ) -> Self { + Self::from_bytes_verbose_with_format( + graphics, + bytes, + dimensions, + None, + view_descriptor, + sampler, + label, + ) + } + + fn from_bytes_verbose_with_format( + graphics: Arc<SharedGraphicsContext>, + bytes: &[u8], + dimensions: Option<(u32, u32)>, + format_override: Option<wgpu::TextureFormat>, + view_descriptor: Option<wgpu::TextureViewDescriptor>, + sampler: Option<wgpu::SamplerDescriptor>, + label: Option<&str>, + ) -> Self { puffin::profile_function!(label.unwrap_or("")); if let Some(l) = label { log::debug!("Loading texture: {l}"); @@ -357,13 +410,14 @@ impl Texture { let mip_level_count = size.width.min(size.height).ilog2() + 1; log::debug!("Mip level count [{:?}]: {}", label, mip_level_count); + let texture_format = format_override.unwrap_or(Texture::TEXTURE_FORMAT); let texture = graphics.device.create_texture(&wgpu::TextureDescriptor { label: Some(format!("{:?} diffuse blit texture", label).as_str()), size, mip_level_count, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: Texture::TEXTURE_FORMAT, + format: texture_format, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST @@ -78,7 +78,7 @@ impl InspectableComponent for Light { ui.label("Uniform"); ui.label("Light Type"); - ComboBox::from_id_salt("Light Type").show_ui(ui, |ui| { + ComboBox::from_id_salt("Light Type").selected_text(format!("{}", self.component.light_type)).show_ui(ui, |ui| { ui.selectable_value(&mut self.component.light_type, LightType::Directional, "Directional"); ui.selectable_value(&mut self.component.light_type, LightType::Point, "Point"); ui.selectable_value(&mut self.component.light_type, LightType::Spot, "Spot"); @@ -1216,14 +1216,20 @@ impl<'a> EditorTabViewer<'a> { queue.push(async move { let path = reference.resolve()?; let buffer = fs::read(&path)?; - let handle = Model::load_from_memory_raw( + let handle = match Model::load_from_memory_raw( graphics.clone(), buffer, Some(reference.clone()), None, ASSET_REGISTRY.clone(), ) - .await?; + .await { + Ok(v) => v, + Err(e) => { + warn!("Unable to load model {}: {}", reference, e); + return Err(e); + }, + }; let mut registry = ASSET_REGISTRY.write(); if let Some(model) = registry.get_model_mut(handle) { @@ -1250,7 +1256,13 @@ impl<'a> EditorTabViewer<'a> { let queue = graphics.future_queue.clone(); queue.push(async move { let path = reference.resolve()?; - let texture = Texture::from_file(graphics.clone(), &path, Some(&label)).await?; + let texture = match Texture::from_file(graphics.clone(), &path, Some(&label)).await{ + Ok(v) => v, + Err(e) => { + warn!("Unable to load texture {}: {}", reference, e); + return Err(e); + }, + }; let mut registry = ASSET_REGISTRY.write(); registry.add_texture_with_label(label.clone(), texture); Ok::<(), anyhow::Error>(()) @@ -98,6 +98,7 @@ pub struct Editor { pub sky_pipeline: Option<SkyPipeline>, pub(crate) default_skinning_buffer: Option<wgpu::Buffer>, pub(crate) default_skinning_bind_group: Option<wgpu::BindGroup>, + pub(crate) light_skin_bind_group: Option<wgpu::BindGroup>, pub active_camera: Arc<Mutex<Option<Entity>>>, @@ -288,6 +289,7 @@ impl Editor { sky_pipeline: None, default_skinning_buffer: None, default_skinning_bind_group: None, + light_skin_bind_group: None, }) } @@ -369,7 +369,7 @@ impl Scene for Editor { } let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new(); - let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::BindGroup)> = Vec::new(); + let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::Buffer)> = Vec::new(); { let mut query = self @@ -383,8 +383,8 @@ impl Scene for Editor { } let instance = renderer.instance.to_raw(); - if let Some(bind_group) = animation.and_then(|anim| anim.bind_group.clone()) { - animated_instances.push((handle.id, instance, bind_group)); + if let Some(buffer) = animation.and_then(|anim| anim.bone_buffer.clone()) { + animated_instances.push((handle.id, instance, buffer)); } else { static_batches.entry(handle.id).or_default().push(instance); } @@ -485,19 +485,50 @@ impl Scene for Editor { self.default_skinning_bind_group = Some(bind_group); } - let default_skinning_bind_group = self - .default_skinning_bind_group + let default_skinning_buffer = self + .default_skinning_buffer .as_ref() - .expect("Default skinning bind group not initialized"); + .expect("Default skinning buffer not initialized"); // model rendering if let Some(lcp) = &self.light_cube_pipeline { for (model, handle, instance_count) in prepared_models { - let globals_bind_group = &self + let globals = self .shader_globals .as_ref() - .expect("Shader globals not initialised") - .bind_group; + .expect("Shader globals not initialised"); + let globals_camera_bind_group = graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("scene globals+camera bind group"), + layout: &graphics.layouts.scene_globals_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: globals.buffer.buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: camera.buffer().as_entire_binding(), + }, + ], + }, + ); + let light_skin_bind_group = graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("scene light+skin bind group"), + layout: &graphics.layouts.scene_light_skin_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: lcp.light_buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: default_skinning_buffer.as_entire_binding(), + }, + ], + }, + ); let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { @@ -531,26 +562,38 @@ impl Scene for Editor { } else { continue; } - render_pass.set_bind_group(3, globals_bind_group, &[]); - render_pass.draw_model_instanced( model, 0..instance_count, - &camera.bind_group, - lcp.bind_group(), - Some(default_skinning_bind_group), + &globals_camera_bind_group, + &light_skin_bind_group, ); } } if let Some(lcp) = &self.light_cube_pipeline { - let globals_bind_group = &self + let globals = self .shader_globals .as_ref() - .expect("Shader globals not initialised") - .bind_group; + .expect("Shader globals not initialised"); + let globals_camera_bind_group = graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("scene globals+camera bind group"), + layout: &graphics.layouts.scene_globals_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: globals.buffer.buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: camera.buffer().as_entire_binding(), + }, + ], + }, + ); - for (handle, instance, skin_bind_group) in animated_instances { + for (handle, instance, skin_buffer) in animated_instances { let Some(model) = registry.get_model(Handle::new(handle)) else { log_once::error_once!("Missing model handle {} in registry", handle); continue; @@ -594,14 +637,30 @@ impl Scene for Editor { render_pass.set_pipeline(pipeline.pipeline()); render_pass.set_vertex_buffer(1, instance_buffer.slice(1)); - render_pass.set_bind_group(3, globals_bind_group, &[]); + if self.light_skin_bind_group.is_none() { + self.light_skin_bind_group = Some(graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("scene light+skin bind group"), + layout: &graphics.layouts.scene_light_skin_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: lcp.light_buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: skin_buffer.as_entire_binding(), + }, + ], + }, + )); + } render_pass.draw_model_instanced( model, 0..1, - &camera.bind_group, - lcp.bind_group(), - Some(&skin_bind_group), + &globals_camera_bind_group, + &self.light_skin_bind_group.as_ref().unwrap(), // safe to do so because of check above ); } } @@ -128,6 +128,7 @@ pub struct PlayMode { sky_pipeline: Option<SkyPipeline>, default_skinning_buffer: Option<wgpu::Buffer>, default_skinning_bind_group: Option<wgpu::BindGroup>, + light_skin_bind_group: Option<wgpu::BindGroup>, initial_scene: Option<String>, current_scene: Option<String>, @@ -217,6 +218,7 @@ impl PlayMode { sky_pipeline: None, default_skinning_buffer: None, default_skinning_bind_group: None, + light_skin_bind_group: None, }; log::debug!("Created new play mode instance"); @@ -641,7 +641,7 @@ impl Scene for PlayMode { } let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new(); - let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::BindGroup)> = Vec::new(); + let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::Buffer)> = Vec::new(); { let mut query = self @@ -655,8 +655,8 @@ impl Scene for PlayMode { } let instance = renderer.instance.to_raw(); - if let Some(bind_group) = animation.and_then(|anim| anim.bind_group.clone()) { - animated_instances.push((handle.id, instance, bind_group)); + if let Some(buffer) = animation.and_then(|anim| anim.bone_buffer.clone()) { + animated_instances.push((handle.id, instance, buffer)); } else { static_batches.entry(handle.id).or_default().push(instance); } @@ -758,19 +758,50 @@ impl Scene for PlayMode { self.default_skinning_bind_group = Some(bind_group); } - let default_skinning_bind_group = self - .default_skinning_bind_group + let default_skinning_buffer = self + .default_skinning_buffer .as_ref() - .expect("Default skinning bind group not initialized"); + .expect("Default skinning buffer not initialized"); // model rendering if let Some(lcp) = &self.light_cube_pipeline { for (model, handle, instance_count) in prepared_models { - let globals_bind_group = &self + let globals = self .shader_globals .as_ref() - .expect("Shader globals not initialised") - .bind_group; + .expect("Shader globals not initialised"); + let globals_camera_bind_group = graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("scene globals+camera bind group"), + layout: &graphics.layouts.scene_globals_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: globals.buffer.buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: camera.buffer().as_entire_binding(), + }, + ], + }, + ); + let light_skin_bind_group = graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("scene light+skin bind group"), + layout: &graphics.layouts.scene_light_skin_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: lcp.light_buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: default_skinning_buffer.as_entire_binding(), + }, + ], + }, + ); let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { @@ -804,26 +835,38 @@ impl Scene for PlayMode { } else { continue; } - render_pass.set_bind_group(3, globals_bind_group, &[]); - render_pass.draw_model_instanced( model, 0..instance_count, - &camera.bind_group, - lcp.bind_group(), - Some(default_skinning_bind_group), + &globals_camera_bind_group, + &light_skin_bind_group, ); } } if let Some(lcp) = &self.light_cube_pipeline { - let globals_bind_group = &self + let globals = self .shader_globals .as_ref() - .expect("Shader globals not initialised") - .bind_group; + .expect("Shader globals not initialised"); + let globals_camera_bind_group = graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("scene globals+camera bind group"), + layout: &graphics.layouts.scene_globals_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: globals.buffer.buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: camera.buffer().as_entire_binding(), + }, + ], + }, + ); - for (handle, instance, skin_bind_group) in animated_instances { + for (handle, instance, skin_buffer) in animated_instances { let Some(model) = registry.get_model(Handle::new(handle)) else { log_once::error_once!("Missing model handle {} in registry", handle); continue; @@ -867,14 +910,30 @@ impl Scene for PlayMode { render_pass.set_pipeline(pipeline.pipeline()); render_pass.set_vertex_buffer(1, instance_buffer.slice(1)); - render_pass.set_bind_group(3, globals_bind_group, &[]); + if self.light_skin_bind_group.is_none() { + self.light_skin_bind_group = Some(graphics.device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("scene light+skin bind group"), + layout: &graphics.layouts.scene_light_skin_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: lcp.light_buffer().as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: skin_buffer.as_entire_binding(), + }, + ], + }, + )); + } render_pass.draw_model_instanced( model, 0..1, - &camera.bind_group, - lcp.bind_group(), - Some(&skin_bind_group), + &globals_camera_bind_group, + &self.light_skin_bind_group.as_ref().unwrap(), // safe to do so because of check above ); } }