use std::collections::HashMap;
use std::num::NonZeroU64;
use egui::Color32;
use egui_wgpu::{RenderState, wgpu};
use crate::core::scene3d::camera::Camera;
use crate::core::scene3d::mat4::{Mat4, Vec3};
pub type Scene3dId = u64;
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Scene3dVertex {
pub pos: [f32; 3],
pub color: [f32; 4],
}
const SCENE3D_VERTEX_ATTRS: [wgpu::VertexAttribute; 2] = [
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 12,
shader_location: 1,
},
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum PointMarker {
#[default]
Circle,
Diamond,
Square,
Plus,
Cross,
Asterisk,
HLine,
VLine,
}
impl PointMarker {
pub fn id(self) -> u32 {
match self {
PointMarker::Circle => 0,
PointMarker::Diamond => 1,
PointMarker::Square => 2,
PointMarker::Plus => 3,
PointMarker::Cross => 4,
PointMarker::Asterisk => 5,
PointMarker::HLine => 6,
PointMarker::VLine => 7,
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Scene3dPoint {
pub pos: [f32; 3],
pub color: [f32; 4],
pub size: f32,
pub marker: u32,
}
const SCENE3D_POINT_ATTRS: [wgpu::VertexAttribute; 4] = [
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 12,
shader_location: 1,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32,
offset: 28,
shader_location: 2,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Uint32,
offset: 32,
shader_location: 3,
},
];
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Scene3dMeshVertex {
pub pos: [f32; 3],
pub color: [f32; 4],
pub normal: [f32; 3],
}
const SCENE3D_MESH_ATTRS: [wgpu::VertexAttribute; 3] = [
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 12,
shader_location: 1,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 28,
shader_location: 2,
},
];
#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Scene3dImageVertex {
pos: [f32; 3],
uv: [f32; 2],
}
const SCENE3D_IMAGE_ATTRS: [wgpu::VertexAttribute; 2] = [
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 12,
shader_location: 1,
},
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ImageInterpolation {
#[default]
Nearest,
Linear,
}
#[derive(Clone, Debug)]
pub struct Scene3dImageLayer {
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
pub origin: [f32; 3],
pub scale: [f32; 2],
pub interpolation: ImageInterpolation,
}
#[derive(Clone, Debug)]
pub struct Scene3dTexturedMesh {
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
pub vertices: Vec<[f32; 3]>,
pub uvs: Vec<[f32; 2]>,
pub interpolation: ImageInterpolation,
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Scene3dParams {
mvp: [[f32; 4]; 4],
fog_info: [f32; 4],
fog_color: [f32; 4],
view_row_z: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Scene3dMeshParams {
mvp: [[f32; 4]; 4],
normal_mat: [[f32; 4]; 4],
fog_info: [f32; 4],
fog_color: [f32; 4],
light: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Scene3dPointParams {
mvp: [[f32; 4]; 4],
fog_info: [f32; 4],
fog_color: [f32; 4],
view_row_z: [f32; 4],
viewport: [f32; 2],
_pad: [f32; 2],
}
#[derive(Clone, Debug, Default)]
pub struct Scene3dGeometry {
pub(crate) lines: Vec<Scene3dVertex>,
pub(crate) triangles: Vec<Scene3dVertex>,
pub(crate) points: Vec<Scene3dPoint>,
pub(crate) meshes: Vec<Scene3dMeshVertex>,
pub(crate) images: Vec<Scene3dImageLayer>,
pub(crate) textured_meshes: Vec<Scene3dTexturedMesh>,
pub(crate) line_pick_anchors: Vec<[f32; 3]>,
}
impl Scene3dGeometry {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
&& self.triangles.is_empty()
&& self.points.is_empty()
&& self.meshes.is_empty()
&& self.images.is_empty()
&& self.textured_meshes.is_empty()
&& self.line_pick_anchors.is_empty()
}
pub fn clear(&mut self) {
self.lines.clear();
self.triangles.clear();
self.points.clear();
self.meshes.clear();
self.images.clear();
self.textured_meshes.clear();
self.line_pick_anchors.clear();
}
pub fn add_image_layer(&mut self, layer: Scene3dImageLayer) {
self.images.push(layer);
}
pub fn add_textured_mesh(&mut self, mesh: Scene3dTexturedMesh) {
self.textured_meshes.push(mesh);
}
pub fn extend_from(&mut self, other: &Scene3dGeometry) {
self.lines.extend_from_slice(&other.lines);
self.triangles.extend_from_slice(&other.triangles);
self.points.extend_from_slice(&other.points);
self.meshes.extend_from_slice(&other.meshes);
self.images.extend_from_slice(&other.images);
self.textured_meshes
.extend_from_slice(&other.textured_meshes);
self.line_pick_anchors
.extend_from_slice(&other.line_pick_anchors);
}
pub fn pick_triangles(&self) -> Vec<[Vec3; 3]> {
let mut out = Vec::with_capacity(self.triangles.len() / 3 + self.meshes.len() / 3);
for tri in self.triangles.chunks_exact(3) {
out.push([
Vec3::from_array(tri[0].pos),
Vec3::from_array(tri[1].pos),
Vec3::from_array(tri[2].pos),
]);
}
for tri in self.meshes.chunks_exact(3) {
out.push([
Vec3::from_array(tri[0].pos),
Vec3::from_array(tri[1].pos),
Vec3::from_array(tri[2].pos),
]);
}
out
}
pub fn pick_points(&self) -> Vec<Vec3> {
self.points
.iter()
.map(|p| Vec3::from_array(p.pos))
.collect()
}
pub fn add_line_pick_anchor(&mut self, pos: [f32; 3]) {
self.line_pick_anchors.push(pos);
}
pub fn apply_transform(&mut self, m: &Mat4) {
let map = |pos: &mut [f32; 3]| {
*pos = m.transform_point(Vec3::from_array(*pos), false).to_array();
};
for v in &mut self.lines {
map(&mut v.pos);
}
for v in &mut self.triangles {
map(&mut v.pos);
}
for p in &mut self.points {
map(&mut p.pos);
}
for a in &mut self.line_pick_anchors {
map(a);
}
let inverse = m.inverse();
for v in &mut self.meshes {
map(&mut v.pos);
if let Some(inv) = &inverse {
let n = v.normal;
let nt = Vec3::new(
inv.rows[0][0] * n[0] + inv.rows[1][0] * n[1] + inv.rows[2][0] * n[2],
inv.rows[0][1] * n[0] + inv.rows[1][1] * n[1] + inv.rows[2][1] * n[2],
inv.rows[0][2] * n[0] + inv.rows[1][2] * n[1] + inv.rows[2][2] * n[2],
);
v.normal = nt.normalized().to_array();
}
}
for mesh in &mut self.textured_meshes {
for v in &mut mesh.vertices {
map(v);
}
}
let diag = axis_aligned_positive_scale(m);
if let Some((sx, sy, _)) = diag {
for layer in &mut self.images {
let origin = m
.transform_point(Vec3::from_array(layer.origin), false)
.to_array();
layer.origin = origin;
layer.scale = [layer.scale[0] * sx, layer.scale[1] * sy];
}
} else {
for layer in std::mem::take(&mut self.images) {
self.textured_meshes
.push(image_layer_to_textured_mesh(&layer, m));
}
}
}
pub fn line_pick_anchors(&self) -> &[[f32; 3]] {
&self.line_pick_anchors
}
pub fn add_line(&mut self, a: [f32; 3], b: [f32; 3], color: Color32) {
let rgba = egui::Rgba::from(color).to_array();
self.add_line_rgba(a, b, rgba);
}
pub fn add_line_rgba(&mut self, a: [f32; 3], b: [f32; 3], rgba: [f32; 4]) {
self.lines.push(Scene3dVertex {
pos: a,
color: rgba,
});
self.lines.push(Scene3dVertex {
pos: b,
color: rgba,
});
}
pub fn add_line_gradient(
&mut self,
a: [f32; 3],
b: [f32; 3],
rgba_a: [f32; 4],
rgba_b: [f32; 4],
) {
self.lines.push(Scene3dVertex {
pos: a,
color: rgba_a,
});
self.lines.push(Scene3dVertex {
pos: b,
color: rgba_b,
});
}
pub fn add_triangle(&mut self, a: [f32; 3], b: [f32; 3], c: [f32; 3], color: Color32) {
let rgba = egui::Rgba::from(color).to_array();
self.add_triangle_rgba(a, b, c, rgba);
}
pub fn add_triangle_rgba(&mut self, a: [f32; 3], b: [f32; 3], c: [f32; 3], rgba: [f32; 4]) {
for pos in [a, b, c] {
self.triangles.push(Scene3dVertex { pos, color: rgba });
}
}
pub fn add_point(&mut self, pos: [f32; 3], color: Color32, size: f32, marker: PointMarker) {
let rgba = egui::Rgba::from(color).to_array();
self.add_point_rgba(pos, rgba, size, marker);
}
pub fn add_point_rgba(
&mut self,
pos: [f32; 3],
rgba: [f32; 4],
size: f32,
marker: PointMarker,
) {
self.points.push(Scene3dPoint {
pos,
color: rgba,
size,
marker: marker.id(),
});
}
pub fn add_mesh_triangle_rgba(
&mut self,
positions: [[f32; 3]; 3],
rgba: [[f32; 4]; 3],
normals: [[f32; 3]; 3],
) {
for i in 0..3 {
self.meshes.push(Scene3dMeshVertex {
pos: positions[i],
color: rgba[i],
normal: normals[i],
});
}
}
pub fn add_mesh_triangle(
&mut self,
positions: [[f32; 3]; 3],
color: Color32,
normals: [[f32; 3]; 3],
) {
let rgba = egui::Rgba::from(color).to_array();
self.add_mesh_triangle_rgba(positions, [rgba; 3], normals);
}
pub fn add_mesh_triangle_flat(
&mut self,
a: [f32; 3],
b: [f32; 3],
c: [f32; 3],
color: Color32,
) {
let n = flat_normal(a, b, c);
self.add_mesh_triangle([a, b, c], color, [n; 3]);
}
pub fn add_bounding_box_with_axes(&mut self, bounds: (Vec3, Vec3), box_color: Color32) {
let (min, max) = bounds;
let size = max - min;
let v = |ux: f32, uy: f32, uz: f32| {
[
min.x + size.x * ux,
min.y + size.y * uy,
min.z + size.z * uz,
]
};
let verts = [
v(0.0, 0.0, 0.0), v(1.0, 0.0, 0.0), v(0.0, 0.0, 0.0), v(0.0, 1.0, 0.0), v(0.0, 0.0, 0.0), v(0.0, 0.0, 1.0), v(1.0, 0.0, 0.0), v(1.0, 1.0, 0.0), v(0.0, 1.0, 0.0), v(0.0, 0.0, 1.0), v(1.0, 0.0, 1.0), v(1.0, 1.0, 1.0), v(0.0, 1.0, 1.0), ];
self.add_line(verts[0], verts[1], Color32::from_rgb(255, 0, 0));
self.add_line(verts[2], verts[3], Color32::from_rgb(0, 255, 0));
self.add_line(verts[4], verts[5], Color32::from_rgb(0, 0, 255));
const BOX_EDGES: [(usize, usize); 9] = [
(6, 7),
(7, 8),
(6, 10),
(7, 11),
(8, 12),
(9, 10),
(10, 11),
(11, 12),
(12, 9),
];
for &(a, b) in &BOX_EDGES {
self.add_line(verts[a], verts[b], box_color);
}
}
}
struct Scene3dPipeline {
target_format: wgpu::TextureFormat,
scene_bgl: wgpu::BindGroupLayout,
line_pipeline: wgpu::RenderPipeline,
tri_pipeline: wgpu::RenderPipeline,
point_bgl: wgpu::BindGroupLayout,
point_pipeline: wgpu::RenderPipeline,
mesh_bgl: wgpu::BindGroupLayout,
mesh_pipeline: wgpu::RenderPipeline,
image_tex_bgl: wgpu::BindGroupLayout,
image_pipeline: wgpu::RenderPipeline,
image_sampler_nearest: wgpu::Sampler,
image_sampler_linear: wgpu::Sampler,
blit_bgl: wgpu::BindGroupLayout,
blit_pipeline: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
}
impl Scene3dPipeline {
fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
let scene_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot scene3d"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d.wgsl").into()),
});
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot scene3d blit"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d_blit.wgsl").into()),
});
let point_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot scene3d points"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d_points.wgsl").into()),
});
let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot scene3d mesh"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d_mesh.wgsl").into()),
});
let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot scene3d image"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d_image.wgsl").into()),
});
let scene_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot scene3d scene bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<Scene3dParams>() as u64),
},
count: None,
}],
});
let scene_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot scene3d scene layout"),
bind_group_layouts: &[Some(&scene_bgl)],
immediate_size: 0,
});
let vertex_buffers = [wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Scene3dVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &SCENE3D_VERTEX_ATTRS,
}];
let make_scene_pipeline = |topology: wgpu::PrimitiveTopology, label: &str| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&scene_layout),
vertex: wgpu::VertexState {
module: &scene_shader,
entry_point: Some("vs_main"),
buffers: &vertex_buffers,
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &scene_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
};
let line_pipeline =
make_scene_pipeline(wgpu::PrimitiveTopology::LineList, "rsplot scene3d lines");
let tri_pipeline = make_scene_pipeline(
wgpu::PrimitiveTopology::TriangleList,
"rsplot scene3d triangles",
);
let point_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot scene3d point bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<Scene3dPointParams>() as u64
),
},
count: None,
}],
});
let point_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot scene3d point layout"),
bind_group_layouts: &[Some(&point_bgl)],
immediate_size: 0,
});
let point_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot scene3d points"),
layout: Some(&point_layout),
vertex: wgpu::VertexState {
module: &point_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Scene3dPoint>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &SCENE3D_POINT_ATTRS,
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &point_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let mesh_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot scene3d mesh bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<Scene3dMeshParams>() as u64
),
},
count: None,
}],
});
let mesh_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot scene3d mesh layout"),
bind_group_layouts: &[Some(&mesh_bgl)],
immediate_size: 0,
});
let mesh_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot scene3d mesh"),
layout: Some(&mesh_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Scene3dMeshVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &SCENE3D_MESH_ATTRS,
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let image_tex_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot scene3d image tex bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
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: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let image_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot scene3d image layout"),
bind_group_layouts: &[Some(&scene_bgl), Some(&image_tex_bgl)],
immediate_size: 0,
});
let image_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot scene3d image"),
layout: Some(&image_layout),
vertex: wgpu::VertexState {
module: &image_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Scene3dImageVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &SCENE3D_IMAGE_ATTRS,
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &image_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let image_sampler_nearest = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("rsplot scene3d image sampler (nearest)"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let image_sampler_linear = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("rsplot scene3d image sampler (linear)"),
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::Nearest,
..Default::default()
});
let blit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot scene3d blit bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
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: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let blit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot scene3d blit layout"),
bind_group_layouts: &[Some(&blit_bgl)],
immediate_size: 0,
});
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot scene3d blit pipeline"),
layout: Some(&blit_layout),
vertex: wgpu::VertexState {
module: &blit_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blit_shader,
entry_point: Some("fs_main"),
targets: &[Some(target_format.into())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("rsplot scene3d blit 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::Nearest,
..Default::default()
});
Self {
target_format,
scene_bgl,
line_pipeline,
tri_pipeline,
point_bgl,
point_pipeline,
mesh_bgl,
mesh_pipeline,
image_tex_bgl,
image_pipeline,
image_sampler_nearest,
image_sampler_linear,
blit_bgl,
blit_pipeline,
sampler,
}
}
}
struct Scene3dImageGpu {
vbuf: wgpu::Buffer,
bind_group: wgpu::BindGroup,
vertex_count: u32,
}
struct Scene3dGpu {
params_buf: wgpu::Buffer,
scene_bind_group: wgpu::BindGroup,
line_vbuf: Option<wgpu::Buffer>,
line_count: u32,
tri_vbuf: Option<wgpu::Buffer>,
tri_count: u32,
point_params_buf: wgpu::Buffer,
point_bind_group: wgpu::BindGroup,
point_vbuf: Option<wgpu::Buffer>,
point_count: u32,
mesh_params_buf: wgpu::Buffer,
mesh_bind_group: wgpu::BindGroup,
mesh_vbuf: Option<wgpu::Buffer>,
mesh_count: u32,
images: Vec<Scene3dImageGpu>,
size: [u32; 2],
color_view: Option<wgpu::TextureView>,
depth_view: Option<wgpu::TextureView>,
blit_bind_group: Option<wgpu::BindGroup>,
}
impl Scene3dGpu {
fn new(device: &wgpu::Device, pipeline: &Scene3dPipeline) -> Self {
let params_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot scene3d params"),
size: std::mem::size_of::<Scene3dParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot scene3d scene bind group"),
layout: &pipeline.scene_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: params_buf.as_entire_binding(),
}],
});
let point_params_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot scene3d point params"),
size: std::mem::size_of::<Scene3dPointParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let point_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot scene3d point bind group"),
layout: &pipeline.point_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: point_params_buf.as_entire_binding(),
}],
});
let mesh_params_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot scene3d mesh params"),
size: std::mem::size_of::<Scene3dMeshParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mesh_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot scene3d mesh bind group"),
layout: &pipeline.mesh_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: mesh_params_buf.as_entire_binding(),
}],
});
Self {
params_buf,
scene_bind_group,
line_vbuf: None,
line_count: 0,
tri_vbuf: None,
tri_count: 0,
point_params_buf,
point_bind_group,
point_vbuf: None,
point_count: 0,
mesh_params_buf,
mesh_bind_group,
mesh_vbuf: None,
mesh_count: 0,
images: Vec::new(),
size: [0, 0],
color_view: None,
depth_view: None,
blit_bind_group: None,
}
}
fn upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &Scene3dPipeline,
geometry: &Scene3dGeometry,
) {
self.line_vbuf = make_vertex_buffer(device, queue, &geometry.lines, "rsplot scene3d lines");
self.line_count = geometry.lines.len() as u32;
self.tri_vbuf =
make_vertex_buffer(device, queue, &geometry.triangles, "rsplot scene3d tris");
self.tri_count = geometry.triangles.len() as u32;
self.point_vbuf =
make_vertex_buffer(device, queue, &geometry.points, "rsplot scene3d points");
self.point_count = geometry.points.len() as u32;
self.mesh_vbuf =
make_vertex_buffer(device, queue, &geometry.meshes, "rsplot scene3d meshes");
self.mesh_count = geometry.meshes.len() as u32;
self.images = geometry
.images
.iter()
.filter_map(|layer| build_image_gpu(device, queue, pipeline, layer))
.chain(
geometry
.textured_meshes
.iter()
.filter_map(|mesh| build_textured_mesh_gpu(device, queue, pipeline, mesh)),
)
.collect();
}
fn ensure_offscreen(
&mut self,
device: &wgpu::Device,
pipeline: &Scene3dPipeline,
size: [u32; 2],
) {
let size = [size[0].max(1), size[1].max(1)];
if self.size == size && self.color_view.is_some() {
return;
}
let extent = wgpu::Extent3d {
width: size[0],
height: size[1],
depth_or_array_layers: 1,
};
let color = device.create_texture(&wgpu::TextureDescriptor {
label: Some("rsplot scene3d color"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: pipeline.target_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let depth = device.create_texture(&wgpu::TextureDescriptor {
label: Some("rsplot scene3d depth"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color.create_view(&wgpu::TextureViewDescriptor::default());
let depth_view = depth.create_view(&wgpu::TextureViewDescriptor::default());
let blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot scene3d blit bind group"),
layout: &pipeline.blit_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&color_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
},
],
});
self.size = size;
self.color_view = Some(color_view);
self.depth_view = Some(depth_view);
self.blit_bind_group = Some(blit_bind_group);
}
fn encode_offscreen(
&self,
encoder: &mut wgpu::CommandEncoder,
pipeline: &Scene3dPipeline,
color_view: &wgpu::TextureView,
depth_view: &wgpu::TextureView,
background: [f32; 4],
) {
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rsplot scene3d offscreen pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: background[0] as f64,
g: background[1] as f64,
b: background[2] as f64,
a: background[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rp.set_bind_group(0, &self.scene_bind_group, &[]);
if let (Some(buf), true) = (&self.tri_vbuf, self.tri_count > 0) {
rp.set_pipeline(&pipeline.tri_pipeline);
rp.set_vertex_buffer(0, buf.slice(..));
rp.draw(0..self.tri_count, 0..1);
}
if let (Some(buf), true) = (&self.line_vbuf, self.line_count > 0) {
rp.set_pipeline(&pipeline.line_pipeline);
rp.set_vertex_buffer(0, buf.slice(..));
rp.draw(0..self.line_count, 0..1);
}
if let (Some(buf), true) = (&self.mesh_vbuf, self.mesh_count > 0) {
rp.set_pipeline(&pipeline.mesh_pipeline);
rp.set_bind_group(0, &self.mesh_bind_group, &[]);
rp.set_vertex_buffer(0, buf.slice(..));
rp.draw(0..self.mesh_count, 0..1);
}
if !self.images.is_empty() {
rp.set_pipeline(&pipeline.image_pipeline);
rp.set_bind_group(0, &self.scene_bind_group, &[]);
for image in &self.images {
rp.set_bind_group(1, &image.bind_group, &[]);
rp.set_vertex_buffer(0, image.vbuf.slice(..));
rp.draw(0..image.vertex_count, 0..1);
}
}
if let (Some(buf), true) = (&self.point_vbuf, self.point_count > 0) {
rp.set_pipeline(&pipeline.point_pipeline);
rp.set_bind_group(0, &self.point_bind_group, &[]);
rp.set_vertex_buffer(0, buf.slice(..));
rp.draw(0..6, 0..self.point_count);
}
}
fn encode_overview(
&self,
encoder: &mut wgpu::CommandEncoder,
pipeline: &Scene3dPipeline,
color_view: &wgpu::TextureView,
depth_view: &wgpu::TextureView,
origin: [u32; 2],
) {
fn begin_overview_pass<'e>(
encoder: &'e mut wgpu::CommandEncoder,
label: &'static str,
color_view: &wgpu::TextureView,
depth_view: &wgpu::TextureView,
origin: [u32; 2],
) -> wgpu::RenderPass<'e> {
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rp.set_viewport(
origin[0] as f32,
origin[1] as f32,
OVERVIEW_SIZE_PX as f32,
OVERVIEW_SIZE_PX as f32,
0.0,
1.0,
);
rp.set_scissor_rect(origin[0], origin[1], OVERVIEW_SIZE_PX, OVERVIEW_SIZE_PX);
rp
}
if let (Some(buf), true) = (&self.point_vbuf, self.point_count > 0) {
let mut rp = begin_overview_pass(
encoder,
"rsplot scene3d overview disc pass",
color_view,
depth_view,
origin,
);
rp.set_pipeline(&pipeline.point_pipeline);
rp.set_bind_group(0, &self.point_bind_group, &[]);
rp.set_vertex_buffer(0, buf.slice(..));
rp.draw(0..6, 0..self.point_count);
}
if let (Some(buf), true) = (&self.line_vbuf, self.line_count > 0) {
let mut rp = begin_overview_pass(
encoder,
"rsplot scene3d overview axes pass",
color_view,
depth_view,
origin,
);
rp.set_pipeline(&pipeline.line_pipeline);
rp.set_bind_group(0, &self.scene_bind_group, &[]);
rp.set_vertex_buffer(0, buf.slice(..));
rp.draw(0..self.line_count, 0..1);
}
}
}
pub(crate) fn flat_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
let va = Vec3::new(a[0], a[1], a[2]);
let vb = Vec3::new(b[0], b[1], b[2]);
let vc = Vec3::new(c[0], c[1], c[2]);
(vb - va).cross(vc - va).normalized().to_array()
}
fn make_vertex_buffer<T: bytemuck::Pod>(
device: &wgpu::Device,
queue: &wgpu::Queue,
verts: &[T],
label: &str,
) -> Option<wgpu::Buffer> {
if verts.is_empty() {
return None;
}
let bytes = bytemuck::cast_slice(verts);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: bytes.len() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&buffer, 0, bytes);
Some(buffer)
}
fn build_image_texture_bind_group(
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &Scene3dPipeline,
pixels: &[u8],
width: u32,
height: u32,
interpolation: ImageInterpolation,
) -> Option<wgpu::BindGroup> {
if width == 0 || height == 0 || pixels.len() != (width as usize * height as usize * 4) {
return None;
}
let extent = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("rsplot scene3d image texture"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
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,
},
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
extent,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = match interpolation {
ImageInterpolation::Nearest => &pipeline.image_sampler_nearest,
ImageInterpolation::Linear => &pipeline.image_sampler_linear,
};
Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot scene3d image bind group"),
layout: &pipeline.image_tex_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
}))
}
fn axis_aligned_positive_scale(m: &Mat4) -> Option<(f32, f32, f32)> {
let r = &m.rows;
let off_diagonal_zero = r[0][1] == 0.0
&& r[0][2] == 0.0
&& r[1][0] == 0.0
&& r[1][2] == 0.0
&& r[2][0] == 0.0
&& r[2][1] == 0.0
&& r[3][0] == 0.0
&& r[3][1] == 0.0
&& r[3][2] == 0.0
&& r[3][3] == 1.0;
(off_diagonal_zero && r[0][0] > 0.0 && r[1][1] > 0.0 && r[2][2] > 0.0)
.then_some((r[0][0], r[1][1], r[2][2]))
}
fn image_layer_to_textured_mesh(layer: &Scene3dImageLayer, m: &Mat4) -> Scene3dTexturedMesh {
let [ox, oy, oz] = layer.origin;
let (sx, sy) = (layer.scale[0], layer.scale[1]);
let (x1, y1) = (ox + layer.width as f32 * sx, oy + layer.height as f32 * sy);
let corner = |x: f32, y: f32| m.transform_point(Vec3::new(x, y, oz), false).to_array();
Scene3dTexturedMesh {
pixels: layer.pixels.clone(),
width: layer.width,
height: layer.height,
vertices: vec![
corner(ox, oy),
corner(x1, oy),
corner(x1, y1),
corner(ox, oy),
corner(x1, y1),
corner(ox, y1),
],
uvs: vec![
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
],
interpolation: layer.interpolation,
}
}
fn build_image_gpu(
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &Scene3dPipeline,
layer: &Scene3dImageLayer,
) -> Option<Scene3dImageGpu> {
let (w, h) = (layer.width, layer.height);
let bind_group = build_image_texture_bind_group(
device,
queue,
pipeline,
&layer.pixels,
w,
h,
layer.interpolation,
)?;
let [ox, oy, oz] = layer.origin;
let (sx, sy) = (layer.scale[0], layer.scale[1]);
let (x1, y1) = (ox + w as f32 * sx, oy + h as f32 * sy);
let v = |x: f32, y: f32, u: f32, vv: f32| Scene3dImageVertex {
pos: [x, y, oz],
uv: [u, vv],
};
let verts = [
v(ox, oy, 0.0, 0.0),
v(x1, oy, 1.0, 0.0),
v(x1, y1, 1.0, 1.0),
v(ox, oy, 0.0, 0.0),
v(x1, y1, 1.0, 1.0),
v(ox, y1, 0.0, 1.0),
];
let vbuf = make_vertex_buffer(device, queue, &verts, "rsplot scene3d image quad")?;
Some(Scene3dImageGpu {
vbuf,
bind_group,
vertex_count: 6,
})
}
fn build_textured_mesh_gpu(
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &Scene3dPipeline,
mesh: &Scene3dTexturedMesh,
) -> Option<Scene3dImageGpu> {
if mesh.vertices.is_empty()
|| mesh.vertices.len() != mesh.uvs.len()
|| !mesh.vertices.len().is_multiple_of(3)
{
return None;
}
let bind_group = build_image_texture_bind_group(
device,
queue,
pipeline,
&mesh.pixels,
mesh.width,
mesh.height,
mesh.interpolation,
)?;
let verts: Vec<Scene3dImageVertex> = mesh
.vertices
.iter()
.zip(&mesh.uvs)
.map(|(&pos, &uv)| Scene3dImageVertex { pos, uv })
.collect();
let vbuf = make_vertex_buffer(device, queue, &verts, "rsplot scene3d textured mesh")?;
Some(Scene3dImageGpu {
vbuf,
bind_group,
vertex_count: verts.len() as u32,
})
}
pub struct Scene3dResources {
pipeline: Scene3dPipeline,
scenes: HashMap<Scene3dId, Scene3dGpu>,
}
impl Scene3dResources {
fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
Self {
pipeline: Scene3dPipeline::new(device, target_format),
scenes: HashMap::new(),
}
}
fn prepare_scene(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
frame: &Scene3dFrame,
) {
let Self { pipeline, scenes } = self;
scenes
.entry(frame.id)
.or_insert_with(|| Scene3dGpu::new(device, pipeline))
.ensure_offscreen(device, pipeline, frame.size_px);
let scene = &scenes[&frame.id];
frame.write_uniforms(queue, scene);
if let (Some(color_view), Some(depth_view)) =
(scene.color_view.as_ref(), scene.depth_view.as_ref())
{
scene.encode_offscreen(encoder, pipeline, color_view, depth_view, frame.background);
if let Some((ov, ov_scene, origin)) = overview_pass(scenes, frame) {
ov.write_uniforms(queue, ov_scene);
ov_scene.encode_overview(encoder, pipeline, color_view, depth_view, origin);
}
}
}
fn snapshot_scene(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
frame: &Scene3dFrame,
) -> Option<Vec<u8>> {
use crate::render::save::{padded_bytes_per_row, rows_to_rgba8};
let Self { pipeline, scenes } = self;
let scene = scenes.get(&frame.id)?;
let (w, h) = (frame.size_px[0].max(1), frame.size_px[1].max(1));
frame.write_uniforms(queue, scene);
let extent = wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
};
let color = device.create_texture(&wgpu::TextureDescriptor {
label: Some("rsplot scene3d snapshot color"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: pipeline.target_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let depth = device.create_texture(&wgpu::TextureDescriptor {
label: Some("rsplot scene3d snapshot depth"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color.create_view(&wgpu::TextureViewDescriptor::default());
let depth_view = depth.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("rsplot scene3d snapshot"),
});
scene.encode_offscreen(
&mut encoder,
pipeline,
&color_view,
&depth_view,
frame.background,
);
if let Some((ov, ov_scene, origin)) = overview_pass(scenes, frame) {
ov.write_uniforms(queue, ov_scene);
ov_scene.encode_overview(&mut encoder, pipeline, &color_view, &depth_view, origin);
}
let bpr = padded_bytes_per_row(w);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot scene3d snapshot readback"),
size: (bpr as u64) * (h as u64),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bpr),
rows_per_image: Some(h),
},
},
extent,
);
queue.submit([encoder.finish()]);
let (tx, rx) = std::sync::mpsc::channel();
buffer.slice(..).map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
rx.recv().ok()?.ok()?;
let rgba = {
let mapped = buffer.slice(..).get_mapped_range();
rows_to_rgba8(&mapped, w, h, bpr, pipeline.target_format)
};
buffer.unmap();
Some(rgba)
}
}
fn overview_pass<'s>(
scenes: &'s HashMap<Scene3dId, Scene3dGpu>,
frame: &Scene3dFrame,
) -> Option<(Scene3dOverviewFrame, &'s Scene3dGpu, [u32; 2])> {
let ov = frame.overview?;
if frame.size_px[0] < OVERVIEW_SIZE_PX || frame.size_px[1] < OVERVIEW_SIZE_PX {
return None;
}
let ov_scene = scenes.get(&ov.id)?;
Some((ov, ov_scene, [frame.size_px[0] - OVERVIEW_SIZE_PX, 0]))
}
pub fn install_scene3d(render_state: &RenderState) {
let mut renderer = render_state.renderer.write();
if renderer
.callback_resources
.get::<Scene3dResources>()
.is_some()
{
return;
}
let resources = Scene3dResources::new(&render_state.device, render_state.target_format);
renderer.callback_resources.insert(resources);
}
pub fn set_scene3d(render_state: &RenderState, id: Scene3dId, geometry: &Scene3dGeometry) {
let mut renderer = render_state.renderer.write();
let res: &mut Scene3dResources = renderer
.callback_resources
.get_mut()
.expect("Scene3dResources not installed — call rsplot::install_scene3d() first");
let Scene3dResources { pipeline, scenes } = res;
let scene = scenes
.entry(id)
.or_insert_with(|| Scene3dGpu::new(&render_state.device, pipeline));
scene.upload(
&render_state.device,
&render_state.queue,
pipeline,
geometry,
);
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Scene3dFog {
pub scale: f32,
pub near: f32,
pub color: [f32; 3],
}
impl Scene3dFog {
pub fn linear(camera: &Camera, bounds: (Vec3, Vec3), background: Color32) -> Self {
let view = camera.extrinsic.matrix();
let (mn, mx) = bounds;
let mut far = f32::INFINITY; let mut near = f32::NEG_INFINITY;
for corner in [
Vec3::new(mn.x, mn.y, mn.z),
Vec3::new(mx.x, mn.y, mn.z),
Vec3::new(mn.x, mx.y, mn.z),
Vec3::new(mx.x, mx.y, mn.z),
Vec3::new(mn.x, mn.y, mx.z),
Vec3::new(mx.x, mn.y, mx.z),
Vec3::new(mn.x, mx.y, mx.z),
Vec3::new(mx.x, mx.y, mx.z),
] {
let z = view.transform_point(corner, false).z;
far = far.min(z);
near = near.max(z);
}
let extent = far - near;
let scale = if extent != 0.0 { 0.9 / extent } else { 0.0 };
let rgba = egui::Rgba::from(background);
Scene3dFog {
scale,
near,
color: [rgba.r(), rgba.g(), rgba.b()],
}
}
pub fn factor_at(&self, cam_z: f32) -> f32 {
(self.scale * (cam_z - self.near)).clamp(0.0, 1.0)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Scene3dShading {
pub fog: Option<Scene3dFog>,
pub shininess: f32,
}
fn build_frame(
id: Scene3dId,
camera: &Camera,
background: Color32,
size_px: [u32; 2],
shading: Scene3dShading,
overview: Option<Scene3dId>,
) -> Scene3dFrame {
let mut cam = *camera;
cam.set_size((size_px[0] as f32, size_px[1] as f32));
let mvp = cam.matrix().to_gpu_clip_cols();
let view_mat = cam.extrinsic.matrix();
let view = view_mat.to_gpu_cols();
let view_row_z = view_mat.rows[2];
let fog = shading.fog.unwrap_or_default();
Scene3dFrame {
id,
mvp,
view,
view_row_z,
size_px,
background: egui::Rgba::from(background).to_array(),
fog_info: [
fog.scale,
fog.near,
if shading.fog.is_some() { 1.0 } else { 0.0 },
0.0,
],
fog_color: [fog.color[0], fog.color[1], fog.color[2], 0.0],
shininess: shading.shininess,
overview: overview.map(|ov_id| build_overview_frame(ov_id, &cam)),
}
}
pub fn paint_scene3d(
ui: &mut egui::Ui,
rect: egui::Rect,
id: Scene3dId,
camera: &Camera,
background: Color32,
) {
paint_scene3d_with(
ui,
rect,
id,
camera,
background,
Scene3dShading::default(),
None,
);
}
pub fn paint_scene3d_with(
ui: &mut egui::Ui,
rect: egui::Rect,
id: Scene3dId,
camera: &Camera,
background: Color32,
shading: Scene3dShading,
overview: Option<Scene3dId>,
) {
let ppp = ui.ctx().pixels_per_point();
let w = (rect.width() * ppp).round().max(1.0) as u32;
let h = (rect.height() * ppp).round().max(1.0) as u32;
let frame = build_frame(id, camera, background, [w, h], shading, overview);
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect,
Scene3dCallback { frame },
));
}
pub fn snapshot_scene3d(
render_state: &RenderState,
id: Scene3dId,
camera: &Camera,
background: Color32,
size_px: (u32, u32),
) -> Option<Vec<u8>> {
snapshot_scene3d_with(
render_state,
id,
camera,
background,
size_px,
Scene3dShading::default(),
None,
)
}
pub fn snapshot_scene3d_with(
render_state: &RenderState,
id: Scene3dId,
camera: &Camera,
background: Color32,
size_px: (u32, u32),
shading: Scene3dShading,
overview: Option<Scene3dId>,
) -> Option<Vec<u8>> {
let (w, h) = (size_px.0.max(1), size_px.1.max(1));
let frame = build_frame(id, camera, background, [w, h], shading, overview);
let renderer = render_state.renderer.read();
let res: &Scene3dResources = renderer.callback_resources.get()?;
res.snapshot_scene(&render_state.device, &render_state.queue, &frame)
}
#[derive(Clone, Copy)]
struct Scene3dFrame {
id: Scene3dId,
mvp: [[f32; 4]; 4],
view: [[f32; 4]; 4],
view_row_z: [f32; 4],
size_px: [u32; 2],
background: [f32; 4],
fog_info: [f32; 4],
fog_color: [f32; 4],
shininess: f32,
overview: Option<Scene3dOverviewFrame>,
}
impl Scene3dFrame {
fn write_uniforms(&self, queue: &wgpu::Queue, scene: &Scene3dGpu) {
write_scene_uniforms(
queue,
scene,
self.mvp,
self.view,
self.view_row_z,
self.fog_info,
self.fog_color,
self.shininess,
[self.size_px[0] as f32, self.size_px[1] as f32],
);
}
}
#[derive(Clone, Copy)]
struct Scene3dOverviewFrame {
id: Scene3dId,
mvp: [[f32; 4]; 4],
view: [[f32; 4]; 4],
view_row_z: [f32; 4],
}
impl Scene3dOverviewFrame {
fn write_uniforms(&self, queue: &wgpu::Queue, scene: &Scene3dGpu) {
write_scene_uniforms(
queue,
scene,
self.mvp,
self.view,
self.view_row_z,
[0.0; 4],
[0.0; 4],
0.0,
[OVERVIEW_SIZE_PX as f32, OVERVIEW_SIZE_PX as f32],
);
}
}
pub const OVERVIEW_SIZE_PX: u32 = 100;
fn build_overview_frame(id: Scene3dId, main_camera: &Camera) -> Scene3dOverviewFrame {
let direction = main_camera.extrinsic.direction();
let up = main_camera.extrinsic.up();
let camera = Camera::new(
30.0,
1.0,
100.0,
(OVERVIEW_SIZE_PX as f32, OVERVIEW_SIZE_PX as f32),
direction * -12.0,
direction,
up,
);
let view_mat = camera.extrinsic.matrix();
Scene3dOverviewFrame {
id,
mvp: camera.matrix().to_gpu_clip_cols(),
view: view_mat.to_gpu_cols(),
view_row_z: view_mat.rows[2],
}
}
#[expect(
clippy::too_many_arguments,
reason = "flat uniform fields; grouping them would just mirror Scene3dFrame"
)]
fn write_scene_uniforms(
queue: &wgpu::Queue,
scene: &Scene3dGpu,
mvp: [[f32; 4]; 4],
view: [[f32; 4]; 4],
view_row_z: [f32; 4],
fog_info: [f32; 4],
fog_color: [f32; 4],
shininess: f32,
viewport: [f32; 2],
) {
let params = Scene3dParams {
mvp,
fog_info,
fog_color,
view_row_z,
};
queue.write_buffer(&scene.params_buf, 0, bytemuck::bytes_of(¶ms));
let point_params = Scene3dPointParams {
mvp,
fog_info,
fog_color,
view_row_z,
viewport,
_pad: [0.0, 0.0],
};
queue.write_buffer(
&scene.point_params_buf,
0,
bytemuck::bytes_of(&point_params),
);
let mesh_params = Scene3dMeshParams {
mvp,
normal_mat: view,
fog_info,
fog_color,
light: [shininess, 0.0, 0.0, 0.0],
};
queue.write_buffer(&scene.mesh_params_buf, 0, bytemuck::bytes_of(&mesh_params));
}
struct Scene3dCallback {
frame: Scene3dFrame,
}
impl egui_wgpu::CallbackTrait for Scene3dCallback {
fn prepare(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
_screen_descriptor: &egui_wgpu::ScreenDescriptor,
egui_encoder: &mut wgpu::CommandEncoder,
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let res: &mut Scene3dResources = resources
.get_mut()
.expect("Scene3dResources not installed — call rsplot::install_scene3d() at startup");
res.prepare_scene(device, queue, egui_encoder, &self.frame);
Vec::new()
}
fn paint(
&self,
_info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
resources: &egui_wgpu::CallbackResources,
) {
let res: &Scene3dResources = resources
.get()
.expect("Scene3dResources not installed — call rsplot::install_scene3d() at startup");
if let Some(scene) = res.scenes.get(&self.frame.id)
&& let Some(blit_bind_group) = &scene.blit_bind_group
{
render_pass.set_pipeline(&res.pipeline.blit_pipeline);
render_pass.set_bind_group(0, blit_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::scene3d::mat4::{mat4_rotate, mat4_scale, mat4_translate};
#[test]
fn bounding_box_with_axes_has_twelve_lines_and_rgb_axes() {
let mut g = Scene3dGeometry::new();
g.add_bounding_box_with_axes(
(Vec3::ZERO, Vec3::new(2.0, 3.0, 4.0)),
Color32::from_rgb(200, 200, 200),
);
assert_eq!(g.lines.len(), 24);
assert!(g.triangles.is_empty());
assert_eq!(g.lines[0].pos, [0.0, 0.0, 0.0]);
assert_eq!(g.lines[1].pos, [2.0, 0.0, 0.0]);
assert_eq!(g.lines[0].color, egui::Rgba::from(Color32::RED).to_array());
assert_eq!(g.lines[3].pos, [0.0, 3.0, 0.0]);
assert_eq!(
g.lines[2].color,
egui::Rgba::from(Color32::GREEN).to_array()
);
assert_eq!(g.lines[5].pos, [0.0, 0.0, 4.0]);
assert_eq!(g.lines[4].color, egui::Rgba::from(Color32::BLUE).to_array());
let box_rgba = egui::Rgba::from(Color32::from_rgb(200, 200, 200)).to_array();
assert_eq!(g.lines[6].color, box_rgba);
assert!(
g.lines.iter().any(|v| v.pos == [2.0, 3.0, 4.0]),
"the far corner (max) should be a box-edge endpoint"
);
}
#[test]
fn extend_from_forwards_every_channel() {
let mut src = Scene3dGeometry::new();
src.add_line([0.0; 3], [1.0; 3], Color32::WHITE); src.add_triangle([0.0; 3], [1.0; 3], [2.0; 3], Color32::RED); src.add_point([0.0; 3], Color32::GREEN, 4.0, PointMarker::Square); src.add_mesh_triangle_flat([0.0; 3], [1.0; 3], [2.0; 3], Color32::BLUE); src.add_image_layer(Scene3dImageLayer {
pixels: vec![0; 4],
width: 1,
height: 1,
origin: [0.0; 3],
scale: [1.0; 2],
interpolation: ImageInterpolation::Nearest,
});
src.add_textured_mesh(Scene3dTexturedMesh {
pixels: vec![0; 4],
width: 1,
height: 1,
vertices: vec![[0.0; 3], [1.0; 3], [2.0; 3]],
uvs: vec![[0.0; 2], [1.0, 0.0], [1.0; 2]],
interpolation: ImageInterpolation::Nearest,
});
src.add_line_pick_anchor([0.5; 3]);
let mut dst = Scene3dGeometry::new();
assert!(dst.is_empty());
dst.extend_from(&src);
assert_eq!(dst.lines.len(), 2);
assert_eq!(dst.triangles.len(), 3);
assert_eq!(dst.points.len(), 1);
assert_eq!(dst.meshes.len(), 3);
assert_eq!(dst.images.len(), 1);
assert_eq!(dst.textured_meshes.len(), 1);
assert_eq!(dst.line_pick_anchors.len(), 1);
dst.extend_from(&src);
assert_eq!(dst.lines.len(), 4);
assert_eq!(dst.textured_meshes.len(), 2);
assert_eq!(dst.line_pick_anchors.len(), 2);
}
#[test]
fn linear_fog_matches_silx_setup_program() {
let camera = Camera::new(
30.0,
0.1,
100.0,
(100.0, 100.0),
Vec3::new(0.0, 0.0, 5.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
);
let bounds = (Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
let fog = Scene3dFog::linear(&camera, bounds, Color32::from_gray(51));
assert!((fog.scale - (-0.45)).abs() < 1e-5, "scale = {}", fog.scale);
assert!((fog.near - (-4.0)).abs() < 1e-5, "near = {}", fog.near);
assert!(fog.color.iter().all(|&c| c > 0.0 && c < 1.0));
assert_eq!(fog.factor_at(-4.0), 0.0);
assert!((fog.factor_at(-6.0) - 0.9).abs() < 1e-5);
assert!((fog.factor_at(-5.0) - 0.45).abs() < 1e-5);
assert_eq!(fog.factor_at(-3.0), 0.0); assert_eq!(fog.factor_at(-100.0), 1.0);
let flat = (Vec3::new(-1.0, -1.0, 0.0), Vec3::new(1.0, 1.0, 0.0));
let camera_front = Camera::new(
30.0,
0.1,
100.0,
(100.0, 100.0),
Vec3::new(0.0, 0.0, 5.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
);
let fog_flat = Scene3dFog::linear(&camera_front, flat, Color32::BLACK);
assert_eq!(fog_flat.scale, 0.0);
}
#[test]
fn apply_transform_maps_positions_and_inverse_transposes_normals() {
let mut g = Scene3dGeometry::new();
g.add_line([0.0; 3], [1.0, 0.0, 0.0], Color32::WHITE);
g.add_triangle([0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], Color32::RED);
g.add_point([1.0, 2.0, 3.0], Color32::GREEN, 4.0, PointMarker::Circle);
g.add_line_pick_anchor([1.0, 1.0, 0.0]);
let s = std::f32::consts::FRAC_1_SQRT_2;
g.add_mesh_triangle(
[[0.0; 3], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
Color32::BLUE,
[[s, s, 0.0]; 3],
);
g.add_textured_mesh(Scene3dTexturedMesh {
pixels: vec![0; 4],
width: 1,
height: 1,
vertices: vec![[0.0; 3], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
uvs: vec![[0.0; 2], [1.0, 0.0], [1.0; 2]],
interpolation: ImageInterpolation::Nearest,
});
let m = mat4_translate(10.0, 0.0, 0.0) * mat4_scale(2.0, 1.0, 1.0);
g.apply_transform(&m);
assert_eq!(g.lines[0].pos, [10.0, 0.0, 0.0]);
assert_eq!(g.lines[1].pos, [12.0, 0.0, 0.0]);
assert_eq!(g.triangles[2].pos, [10.0, 1.0, 0.0]);
assert_eq!(g.points[0].pos, [12.0, 2.0, 3.0]);
assert_eq!(g.line_pick_anchors[0], [12.0, 1.0, 0.0]);
assert_eq!(g.textured_meshes[0].vertices[2], [12.0, 1.0, 0.0]);
assert_eq!(g.meshes[1].pos, [12.0, 0.0, 0.0]);
let n = g.meshes[0].normal;
let expect = [1.0 / 5.0f32.sqrt(), 2.0 / 5.0f32.sqrt(), 0.0];
for (got, want) in n.iter().zip(expect) {
assert!((got - want).abs() < 1e-5, "normal {n:?}, want {expect:?}");
}
}
#[test]
fn apply_transform_scales_axis_aligned_image_layers_in_place() {
let mut g = Scene3dGeometry::new();
g.add_image_layer(Scene3dImageLayer {
pixels: vec![0; 4],
width: 1,
height: 1,
origin: [1.0, 1.0, 0.0],
scale: [0.5, 0.5],
interpolation: ImageInterpolation::Nearest,
});
let m = mat4_translate(1.0, 2.0, 3.0) * mat4_scale(2.0, 3.0, 4.0);
g.apply_transform(&m);
assert_eq!(g.images.len(), 1);
assert!(g.textured_meshes.is_empty());
assert_eq!(g.images[0].origin, [3.0, 5.0, 3.0]);
assert_eq!(g.images[0].scale, [1.0, 1.5]);
}
#[test]
fn apply_transform_converts_rotated_image_layers_to_textured_quads() {
let mut g = Scene3dGeometry::new();
g.add_image_layer(Scene3dImageLayer {
pixels: vec![0; 2 * 4],
width: 2,
height: 1,
origin: [0.0; 3],
scale: [1.0, 1.0],
interpolation: ImageInterpolation::Linear,
});
let m = mat4_rotate(std::f32::consts::FRAC_PI_2, 0.0, 0.0, 1.0);
g.apply_transform(&m);
assert!(g.images.is_empty());
assert_eq!(g.textured_meshes.len(), 1);
let quad = &g.textured_meshes[0];
assert_eq!(quad.vertices.len(), 6);
assert_eq!(
quad.uvs,
vec![
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 0.0],
[1.0, 1.0],
[0.0, 1.0]
]
);
let far = quad.vertices[2];
assert!((far[0] - (-1.0)).abs() < 1e-5 && (far[1] - 2.0).abs() < 1e-5);
assert_eq!(quad.interpolation, ImageInterpolation::Linear);
}
#[test]
fn overview_frame_slaves_the_camera_orientation() {
let direction = Vec3::new(1.0, 2.0, -2.0) * (1.0 / 3.0); let main = Camera::new(
45.0,
0.5,
2000.0,
(800.0, 600.0),
Vec3::new(5.0, 4.0, 3.0),
direction,
Vec3::new(0.0, 1.0, 0.0),
);
let ov = build_overview_frame(7, &main);
assert_eq!(ov.id, 7);
let expected = Camera::new(
30.0,
1.0,
100.0,
(OVERVIEW_SIZE_PX as f32, OVERVIEW_SIZE_PX as f32),
main.extrinsic.direction() * -12.0,
main.extrinsic.direction(),
main.extrinsic.up(),
);
assert_eq!(ov.mvp, expected.matrix().to_gpu_clip_cols());
assert_eq!(ov.view, expected.extrinsic.matrix().to_gpu_cols());
let origin_ndc = expected.matrix().transform_point(Vec3::ZERO, true);
assert!(origin_ndc.x.abs() < 1e-5 && origin_ndc.y.abs() < 1e-5);
assert!((-1.0..=1.0).contains(&origin_ndc.z));
let up_ndc = expected.matrix().transform_point(main.extrinsic.up(), true);
assert!(up_ndc.y > 0.0 && up_ndc.x.abs() < 1e-5);
}
}