use crate::interaction::gizmo::{GizmoAxis, GizmoMode};
use crate::interaction::snap::ConstraintOverlay;
use crate::resources::{CameraUniform, ColormapId};
use crate::scene::material::Material;
pub(super) const INSTANCING_THRESHOLD: usize = 1;
#[derive(Debug, Clone)]
pub(crate) struct InstancedBatch {
pub mesh_index: usize,
pub texture_id: Option<u64>,
pub normal_map_id: Option<u64>,
pub ao_map_id: Option<u64>,
pub instance_offset: u32,
pub instance_count: u32,
pub is_transparent: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct ClipPlane {
pub normal: [f32; 3],
pub distance: f32,
pub enabled: bool,
pub cap_color: Option<[f32; 4]>,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ClipVolume {
None,
Plane {
normal: [f32; 3],
distance: f32,
},
Box {
center: [f32; 3],
half_extents: [f32; 3],
orientation: [[f32; 3]; 3],
},
Sphere {
center: [f32; 3],
radius: f32,
},
}
impl Default for ClipVolume {
fn default() -> Self {
ClipVolume::None
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ToneMapping {
Reinhard,
#[default]
Aces,
KhronosNeutral,
}
#[derive(Clone, Debug)]
pub struct PostProcessSettings {
pub enabled: bool,
pub tone_mapping: ToneMapping,
pub exposure: f32,
pub ssao: bool,
pub bloom: bool,
pub bloom_threshold: f32,
pub bloom_intensity: f32,
pub fxaa: bool,
pub contact_shadows: bool,
pub contact_shadow_max_distance: f32,
pub contact_shadow_steps: u32,
pub contact_shadow_thickness: f32,
}
impl Default for PostProcessSettings {
fn default() -> Self {
Self {
enabled: false,
tone_mapping: ToneMapping::Aces,
exposure: 1.0,
ssao: false,
bloom: false,
bloom_threshold: 1.0,
bloom_intensity: 0.1,
fxaa: false,
contact_shadows: false,
contact_shadow_max_distance: 0.5,
contact_shadow_steps: 16,
contact_shadow_thickness: 0.1,
}
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum LightKind {
Directional {
direction: [f32; 3],
},
Point {
position: [f32; 3],
range: f32,
},
Spot {
position: [f32; 3],
direction: [f32; 3],
range: f32,
inner_angle: f32,
outer_angle: f32,
},
}
#[derive(Clone, Debug)]
pub struct LightSource {
pub kind: LightKind,
pub color: [f32; 3],
pub intensity: f32,
}
impl Default for LightSource {
fn default() -> Self {
Self {
kind: LightKind::Directional {
direction: [0.3, 1.0, 0.5],
},
color: [1.0, 1.0, 1.0],
intensity: 1.0,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ShadowFilter {
#[default]
Pcf,
Pcss,
}
#[derive(Clone, Debug)]
pub struct LightingSettings {
pub lights: Vec<LightSource>,
pub shadow_bias: f32,
pub shadows_enabled: bool,
pub sky_color: [f32; 3],
pub ground_color: [f32; 3],
pub hemisphere_intensity: f32,
pub shadow_extent_override: Option<f32>,
pub shadow_cascade_count: u32,
pub cascade_split_lambda: f32,
pub shadow_atlas_resolution: u32,
pub shadow_filter: ShadowFilter,
pub pcss_light_radius: f32,
}
impl Default for LightingSettings {
fn default() -> Self {
Self {
lights: vec![LightSource::default()],
shadow_bias: 0.0001,
shadows_enabled: true,
sky_color: [0.8, 0.9, 1.0],
ground_color: [0.3, 0.2, 0.1],
hemisphere_intensity: 0.0,
shadow_extent_override: None,
shadow_cascade_count: 4,
cascade_split_lambda: 0.75,
shadow_atlas_resolution: 4096,
shadow_filter: ShadowFilter::Pcf,
pcss_light_radius: 0.02,
}
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct SceneRenderItem {
pub mesh_index: usize,
pub model: [[f32; 4]; 4],
pub selected: bool,
pub visible: bool,
pub show_normals: bool,
pub material: Material,
pub active_attribute: Option<crate::resources::AttributeRef>,
pub scalar_range: Option<(f32, f32)>,
pub colormap_id: Option<crate::resources::ColormapId>,
pub nan_color: Option<[f32; 4]>,
pub two_sided: bool,
pub pick_id: u64,
}
impl Default for SceneRenderItem {
fn default() -> Self {
Self {
mesh_index: 0,
model: glam::Mat4::IDENTITY.to_cols_array_2d(),
selected: false,
visible: true,
show_normals: false,
material: Material::default(),
active_attribute: None,
scalar_range: None,
colormap_id: None,
nan_color: None,
two_sided: false,
pick_id: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct ScalarBar {
pub colormap_id: crate::resources::ColormapId,
pub scalar_min: f32,
pub scalar_max: f32,
pub title: String,
pub anchor: ScalarBarAnchor,
pub orientation: ScalarBarOrientation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarBarAnchor {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarBarOrientation {
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PointRenderMode {
#[default]
ScreenSpaceCircle,
}
#[non_exhaustive]
pub struct PointCloudItem {
pub positions: Vec<[f32; 3]>,
pub colors: Vec<[f32; 4]>,
pub scalars: Vec<f32>,
pub scalar_range: Option<(f32, f32)>,
pub colormap_id: Option<ColormapId>,
pub point_size: f32,
pub default_color: [f32; 4],
pub model: [[f32; 4]; 4],
pub render_mode: PointRenderMode,
pub id: u64,
}
impl Default for PointCloudItem {
fn default() -> Self {
Self {
positions: Vec::new(),
colors: Vec::new(),
scalars: Vec::new(),
scalar_range: None,
colormap_id: None,
point_size: 4.0,
default_color: [1.0, 1.0, 1.0, 1.0],
model: glam::Mat4::IDENTITY.to_cols_array_2d(),
render_mode: PointRenderMode::ScreenSpaceCircle,
id: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GlyphType {
#[default]
Arrow,
Sphere,
Cube,
}
#[non_exhaustive]
pub struct GlyphItem {
pub positions: Vec<[f32; 3]>,
pub vectors: Vec<[f32; 3]>,
pub scale: f32,
pub scale_by_magnitude: bool,
pub magnitude_clamp: Option<(f32, f32)>,
pub scalars: Vec<f32>,
pub scalar_range: Option<(f32, f32)>,
pub colormap_id: Option<ColormapId>,
pub glyph_type: GlyphType,
pub model: [[f32; 4]; 4],
pub id: u64,
}
impl Default for GlyphItem {
fn default() -> Self {
Self {
positions: Vec::new(),
vectors: Vec::new(),
scale: 1.0,
scale_by_magnitude: true,
magnitude_clamp: None,
scalars: Vec::new(),
scalar_range: None,
colormap_id: None,
glyph_type: GlyphType::Arrow,
model: glam::Mat4::IDENTITY.to_cols_array_2d(),
id: 0,
}
}
}
#[non_exhaustive]
pub struct VolumeItem {
pub volume_id: crate::resources::VolumeId,
pub color_lut: Option<ColormapId>,
pub opacity_lut: Option<ColormapId>,
pub scalar_range: (f32, f32),
pub bbox_min: [f32; 3],
pub bbox_max: [f32; 3],
pub step_scale: f32,
pub model: [[f32; 4]; 4],
pub enable_shading: bool,
pub opacity_scale: f32,
pub threshold_min: f32,
pub threshold_max: f32,
pub nan_color: Option<[f32; 4]>,
}
impl Default for VolumeItem {
fn default() -> Self {
Self {
volume_id: crate::resources::VolumeId(0),
color_lut: None,
opacity_lut: None,
scalar_range: (0.0, 1.0),
bbox_min: [0.0, 0.0, 0.0],
bbox_max: [1.0, 1.0, 1.0],
step_scale: 1.0,
model: glam::Mat4::IDENTITY.to_cols_array_2d(),
enable_shading: false,
opacity_scale: 1.0,
threshold_min: 0.0,
threshold_max: 1.0,
nan_color: None,
}
}
}
#[non_exhaustive]
pub struct PolylineItem {
pub positions: Vec<[f32; 3]>,
pub scalars: Vec<f32>,
pub strip_lengths: Vec<u32>,
pub scalar_range: Option<(f32, f32)>,
pub colormap_id: Option<ColormapId>,
pub default_color: [f32; 4],
pub line_width: f32,
pub id: u64,
}
impl Default for PolylineItem {
fn default() -> Self {
Self {
positions: Vec::new(),
scalars: Vec::new(),
strip_lengths: Vec::new(),
scalar_range: None,
colormap_id: None,
default_color: [0.9, 0.92, 0.96, 1.0],
line_width: 2.0,
id: 0,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct StreamtubeItem {
pub positions: Vec<[f32; 3]>,
pub strip_lengths: Vec<u32>,
pub radius: f32,
pub color: [f32; 4],
pub id: u64,
}
impl Default for StreamtubeItem {
fn default() -> Self {
Self {
positions: Vec::new(),
strip_lengths: Vec::new(),
radius: 0.05,
color: [1.0, 1.0, 1.0, 1.0],
id: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FilterMode {
#[default]
Cpu,
Gpu,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ComputeFilterKind {
Clip {
plane_normal: [f32; 3],
plane_dist: f32,
},
ClipBox {
center: [f32; 3],
half_extents: [f32; 3],
orientation: [[f32; 3]; 3],
},
ClipSphere {
center: [f32; 3],
radius: f32,
},
Threshold {
min: f32,
max: f32,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ComputeFilterItem {
pub mesh_index: usize,
pub kind: ComputeFilterKind,
pub attribute_name: Option<String>,
}
impl Default for ComputeFilterItem {
fn default() -> Self {
Self {
mesh_index: 0,
kind: ComputeFilterKind::Clip {
plane_normal: [0.0, 0.0, 1.0],
plane_dist: 0.0,
},
attribute_name: None,
}
}
}
#[derive(Debug, Clone)]
pub struct RenderCamera {
pub view: glam::Mat4,
pub projection: glam::Mat4,
pub eye_position: [f32; 3],
pub forward: [f32; 3],
pub orientation: glam::Quat,
pub near: f32,
pub far: f32,
pub fov: f32,
pub aspect: f32,
}
impl RenderCamera {
pub fn camera_uniform(&self) -> CameraUniform {
CameraUniform {
view_proj: self.view_proj().to_cols_array_2d(),
eye_pos: self.eye_position,
_pad: 0.0,
forward: self.forward,
_pad1: 0.0,
}
}
pub fn view_proj(&self) -> glam::Mat4 {
self.projection * self.view
}
pub fn from_camera(cam: &crate::camera::Camera) -> Self {
let eye = cam.eye_position();
let forward = (cam.center - eye).normalize_or_zero();
Self {
view: cam.view_matrix(),
projection: cam.proj_matrix(),
eye_position: eye.to_array(),
forward: forward.to_array(),
orientation: cam.orientation,
near: cam.znear,
far: cam.zfar,
fov: cam.fov_y,
aspect: cam.aspect,
}
}
}
impl Default for RenderCamera {
fn default() -> Self {
Self {
view: glam::Mat4::IDENTITY,
projection: glam::Mat4::IDENTITY,
eye_position: [0.0, 0.0, 5.0],
forward: [0.0, 0.0, -1.0],
orientation: glam::Quat::IDENTITY,
near: 0.1,
far: 1000.0,
fov: std::f32::consts::FRAC_PI_4,
aspect: 1.333,
}
}
}
#[non_exhaustive]
pub struct CameraFrame {
pub render_camera: RenderCamera,
pub viewport_size: [f32; 2],
pub viewport_index: usize,
}
impl Default for CameraFrame {
fn default() -> Self {
Self {
render_camera: RenderCamera::default(),
viewport_size: [800.0, 600.0],
viewport_index: 0,
}
}
}
impl CameraFrame {
pub fn new(render_camera: RenderCamera, viewport_size: [f32; 2]) -> Self {
Self {
render_camera,
viewport_size,
viewport_index: 0,
}
}
pub fn from_camera(cam: &crate::camera::Camera, viewport_size: [f32; 2]) -> Self {
Self::new(RenderCamera::from_camera(cam), viewport_size)
}
pub fn with_viewport_index(mut self, viewport_index: usize) -> Self {
self.viewport_index = viewport_index;
self
}
}
#[non_exhaustive]
pub enum SurfaceSubmission {
Flat(Vec<SceneRenderItem>),
}
impl Default for SurfaceSubmission {
fn default() -> Self {
SurfaceSubmission::Flat(Vec::new())
}
}
#[non_exhaustive]
pub struct SceneFrame {
pub generation: u64,
pub surfaces: SurfaceSubmission,
pub point_clouds: Vec<PointCloudItem>,
pub glyphs: Vec<GlyphItem>,
pub polylines: Vec<PolylineItem>,
pub volumes: Vec<VolumeItem>,
pub isolines: Vec<crate::geometry::isoline::IsolineItem>,
pub streamtube_items: Vec<StreamtubeItem>,
}
impl Default for SceneFrame {
fn default() -> Self {
Self {
generation: 0,
surfaces: SurfaceSubmission::default(),
point_clouds: Vec::new(),
glyphs: Vec::new(),
polylines: Vec::new(),
volumes: Vec::new(),
isolines: Vec::new(),
streamtube_items: Vec::new(),
}
}
}
impl SceneFrame {
pub fn new(surfaces: SurfaceSubmission) -> Self {
Self {
surfaces,
..Self::default()
}
}
pub fn from_surface_items(items: Vec<SceneRenderItem>) -> Self {
Self::new(SurfaceSubmission::Flat(items))
}
}
#[non_exhaustive]
pub struct ViewportFrame {
pub background_color: Option<[f32; 4]>,
pub wireframe_mode: bool,
pub show_grid: bool,
pub grid_cell_size: f32,
pub grid_half_extent: f32,
pub grid_y: f32,
pub show_axes_indicator: bool,
}
impl Default for ViewportFrame {
fn default() -> Self {
Self {
background_color: None,
wireframe_mode: false,
show_grid: false,
grid_cell_size: 0.0,
grid_half_extent: 0.0,
grid_y: 0.0,
show_axes_indicator: true,
}
}
}
#[non_exhaustive]
pub struct InteractionFrame {
pub selection_generation: u64,
pub gizmo_model: Option<glam::Mat4>,
pub gizmo_mode: GizmoMode,
pub gizmo_hovered: GizmoAxis,
pub gizmo_space_orientation: glam::Quat,
pub constraint_overlays: Vec<ConstraintOverlay>,
pub outline_selected: bool,
pub outline_color: [f32; 4],
pub outline_width_px: f32,
pub xray_selected: bool,
pub xray_color: [f32; 4],
}
impl Default for InteractionFrame {
fn default() -> Self {
Self {
selection_generation: 0,
gizmo_model: None,
gizmo_mode: GizmoMode::Translate,
gizmo_hovered: GizmoAxis::None,
gizmo_space_orientation: glam::Quat::IDENTITY,
constraint_overlays: Vec::new(),
outline_selected: false,
outline_color: [1.0, 0.5, 0.0, 1.0],
outline_width_px: 2.0,
xray_selected: false,
xray_color: [0.3, 0.7, 1.0, 0.25],
}
}
}
#[non_exhaustive]
pub struct EffectsFrame {
pub lighting: LightingSettings,
pub clip_planes: Vec<ClipPlane>,
pub cap_fill_enabled: bool,
pub post_process: PostProcessSettings,
pub compute_filter_items: Vec<ComputeFilterItem>,
pub clip_volume: ClipVolume,
}
impl Default for EffectsFrame {
fn default() -> Self {
Self {
lighting: LightingSettings::default(),
clip_planes: Vec::new(),
cap_fill_enabled: true,
post_process: PostProcessSettings::default(),
compute_filter_items: Vec::new(),
clip_volume: ClipVolume::None,
}
}
}
#[non_exhaustive]
pub struct FrameData {
pub camera: CameraFrame,
pub scene: SceneFrame,
pub viewport: ViewportFrame,
pub interaction: InteractionFrame,
pub effects: EffectsFrame,
}
impl Default for FrameData {
fn default() -> Self {
Self {
camera: CameraFrame::default(),
scene: SceneFrame::default(),
viewport: ViewportFrame::default(),
interaction: InteractionFrame::default(),
effects: EffectsFrame::default(),
}
}
}
impl FrameData {
pub fn new(camera: CameraFrame, scene: SceneFrame) -> Self {
Self {
camera,
scene,
..Self::default()
}
}
}
macro_rules! emit_draw_calls {
($resources:expr, $render_pass:expr, $frame:expr, $use_instancing:expr, $batches:expr, $camera_bg:expr, $compute_filter_results:expr) => {{
let resources = $resources;
let render_pass = $render_pass;
let frame = $frame;
let use_instancing: bool = $use_instancing;
let compute_filter_results: &[crate::resources::ComputeFilterResult] = $compute_filter_results;
let batches: &[InstancedBatch] = $batches;
let camera_bg: &wgpu::BindGroup = $camera_bg;
let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
SurfaceSubmission::Flat(items) => items,
};
render_pass.set_bind_group(0, camera_bg, &[]);
if frame.viewport.show_grid {
render_pass.set_pipeline(&resources.grid_pipeline);
render_pass.set_bind_group(0, &resources.grid_bind_group, &[]);
render_pass.draw(0..3, 0..1);
render_pass.set_bind_group(0, camera_bg, &[]);
}
if !scene_items.is_empty() {
if use_instancing && !batches.is_empty() {
let excluded_items: Vec<&SceneRenderItem> = scene_items
.iter()
.filter(|item| {
item.visible
&& (item.active_attribute.is_some() || item.two_sided)
&& resources
.mesh_store
.get(crate::resources::mesh_store::MeshId(item.mesh_index))
.is_some()
})
.collect();
let mut opaque_batches: Vec<&InstancedBatch> = Vec::new();
let mut transparent_batches: Vec<&InstancedBatch> = Vec::new();
for batch in batches {
if batch.is_transparent {
transparent_batches.push(batch);
} else {
opaque_batches.push(batch);
}
}
if !opaque_batches.is_empty() && !frame.viewport.wireframe_mode {
if let Some(ref pipeline) = resources.solid_instanced_pipeline {
render_pass.set_pipeline(pipeline);
for batch in &opaque_batches {
let Some(mesh) = resources.mesh_store.get(crate::resources::mesh_store::MeshId(batch.mesh_index)) else { continue };
let mat_key = (
batch.texture_id.unwrap_or(u64::MAX),
batch.normal_map_id.unwrap_or(u64::MAX),
batch.ao_map_id.unwrap_or(u64::MAX),
);
let Some(inst_tex_bg) = resources.instance_bind_groups.get(&mat_key) else { continue };
render_pass.set_bind_group(1, inst_tex_bg, &[]);
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
render_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(
0..mesh.index_count,
0,
batch.instance_offset..batch.instance_offset + batch.instance_count,
);
}
}
}
if !transparent_batches.is_empty() && !frame.viewport.wireframe_mode {
if let Some(ref pipeline) = resources.transparent_instanced_pipeline {
render_pass.set_pipeline(pipeline);
for batch in &transparent_batches {
let Some(mesh) = resources.mesh_store.get(crate::resources::mesh_store::MeshId(batch.mesh_index)) else { continue };
let mat_key = (
batch.texture_id.unwrap_or(u64::MAX),
batch.normal_map_id.unwrap_or(u64::MAX),
batch.ao_map_id.unwrap_or(u64::MAX),
);
let Some(inst_tex_bg) = resources.instance_bind_groups.get(&mat_key) else { continue };
render_pass.set_bind_group(1, inst_tex_bg, &[]);
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
render_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(
0..mesh.index_count,
0,
batch.instance_offset..batch.instance_offset + batch.instance_count,
);
}
}
}
if frame.viewport.wireframe_mode {
for item in scene_items {
if !item.visible { continue; }
let Some(mesh) = resources.mesh_store.get(crate::resources::mesh_store::MeshId(item.mesh_index)) else { continue };
render_pass.set_pipeline(&resources.wireframe_pipeline);
render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
render_pass.set_index_buffer(mesh.edge_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
}
} else {
for item in &excluded_items {
let Some(mesh) = resources
.mesh_store
.get(crate::resources::mesh_store::MeshId(item.mesh_index))
else {
continue;
};
let pipeline = if item.material.opacity < 1.0 {
&resources.transparent_pipeline
} else if item.two_sided {
&resources.solid_two_sided_pipeline
} else {
&resources.solid_pipeline
};
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
render_pass.set_index_buffer(
mesh.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
}
}
} else {
let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
let dist_from_eye = |item: &&SceneRenderItem| -> f32 {
let pos = glam::Vec3::new(
item.model[3][0],
item.model[3][1],
item.model[3][2],
);
(pos - eye).length()
};
let mut opaque: Vec<&SceneRenderItem> = Vec::new();
let mut transparent: Vec<&SceneRenderItem> = Vec::new();
for item in scene_items {
if !item.visible || resources.mesh_store.get(crate::resources::mesh_store::MeshId(item.mesh_index)).is_none() {
continue;
}
if item.material.opacity < 1.0 {
transparent.push(item);
} else {
opaque.push(item);
}
}
opaque.sort_by(|a, b| dist_from_eye(a).partial_cmp(&dist_from_eye(b)).unwrap_or(std::cmp::Ordering::Equal));
transparent.sort_by(|a, b| dist_from_eye(b).partial_cmp(&dist_from_eye(a)).unwrap_or(std::cmp::Ordering::Equal));
macro_rules! draw_item {
($item:expr, $pipeline:expr) => {{
let item = $item;
let mesh = resources.mesh_store.get(crate::resources::mesh_store::MeshId(item.mesh_index)).unwrap();
render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
if frame.viewport.wireframe_mode {
render_pass.set_pipeline(&resources.wireframe_pipeline);
render_pass.set_index_buffer(
mesh.edge_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
} else {
let filter_result = compute_filter_results
.iter()
.find(|r| r.mesh_index == item.mesh_index);
render_pass.set_pipeline($pipeline);
if let Some(fr) = filter_result {
render_pass.set_index_buffer(
fr.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..fr.index_count, 0, 0..1);
} else {
render_pass.set_index_buffer(
mesh.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
}
}
if item.show_normals {
if let Some(ref nl_buf) = mesh.normal_line_buffer {
if mesh.normal_line_count > 0 {
render_pass.set_pipeline(&resources.wireframe_pipeline);
render_pass.set_bind_group(1, &mesh.normal_bind_group, &[]);
render_pass.set_vertex_buffer(0, nl_buf.slice(..));
render_pass.draw(0..mesh.normal_line_count, 0..1);
}
}
}
}};
}
for item in &opaque {
let pl = if item.two_sided {
&resources.solid_two_sided_pipeline
} else {
&resources.solid_pipeline
};
draw_item!(item, pl);
}
for item in &transparent {
draw_item!(item, &resources.transparent_pipeline);
}
}
}
if frame.interaction.gizmo_model.is_some() && resources.gizmo_index_count > 0 {
render_pass.set_pipeline(&resources.gizmo_pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
render_pass.set_bind_group(1, &resources.gizmo_bind_group, &[]);
render_pass.set_vertex_buffer(0, resources.gizmo_vertex_buffer.slice(..));
render_pass.set_index_buffer(
resources.gizmo_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..resources.gizmo_index_count, 0, 0..1);
}
if !resources.constraint_line_buffers.is_empty() {
render_pass.set_pipeline(&resources.overlay_line_pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
for (vbuf, ibuf, index_count, _ubuf, bg) in &resources.constraint_line_buffers {
render_pass.set_bind_group(1, bg, &[]);
render_pass.set_vertex_buffer(0, vbuf.slice(..));
render_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..*index_count, 0, 0..1);
}
}
if !resources.cap_buffers.is_empty() {
render_pass.set_pipeline(&resources.overlay_pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
for (vbuf, ibuf, idx_count, _ubuf, bg) in &resources.cap_buffers {
render_pass.set_bind_group(1, bg, &[]);
render_pass.set_vertex_buffer(0, vbuf.slice(..));
render_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..*idx_count, 0, 0..1);
}
}
if !resources.xray_object_buffers.is_empty() {
render_pass.set_pipeline(&resources.xray_pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
for (mesh_idx, _buf, bg) in &resources.xray_object_buffers {
let Some(mesh) = resources.mesh_store.get(crate::resources::mesh_store::MeshId(*mesh_idx)) else { continue };
render_pass.set_bind_group(1, bg, &[]);
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
render_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
}
}
if !resources.outline_object_buffers.is_empty() {
if let (Some(pipeline), Some(bg)) = (
&resources.outline_composite_pipeline_msaa,
&resources.outline_composite_bind_group,
) {
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bg, &[]);
render_pass.draw(0..3, 0..1);
}
}
if frame.viewport.show_axes_indicator && resources.axes_vertex_count > 0 {
render_pass.set_pipeline(&resources.axes_pipeline);
render_pass.set_vertex_buffer(0, resources.axes_vertex_buffer.slice(..));
render_pass.draw(0..resources.axes_vertex_count, 0..1);
}
}};
}
macro_rules! emit_scivis_draw_calls {
($resources:expr, $render_pass:expr, $pc_gpu_data:expr, $glyph_gpu_data:expr, $polyline_gpu_data:expr, $volume_gpu_data:expr, $streamtube_gpu_data:expr, $camera_bg:expr) => {{
let resources = $resources;
let render_pass = $render_pass;
let camera_bg: &wgpu::BindGroup = $camera_bg;
if !$pc_gpu_data.is_empty() {
if let Some(ref pipeline) = resources.point_cloud_pipeline {
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
for pc in $pc_gpu_data.iter() {
render_pass.set_bind_group(1, &pc.bind_group, &[]);
render_pass.set_vertex_buffer(0, pc.vertex_buffer.slice(..));
render_pass.draw(0..6, 0..pc.point_count);
}
}
}
if !$glyph_gpu_data.is_empty() {
if let Some(ref pipeline) = resources.glyph_pipeline {
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
for glyph in $glyph_gpu_data.iter() {
render_pass.set_bind_group(1, &glyph.uniform_bind_group, &[]);
render_pass.set_bind_group(2, &glyph.instance_bind_group, &[]);
render_pass.set_vertex_buffer(0, glyph.mesh_vertex_buffer.slice(..));
render_pass.set_index_buffer(
glyph.mesh_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..glyph.mesh_index_count, 0, 0..glyph.instance_count);
}
}
}
if !$polyline_gpu_data.is_empty() {
if let Some(ref pipeline) = resources.polyline_pipeline {
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
for pl in $polyline_gpu_data.iter() {
render_pass.set_bind_group(1, &pl.bind_group, &[]);
render_pass.set_vertex_buffer(0, pl.vertex_buffer.slice(..));
for range in &pl.strip_ranges {
render_pass.draw(range.clone(), 0..1);
}
}
}
}
if !$volume_gpu_data.is_empty() {
if let Some(ref pipeline) = resources.volume_pipeline {
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
for vol in $volume_gpu_data.iter() {
render_pass.set_bind_group(1, &vol.bind_group, &[]);
render_pass.set_vertex_buffer(0, vol.vertex_buffer.slice(..));
render_pass
.set_index_buffer(vol.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..36, 0, 0..1);
}
}
}
if !$streamtube_gpu_data.is_empty() {
if let Some(ref pipeline) = resources.streamtube_pipeline {
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, camera_bg, &[]);
for tube in $streamtube_gpu_data.iter() {
render_pass.set_bind_group(1, &tube.uniform_bind_group, &[]);
render_pass.set_bind_group(2, &tube.instance_bind_group, &[]);
render_pass.set_vertex_buffer(0, tube.mesh_vertex_buffer.slice(..));
render_pass.set_index_buffer(
tube.mesh_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..tube.mesh_index_count, 0, 0..tube.instance_count);
}
}
}
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_camera_from_camera_roundtrip() {
let cam = crate::camera::Camera::default();
let rc = RenderCamera::from_camera(&cam);
assert_eq!(rc.eye_position, cam.eye_position().to_array());
assert_eq!(rc.orientation, cam.orientation);
assert_eq!(rc.near, cam.znear);
assert_eq!(rc.far, cam.zfar);
assert_eq!(rc.fov, cam.fov_y);
assert_eq!(rc.aspect, cam.aspect);
let expected_vp = cam.view_proj_matrix();
let actual_vp = rc.view_proj();
assert!(
(expected_vp - actual_vp).abs_diff_eq(glam::Mat4::ZERO, 1e-5),
"view_proj mismatch"
);
}
#[test]
fn render_camera_uniform_contains_eye_and_forward() {
let rc = RenderCamera {
eye_position: [1.0, 2.0, 3.0],
forward: [0.0, 0.0, -1.0],
..RenderCamera::default()
};
let u = rc.camera_uniform();
assert_eq!(u.eye_pos, [1.0, 2.0, 3.0]);
assert_eq!(u.forward, [0.0, 0.0, -1.0]);
assert_eq!(u.view_proj, rc.view_proj().to_cols_array_2d());
}
}