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::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],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Scene3dMeshParams {
mvp: [[f32; 4]; 4],
normal_mat: [[f32; 4]; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Scene3dPointParams {
mvp: [[f32; 4]; 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>,
}
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()
}
pub fn clear(&mut self) {
self.lines.clear();
self.triangles.clear();
self.points.clear();
self.meshes.clear();
self.images.clear();
self.textured_meshes.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);
}
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(&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("siplot scene3d"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d.wgsl").into()),
});
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("siplot scene3d blit"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d_blit.wgsl").into()),
});
let point_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("siplot scene3d points"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d_points.wgsl").into()),
});
let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("siplot scene3d mesh"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/scene3d_mesh.wgsl").into()),
});
let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("siplot 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("siplot scene3d scene bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(64),
},
count: None,
}],
});
let scene_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("siplot 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(target_format.into())],
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::Less),
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, "siplot scene3d lines");
let tri_pipeline = make_scene_pipeline(
wgpu::PrimitiveTopology::TriangleList,
"siplot scene3d triangles",
);
let point_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("siplot scene3d point bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
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("siplot scene3d point layout"),
bind_group_layouts: &[Some(&point_bgl)],
immediate_size: 0,
});
let point_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("siplot 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::Less),
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("siplot scene3d mesh bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
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("siplot scene3d mesh layout"),
bind_group_layouts: &[Some(&mesh_bgl)],
immediate_size: 0,
});
let mesh_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("siplot 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(target_format.into())],
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::Less),
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("siplot 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("siplot 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("siplot 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::Less),
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("siplot 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("siplot 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("siplot 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("siplot scene3d blit layout"),
bind_group_layouts: &[Some(&blit_bgl)],
immediate_size: 0,
});
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("siplot 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("siplot 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("siplot 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("siplot 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("siplot 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("siplot 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("siplot 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("siplot 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, "siplot scene3d lines");
self.line_count = geometry.lines.len() as u32;
self.tri_vbuf =
make_vertex_buffer(device, queue, &geometry.triangles, "siplot scene3d tris");
self.tri_count = geometry.triangles.len() as u32;
self.point_vbuf =
make_vertex_buffer(device, queue, &geometry.points, "siplot scene3d points");
self.point_count = geometry.points.len() as u32;
self.mesh_vbuf =
make_vertex_buffer(device, queue, &geometry.meshes, "siplot 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("siplot 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("siplot 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("siplot 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("siplot 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);
}
}
}
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("siplot 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("siplot 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 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, "siplot 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, "siplot 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;
let scene = scenes
.entry(frame.id)
.or_insert_with(|| Scene3dGpu::new(device, pipeline));
scene.ensure_offscreen(device, pipeline, frame.size_px);
let params = Scene3dParams { mvp: frame.mvp };
queue.write_buffer(&scene.params_buf, 0, bytemuck::bytes_of(¶ms));
let point_params = Scene3dPointParams {
mvp: frame.mvp,
viewport: [frame.size_px[0] as f32, frame.size_px[1] as f32],
_pad: [0.0, 0.0],
};
queue.write_buffer(
&scene.point_params_buf,
0,
bytemuck::bytes_of(&point_params),
);
let mesh_params = Scene3dMeshParams {
mvp: frame.mvp,
normal_mat: frame.view,
};
queue.write_buffer(&scene.mesh_params_buf, 0, bytemuck::bytes_of(&mesh_params));
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);
}
}
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));
let params = Scene3dParams { mvp: frame.mvp };
queue.write_buffer(&scene.params_buf, 0, bytemuck::bytes_of(¶ms));
let point_params = Scene3dPointParams {
mvp: frame.mvp,
viewport: [w as f32, h as f32],
_pad: [0.0, 0.0],
};
queue.write_buffer(
&scene.point_params_buf,
0,
bytemuck::bytes_of(&point_params),
);
let mesh_params = Scene3dMeshParams {
mvp: frame.mvp,
normal_mat: frame.view,
};
queue.write_buffer(&scene.mesh_params_buf, 0, bytemuck::bytes_of(&mesh_params));
let extent = wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
};
let color = device.create_texture(&wgpu::TextureDescriptor {
label: Some("siplot 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("siplot 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("siplot scene3d snapshot"),
});
scene.encode_offscreen(
&mut encoder,
pipeline,
&color_view,
&depth_view,
frame.background,
);
let bpr = padded_bytes_per_row(w);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("siplot 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)
}
}
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 siplot::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,
);
}
pub fn paint_scene3d(
ui: &mut egui::Ui,
rect: egui::Rect,
id: Scene3dId,
camera: &Camera,
background: Color32,
) {
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 mut cam = *camera;
cam.set_size((w as f32, h as f32));
let mvp = cam.matrix().to_gpu_clip_cols();
let view = cam.extrinsic.matrix().to_gpu_cols();
let background = egui::Rgba::from(background).to_array();
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect,
Scene3dCallback {
frame: Scene3dFrame {
id,
mvp,
view,
size_px: [w, h],
background,
},
},
));
}
pub fn snapshot_scene3d(
render_state: &RenderState,
id: Scene3dId,
camera: &Camera,
background: Color32,
size_px: (u32, u32),
) -> Option<Vec<u8>> {
let (w, h) = (size_px.0.max(1), size_px.1.max(1));
let mut cam = *camera;
cam.set_size((w as f32, h as f32));
let mvp = cam.matrix().to_gpu_clip_cols();
let view = cam.extrinsic.matrix().to_gpu_cols();
let background = egui::Rgba::from(background).to_array();
let frame = Scene3dFrame {
id,
mvp,
view,
size_px: [w, h],
background,
};
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],
size_px: [u32; 2],
background: [f32; 4],
}
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 siplot::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 siplot::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::*;
#[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,
});
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);
dst.extend_from(&src);
assert_eq!(dst.lines.len(), 4);
assert_eq!(dst.textured_meshes.len(), 2);
}
}