diff --git a/Cargo.lock b/Cargo.lock index 9c9324d75b12..7c03b35eb429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2084,6 +2084,15 @@ dependencies = [ "rerun", ] +[[package]] +name = "custom_visualizer" +version = "0.24.0-alpha.3+dev" +dependencies = [ + "bytemuck", + "mimalloc", + "rerun", +] + [[package]] name = "darling" version = "0.20.10" diff --git a/crates/viewer/re_view_spatial/src/lib.rs b/crates/viewer/re_view_spatial/src/lib.rs index 134b5cb9fb42..ba206bcafe44 100644 --- a/crates/viewer/re_view_spatial/src/lib.rs +++ b/crates/viewer/re_view_spatial/src/lib.rs @@ -32,6 +32,9 @@ pub use ui::SpatialViewState; pub use view_2d::SpatialView2D; pub use view_3d::SpatialView3D; +// Export some other types that are useful for extensions. +pub use contexts::TransformTreeContext; + pub(crate) use pickable_textured_rect::{PickableRectSourceData, PickableTexturedRect}; pub(crate) use pinhole::Pinhole; diff --git a/crates/viewer/re_viewer/src/lib.rs b/crates/viewer/re_viewer/src/lib.rs index 3b32c650fef2..ab8e13d81ce5 100644 --- a/crates/viewer/re_viewer/src/lib.rs +++ b/crates/viewer/re_viewer/src/lib.rs @@ -42,8 +42,7 @@ pub use re_viewer_context::{ }; pub mod external { - pub use parking_lot; - pub use {eframe, egui}; + pub use {eframe, egui, parking_lot}; pub use { re_chunk, re_chunk::external::*, re_chunk_store, re_chunk_store::external::*, re_data_ui, re_entity_db, re_log, re_log_types, re_memory, re_renderer, re_smart_channel, re_types, diff --git a/crates/viewer/re_viewer_context/src/view/view_class_registry.rs b/crates/viewer/re_viewer_context/src/view/view_class_registry.rs index 210eabec637f..94e1d9cf4cf0 100644 --- a/crates/viewer/re_viewer_context/src/view/view_class_registry.rs +++ b/crates/viewer/re_viewer_context/src/view/view_class_registry.rs @@ -31,12 +31,15 @@ pub enum ViewClassRegistryError { UnknownClassIdentifier(ViewClassIdentifier), } -/// Utility for registering view systems, passed on to [`crate::ViewClass::on_register`]. +/// Utility for registering view systems to a given class, passed on to [`crate::ViewClass::on_register`]. pub struct ViewSystemRegistrator<'a> { - registry: &'a mut ViewClassRegistry, identifier: ViewClassIdentifier, - context_systems: HashSet, - visualizers: HashSet, + + all_context_systems: &'a mut HashMap, + all_visualizers: &'a mut HashMap, + + class_context_system: &'a mut HashSet, + class_visualizer_system: &'a mut HashSet, } impl ViewSystemRegistrator<'_> { @@ -50,15 +53,14 @@ impl ViewSystemRegistrator<'_> { &mut self, ) -> Result<(), ViewClassRegistryError> { // Name should not overlap with context systems. - if self.registry.visualizers.contains_key(&T::identifier()) { + if self.all_visualizers.contains_key(&T::identifier()) { return Err(ViewClassRegistryError::IdentifierAlreadyInUseForVisualizer( T::identifier().as_str(), )); } - if self.context_systems.insert(T::identifier()) { - self.registry - .context_systems + if self.class_context_system.insert(T::identifier()) { + self.all_context_systems .entry(T::identifier()) .or_insert_with(|| ContextSystemTypeRegistryEntry { factory_method: Box::new(|| Box::::default()), @@ -85,7 +87,7 @@ impl ViewSystemRegistrator<'_> { &mut self, ) -> Result<(), ViewClassRegistryError> { // Name should not overlap with context systems. - if self.registry.context_systems.contains_key(&T::identifier()) { + if self.all_context_systems.contains_key(&T::identifier()) { return Err( ViewClassRegistryError::IdentifierAlreadyInUseForContextSystem( T::identifier().as_str(), @@ -93,9 +95,8 @@ impl ViewSystemRegistrator<'_> { ); } - if self.visualizers.insert(T::identifier()) { - self.registry - .visualizers + if self.class_visualizer_system.insert(T::identifier()) { + self.all_visualizers .entry(T::identifier()) .or_insert_with(|| { let entity_subscriber_handle = ChunkStore::register_subscriber(Box::new( @@ -180,41 +181,84 @@ impl ViewClassRegistry { pub fn add_class( &mut self, ) -> Result<(), ViewClassRegistryError> { - let class = Box::::default(); + let mut class_entry = ViewClassRegistryEntry { + class: Box::::default(), + identifier: T::identifier(), + context_system_ids: Default::default(), + visualizer_system_ids: Default::default(), + }; let mut registrator = ViewSystemRegistrator { - registry: self, identifier: T::identifier(), - context_systems: Default::default(), - visualizers: Default::default(), + all_visualizers: &mut self.visualizers, + all_context_systems: &mut self.context_systems, + class_context_system: &mut class_entry.context_system_ids, + class_visualizer_system: &mut class_entry.visualizer_system_ids, }; - class.on_register(&mut registrator)?; - - let ViewSystemRegistrator { - registry: _, - identifier, - context_systems, - visualizers, - } = registrator; + class_entry.class.on_register(&mut registrator)?; if self .view_classes - .insert( - identifier, - ViewClassRegistryEntry { - class, - identifier, - context_system_ids: context_systems, - visualizer_system_ids: visualizers, - }, - ) + .insert(T::identifier(), class_entry) .is_some() { - return Err(ViewClassRegistryError::DuplicateClassIdentifier(identifier)); + Err(ViewClassRegistryError::DuplicateClassIdentifier( + T::identifier(), + )) + } else { + Ok(()) } + } - Ok(()) + /// Registers an additional [`VisualizerSystem`] for a view class. + /// + /// Usually, visualizers are registered in [`ViewClass::on_register`] which is called when + /// the class is first registered. + /// This method allows to extend an existing class with new visualizers. + pub fn register_visualizer( + &mut self, + view_class: ViewClassIdentifier, + ) -> Result<(), ViewClassRegistryError> { + let Some(class_entry) = self.view_classes.get_mut(&view_class) else { + return Err(ViewClassRegistryError::UnknownClassIdentifier(view_class)); + }; + + let mut registrator = ViewSystemRegistrator { + identifier: class_entry.identifier, + all_visualizers: &mut self.visualizers, + all_context_systems: &mut self.context_systems, + class_context_system: &mut class_entry.context_system_ids, + class_visualizer_system: &mut class_entry.visualizer_system_ids, + }; + + registrator.register_visualizer::() + } + + /// Registers an additional [`ViewContextSystem`] for a view class. + /// + /// Usually, context systems are registered in [`ViewClass::on_register`] which is called when + /// the class is first registered. + /// This method allows to extend an existing class with new context systems. + pub fn register_context_system< + T: ViewContextSystem + IdentifiedViewSystem + Default + 'static, + >( + &mut self, + view_class: ViewClassIdentifier, + ) -> Result<(), ViewClassRegistryError> { + let Some(class_entry) = self.view_classes.get_mut(&view_class) else { + return Err(ViewClassRegistryError::UnknownClassIdentifier(view_class)); + }; + + let mut registrator = ViewSystemRegistrator { + identifier: class_entry.identifier, + all_visualizers: &mut self.visualizers, + all_context_systems: &mut self.context_systems, + class_context_system: &mut class_entry.context_system_ids, + class_visualizer_system: &mut class_entry.visualizer_system_ids, + }; + + registrator.register_context_system::() } /// Removes a view class from the registry. diff --git a/examples/rust/custom_visualizer/Cargo.toml b/examples/rust/custom_visualizer/Cargo.toml new file mode 100644 index 000000000000..358c04f0ea70 --- /dev/null +++ b/examples/rust/custom_visualizer/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "custom_visualizer" +version = "0.24.0-alpha.3+dev" +edition = "2021" +rust-version = "1.85" +license = "MIT OR Apache-2.0" +publish = false + +[features] +default = [] + +# Turn on if you want to share analytics with Rerun (e.g. callstacks on crashes). +analytics = ["rerun/analytics"] + +[dependencies] +rerun = { path = "../../../crates/top/rerun", default-features = false, features = [ + "native_viewer", + "run", +] } + +# Can't rely on export from `rerun` crate since proc macros can't be re-exported. +bytemuck = "1" + +# mimalloc is a much faster allocator: +mimalloc = "0.1.43" diff --git a/examples/rust/custom_visualizer/README.md b/examples/rust/custom_visualizer/README.md new file mode 100644 index 000000000000..24e9a811e0f5 --- /dev/null +++ b/examples/rust/custom_visualizer/README.md @@ -0,0 +1,32 @@ +TODO: copied from custom_view, need to update everything in here. + + + + + + + + + + Custom View UI example screenshot + + +Example showing how to add custom View classes to extend the Rerun Viewer. + +The example is really basic, but should be something you can build upon. + +The example starts an SDK server which the Python or Rust logging SDK can connect to. + + +[#2337](https://github.com/rerun-io/rerun/issues/2337): Note that in order to spawn a web viewer with these customizations applied, +you have to build the web viewer of the version yourself. +This is currently not supported outside of the Rerun repository. + +## Testing it +Start it with `cargo run -p custom_view`. + +Then put some data into it with: `cargo run -p minimal_options -- --connect` diff --git a/examples/rust/custom_visualizer/shader/custom.wgsl b/examples/rust/custom_visualizer/shader/custom.wgsl new file mode 100644 index 000000000000..4a3ec80234af --- /dev/null +++ b/examples/rust/custom_visualizer/shader/custom.wgsl @@ -0,0 +1,61 @@ +#import +#import + +struct UniformBuffer { + world_from_obj: mat4x4f, + + color: vec4f, + + picking_layer_object_id: vec2u, + picking_instance_id: vec2u, + + outline_mask: vec2u, +}; +@group(1) @binding(0) +var ubo: UniformBuffer; + +struct VertexOut { + @location(0) color: vec4f, + @builtin(position) position: vec4f, +}; + +var v_positions: array = array( + vec2f(0.0, 1.0), + vec2f(1.0, -1.0), + vec2f(-1.0, -1.0), +); + +var v_colors: array = array( + vec4f(1.0, 0.0, 0.0, 1.0), + vec4f(0.0, 1.0, 0.0, 1.0), + vec4f(0.0, 0.0, 1.0, 1.0), +); + +@vertex +fn vs_main(@builtin(vertex_index) v_idx: u32) -> VertexOut { + var out: VertexOut; + + let position_obj = vec4f(v_positions[v_idx], 0.0, 1.0); + let position_world = ubo.world_from_obj * position_obj; + let position_clip = frame.projection_from_world * position_world; + + out.position = position_clip; + out.color = v_colors[v_idx] * ubo.color; + + return out; +} + +@fragment +fn fs_main(in: VertexOut) -> @location(0) vec4f { + return in.color; +} + +@fragment +fn fs_main_picking_layer(in: VertexOut) -> @location(0) vec4u { + return vec4u(ubo.picking_layer_object_id, ubo.picking_instance_id); +} + +@fragment +fn fs_main_outline_mask(in: VertexOut) -> @location(0) vec2u { + return ubo.outline_mask; +} diff --git a/examples/rust/custom_visualizer/src/custom_archetype.rs b/examples/rust/custom_visualizer/src/custom_archetype.rs new file mode 100644 index 000000000000..4b2259644cb6 --- /dev/null +++ b/examples/rust/custom_visualizer/src/custom_archetype.rs @@ -0,0 +1,108 @@ +use rerun::{external::re_types::try_serialize_field, Component}; + +/// Custom archetype for drawing ??TODO?? in the 3D view. +#[derive(Default)] +pub struct Custom { + pub positions: Option, + pub colors: Option, +} + +impl rerun::Archetype for Custom { + type Indicator = rerun::GenericIndicatorComponent; + + fn indicator() -> rerun::SerializedComponentBatch { + use rerun::ComponentBatch as _; + #[allow(clippy::unwrap_used)] + Self::Indicator::default() + .serialized(Self::descriptor_indicator()) + .unwrap() + } + + fn name() -> rerun::ArchetypeName { + "Custom".into() + } + + fn display_name() -> &'static str { + "Custom" + } + + fn required_components() -> ::std::borrow::Cow<'static, [rerun::ComponentDescriptor]> { + vec![Self::descriptor_positions()].into() + } + + fn optional_components() -> std::borrow::Cow<'static, [rerun::ComponentDescriptor]> { + vec![Self::descriptor_colors()].into() + } +} + +impl Custom { + /// Returns the [`rerun::ComponentDescriptor`] for [`Self::positions`]. + #[inline] + pub fn descriptor_positions() -> rerun::ComponentDescriptor { + rerun::ComponentDescriptor { + archetype: Some("Custom".into()), + component: "Custom:positions".into(), + component_type: Some(rerun::components::Position3D::name()), + } + } + + /// Returns the [`rerun::ComponentDescriptor`] for [`Self::colors`]. + #[inline] + pub fn descriptor_colors() -> rerun::ComponentDescriptor { + rerun::ComponentDescriptor { + archetype: Some("Custom".into()), + component: "Custom:colors".into(), + component_type: Some(rerun::components::Color::name()), + } + } + + /// Returns the [`rerun::ComponentDescriptor`] for the associated indicator component. + #[inline] + pub fn descriptor_indicator() -> rerun::ComponentDescriptor { + rerun::ComponentDescriptor { + archetype: None, + component: "CustomIndicator".into(), + component_type: None, + } + } + + #[inline] + pub fn new( + positions: impl IntoIterator>, + ) -> Self { + Self::default().with_positions(positions) + } + + #[inline] + pub fn with_positions( + mut self, + positions: impl IntoIterator>, + ) -> Self { + self.positions = try_serialize_field(Self::descriptor_positions(), positions); + self + } + + #[inline] + pub fn with_colors( + mut self, + vertex_colors: impl IntoIterator>, + ) -> Self { + self.colors = try_serialize_field(Self::descriptor_colors(), vertex_colors); + self + } +} + +impl rerun::AsComponents for Custom { + #[inline] + fn as_serialized_batches(&self) -> Vec { + use rerun::Archetype as _; + [ + Some(Self::indicator()), + self.positions.clone(), + self.colors.clone(), + ] + .into_iter() + .flatten() + .collect() + } +} diff --git a/examples/rust/custom_visualizer/src/custom_renderer.rs b/examples/rust/custom_visualizer/src/custom_renderer.rs new file mode 100644 index 000000000000..373897cf7075 --- /dev/null +++ b/examples/rust/custom_visualizer/src/custom_renderer.rs @@ -0,0 +1,242 @@ +use std::num::NonZeroU64; + +use rerun::external::{ + glam, + re_renderer::{ + self, + external::{smallvec::smallvec, wgpu}, + }, +}; + +/// Implements a simple custom [`re_renderer::renderer::Renderer`] for drawing some shader defined 3D ??TODO??. +pub struct CustomRenderer { + bind_group_layout: re_renderer::GpuBindGroupLayoutHandle, + + render_pipeline_color: re_renderer::GpuRenderPipelineHandle, + render_pipeline_picking_layer: re_renderer::GpuRenderPipelineHandle, + render_pipeline_outline_mask: re_renderer::GpuRenderPipelineHandle, +} + +mod gpu_data { + use rerun::external::re_renderer::{self, wgpu_buffer_types}; + + /// Keep in sync with [`UniformBuffer`] in `custom.wgsl` + #[repr(C)] + #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] + pub struct UniformBuffer { + pub world_from_obj: wgpu_buffer_types::Mat4, + + pub color: re_renderer::Rgba, + + pub picking_layer_object_id: re_renderer::PickingLayerObjectId, + pub picking_instance_id: re_renderer::PickingLayerInstanceId, + + pub outline_mask: wgpu_buffer_types::UVec2RowPadded, + + pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 7], + } +} +/// GPU draw data for drawing ??TODO?? instances using [`CustomRenderer`]. +/// +/// Note that a single draw data is used for many instances of the same drawable. +#[derive(Clone)] +pub struct CustomDrawData { + instances: Vec, +} + +#[derive(Clone)] +struct Instance { + /// Bindgroup per instance. + /// + /// It is much more efficient to batch everything in a single draw call by using instancing + /// or other more dynamic buffer access. However, for simplicity, we draw each instance individually + /// with a separate bind group here. + bind_group: re_renderer::GpuBindGroup, + + has_outline: bool, +} + +impl re_renderer::renderer::DrawData for CustomDrawData { + type Renderer = CustomRenderer; +} + +impl CustomDrawData { + pub fn new(ctx: &re_renderer::RenderContext) -> Self { + let _ = ctx.renderer::(); // TODO(andreas): This line ensures that the renderer exists. Currently this needs to be done ahead of time, but should be fully automatic! + Self { + instances: Vec::new(), + } + } + + /// Adds an instance to this draw data. + pub fn add( + &mut self, + ctx: &re_renderer::RenderContext, + label: &str, + world_from_obj: glam::Affine3A, + color: re_renderer::Rgba, + picking_layer_object_id: re_renderer::PickingLayerObjectId, + picking_instance_id: re_renderer::PickingLayerInstanceId, + outline_mask: re_renderer::OutlineMaskPreference, + ) { + let renderer = ctx.renderer::(); + + // See `CustomRenderer::bind_groups`: It would be much more efficient to batch instances, + // but for simplicity we create fresh buffers here for each instance. + let bind_group = ctx.gpu_resources.bind_groups.alloc( + &ctx.device, + &ctx.gpu_resources, + &re_renderer::BindGroupDesc { + label: label.into(), + entries: smallvec![re_renderer::create_and_fill_uniform_buffer( + ctx, + label.into(), + gpu_data::UniformBuffer { + world_from_obj: world_from_obj.into(), + color, + picking_layer_object_id, + picking_instance_id, + outline_mask: outline_mask.0.unwrap_or_default().into(), + end_padding: Default::default(), + }, + )], + layout: renderer.bind_group_layout, + }, + ); + self.instances.push(Instance { + bind_group, + has_outline: outline_mask.is_some(), + }); + } +} + +impl re_renderer::renderer::Renderer for CustomRenderer { + type RendererDrawData = CustomDrawData; + + fn create_renderer(ctx: &re_renderer::RenderContext) -> Self { + let shader_modules = &ctx.gpu_resources.shader_modules; + let shader_module = shader_modules.get_or_create( + ctx, + &re_renderer::include_shader_module!("../shader/custom.wgsl"), + ); + + let bind_group_layout = ctx.gpu_resources.bind_group_layouts.get_or_create( + &ctx.device, + &re_renderer::BindGroupLayoutDesc { + label: "CustomRenderer::bind_group_layout".into(), + entries: vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: NonZeroU64::new( + std::mem::size_of::() as _, + ), + }, + count: None, + }], + }, + ); + + let pipeline_layout = ctx.gpu_resources.pipeline_layouts.get_or_create( + ctx, + &re_renderer::PipelineLayoutDesc { + label: "CustomRenderer".into(), + entries: vec![ctx.global_bindings.layout, bind_group_layout], + }, + ); + + let render_pipeline_desc_color = re_renderer::RenderPipelineDesc { + label: "CustomRenderer::color".into(), + pipeline_layout, + vertex_entrypoint: "vs_main".into(), + vertex_handle: shader_module, + fragment_entrypoint: "fs_main".into(), + fragment_handle: shader_module, + vertex_buffers: smallvec![], + render_targets: smallvec![Some( + re_renderer::ViewBuilder::MAIN_TARGET_COLOR_FORMAT.into() + )], + primitive: wgpu::PrimitiveState::default(), + depth_stencil: re_renderer::ViewBuilder::MAIN_TARGET_DEFAULT_DEPTH_STATE, + multisample: re_renderer::ViewBuilder::main_target_default_msaa_state( + ctx.render_config(), + false, + ), + }; + + let render_pipelines = &ctx.gpu_resources.render_pipelines; + let render_pipeline_color = + render_pipelines.get_or_create(ctx, &render_pipeline_desc_color); + let render_pipeline_picking_layer = render_pipelines.get_or_create( + ctx, + &re_renderer::RenderPipelineDesc { + label: "CustomRenderer::picking_layer".into(), + fragment_entrypoint: "fs_main_picking_layer".into(), + render_targets: smallvec![Some( + re_renderer::PickingLayerProcessor::PICKING_LAYER_FORMAT.into() + )], + depth_stencil: re_renderer::PickingLayerProcessor::PICKING_LAYER_DEPTH_STATE, + multisample: re_renderer::PickingLayerProcessor::PICKING_LAYER_MSAA_STATE, + ..render_pipeline_desc_color.clone() + }, + ); + let render_pipeline_outline_mask = render_pipelines.get_or_create( + ctx, + &re_renderer::RenderPipelineDesc { + label: "CustomRenderer::outline_mask".into(), + fragment_entrypoint: "fs_main_outline_mask".into(), + render_targets: smallvec![Some( + re_renderer::OutlineMaskProcessor::MASK_FORMAT.into() + )], + depth_stencil: re_renderer::OutlineMaskProcessor::MASK_DEPTH_STATE, + ..render_pipeline_desc_color + }, + ); + + Self { + bind_group_layout, + render_pipeline_color, + render_pipeline_outline_mask, + render_pipeline_picking_layer, + } + } + + fn draw( + &self, + render_pipelines: &re_renderer::GpuRenderPipelinePoolAccessor<'_>, + phase: re_renderer::DrawPhase, + pass: &mut wgpu::RenderPass<'_>, + draw_data: &CustomDrawData, + ) -> Result<(), re_renderer::renderer::DrawError> { + let pipeline_handle = match phase { + re_renderer::DrawPhase::Opaque => self.render_pipeline_color, + re_renderer::DrawPhase::OutlineMask => self.render_pipeline_outline_mask, + re_renderer::DrawPhase::PickingLayer => self.render_pipeline_picking_layer, + _ => unreachable!("We were called on a phase we weren't subscribed to: {phase:?}"), + }; + + let pipeline = render_pipelines.get(pipeline_handle)?; + pass.set_pipeline(pipeline); + + for instance in &draw_data.instances { + if phase == re_renderer::DrawPhase::OutlineMask && !instance.has_outline { + continue; + } + + pass.set_bind_group(1, &instance.bind_group, &[]); + pass.draw(0..3, 0..1); + } + + Ok(()) + } + + fn participated_phases() -> &'static [re_renderer::DrawPhase] { + &[ + re_renderer::DrawPhase::Opaque, + re_renderer::DrawPhase::OutlineMask, + re_renderer::DrawPhase::PickingLayer, + ] + } +} diff --git a/examples/rust/custom_visualizer/src/custom_visualizer.rs b/examples/rust/custom_visualizer/src/custom_visualizer.rs new file mode 100644 index 000000000000..4fe81af7c865 --- /dev/null +++ b/examples/rust/custom_visualizer/src/custom_visualizer.rs @@ -0,0 +1,124 @@ +use rerun::{ + external::{ + re_query, re_renderer, re_types, + re_view::{DataResultQuery as _, RangeResultsExt as _}, + re_view_spatial, + re_viewer_context::{ + self, auto_color_for_entity_path, IdentifiedViewSystem, QueryContext, + TypedComponentFallbackProvider, ViewContext, ViewContextCollection, ViewQuery, + ViewSystemExecutionError, ViewSystemIdentifier, VisualizerQueryInfo, VisualizerSystem, + }, + }, + Archetype as _, +}; + +use crate::{custom_archetype::Custom, custom_renderer::CustomDrawData}; + +#[derive(Default)] +pub struct CustomVisualizer {} + +impl IdentifiedViewSystem for CustomVisualizer { + fn identifier() -> ViewSystemIdentifier { + "Custom".into() + } +} + +// TODO: copy pasted out of re_view_spatial, but it's generally useful. +/// Iterate over all the values in the slice, then repeat the last value forever. +/// +/// If the input slice is empty, the second argument is returned forever. +#[inline] +pub fn clamped_or<'a, T>(values: &'a [T], if_empty: &'a T) -> impl Iterator + Clone { + let repeated = values.last().unwrap_or(if_empty); + values.iter().chain(std::iter::repeat(repeated)) +} + +impl VisualizerSystem for CustomVisualizer { + fn visualizer_query_info(&self) -> VisualizerQueryInfo { + VisualizerQueryInfo::from_archetype::() + } + + fn execute( + &mut self, + ctx: &ViewContext<'_>, + query: &ViewQuery<'_>, + context_systems: &ViewContextCollection, + ) -> Result, ViewSystemExecutionError> { + let transforms = context_systems.get::()?; + let render_ctx = ctx.render_ctx(); + + let mut draw_data = CustomDrawData::new(render_ctx); + + for data_result in query.iter_visible_data_results(Self::identifier()) { + let ent_path = &data_result.entity_path; + let Some(transform_info) = transforms.transform_info_for_entity(ent_path.hash()) else { + continue; // No valid transform info for this entity. + }; + + let results = data_result.query_archetype_with_history::(ctx, query); + + // TODO: handle component instances etc. + // TODO: handle ziping of primary component and transform info + // for (instance, transform) in transform_info.reference_from_instances.iter().enumerate() + let transform = transform_info + .reference_from_instances(Custom::name()) + .first(); + + // gather all relevant chunks + let timeline = query.timeline; + let all_positions = results.iter_as(timeline, Custom::descriptor_positions()); + let all_colors = results.iter_as(timeline, Custom::descriptor_colors()); + + let picking_layer_object_id = re_renderer::PickingLayerObjectId(ent_path.hash64()); + let entity_outline_mask = query.highlights.entity_outline_mask(ent_path.hash()); + + let fallback_color: rerun::Color = + self.fallback_for(&ctx.query_context(data_result, &query.latest_at_query())); + + for (_index, positions, colors) in re_query::range_zip_1x1( + all_positions.slice::<[f32; 3]>(), + all_colors.slice::(), + ) { + let colors: &[rerun::Color] = + colors.map_or(&[], |colors| bytemuck::cast_slice(colors)); + let colors = clamped_or(colors, &fallback_color); + + for (instance_index, (position, color)) in + positions.into_iter().zip(colors.into_iter()).enumerate() + { + let instance = instance_index as u64; + let picking_layer_instance_id = re_renderer::PickingLayerInstanceId(instance); + let outline_mask = entity_outline_mask.index_outline_mask(instance.into()); + + draw_data.add( + render_ctx, + &ent_path.to_string(), + *transform, + (*color).into(), + picking_layer_object_id, + picking_layer_instance_id, + outline_mask, + ); + } + } + } + + Ok(vec![draw_data.into()]) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn fallback_provider(&self) -> &dyn re_viewer_context::ComponentFallbackProvider { + self + } +} + +impl TypedComponentFallbackProvider for CustomVisualizer { + fn fallback_for(&self, ctx: &QueryContext<'_>) -> rerun::Color { + auto_color_for_entity_path(ctx.target_entity_path) + } +} + +re_viewer_context::impl_component_fallback_provider!(CustomVisualizer => [rerun::Color]); diff --git a/examples/rust/custom_visualizer/src/main.rs b/examples/rust/custom_visualizer/src/main.rs new file mode 100644 index 000000000000..c110e6f8f11b --- /dev/null +++ b/examples/rust/custom_visualizer/src/main.rs @@ -0,0 +1,140 @@ +//! This example shows how to add custom Views to the Rerun Viewer. + +use rerun::external::{ + glam, re_crash_handler, re_grpc_server, re_log, re_memory, re_smart_channel, + re_types::{self, View as _}, + re_viewer, tokio, +}; + +mod custom_archetype; +mod custom_renderer; +mod custom_visualizer; + +use custom_visualizer::CustomVisualizer; + +// By using `re_memory::AccountingAllocator` Rerun can keep track of exactly how much memory it is using, +// and prune the data store when it goes above a certain limit. +// By using `mimalloc` we get faster allocations. +#[global_allocator] +static GLOBAL: re_memory::AccountingAllocator = + re_memory::AccountingAllocator::new(mimalloc::MiMalloc); + +#[tokio::main] +async fn main() -> Result<(), Box> { + let main_thread_token = re_viewer::MainThreadToken::i_promise_i_am_on_the_main_thread(); + + // Direct calls using the `log` crate to stderr. Control with `RUST_LOG=debug` etc. + re_log::setup_logging(); + + // Install handlers for panics and crashes that prints to stderr and send + // them to Rerun analytics (if the `analytics` feature is on in `Cargo.toml`). + re_crash_handler::install_crash_handlers(re_viewer::build_info()); + + // Listen for gRPC connections from Rerun's logging SDKs. + // There are other ways of "feeding" the viewer though - all you need is a `re_smart_channel::Receiver`. + let (grpc_rx, _) = re_grpc_server::spawn_with_recv( + "0.0.0.0:9876".parse()?, + "75%".parse()?, + re_grpc_server::shutdown::never(), + ); + + // Provide a builtin recording with an example recording using the custom archetype. + let builtin_recording_rx = builtin_recording()?; + + let startup_options = re_viewer::StartupOptions::default(); + + // This is used for analytics, if the `analytics` feature is on in `Cargo.toml` + let app_env = re_viewer::AppEnvironment::Custom("My extended Rerun Viewer".to_owned()); + + println!( + "This example starts a custom Rerun Viewer that is ready to accept data. But for convenience it comes with a built-in recording!" + ); + println!( + "You can connect through the SDK as per usual, for example to run: `cargo run -p minimal_options -- --connect` in another terminal instance." + ); + + re_viewer::run_native_app( + main_thread_token, + Box::new(move |cc| { + let mut app = re_viewer::App::new( + main_thread_token, + re_viewer::build_info(), + &app_env, + startup_options, + cc, + None, + re_viewer::AsyncRuntimeHandle::from_current_tokio_runtime_or_wasmbindgen().expect( + "Could not get a runtime handle from the current Tokio runtime or Wasm bindgen.", + ), + ); + app.add_log_receiver(grpc_rx); + app.add_log_receiver(builtin_recording_rx); + + // Register a custom visualizer for the builtin 3D view. + app.view_class_registry() + .register_visualizer::( + re_types::blueprint::views::Spatial3DView::identifier(), + ) + .unwrap(); + + Box::new(app) + }), + None, + )?; + + Ok(()) +} + +pub fn builtin_recording( +) -> Result, rerun::RecordingStreamError> { + // TODO(andreas): Would be great if there was a log sink that's directly tied to a smartchannel + // so that this could run in the background. + let (rec, memory_sink) = + rerun::RecordingStreamBuilder::new("rerun_example_custom_visualizer").memory()?; + + // Log an entity with two custom ???TODO??. + rec.log_static( + "custom", + // &custom_archetype::Custom::new([[0.0, 0.0, 0.0], [2.0, 2.0, 2.0]]).with_colors([ + // rerun::Color::from_rgb(255, 0, 0), + // rerun::Color::from_rgb(0, 0, 255), + // ]), + &custom_archetype::Custom::new([[0.0, 0.0, 0.0]]) + .with_colors([rerun::Color::from_rgb(255, 0, 0)]), + )?; + + // Log a solid box to demonstrate interaction of the custom ???TODO?? with existing view contents. + rec.log_static( + "box", + &rerun::Boxes3D::from_half_sizes([[0.5, 0.5, 0.5]]) + .with_fill_mode(rerun::FillMode::Solid) + .with_colors([rerun::Color::from_rgb(0, 255, 0)]), + )?; + + // Move things around a little bit. + for i in 0..(std::f32::consts::TAU * 100.0) as i32 { + rec.set_duration_secs("time", i as f32 / 100.0); + rec.log( + "box", + &rerun::Transform3D::from_rotation(glam::Quat::from_rotation_x(i as f32 / 100.0)), + )?; + rec.log( + "custom", + &rerun::Transform3D::from_rotation(glam::Quat::from_rotation_z(i as f32 / 100.0)), + )?; + } + + // Forward the content of the memory recording to a smartchannel. + let (builtin_recording_tx, builtin_recording_rx) = re_smart_channel::smart_channel( + re_smart_channel::SmartMessageSource::Sdk, + re_smart_channel::SmartChannelSource::Sdk, + ); + rec.flush_blocking(); + for msg in memory_sink.take() { + builtin_recording_tx + .send(msg) + .expect("Failed to send message to builtin recording"); + } + + Ok(builtin_recording_rx) +}