use crate::error::Result;
use crate::math_util::IDENTITY_MAT4;
use crate::vertex::Vertex3D;
use wgpu::util::DeviceExt;
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CameraUniforms {
pub view_proj: [f32; 16],
pub model: [f32; 16],
pub camera_pos: [f32; 4],
pub normal_matrix_0: [f32; 4],
pub normal_matrix_1: [f32; 4],
pub normal_matrix_2: [f32; 4],
}
impl Default for CameraUniforms {
fn default() -> Self {
Self {
view_proj: IDENTITY_MAT4,
model: IDENTITY_MAT4,
camera_pos: [0.0, 0.0, 5.0, 0.0],
normal_matrix_0: [1.0, 0.0, 0.0, 0.0],
normal_matrix_1: [0.0, 1.0, 0.0, 0.0],
normal_matrix_2: [0.0, 0.0, 1.0, 0.0],
}
}
}
impl CameraUniforms {
pub fn set_model(&mut self, model: [f32; 16]) {
self.model = model;
let (nm0, nm1, nm2) = inverse_transpose_3x3(&model);
self.normal_matrix_0 = [nm0[0], nm0[1], nm0[2], 0.0];
self.normal_matrix_1 = [nm1[0], nm1[1], nm1[2], 0.0];
self.normal_matrix_2 = [nm2[0], nm2[1], nm2[2], 0.0];
}
}
#[inline]
fn inverse_transpose_3x3(m: &[f32; 16]) -> ([f32; 3], [f32; 3], [f32; 3]) {
let a = [m[0], m[1], m[2]];
let b = [m[4], m[5], m[6]];
let c = [m[8], m[9], m[10]];
let bc = [
b[1] * c[2] - b[2] * c[1],
b[2] * c[0] - b[0] * c[2],
b[0] * c[1] - b[1] * c[0],
];
let ca = [
c[1] * a[2] - c[2] * a[1],
c[2] * a[0] - c[0] * a[2],
c[0] * a[1] - c[1] * a[0],
];
let ab = [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
let det = a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2];
let inv_det = if det.abs() > 1e-10 { 1.0 / det } else { 1.0 };
(
[bc[0] * inv_det, bc[1] * inv_det, bc[2] * inv_det],
[ca[0] * inv_det, ca[1] * inv_det, ca[2] * inv_det],
[ab[0] * inv_det, ab[1] * inv_det, ab[2] * inv_det],
)
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LightUniforms {
pub ambient_color: [f32; 4],
pub light_direction: [f32; 4],
pub light_color: [f32; 4],
pub light_view_proj: [f32; 16],
}
impl Default for LightUniforms {
fn default() -> Self {
Self {
ambient_color: [1.0, 1.0, 1.0, 0.1],
light_direction: [
0.0,
-std::f32::consts::FRAC_1_SQRT_2,
-std::f32::consts::FRAC_1_SQRT_2,
0.0,
],
light_color: [1.0, 1.0, 1.0, 1.0],
light_view_proj: IDENTITY_MAT4,
}
}
}
pub struct Mesh {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
impl Mesh {
#[must_use]
pub fn new(device: &wgpu::Device, vertices: &[Vertex3D], indices: &[u32]) -> Self {
tracing::debug!(
vertex_count = vertices.len(),
index_count = indices.len(),
"creating mesh"
);
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh_vertex_buffer"),
contents: bytemuck::cast_slice(vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh_index_buffer"),
contents: bytemuck::cast_slice(indices),
usage: wgpu::BufferUsages::INDEX,
});
let index_count = u32::try_from(indices.len()).unwrap_or_else(|_| {
tracing::warn!(
len = indices.len(),
"mesh index count exceeds u32::MAX, clamping"
);
u32::MAX
});
Self {
vertex_buffer,
index_buffer,
index_count,
}
}
}
pub struct DepthBuffer {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
}
impl DepthBuffer {
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
#[must_use]
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("depth_buffer"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: Self::FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Self { texture, view }
}
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
*self = Self::new(device, width, height);
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ShadowPassUniforms {
pub light_view_proj: [f32; 16],
pub shadow_map_size: [f32; 4],
}
impl Default for ShadowPassUniforms {
fn default() -> Self {
Self {
light_view_proj: IDENTITY_MAT4,
shadow_map_size: [2048.0, 0.0, 0.0, 0.0],
}
}
}
pub struct MeshDrawParams<'a> {
pub color_view: &'a wgpu::TextureView,
pub depth: &'a DepthBuffer,
pub mesh: &'a Mesh,
pub material_bind_group: &'a wgpu::BindGroup,
pub shadow_bind_group: &'a wgpu::BindGroup,
pub ibl_bind_group: &'a wgpu::BindGroup,
pub clear_color: Option<crate::color::Color>,
}
pub struct MeshPipeline {
pipeline: mabda::RenderPipeline,
camera_buffer: wgpu::Buffer,
light_buffer: wgpu::Buffer,
material_buffer: wgpu::Buffer,
shadow_pass_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
}
impl MeshPipeline {
pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Result<Self> {
tracing::debug!(?surface_format, "creating mesh pipeline");
let group0_entries = mabda::BindGroupLayoutBuilder::new()
.uniform_buffer(wgpu::ShaderStages::VERTEX_FRAGMENT) .uniform_buffer(wgpu::ShaderStages::FRAGMENT) .uniform_buffer(wgpu::ShaderStages::FRAGMENT) .uniform_buffer(wgpu::ShaderStages::VERTEX_FRAGMENT) .into_entries();
let group1_entries = mabda::BindGroupLayoutBuilder::new()
.texture_2d(wgpu::ShaderStages::FRAGMENT)
.sampler(wgpu::ShaderStages::FRAGMENT)
.into_entries();
let group2_entries = mabda::BindGroupLayoutBuilder::new()
.texture_depth_2d(wgpu::ShaderStages::FRAGMENT)
.comparison_sampler(wgpu::ShaderStages::FRAGMENT)
.into_entries();
let group3_entries = mabda::BindGroupLayoutBuilder::new()
.texture_cube(wgpu::ShaderStages::FRAGMENT)
.sampler(wgpu::ShaderStages::FRAGMENT)
.texture_cube(wgpu::ShaderStages::FRAGMENT)
.sampler(wgpu::ShaderStages::FRAGMENT)
.texture_2d(wgpu::ShaderStages::FRAGMENT)
.sampler(wgpu::ShaderStages::FRAGMENT)
.into_entries();
let pipeline = mabda::RenderPipelineBuilder::new(
device,
include_str!("pbr.wgsl"),
"vs_main",
"fs_main",
)
.label("mesh_pipeline")
.vertex_layout(Vertex3D::layout())
.bind_group(group0_entries)
.bind_group(group1_entries)
.bind_group(group2_entries)
.bind_group(group3_entries)
.color_target(surface_format, Some(wgpu::BlendState::ALPHA_BLENDING))
.cull_mode(Some(wgpu::Face::Back))
.depth_stencil(wgpu::DepthStencilState {
format: DepthBuffer::FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
})
.build()?;
let cam_defaults = CameraUniforms::default();
let camera_buffer = mabda::create_uniform_buffer(
device,
bytemuck::bytes_of(&cam_defaults),
"camera_uniform_buffer",
);
let light_defaults = crate::lights::LightArrayUniforms::default();
let light_buffer = mabda::create_uniform_buffer(
device,
bytemuck::bytes_of(&light_defaults),
"light_array_uniform_buffer",
);
let mat_defaults = crate::pbr_material::MaterialUniforms::default();
let material_buffer = mabda::create_uniform_buffer(
device,
bytemuck::bytes_of(&mat_defaults),
"material_uniform_buffer",
);
let shadow_defaults = ShadowPassUniforms::default();
let shadow_pass_buffer = mabda::create_uniform_buffer(
device,
bytemuck::bytes_of(&shadow_defaults),
"shadow_pass_uniform_buffer",
);
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("pbr_uniform_bind_group"),
layout: pipeline.bind_group_layout(0).ok_or_else(|| {
crate::error::RenderError::Pipeline(
"mesh pipeline missing bind group layout 0".into(),
)
})?,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: camera_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: light_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: material_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: shadow_pass_buffer.as_entire_binding(),
},
],
});
Ok(Self {
pipeline,
camera_buffer,
light_buffer,
material_buffer,
shadow_pass_buffer,
uniform_bind_group,
})
}
pub fn material_bind_group_layout(&self) -> Result<&wgpu::BindGroupLayout> {
self.pipeline.bind_group_layout(1).ok_or_else(|| {
crate::error::RenderError::Pipeline("mesh pipeline missing bind group layout 1".into())
})
}
pub fn update_camera(&self, queue: &wgpu::Queue, camera: &CameraUniforms) {
queue.write_buffer(&self.camera_buffer, 0, bytemuck::bytes_of(camera));
}
pub fn update_lights(&self, queue: &wgpu::Queue, lights: &crate::lights::LightArrayUniforms) {
queue.write_buffer(&self.light_buffer, 0, bytemuck::bytes_of(lights));
}
pub fn update_shadow_pass(&self, queue: &wgpu::Queue, shadow: &ShadowPassUniforms) {
queue.write_buffer(&self.shadow_pass_buffer, 0, bytemuck::bytes_of(shadow));
}
pub fn update_material(
&self,
queue: &wgpu::Queue,
material: &crate::pbr_material::MaterialUniforms,
) {
queue.write_buffer(&self.material_buffer, 0, bytemuck::bytes_of(material));
}
pub fn shadow_bind_group_layout(&self) -> Result<&wgpu::BindGroupLayout> {
self.pipeline.bind_group_layout(2).ok_or_else(|| {
crate::error::RenderError::Pipeline("mesh pipeline missing bind group layout 2".into())
})
}
pub fn ibl_bind_group_layout(&self) -> Result<&wgpu::BindGroupLayout> {
self.pipeline.bind_group_layout(3).ok_or_else(|| {
crate::error::RenderError::Pipeline("mesh pipeline missing bind group layout 3".into())
})
}
pub fn create_shadow_bind_group(
&self,
device: &wgpu::Device,
shadow_map: &crate::shadow::ShadowMap,
) -> Result<wgpu::BindGroup> {
Ok(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shadow_bind_group"),
layout: self.shadow_bind_group_layout()?,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&shadow_map.depth_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&shadow_map.sampler),
},
],
}))
}
pub fn draw(&self, device: &wgpu::Device, queue: &wgpu::Queue, params: &MeshDrawParams<'_>) {
tracing::debug!(
index_count = params.mesh.index_count,
has_clear = params.clear_color.is_some(),
"drawing mesh"
);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("mesh_encoder"),
});
{
let color_load = match params.clear_color {
Some(c) => wgpu::LoadOp::Clear(c.to_wgpu()),
None => wgpu::LoadOp::Load,
};
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("mesh_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: params.color_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: color_load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: ¶ms.depth.view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
render_pass.set_pipeline(self.pipeline.raw());
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_bind_group(1, params.material_bind_group, &[]);
render_pass.set_bind_group(2, params.shadow_bind_group, &[]);
render_pass.set_bind_group(3, params.ibl_bind_group, &[]);
render_pass.set_vertex_buffer(0, params.mesh.vertex_buffer.slice(..));
render_pass.set_index_buffer(
params.mesh.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..params.mesh.index_count, 0, 0..1);
}
queue.submit(std::iter::once(encoder.finish()));
}
pub fn draw_into_pass<'a>(
&'a self,
render_pass: &mut wgpu::RenderPass<'a>,
mesh: &'a Mesh,
material_bind_group: &'a wgpu::BindGroup,
shadow_bind_group: &'a wgpu::BindGroup,
ibl_bind_group: &'a wgpu::BindGroup,
) {
render_pass.set_pipeline(self.pipeline.raw());
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_bind_group(1, material_bind_group, &[]);
render_pass.set_bind_group(2, shadow_bind_group, &[]);
render_pass.set_bind_group(3, ibl_bind_group, &[]);
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
render_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn camera_uniforms_size() {
assert_eq!(std::mem::size_of::<CameraUniforms>(), 192);
}
#[test]
fn light_uniforms_size() {
assert_eq!(std::mem::size_of::<LightUniforms>(), 112); }
#[test]
fn shadow_pass_uniforms_size() {
assert_eq!(std::mem::size_of::<ShadowPassUniforms>(), 80); }
#[test]
fn camera_uniforms_default() {
let cam = CameraUniforms::default();
assert_eq!(cam.view_proj[0], 1.0);
assert_eq!(cam.view_proj[5], 1.0);
assert_eq!(cam.view_proj[10], 1.0);
assert_eq!(cam.view_proj[15], 1.0);
}
#[test]
fn light_uniforms_default() {
let light = LightUniforms::default();
assert_eq!(light.ambient_color[3], 0.1); assert!(light.light_direction[1] < 0.0); let d = light.light_direction;
let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
assert!((len - 1.0).abs() < 0.001);
}
#[test]
fn camera_uniforms_bytemuck() {
let cam = CameraUniforms::default();
let bytes = bytemuck::bytes_of(&cam);
assert_eq!(bytes.len(), 192);
}
#[test]
fn camera_set_model_computes_normals() {
let mut cam = CameraUniforms::default();
cam.set_model(IDENTITY_MAT4);
assert!((cam.normal_matrix_0[0] - 1.0).abs() < 0.001);
assert!((cam.normal_matrix_1[1] - 1.0).abs() < 0.001);
assert!((cam.normal_matrix_2[2] - 1.0).abs() < 0.001);
}
#[test]
fn camera_set_model_non_uniform_scale() {
let mut cam = CameraUniforms::default();
let mut model = IDENTITY_MAT4;
model[0] = 2.0; model[10] = 0.5; cam.set_model(model);
assert!((cam.normal_matrix_0[0] - 0.5).abs() < 0.001); assert!((cam.normal_matrix_1[1] - 1.0).abs() < 0.001); assert!((cam.normal_matrix_2[2] - 2.0).abs() < 0.001); }
#[test]
fn shadow_pass_uniforms_default() {
let s = ShadowPassUniforms::default();
assert_eq!(s.shadow_map_size[0], 2048.0);
assert_eq!(s.light_view_proj[0], 1.0); }
#[test]
fn shadow_pass_uniforms_bytemuck() {
let s = ShadowPassUniforms::default();
let bytes = bytemuck::bytes_of(&s);
assert_eq!(bytes.len(), 80);
}
#[test]
fn light_uniforms_bytemuck() {
let light = LightUniforms::default();
let bytes = bytemuck::bytes_of(&light);
assert_eq!(bytes.len(), 112);
}
#[test]
fn depth_buffer_format() {
assert_eq!(DepthBuffer::FORMAT, wgpu::TextureFormat::Depth32Float);
}
#[test]
fn camera_uniforms_set_model_singular() {
let mut cam = CameraUniforms::default();
let zero_model = [0.0_f32; 16];
cam.set_model(zero_model);
for &v in &cam.normal_matrix_0 {
assert!(!v.is_nan(), "normal_matrix_0 contains NaN");
}
for &v in &cam.normal_matrix_1 {
assert!(!v.is_nan(), "normal_matrix_1 contains NaN");
}
for &v in &cam.normal_matrix_2 {
assert!(!v.is_nan(), "normal_matrix_2 contains NaN");
}
let mut singular = IDENTITY_MAT4;
singular[4] = singular[0]; singular[5] = singular[1];
singular[6] = singular[2];
cam.set_model(singular);
for &v in &cam.normal_matrix_0 {
assert!(
!v.is_nan(),
"normal_matrix_0 contains NaN for singular matrix"
);
}
for &v in &cam.normal_matrix_1 {
assert!(
!v.is_nan(),
"normal_matrix_1 contains NaN for singular matrix"
);
}
for &v in &cam.normal_matrix_2 {
assert!(
!v.is_nan(),
"normal_matrix_2 contains NaN for singular matrix"
);
}
}
}