use glam::Vec3;
use wgpu::util::DeviceExt;
use crate::surface_mesh_render::ShadowModelUniforms;
const COLORMAP_RESOLUTION: u32 = 256;
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
#[allow(clippy::pub_underscore_fields)]
pub struct SimpleMeshUniforms {
pub model: [[f32; 4]; 4],
pub base_color: [f32; 4],
pub transparency: f32,
pub slice_planes_enabled: u32,
pub backface_policy: u32,
pub _pad: f32,
}
impl Default for SimpleMeshUniforms {
fn default() -> Self {
Self {
model: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
base_color: [0.047, 0.451, 0.690, 1.0], transparency: 0.0,
slice_planes_enabled: 1,
backface_policy: 0,
_pad: 0.0,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
#[allow(clippy::pub_underscore_fields)]
pub struct GridcubeUniforms {
pub model: [[f32; 4]; 4],
pub cube_size_factor: f32,
pub data_min: f32,
pub data_max: f32,
pub transparency: f32,
pub slice_planes_enabled: u32,
pub _pad0: f32,
pub _pad1: f32,
pub _pad2: f32,
}
impl Default for GridcubeUniforms {
fn default() -> Self {
Self {
model: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
cube_size_factor: 1.0,
data_min: 0.0,
data_max: 1.0,
transparency: 0.0,
slice_planes_enabled: 1,
_pad0: 0.0,
_pad1: 0.0,
_pad2: 0.0,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
#[allow(clippy::pub_underscore_fields)]
pub struct GridcubePickUniforms {
pub model: [[f32; 4]; 4],
pub global_start: u32,
pub cube_size_factor: f32,
pub _pad0: f32,
pub _pad1: f32,
}
impl Default for GridcubePickUniforms {
fn default() -> Self {
Self {
model: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
global_start: 0,
cube_size_factor: 1.0,
_pad0: 0.0,
_pad1: 0.0,
}
}
}
pub struct IsosurfaceRenderData {
pub vertex_buffer: wgpu::Buffer,
pub normal_buffer: wgpu::Buffer,
pub uniform_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
pub num_vertices: u32,
pub shadow_bind_group: Option<wgpu::BindGroup>,
pub shadow_model_buffer: Option<wgpu::Buffer>,
}
impl IsosurfaceRenderData {
#[must_use]
pub fn new(
device: &wgpu::Device,
bind_group_layout: &wgpu::BindGroupLayout,
camera_buffer: &wgpu::Buffer,
vertices: &[Vec3],
normals: &[Vec3],
indices: &[u32],
) -> Self {
let num_triangles = indices.len() / 3;
let num_vertices = (num_triangles * 3) as u32;
let mut expanded_positions: Vec<f32> = Vec::with_capacity(num_triangles * 3 * 4);
let mut expanded_normals: Vec<f32> = Vec::with_capacity(num_triangles * 3 * 4);
for tri_idx in 0..num_triangles {
for v in 0..3 {
let vi = indices[tri_idx * 3 + v] as usize;
let p = vertices[vi];
expanded_positions.extend_from_slice(&[p.x, p.y, p.z, 1.0]);
let n = normals[vi];
expanded_normals.extend_from_slice(&[n.x, n.y, n.z, 0.0]);
}
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("isosurface vertices"),
contents: bytemuck::cast_slice(&expanded_positions),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let normal_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("isosurface normals"),
contents: bytemuck::cast_slice(&expanded_normals),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let uniforms = SimpleMeshUniforms::default();
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("isosurface uniforms"),
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("isosurface bind group"),
layout: bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: camera_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: vertex_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: normal_buffer.as_entire_binding(),
},
],
});
Self {
vertex_buffer,
normal_buffer,
uniform_buffer,
bind_group,
num_vertices,
shadow_bind_group: None,
shadow_model_buffer: None,
}
}
pub fn update_uniforms(&self, queue: &wgpu::Queue, uniforms: &SimpleMeshUniforms) {
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[*uniforms]));
}
pub fn init_shadow_resources(
&mut self,
device: &wgpu::Device,
shadow_bind_group_layout: &wgpu::BindGroupLayout,
light_buffer: &wgpu::Buffer,
) {
let shadow_model_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("isosurface shadow model buffer"),
contents: bytemuck::cast_slice(&[ShadowModelUniforms::default()]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let shadow_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("isosurface shadow bind group"),
layout: shadow_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: light_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: shadow_model_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.vertex_buffer.as_entire_binding(),
},
],
});
self.shadow_model_buffer = Some(shadow_model_buffer);
self.shadow_bind_group = Some(shadow_bind_group);
}
#[must_use]
pub fn has_shadow_resources(&self) -> bool {
self.shadow_bind_group.is_some()
}
pub fn update_shadow_model(&self, queue: &wgpu::Queue, model_matrix: [[f32; 4]; 4]) {
if let Some(buffer) = &self.shadow_model_buffer {
let uniforms = ShadowModelUniforms {
model: model_matrix,
};
queue.write_buffer(buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
}
}
pub struct GridcubeRenderData {
pub position_buffer: wgpu::Buffer,
pub normal_buffer: wgpu::Buffer,
pub scalar_buffer: wgpu::Buffer,
pub uniform_buffer: wgpu::Buffer,
pub colormap_texture: wgpu::Texture,
pub colormap_view: wgpu::TextureView,
pub colormap_sampler: wgpu::Sampler,
pub bind_group: wgpu::BindGroup,
pub num_instances: u32,
pub shadow_bind_group: Option<wgpu::BindGroup>,
pub shadow_model_buffer: Option<wgpu::Buffer>,
}
fn generate_unit_cube() -> (Vec<[f32; 4]>, Vec<[f32; 4]>) {
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
(
[1.0, 0.0, 0.0],
[
[0.5, -0.5, -0.5],
[0.5, 0.5, -0.5],
[0.5, 0.5, 0.5],
[0.5, -0.5, 0.5],
],
),
(
[-1.0, 0.0, 0.0],
[
[-0.5, -0.5, 0.5],
[-0.5, 0.5, 0.5],
[-0.5, 0.5, -0.5],
[-0.5, -0.5, -0.5],
],
),
(
[0.0, 1.0, 0.0],
[
[-0.5, 0.5, -0.5],
[-0.5, 0.5, 0.5],
[0.5, 0.5, 0.5],
[0.5, 0.5, -0.5],
],
),
(
[0.0, -1.0, 0.0],
[
[-0.5, -0.5, 0.5],
[-0.5, -0.5, -0.5],
[0.5, -0.5, -0.5],
[0.5, -0.5, 0.5],
],
),
(
[0.0, 0.0, 1.0],
[
[-0.5, -0.5, 0.5],
[0.5, -0.5, 0.5],
[0.5, 0.5, 0.5],
[-0.5, 0.5, 0.5],
],
),
(
[0.0, 0.0, -1.0],
[
[0.5, -0.5, -0.5],
[-0.5, -0.5, -0.5],
[-0.5, 0.5, -0.5],
[0.5, 0.5, -0.5],
],
),
];
let mut positions = Vec::with_capacity(36);
let mut normals = Vec::with_capacity(36);
for (normal, verts) in &faces {
let tri_indices = [[0, 1, 2], [0, 2, 3]];
for tri in &tri_indices {
for &vi in tri {
let v = verts[vi];
positions.push([v[0], v[1], v[2], 1.0]);
normals.push([normal[0], normal[1], normal[2], 0.0]);
}
}
}
(positions, normals)
}
fn create_colormap_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
colors: &[Vec3],
) -> (wgpu::Texture, wgpu::TextureView, wgpu::Sampler) {
let mut pixel_data: Vec<u8> = Vec::with_capacity(COLORMAP_RESOLUTION as usize * 4);
let n = colors.len();
for i in 0..COLORMAP_RESOLUTION {
let t = i as f32 / (COLORMAP_RESOLUTION - 1) as f32;
let t_clamped = t.clamp(0.0, 1.0);
let color = if n <= 1 {
colors.first().copied().unwrap_or(Vec3::ZERO)
} else {
let segments = n - 1;
let idx = (t_clamped * segments as f32).floor() as usize;
let idx = idx.min(segments - 1);
let frac = t_clamped * segments as f32 - idx as f32;
colors[idx].lerp(colors[idx + 1], frac)
};
pixel_data.push((color.x * 255.0) as u8);
pixel_data.push((color.y * 255.0) as u8);
pixel_data.push((color.z * 255.0) as u8);
pixel_data.push(255); }
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("colormap texture"),
size: wgpu::Extent3d {
width: COLORMAP_RESOLUTION,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D1,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&pixel_data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(COLORMAP_RESOLUTION * 4),
rows_per_image: None,
},
wgpu::Extent3d {
width: COLORMAP_RESOLUTION,
height: 1,
depth_or_array_layers: 1,
},
);
let view = texture.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::D1),
..Default::default()
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("colormap sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
(texture, view, sampler)
}
impl GridcubeRenderData {
#[must_use]
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
bind_group_layout: &wgpu::BindGroupLayout,
camera_buffer: &wgpu::Buffer,
centers: &[Vec3],
half_size: f32,
scalars: &[f32],
colormap_colors: &[Vec3],
) -> Self {
let num_instances = centers.len() as u32;
let (cube_positions, cube_normals) = generate_unit_cube();
let mut position_data: Vec<[f32; 4]> = cube_positions;
for center in centers {
position_data.push([center.x, center.y, center.z, half_size]);
}
let position_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("gridcube positions"),
contents: bytemuck::cast_slice(&position_data),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let normal_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("gridcube normals"),
contents: bytemuck::cast_slice(&cube_normals),
usage: wgpu::BufferUsages::STORAGE,
});
let scalar_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("gridcube scalars"),
contents: bytemuck::cast_slice(scalars),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let uniforms = GridcubeUniforms::default();
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("gridcube uniforms"),
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let (colormap_texture, colormap_view, colormap_sampler) =
create_colormap_texture(device, queue, colormap_colors);
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("gridcube bind group"),
layout: bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: camera_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: position_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: normal_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: scalar_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(&colormap_view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Sampler(&colormap_sampler),
},
],
});
Self {
position_buffer,
normal_buffer,
scalar_buffer,
uniform_buffer,
colormap_texture,
colormap_view,
colormap_sampler,
bind_group,
num_instances,
shadow_bind_group: None,
shadow_model_buffer: None,
}
}
pub fn update_uniforms(&self, queue: &wgpu::Queue, uniforms: &GridcubeUniforms) {
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[*uniforms]));
}
pub fn update_colormap(&self, queue: &wgpu::Queue, colors: &[Vec3]) {
let mut pixel_data: Vec<u8> = Vec::with_capacity(COLORMAP_RESOLUTION as usize * 4);
let n = colors.len();
for i in 0..COLORMAP_RESOLUTION {
let t = i as f32 / (COLORMAP_RESOLUTION - 1) as f32;
let t_clamped = t.clamp(0.0, 1.0);
let color = if n <= 1 {
colors.first().copied().unwrap_or(Vec3::ZERO)
} else {
let segments = n - 1;
let idx = (t_clamped * segments as f32).floor() as usize;
let idx = idx.min(segments - 1);
let frac = t_clamped * segments as f32 - idx as f32;
colors[idx].lerp(colors[idx + 1], frac)
};
pixel_data.push((color.x * 255.0) as u8);
pixel_data.push((color.y * 255.0) as u8);
pixel_data.push((color.z * 255.0) as u8);
pixel_data.push(255);
}
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.colormap_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&pixel_data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(COLORMAP_RESOLUTION * 4),
rows_per_image: None,
},
wgpu::Extent3d {
width: COLORMAP_RESOLUTION,
height: 1,
depth_or_array_layers: 1,
},
);
}
#[must_use]
pub fn total_vertices(&self) -> u32 {
36 * self.num_instances
}
pub fn init_shadow_resources(
&mut self,
device: &wgpu::Device,
shadow_bind_group_layout: &wgpu::BindGroupLayout,
light_buffer: &wgpu::Buffer,
) {
let shadow_model_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("gridcube shadow model buffer"),
contents: bytemuck::cast_slice(&[ShadowModelUniforms::default()]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let shadow_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("gridcube shadow bind group"),
layout: shadow_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: light_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: shadow_model_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.position_buffer.as_entire_binding(),
},
],
});
self.shadow_model_buffer = Some(shadow_model_buffer);
self.shadow_bind_group = Some(shadow_bind_group);
}
#[must_use]
pub fn has_shadow_resources(&self) -> bool {
self.shadow_bind_group.is_some()
}
pub fn update_shadow_model(&self, queue: &wgpu::Queue, model_matrix: [[f32; 4]; 4]) {
if let Some(buffer) = &self.shadow_model_buffer {
let uniforms = ShadowModelUniforms {
model: model_matrix,
};
queue.write_buffer(buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_mesh_uniforms_size() {
let size = std::mem::size_of::<SimpleMeshUniforms>();
assert_eq!(
size % 16,
0,
"SimpleMeshUniforms size ({size} bytes) must be 16-byte aligned"
);
assert_eq!(
size, 96,
"SimpleMeshUniforms should be 96 bytes, got {size}"
);
}
#[test]
fn test_gridcube_pick_uniforms_size() {
let size = std::mem::size_of::<GridcubePickUniforms>();
assert_eq!(
size % 16,
0,
"GridcubePickUniforms size ({size} bytes) must be 16-byte aligned"
);
assert_eq!(
size, 80,
"GridcubePickUniforms should be 80 bytes, got {size}"
);
}
#[test]
fn test_gridcube_uniforms_size() {
let size = std::mem::size_of::<GridcubeUniforms>();
assert_eq!(
size % 16,
0,
"GridcubeUniforms size ({size} bytes) must be 16-byte aligned"
);
assert_eq!(size, 96, "GridcubeUniforms should be 96 bytes, got {size}");
}
#[test]
fn test_unit_cube_generation() {
let (positions, normals) = generate_unit_cube();
assert_eq!(positions.len(), 36);
assert_eq!(normals.len(), 36);
for p in &positions {
assert!(p[0].abs() <= 0.5 + f32::EPSILON);
assert!(p[1].abs() <= 0.5 + f32::EPSILON);
assert!(p[2].abs() <= 0.5 + f32::EPSILON);
assert!((p[3] - 1.0).abs() < f32::EPSILON); }
for n in &normals {
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((len - 1.0).abs() < 0.01);
assert!((n[3]).abs() < f32::EPSILON); }
}
}