diff --git a/Cargo.lock b/Cargo.lock index 02d45a882a63..13b791233470 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3734,6 +3734,18 @@ dependencies = [ "cty", ] +[[package]] +name = "raw_mesh" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "gltf", + "mimalloc", + "reqwest", + "rerun_sdk", +] + [[package]] name = "rawpointer" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index afffc6c5287c..0b2cc0176238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/*", "rerun_py", "run_wasm"] +members = ["crates/*", "rerun_py", "run_wasm", "examples/raw_mesh"] [workspace.package] version = "0.1.0" @@ -31,12 +31,12 @@ polars-core = "0.26" polars-lazy = "0.26" polars-ops = "0.26" puffin = "0.14" +reqwest = { version = "0.11", default-features = false } thiserror = "1.0" tokio = "1.24" wgpu = { version = "0.15", default-features = false } wgpu-core = { version = "0.15", default-features = false } - # Because gltf hasn't published a new version: https://github.com/gltf-rs/gltf/issues/357 gltf = { git = "https://github.com/rerun-io/gltf", rev = "3c14ded73755d1ce9e47010edb06db63cb7e2cca" } diff --git a/crates/re_analytics/Cargo.toml b/crates/re_analytics/Cargo.toml index 6b3da0cd13f6..ff66ab5341b3 100644 --- a/crates/re_analytics/Cargo.toml +++ b/crates/re_analytics/Cargo.toml @@ -25,10 +25,7 @@ derive_more.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] directories-next = "2" -reqwest = { version = "0.11", default-features = false, features = [ - "blocking", - "rustls-tls", -] } +reqwest = { workspace = true, features = ["blocking", "rustls-tls"] } [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3.58", features = ["Storage"] } diff --git a/crates/re_log_types/src/component_types/mesh3d.rs b/crates/re_log_types/src/component_types/mesh3d.rs index 5a69b40ca053..1208567f23a4 100644 --- a/crates/re_log_types/src/component_types/mesh3d.rs +++ b/crates/re_log_types/src/component_types/mesh3d.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use arrow2::array::{FixedSizeBinaryArray, MutableFixedSizeBinaryArray}; use arrow2::datatypes::DataType; use arrow2_convert::arrow_enable_vec_for_type; @@ -72,25 +74,113 @@ impl ArrowDeserialize for MeshId { // ---------------------------------------------------------------------------- -// TODO(#749) Re-enable `RawMesh3D` -// These seem totally unused at the moment and not even supported by the SDK -#[derive(Clone, Debug, PartialEq)] +// TODO(cmc): Let's make both mesh Component types use friendlier types for their inner elements +// (e.g. positions should be a vec of Vec3D, transform should be a Mat4, etc). +// This will also make error checking for invalid user data much nicer. +// +// But first let's do the python example and see how everything starts to take shape... + +// TODO(cmc): Let's move all the RefCounting stuff to the top-level. + +/// A raw "triangle soup" mesh. +/// +/// ``` +/// # use re_log_types::component_types::RawMesh3D; +/// # use arrow2_convert::field::ArrowField; +/// # use arrow2::datatypes::{DataType, Field, UnionMode}; +/// assert_eq!( +/// RawMesh3D::data_type(), +/// DataType::Struct(vec![ +/// Field::new("mesh_id", DataType::FixedSizeBinary(16), false), +/// Field::new("positions", DataType::List(Box::new( +/// Field::new("item", DataType::Float32, false)), +/// ), false), +/// Field::new("indices", DataType::List(Box::new( +/// Field::new("item", DataType::UInt32, false)), +/// ), true), +/// Field::new("normals", DataType::List(Box::new( +/// Field::new("item", DataType::Float32, false)), +/// ), true), +/// ]), +/// ); +/// ``` +#[derive(ArrowField, ArrowSerialize, ArrowDeserialize, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct RawMesh3D { pub mesh_id: MeshId, - pub positions: Vec<[f32; 3]>, - pub indices: Vec<[u32; 3]>, + /// The flattened positions array of this mesh. + /// + /// Meshes are always triangle lists, i.e. the length of this vector should always be + /// divisible by 3. + pub positions: Vec, + /// Optionally, the flattened indices array for this mesh. + /// + /// Meshes are always triangle lists, i.e. the length of this vector should always be + /// divisible by 3. + pub indices: Option>, + /// Optionally, the flattened normals array for this mesh. + /// + /// If specified, this must match the length of `Self::positions`. + pub normals: Option>, + // TODO(cmc): We need to support vertex colors and/or texturing, otherwise it's pretty + // hard to see anything with complex enough meshes (and hovering doesn't really help + // when everything's white). + // pub colors: Option>, + // pub texcoords: Option>, +} + +impl RawMesh3D { + pub fn sanity_check(&self) { + assert!(self.positions.len() % 3 == 0); + if let Some(indices) = &self.indices { + assert!(indices.len() % 3 == 0); + } + if let Some(normals) = &self.normals { + assert!(normals.len() == self.positions.len()); + } + } + + pub fn num_triangles(&self) -> usize { + // TODO(cmc): will need to properly expose actual mesh-related errors all the way down to + // the SDKs soon, but right now this will do. + #[cfg(debug_assertions)] + self.sanity_check(); + + self.positions.len() / 3 + } } // ---------------------------------------------------------------------------- /// Compressed/encoded mesh format +/// +/// ``` +/// # use re_log_types::component_types::EncodedMesh3D; +/// # use arrow2_convert::field::ArrowField; +/// # use arrow2::datatypes::{DataType, Field, UnionMode}; +/// assert_eq!( +/// EncodedMesh3D::data_type(), +/// DataType::Struct(vec![ +/// Field::new("mesh_id", DataType::FixedSizeBinary(16), false), +/// Field::new("format", DataType::Union(vec![ +/// Field::new("Gltf", DataType::Boolean, false), +/// Field::new("Glb", DataType::Boolean, false), +/// Field::new("Obj", DataType::Boolean, false), +/// ], None, UnionMode::Dense), false), +/// Field::new("bytes", DataType::Binary, false), +/// Field::new("transform", DataType::FixedSizeList( +/// Box::new(Field::new("item", DataType::Float32, false)), +/// 12, +/// ), false), +/// ]), +/// ); +/// ``` #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct EncodedMesh3D { pub mesh_id: MeshId, pub format: MeshFormat, - pub bytes: std::sync::Arc<[u8]>, + pub bytes: Arc<[u8]>, /// four columns of an affine transformation matrix pub transform: [[f32; 3]; 4], } @@ -202,47 +292,20 @@ impl std::fmt::Display for MeshFormat { } } -/// A Generic 3D Mesh +/// A Generic 3D Mesh. +/// +/// Cheaply clonable as it is all refcounted internally. /// /// ``` -/// # use re_log_types::component_types::Mesh3D; +/// # use re_log_types::component_types::{Mesh3D, EncodedMesh3D, RawMesh3D}; /// # use arrow2_convert::field::ArrowField; /// # use arrow2::datatypes::{DataType, Field, UnionMode}; /// assert_eq!( /// Mesh3D::data_type(), -/// DataType::Union( -/// vec![Field::new( -/// "Encoded", -/// DataType::Struct(vec![ -/// Field::new("mesh_id", DataType::FixedSizeBinary(16), false), -/// Field::new( -/// "format", -/// DataType::Union( -/// vec![ -/// Field::new("Gltf", DataType::Boolean, false), -/// Field::new("Glb", DataType::Boolean, false), -/// Field::new("Obj", DataType::Boolean, false) -/// ], -/// None, -/// UnionMode::Dense -/// ), -/// false -/// ), -/// Field::new("bytes", DataType::Binary, false), -/// Field::new( -/// "transform", -/// DataType::FixedSizeList( -/// Box::new(Field::new("item", DataType::Float32, false)), -/// 12 -/// ), -/// false -/// ) -/// ]), -/// false -/// )], -/// None, -/// UnionMode::Dense -/// ) +/// DataType::Union(vec![ +/// Field::new("Encoded", EncodedMesh3D::data_type(), false), +/// Field::new("Raw", RawMesh3D::data_type(), false), +/// ], None, UnionMode::Dense), /// ); /// ``` #[derive(Clone, Debug, PartialEq, ArrowField, ArrowSerialize, ArrowDeserialize)] @@ -250,8 +313,7 @@ impl std::fmt::Display for MeshFormat { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum Mesh3D { Encoded(EncodedMesh3D), - // TODO(#749) Re-enable `RawMesh3D` - // Raw(Arc), + Raw(RawMesh3D), } impl Component for Mesh3D { @@ -264,32 +326,44 @@ impl Mesh3D { pub fn mesh_id(&self) -> MeshId { match self { Mesh3D::Encoded(mesh) => mesh.mesh_id, - // TODO(#749) Re-enable `RawMesh3D` - // Mesh3D::Raw(mesh) => mesh.mesh_id, + Mesh3D::Raw(mesh) => mesh.mesh_id, } } } -#[test] -fn test_datatype() {} - #[test] fn test_mesh_roundtrip() { use arrow2::array::Array; use arrow2_convert::{deserialize::TryIntoCollection, serialize::TryIntoArrow}; - let mesh_in = vec![Mesh3D::Encoded(EncodedMesh3D { - mesh_id: MeshId::random(), - format: MeshFormat::Glb, - bytes: std::sync::Arc::new([5, 9, 13, 95, 38, 42, 98, 17]), - transform: [ - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0], - [7.0, 8.0, 9.0], - [10.0, 11.0, 12.], - ], - })]; - let array: Box = mesh_in.try_into_arrow().unwrap(); - let mesh_out: Vec = TryIntoCollection::try_into_collection(array).unwrap(); - assert_eq!(mesh_in, mesh_out); + // Encoded + { + let mesh_in = vec![Mesh3D::Encoded(EncodedMesh3D { + mesh_id: MeshId::random(), + format: MeshFormat::Glb, + bytes: std::sync::Arc::new([5, 9, 13, 95, 38, 42, 98, 17]), + transform: [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0], + [10.0, 11.0, 12.], + ], + })]; + let array: Box = mesh_in.try_into_arrow().unwrap(); + let mesh_out: Vec = TryIntoCollection::try_into_collection(array).unwrap(); + assert_eq!(mesh_in, mesh_out); + } + + // Raw + { + let mesh_in = vec![Mesh3D::Raw(RawMesh3D { + mesh_id: MeshId::random(), + positions: vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 9.0, 10.0], + indices: vec![1, 2, 3].into(), + normals: vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 80.0, 90.0, 100.0].into(), + })]; + let array: Box = mesh_in.try_into_arrow().unwrap(); + let mesh_out: Vec = TryIntoCollection::try_into_collection(array).unwrap(); + assert_eq!(mesh_in, mesh_out); + } } diff --git a/crates/re_log_types/src/component_types/size.rs b/crates/re_log_types/src/component_types/size.rs index fb4ba63d84e9..640401955138 100644 --- a/crates/re_log_types/src/component_types/size.rs +++ b/crates/re_log_types/src/component_types/size.rs @@ -2,6 +2,7 @@ use arrow2_convert::{ArrowDeserialize, ArrowField, ArrowSerialize}; use crate::msg_bundle::Component; +// TODO(cmc): should just embed a Vec3D? #[derive(Debug, ArrowField, ArrowSerialize, ArrowDeserialize)] pub struct Size3D { pub x: f32, diff --git a/crates/re_log_types/src/component_types/vec.rs b/crates/re_log_types/src/component_types/vec.rs index e819b8eea765..c9baa1d6aa6d 100644 --- a/crates/re_log_types/src/component_types/vec.rs +++ b/crates/re_log_types/src/component_types/vec.rs @@ -3,6 +3,8 @@ use arrow2_convert::{ArrowDeserialize, ArrowField, ArrowSerialize}; use super::FixedSizeArrayField; use crate::msg_bundle::Component; +// --- Vec2D --- + /// A vector in 2D space. /// /// ``` @@ -71,6 +73,8 @@ impl From for Vec2D { } } +// --- Vec3D --- + /// A vector in 3D space. /// /// ``` @@ -145,6 +149,8 @@ impl From for Vec3D { } } +// --- Vec4D --- + /// A vector in 4D space. /// /// ``` diff --git a/crates/re_log_types/src/path/entity_path.rs b/crates/re_log_types/src/path/entity_path.rs index a6abc3f01225..259c7332118b 100644 --- a/crates/re_log_types/src/path/entity_path.rs +++ b/crates/re_log_types/src/path/entity_path.rs @@ -126,6 +126,16 @@ impl EntityPath { pub fn parent(&self) -> Option { self.path.parent().map(Self::from) } + + pub fn join(&self, other: &Self) -> Self { + self.iter().chain(other.iter()).cloned().collect() + } +} + +impl FromIterator for EntityPath { + fn from_iter>(parts: T) -> Self { + Self::new(parts.into_iter().collect()) + } } impl From for EntityPath { diff --git a/crates/re_renderer/src/mesh.rs b/crates/re_renderer/src/mesh.rs index 2f26a41f22e7..bc08f6cc6548 100644 --- a/crates/re_renderer/src/mesh.rs +++ b/crates/re_renderer/src/mesh.rs @@ -23,7 +23,7 @@ pub mod mesh_vertices { /// Mesh vertex as used in gpu residing vertex buffers. #[repr(C, packed)] - #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] + #[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] pub struct MeshVertexData { pub normal: glam::Vec3, // TODO(andreas): Compress. Afaik Octahedral Mapping is the best by far, see https://jcgt.org/published/0003/02/01/ pub texcoord: glam::Vec2, diff --git a/crates/re_viewer/src/misc/mesh_loader.rs b/crates/re_viewer/src/misc/mesh_loader.rs index 528d3e8fb4f8..0b51629764cf 100644 --- a/crates/re_viewer/src/misc/mesh_loader.rs +++ b/crates/re_viewer/src/misc/mesh_loader.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use re_log_types::{EncodedMesh3D, Mesh3D, MeshFormat, RawMesh3D}; use re_renderer::{resource_managers::ResourceLifeTime, RenderContext}; @@ -19,13 +20,12 @@ impl LoadedMesh { ) -> anyhow::Result { // TODO(emilk): load CpuMesh in background thread. match mesh { - // Mesh from user logging some triangles. + // Mesh from some file format. File passed in bytes. Mesh3D::Encoded(encoded_mesh) => { Self::load_encoded_mesh(name, encoded_mesh, render_ctx) } - // Mesh from some file format. File passed in bytes. - // TODO(#749) Re-enable `RawMesh3D` - //Mesh3D::Raw(raw_mesh) => Ok(Self::load_raw_mesh(name, raw_mesh, render_ctx)?), + // Mesh from user logging some triangles. + Mesh3D::Raw(raw_mesh) => Ok(Self::load_raw_mesh(name, raw_mesh, render_ctx)?), } } @@ -71,6 +71,7 @@ impl LoadedMesh { let mut slf = Self::load_raw(name, *format, bytes, render_ctx)?; + // TODO(cmc): Why are we creating the matrix twice here? let (scale, rotation, translation) = glam::Affine3A::from_cols_array_2d(transform).to_scale_rotation_translation(); let transform = @@ -83,8 +84,6 @@ impl LoadedMesh { Ok(slf) } - // TODO(#749) Re-enable `RawMesh3D` - #[allow(dead_code)] fn load_raw_mesh( name: String, raw_mesh: &RawMesh3D, @@ -92,9 +91,49 @@ impl LoadedMesh { ) -> anyhow::Result { crate::profile_function!(); - let bbox = macaw::BoundingBox::from_points( - raw_mesh.positions.iter().map(|p| glam::Vec3::from(*p)), - ); + // TODO(cmc): Having to do all of these data conversions, copies and allocations doesn't + // really make sense when you consider that both the component and the renderer are native + // Rust. Need to clean all of that up later. + + let RawMesh3D { + mesh_id: _, + positions, + indices, + normals, + } = raw_mesh; + + let positions: Vec = + bytemuck::try_cast_vec(positions.clone()).map_err(|(err, _)| anyhow!(err))?; + let nb_positions = positions.len(); + + let indices = if let Some(indices) = indices { + indices.clone() + } else { + (0..positions.len() as u32).collect() + }; + let nb_indices = indices.len(); + + let normals = if let Some(normals) = normals { + normals + .chunks_exact(3) + .map(|v| glam::Vec3::from([v[0], v[1], v[2]])) + .map(|normal| re_renderer::mesh::mesh_vertices::MeshVertexData { + normal, + texcoord: glam::Vec2::ZERO, + }) + .collect::>() + } else { + // TODO(andreas): Calculate normals + // TODO(cmc): support colored and/or textured raw meshes + std::iter::repeat(re_renderer::mesh::mesh_vertices::MeshVertexData { + normal: glam::Vec3::ZERO, + texcoord: glam::Vec2::ZERO, + }) + .take(nb_positions) + .collect() + }; + + let bbox = macaw::BoundingBox::from_points(positions.iter().copied()); let mesh_instances = vec![re_renderer::renderer::MeshInstance { gpu_mesh: render_ctx.mesh_manager.create( @@ -102,24 +141,12 @@ impl LoadedMesh { &render_ctx.texture_manager_2d, &re_renderer::mesh::Mesh { label: name.clone().into(), - indices: raw_mesh.indices.iter().flatten().cloned().collect(), - vertex_positions: raw_mesh - .positions - .iter() - .map(|p| glam::Vec3::from(*p)) - .collect(), - // TODO(andreas): Calculate normals - vertex_data: std::iter::repeat( - re_renderer::mesh::mesh_vertices::MeshVertexData { - normal: glam::Vec3::ZERO, - texcoord: glam::Vec2::ZERO, - }, - ) - .take(raw_mesh.positions.len()) - .collect(), + indices, + vertex_positions: positions, + vertex_data: normals, materials: smallvec::smallvec![re_renderer::mesh::Material { label: name.clone().into(), - index_range: 0..raw_mesh.indices.len() as _, + index_range: 0..nb_indices as _, albedo: render_ctx.texture_manager_2d.white_texture_handle().clone(), albedo_multiplier: re_renderer::Rgba::WHITE, }], diff --git a/crates/re_viewer/src/ui/data_ui/component_ui_registry.rs b/crates/re_viewer/src/ui/data_ui/component_ui_registry.rs index 528199991331..9a9c32b58835 100644 --- a/crates/re_viewer/src/ui/data_ui/component_ui_registry.rs +++ b/crates/re_viewer/src/ui/data_ui/component_ui_registry.rs @@ -184,6 +184,7 @@ impl DataUi for re_log_types::component_types::Mesh3D { ) { match self { re_log_types::Mesh3D::Encoded(mesh) => mesh.data_ui(ctx, ui, verbosity, query), + re_log_types::Mesh3D::Raw(mesh) => mesh.data_ui(ctx, ui, verbosity, query), } } } @@ -199,3 +200,18 @@ impl DataUi for re_log_types::component_types::EncodedMesh3D { ui.label(format!("{} mesh", self.format)); } } + +impl DataUi for re_log_types::component_types::RawMesh3D { + fn data_ui( + &self, + _ctx: &mut ViewerContext<'_>, + ui: &mut egui::Ui, + _verbosity: UiVerbosity, + _query: &re_arrow_store::LatestAtQuery, + ) { + ui.label(format!( + "mesh ({} triangles)", + re_format::format_number(self.num_triangles()) + )); + } +} diff --git a/crates/rerun_sdk/src/lib.rs b/crates/rerun_sdk/src/lib.rs index bfd71056bc20..b589c64b03ea 100644 --- a/crates/rerun_sdk/src/lib.rs +++ b/crates/rerun_sdk/src/lib.rs @@ -16,3 +16,44 @@ mod global; pub use self::global::global_session; pub mod viewer; + +// TODO(cmc): clean all that up? + +pub use re_log_types::msg_bundle::MsgBundle; +pub use re_log_types::{EntityPath, LogMsg, MsgId}; +pub use re_log_types::{Time, TimePoint, TimeType, Timeline}; + +// TODO(cmc): separate datatypes (e.g. Vec3D) from components (e.g. Size3D). +pub use re_log_types::component_types::AnnotationContext; +pub use re_log_types::component_types::Arrow3D; +pub use re_log_types::component_types::Box3D; +pub use re_log_types::component_types::ClassId; +pub use re_log_types::component_types::ColorRGBA; +pub use re_log_types::component_types::InstanceKey; +pub use re_log_types::component_types::KeypointId; +pub use re_log_types::component_types::Label; +pub use re_log_types::component_types::Mat3x3; +pub use re_log_types::component_types::Quaternion; +pub use re_log_types::component_types::Radius; +pub use re_log_types::component_types::Rect2D; +pub use re_log_types::component_types::Size3D; +pub use re_log_types::component_types::TextEntry; +pub use re_log_types::component_types::{ + coordinates::{Axis3, Handedness, Sign, SignedAxis3}, + ViewCoordinates, +}; +pub use re_log_types::component_types::{EncodedMesh3D, Mesh3D, MeshFormat, MeshId, RawMesh3D}; +pub use re_log_types::component_types::{LineStrip2D, LineStrip3D}; +pub use re_log_types::component_types::{Pinhole, Rigid3, Transform}; +pub use re_log_types::component_types::{Point2D, Point3D}; +pub use re_log_types::component_types::{Scalar, ScalarPlotProps}; +pub use re_log_types::component_types::{ + Tensor, TensorData, TensorDataMeaning, TensorDimension, TensorId, TensorTrait, +}; +pub use re_log_types::component_types::{Vec2D, Vec3D, Vec4D}; + +pub mod reexports { + pub use re_log; + pub use re_log_types; + pub use re_memory; +} diff --git a/examples/raw_mesh/Cargo.toml b/examples/raw_mesh/Cargo.toml new file mode 100644 index 000000000000..0701dbede0be --- /dev/null +++ b/examples/raw_mesh/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "raw_mesh" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[dependencies] +anyhow.workspace = true +bytes = "1.3" +gltf.workspace = true +mimalloc = "0.1" +reqwest = { workspace = true, features = ["blocking", "rustls-tls"] } + +# TODO(cmc): is the crate actually gonna be called just 'rerun' in the end? +rerun = { package = "rerun_sdk", path = "../../crates/rerun_sdk" } diff --git a/examples/raw_mesh/src/main.rs b/examples/raw_mesh/src/main.rs new file mode 100644 index 000000000000..89b98616f7ce --- /dev/null +++ b/examples/raw_mesh/src/main.rs @@ -0,0 +1,294 @@ +//! This example demonstrates how to use the Rerun Rust SDK to log raw 3D meshes (so-called +//! "triangle soups") and their transform hierarchy. +//! +//! Usage: +//! ``` +//! cargo run -p raw_mesh +//! ``` + +#![allow(clippy::doc_markdown)] + +use anyhow::{bail, Context}; +use bytes::Bytes; +use rerun::{ + reexports::{re_log, re_memory::AccountingAllocator}, + EntityPath, LogMsg, Mesh3D, MeshId, MsgBundle, MsgId, RawMesh3D, Session, Time, TimePoint, + TimeType, Timeline, Transform, ViewCoordinates, +}; + +// TODO(cmc): This example needs to support animations to showcase Rerun's time capabilities. + +// --- Rerun logging --- + +// Declare how to turn a glTF primitive into a Rerun component (`Mesh3D`). +impl From for Mesh3D { + fn from(primitive: GltfPrimitive) -> Self { + Mesh3D::Raw(RawMesh3D { + mesh_id: MeshId::random(), + indices: primitive.indices, + positions: primitive.positions.into_iter().flatten().collect(), + normals: primitive + .normals + .map(|normals| normals.into_iter().flatten().collect()), + // TODO(cmc): We need to support vertex colors and/or texturing, otherwise it's pretty + // hard to see anything with complex enough meshes (and hovering doesn't really help + // when everything's white). + // colors: primitive + // .colors + // .map(|colors| colors.into_iter().flatten().collect()), + // texcoords: primitive + // .texcoords + // .map(|texcoords| texcoords.into_iter().flatten().collect()), + }) + } +} + +// Declare how to turn a glTF transform into a Rerun component (`Transform`). +impl From for Transform { + fn from(transform: GltfTransform) -> Self { + Transform::Rigid3(rerun::Rigid3 { + rotation: rerun::Quaternion { + x: transform.r[0], + y: transform.r[1], + z: transform.r[2], + w: transform.r[3], + }, + translation: rerun::Vec3D(transform.t), + }) + } +} + +/// Log a glTF node with Rerun. +fn log_node(session: &mut Session, node: GltfNode) { + let ent_path = EntityPath::from(node.name.as_str()); + + // What time is it? + let timeline_keyframe = Timeline::new("keyframe", TimeType::Sequence); + let time_point = TimePoint::from([ + // TODO(cmc): this _has_ to be inserted by the SDK + (Timeline::log_time(), Time::now().into()), + // TODO(cmc): animations! + (timeline_keyframe, 0.into()), + ]); + + // Convert glTF objects into Rerun components. + let transform = node.transform.map(Transform::from); + let primitives = node + .primitives + .into_iter() + .map(Mesh3D::from) + .collect::>(); + + // TODO(cmc): Transforms have to be logged separately because they are neither batches nor + // splats... the user shouldn't have to know that though! + // The SDK needs to split things up as needed when it sees a Transform component. + // + // We're going to have the same issue with splats: the SDK needs to automagically detect the + // user intention to use splats and do the necessary (like the python SDK does iirc). + if let Some(transform) = transform { + let bundle = MsgBundle::new( + MsgId::random(), + ent_path.clone(), + time_point.clone(), + // TODO(cmc): need to reproduce the viewer crash I had earlier and fix/log-an-issue for + // it. + vec![vec![transform].try_into().unwrap()], + ); + // TODO(cmc): These last conversion details need to be hidden in the SDK. + let msg = bundle.try_into().unwrap(); + session.send(LogMsg::ArrowMsg(msg)); + } + + // TODO(cmc): Working at the `ComponentBundle`/`TryIntoArrow` layer feels too low-level, + // something like a MsgBuilder kinda thing would probably be quite nice. + let bundle = MsgBundle::new( + MsgId::random(), + ent_path, + time_point, + vec![primitives.try_into().unwrap()], + ); + + // Create and send one message to the sdk + // TODO(cmc): These last conversion details need to be hidden in the SDK. + let msg = bundle.try_into().unwrap(); + session.send(LogMsg::ArrowMsg(msg)); + + // Recurse through all of the node's children! + for mut child in node.children { + child.name = [node.name.as_str(), child.name.as_str()].join("/"); + log_node(session, child); + } +} + +// TODO(cmc): The SDK should make this call so trivial that it doesn't require this helper at all. +fn log_axis(session: &mut Session, ent_path: &EntityPath) { + // From the glTF spec: + // > glTF uses a right-handed coordinate system. glTF defines +Y as up, +Z as forward, and + // > -X as right; the front of a glTF asset faces +Z. + let view_coords: ViewCoordinates = "RUB".parse().unwrap(); + + let bundle = MsgBundle::new( + MsgId::random(), + ent_path.clone(), + [].into(), // TODO(cmc): doing timeless stuff shouldn't be so weird + vec![vec![view_coords].try_into().unwrap()], + ); + + let msg = bundle.try_into().unwrap(); + session.send(LogMsg::ArrowMsg(msg)); +} + +// --- Init --- + +// Use MiMalloc as global allocator (because it is fast), wrapped in Rerun's allocation tracker +// so that the rerun viewer can show how much memory it is using when calling `show`. +#[global_allocator] +static GLOBAL: AccountingAllocator = + AccountingAllocator::new(mimalloc::MiMalloc); + +fn main() -> anyhow::Result<()> { + re_log::setup_native_logging(); + + // TODO(cmc): Here we shall pass argv to the SDK which will strip it out of all SDK flags, and + // give us back our actual CLI flags. + // The name of the gltf sample to load should then come from there. + + // Read glTF asset + let args = std::env::args().collect::>(); + let bytes = if let Some(path) = args.get(1) { + Bytes::from(std::fs::read(path)?) + } else { + bail!("Usage: {} ", args[0]); + }; + + // Parse glTF asset + let (doc, buffers, _) = gltf::import_slice(bytes).unwrap(); + let nodes = load_gltf(&doc, &buffers); + + // Log raw glTF nodes and their transforms with Rerun + let mut session = Session::new(); + for root in nodes { + re_log::info!(scene = root.name, "logging glTF scene"); + log_axis(&mut session, &root.name.as_str().into()); + log_node(&mut session, root); + } + + // TODO(cmc): provide high-level tools to pick and handle the different modes. + // TODO(cmc): connect, spawn_and_connect; show() probably doesn't make sense with pure rust + let log_messages = session.drain_log_messages_buffer(); + rerun::viewer::show(log_messages).context("failed to start viewer") +} + +// --- glTF parsing --- + +struct GltfNode { + name: String, + transform: Option, + primitives: Vec, + children: Vec, +} + +struct GltfPrimitive { + positions: Vec<[f32; 3]>, + indices: Option>, + normals: Option>, + #[allow(dead_code)] + colors: Option>, + #[allow(dead_code)] + texcoords: Option>, +} + +struct GltfTransform { + t: [f32; 3], + r: [f32; 4], + #[allow(dead_code)] + s: [f32; 3], +} + +impl GltfNode { + fn from_gltf(buffers: &[gltf::buffer::Data], node: &gltf::Node<'_>) -> Self { + let name = node_name(node); + + let transform = { + let (t, r, s) = node.transform().decomposed(); + GltfTransform { t, r, s } + }; + let primitives = node_primitives(buffers, node).collect(); + + let children = node + .children() + .map(|child| GltfNode::from_gltf(buffers, &child)) + .collect(); + + Self { + name, + transform: Some(transform), + primitives, + children, + } + } +} + +fn node_name(node: &gltf::Node<'_>) -> String { + node.name() + .map_or_else(|| format!("node_#{}", node.index()), ToOwned::to_owned) +} + +fn node_primitives<'data>( + buffers: &'data [gltf::buffer::Data], + node: &'data gltf::Node<'_>, +) -> impl Iterator + 'data { + node.mesh().into_iter().flat_map(|mesh| { + mesh.primitives().map(|primitive| { + assert!(primitive.mode() == gltf::mesh::Mode::Triangles); + + let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); + + let positions = reader.read_positions().unwrap(); + let positions = positions.collect(); + + let indices = reader.read_indices(); + let indices = indices.map(|indices| indices.into_u32().into_iter().collect()); + + let normals = reader.read_normals(); + let normals = normals.map(|normals| normals.collect()); + + let colors = reader.read_colors(0); // TODO(cmc): pick correct set + let colors = colors.map(|colors| colors.into_rgba_u8().collect()); + + let texcoords = reader.read_tex_coords(0); // TODO(cmc): pick correct set + let texcoords = texcoords.map(|texcoords| texcoords.into_f32().collect()); + + GltfPrimitive { + positions, + indices, + normals, + colors, + texcoords, + } + }) + }) +} + +fn load_gltf<'data>( + doc: &'data gltf::Document, + buffers: &'data [gltf::buffer::Data], +) -> impl Iterator + 'data { + doc.scenes().map(move |scene| { + let name = scene + .name() + .map_or_else(|| format!("scene_#{}", scene.index()), ToOwned::to_owned); + + re_log::info!(scene = name, "parsing glTF scene"); + + GltfNode { + name, + transform: None, + primitives: Default::default(), + children: scene + .nodes() + .map(|node| GltfNode::from_gltf(buffers, &node)) + .collect(), + } + }) +}