use crate::model::{SceneGlobals, Vertex};
use bytemuck::{Pod, Zeroable};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct MaterialBindGroupKey {
pub texture_source_ids: [u32; 5],
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Default)]
pub struct ModelGlobals {
pub mvp: [[f32; 4]; 4],
pub model: [[f32; 4]; 4],
pub extra: [f32; 4], pub albedo_uv: [f32; 4],
pub pbr_uv: [f32; 4],
pub normal_uv: [f32; 4],
pub ao_uv: [f32; 4],
pub emissive_uv: [f32; 4],
}
pub struct ModelRenderer {
pub(crate) globals_bind_group_layout: wgpu::BindGroupLayout, pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout, pub(crate) bone_matrices_bind_group_layout: wgpu::BindGroupLayout, pub(crate) environment_bind_group_layout: wgpu::BindGroupLayout,
pub(crate) model_globals_buffer: wgpu::Buffer,
pub(crate) user_shader_opts_buffer: wgpu::Buffer,
pub(crate) bone_matrices_buffer: wgpu::Buffer,
pub(crate) instance_buffer: wgpu::Buffer,
pub(crate) scene_globals_buffer: wgpu::Buffer,
pub(crate) globals_bind_group: wgpu::BindGroup,
pub(crate) bone_matrices_bind_group: wgpu::BindGroup,
pub(crate) sampler: wgpu::Sampler,
pub(crate) shadow_sampler: wgpu::Sampler,
pub(crate) ibl_sampler: wgpu::Sampler,
pub(crate) model_globals_stride: u32,
pub(crate) user_opts_stride: u32,
pub(crate) bone_matrices_stride: u32,
pub(crate) next_model_globals: u32,
pub(crate) next_user_shader_opts: u32,
pub(crate) next_bone_batch: u32,
pub(crate) max_model_globals: u32,
pub(crate) max_user_shader_opts: u32,
pub(crate) max_instances: u32,
pub(crate) skins: HashMap<u32, crate::model::SkinData>,
pub(crate) texture_bind_groups: HashMap<MaterialBindGroupKey, wgpu::BindGroup>,
pub(crate) skin_bone_offsets: HashMap<u32, u32>,
}
impl ModelRenderer {
pub const GLOBALS_SIZE_BYTES: usize = std::mem::size_of::<ModelGlobals>();
pub const USER_SHADER_OPTS_SIZE: usize = 256;
pub fn new(device: &wgpu::Device) -> Self {
let align = device.limits().min_uniform_buffer_offset_alignment;
let model_globals_stride = (Self::GLOBALS_SIZE_BYTES as u32).div_ceil(align) * align;
let user_opts_stride = (Self::USER_SHADER_OPTS_SIZE as u32).div_ceil(align) * align;
let max_model_globals = 4096u32;
let max_user_shader_opts = 4096u32;
let max_instances = 65536u32;
let model_globals_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("model_globals_ubo"),
size: (model_globals_stride * max_model_globals) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let user_shader_opts_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("model_user_shader_opts_ubo"),
size: (user_opts_stride * max_user_shader_opts) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let scene_globals_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("model_scene_globals_ubo"),
size: std::mem::size_of::<SceneGlobals>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("model_instance_buffer"),
size: (max_instances as usize * std::mem::size_of::<[[f32; 4]; 4]>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let globals_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("model_globals_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: std::num::NonZeroU64::new(
Self::GLOBALS_SIZE_BYTES as u64,
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
SceneGlobals,
>(
)
as u64),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: std::num::NonZeroU64::new(
Self::USER_SHADER_OPTS_SIZE as u64,
),
},
count: None,
},
],
});
let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("model_globals_bg"),
layout: &globals_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &model_globals_buffer,
offset: 0,
size: std::num::NonZeroU64::new(Self::GLOBALS_SIZE_BYTES as u64),
}),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &scene_globals_buffer,
offset: 0,
size: std::num::NonZeroU64::new(std::mem::size_of::<SceneGlobals>() as u64),
}),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &user_shader_opts_buffer,
offset: 0,
size: std::num::NonZeroU64::new(Self::USER_SHADER_OPTS_SIZE as u64),
}),
},
],
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("model_texture_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
],
});
let bone_matrices_stride = 256 * 64;
let bone_matrices_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("model_bone_matrices_bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: std::num::NonZeroU64::new(bone_matrices_stride as u64),
},
count: None,
}],
});
let bone_matrices_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("model_bone_matrices_uniform"),
size: (bone_matrices_stride as u64 * max_model_globals as u64),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bone_matrices_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("model_bone_matrices_bg"),
layout: &bone_matrices_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &bone_matrices_buffer,
offset: 0,
size: std::num::NonZeroU64::new(bone_matrices_stride as u64),
}),
}],
});
let environment_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("model_env_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::Cube,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::Cube,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("model_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
lod_min_clamp: 0.0,
lod_max_clamp: 32.0,
..Default::default()
});
let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("shadow_sampler"),
compare: Some(wgpu::CompareFunction::LessEqual),
..Default::default()
});
let ibl_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("ibl_sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
Self {
globals_bind_group_layout,
texture_bind_group_layout,
bone_matrices_bind_group_layout,
environment_bind_group_layout,
model_globals_buffer,
user_shader_opts_buffer,
bone_matrices_buffer,
instance_buffer,
scene_globals_buffer,
globals_bind_group,
bone_matrices_bind_group,
sampler,
shadow_sampler,
ibl_sampler,
model_globals_stride,
user_opts_stride,
bone_matrices_stride: bone_matrices_stride as u32,
next_model_globals: 0,
next_user_shader_opts: 0,
next_bone_batch: 0,
max_model_globals,
max_user_shader_opts,
max_instances,
skins: HashMap::new(),
texture_bind_groups: HashMap::new(),
skin_bone_offsets: HashMap::new(),
}
}
pub fn create_environment_bind_group(
&self,
device: &wgpu::Device,
shadow_view: &wgpu::TextureView,
irradiance_view: &wgpu::TextureView,
prefiltered_view: &wgpu::TextureView,
brdf_lut_view: &wgpu::TextureView,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("model_env_bg"),
layout: &self.environment_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(shadow_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(irradiance_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(prefiltered_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(brdf_lut_view),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::Sampler(&self.ibl_sampler),
},
],
})
}
pub fn create_texture_bind_group(
&self,
device: &wgpu::Device,
albedo: &wgpu::TextureView,
pbr: &wgpu::TextureView,
normal: &wgpu::TextureView,
ao: &wgpu::TextureView,
emissive: &wgpu::TextureView,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("model_texture_bg"),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(albedo),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(pbr),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(normal),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(ao),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(emissive),
},
],
})
}
pub fn texture_bind_group_for_materials(
&mut self,
device: &wgpu::Device,
key: MaterialBindGroupKey,
views: [&wgpu::TextureView; 5],
) -> &wgpu::BindGroup {
if !self.texture_bind_groups.contains_key(&key) {
let bind_group = self.create_texture_bind_group(
device, views[0], views[1], views[2], views[3], views[4],
);
self.texture_bind_groups.insert(key, bind_group);
}
self.texture_bind_groups
.get(&key)
.expect("texture bind group should be cached")
}
pub fn clear_texture_bind_group_cache(&mut self) {
self.texture_bind_groups.clear();
}
pub fn begin_frame(&mut self) {
self.next_model_globals = 0;
self.next_user_shader_opts = 0;
self.next_bone_batch = 0;
self.skin_bone_offsets.clear();
}
pub fn bone_offset_for_skin(
&mut self,
queue: &wgpu::Queue,
skin_id: u32,
matrices: &[[[f32; 4]; 4]],
) -> anyhow::Result<u32> {
if let Some(offset) = self.skin_bone_offsets.get(&skin_id) {
return Ok(*offset);
}
let offset = self.upload_bone_matrices(queue, matrices)?;
self.skin_bone_offsets.insert(skin_id, offset);
Ok(offset)
}
pub fn upload_globals(
&mut self,
queue: &wgpu::Queue,
globals: &ModelGlobals,
) -> anyhow::Result<u32> {
if self.next_model_globals >= self.max_model_globals {
return Err(anyhow::anyhow!("max model globals exceeded"));
}
let offset = self.next_model_globals * self.model_globals_stride;
queue.write_buffer(
&self.model_globals_buffer,
offset as wgpu::BufferAddress,
bytemuck::bytes_of(globals),
);
let dyn_offset = offset;
self.next_model_globals += 1;
Ok(dyn_offset)
}
pub fn upload_shader_opts_bytes(
&mut self,
queue: &wgpu::Queue,
bytes: &[u8],
) -> anyhow::Result<u32> {
if bytes.len() != Self::USER_SHADER_OPTS_SIZE {
return Err(anyhow::anyhow!(
"model shader opts must be exactly {} bytes",
Self::USER_SHADER_OPTS_SIZE
));
}
if self.next_user_shader_opts >= self.max_user_shader_opts {
return Err(anyhow::anyhow!("max model shader opts exceeded"));
}
let slot = self.next_user_shader_opts;
self.next_user_shader_opts += 1;
let offset = slot as wgpu::BufferAddress * self.user_opts_stride as wgpu::BufferAddress;
queue.write_buffer(&self.user_shader_opts_buffer, offset, bytes);
Ok(offset as u32)
}
pub fn upload_bone_matrices(
&mut self,
queue: &wgpu::Queue,
matrices: &[[[f32; 4]; 4]],
) -> anyhow::Result<u32> {
if matrices.is_empty() {
return Ok(0);
}
if self.next_bone_batch >= self.max_model_globals {
return Err(anyhow::anyhow!("max model bone batches exceeded"));
}
let slot = self.next_bone_batch;
self.next_bone_batch += 1;
let offset = slot as wgpu::BufferAddress * self.bone_matrices_stride as wgpu::BufferAddress;
let bytes = bytemuck::cast_slice(matrices);
if bytes.len() > self.bone_matrices_stride as usize {
return Err(anyhow::anyhow!("too many bones! max is 256"));
}
queue.write_buffer(&self.bone_matrices_buffer, offset, bytes);
Ok(offset as u32)
}
pub fn upload_scene_globals(&self, queue: &wgpu::Queue, scene: &SceneGlobals) {
queue.write_buffer(&self.scene_globals_buffer, 0, bytemuck::bytes_of(scene));
}
pub fn upload_instances(
&self,
queue: &wgpu::Queue,
instances: &[[[f32; 4]; 4]],
) -> anyhow::Result<()> {
if instances.len() > self.max_instances as usize {
return Err(anyhow::anyhow!(
"too many instances! max is {}",
self.max_instances
));
}
queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(instances));
Ok(())
}
}
pub struct MeshData {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
impl MeshData {
pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: &[u32]) -> Self {
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mesh_vertex_buffer"),
size: std::mem::size_of_val(vertices) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mesh_index_buffer"),
size: std::mem::size_of_val(indices) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
}
}
pub fn upload(&self, queue: &wgpu::Queue, vertices: &[Vertex], indices: &[u32]) {
queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(vertices));
queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(indices));
}
}
#[cfg(test)]
mod tests {
fn project_point(matrix: [[f32; 4]; 4], point: [f32; 3]) -> [f32; 4] {
let v = [point[0], point[1], point[2], 1.0];
let mut out = [0.0; 4];
for j in 0..4 {
for i in 0..4 {
out[j] += matrix[i][j] * v[i];
}
}
out
}
#[test]
fn test_projection_aspect_ratio() {
let aspect = 2.0;
let fovy = 90.0; let proj = crate::math::projection::perspective_degrees(fovy, aspect, 0.1, 100.0);
assert!((proj[0][0] - 0.5).abs() < 1e-6);
assert!((proj[1][1] - 1.0).abs() < 1e-6);
let v = [1.0, 1.0, -10.0, 1.0];
let mut res = [0.0; 4];
for j in 0..4 {
for i in 0..4 {
res[j] += proj[i][j] * v[i];
}
}
let ndc_x = res[0] / res[3];
let ndc_y = res[1] / res[3];
assert!((ndc_x * 2.0 - ndc_y).abs() < 1e-6);
}
#[test]
fn test_square_stays_square_on_wide_viewport() {
let width = 800.0;
let height = 600.0;
let proj = crate::math::projection::perspective_degrees(45.0, width / height, 0.1, 1000.0);
let corners = [
[-1.0, -1.0, -5.0],
[1.0, -1.0, -5.0],
[1.0, 1.0, -5.0],
[-1.0, 1.0, -5.0],
];
let mut screen = [[0.0; 2]; 4];
for (index, corner) in corners.iter().enumerate() {
let projected = project_point(proj, *corner);
let ndc_x = projected[0] / projected[3];
let ndc_y = projected[1] / projected[3];
screen[index] = [
(ndc_x * 0.5 + 0.5) * width,
(1.0 - (ndc_y * 0.5 + 0.5)) * height,
];
}
let screen_width = screen[1][0] - screen[0][0];
let screen_height = screen[0][1] - screen[3][1];
assert!(
(screen_width - screen_height).abs() < 1e-4,
"expected projected square to stay square, got width={} height={}",
screen_width,
screen_height
);
}
}