diff --git a/crates/store/re_types/definitions/rerun/archetypes/asset3d.fbs b/crates/store/re_types/definitions/rerun/archetypes/asset3d.fbs index 2018e21dcdfa..b0625fbf2ff6 100644 --- a/crates/store/re_types/definitions/rerun/archetypes/asset3d.fbs +++ b/crates/store/re_types/definitions/rerun/archetypes/asset3d.fbs @@ -33,4 +33,12 @@ table Asset3D ( /// If omitted, the viewer will try to guess from the data blob. /// If it cannot guess, it won't be able to render the asset. media_type: rerun.components.MediaType ("attr.rerun.component_recommended", nullable, order: 2000); + + // --- Optional --- + + /// A color multiplier applied to the whole asset. + /// + /// For mesh who already have `albedo_factor` in materials, + /// it will be overwritten by actual `albedo_factor` of [archetypes.Asset3D] (if specified). + albedo_factor: rerun.components.AlbedoFactor ("attr.rerun.component_optional", nullable, order: 3100); } diff --git a/crates/store/re_types/src/archetypes/asset3d.rs b/crates/store/re_types/src/archetypes/asset3d.rs index ab48155cc121..5983dff798c7 100644 --- a/crates/store/re_types/src/archetypes/asset3d.rs +++ b/crates/store/re_types/src/archetypes/asset3d.rs @@ -70,17 +70,27 @@ pub struct Asset3D { /// If omitted, the viewer will try to guess from the data blob. /// If it cannot guess, it won't be able to render the asset. pub media_type: Option, + + /// A color multiplier applied to the whole asset. + /// + /// For mesh who already have `albedo_factor` in materials, + /// it will be overwritten by actual `albedo_factor` of [`archetypes::Asset3D`][crate::archetypes::Asset3D] (if specified). + pub albedo_factor: Option, } impl ::re_types_core::SizeBytes for Asset3D { #[inline] fn heap_size_bytes(&self) -> u64 { - self.blob.heap_size_bytes() + self.media_type.heap_size_bytes() + self.blob.heap_size_bytes() + + self.media_type.heap_size_bytes() + + self.albedo_factor.heap_size_bytes() } #[inline] fn is_pod() -> bool { - ::is_pod() && >::is_pod() + ::is_pod() + && >::is_pod() + && >::is_pod() } } @@ -95,21 +105,22 @@ static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 2usize]> = ] }); -static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 0usize]> = - once_cell::sync::Lazy::new(|| []); +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = + once_cell::sync::Lazy::new(|| ["rerun.components.AlbedoFactor".into()]); -static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 3usize]> = +static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 4usize]> = once_cell::sync::Lazy::new(|| { [ "rerun.components.Blob".into(), "rerun.components.MediaType".into(), "rerun.components.Asset3DIndicator".into(), + "rerun.components.AlbedoFactor".into(), ] }); impl Asset3D { - /// The total number of components in the archetype: 1 required, 2 recommended, 0 optional - pub const NUM_COMPONENTS: usize = 3usize; + /// The total number of components in the archetype: 1 required, 2 recommended, 1 optional + pub const NUM_COMPONENTS: usize = 4usize; } /// Indicator component for the [`Asset3D`] [`::re_types_core::Archetype`] @@ -186,7 +197,21 @@ impl ::re_types_core::Archetype for Asset3D { } else { None }; - Ok(Self { blob, media_type }) + let albedo_factor = if let Some(array) = arrays_by_name.get("rerun.components.AlbedoFactor") + { + ::from_arrow_opt(&**array) + .with_context("rerun.archetypes.Asset3D#albedo_factor")? + .into_iter() + .next() + .flatten() + } else { + None + }; + Ok(Self { + blob, + media_type, + albedo_factor, + }) } } @@ -200,6 +225,9 @@ impl ::re_types_core::AsComponents for Asset3D { self.media_type .as_ref() .map(|comp| (comp as &dyn ComponentBatch).into()), + self.albedo_factor + .as_ref() + .map(|comp| (comp as &dyn ComponentBatch).into()), ] .into_iter() .flatten() @@ -216,6 +244,7 @@ impl Asset3D { Self { blob: blob.into(), media_type: None, + albedo_factor: None, } } @@ -234,4 +263,17 @@ impl Asset3D { self.media_type = Some(media_type.into()); self } + + /// A color multiplier applied to the whole asset. + /// + /// For mesh who already have `albedo_factor` in materials, + /// it will be overwritten by actual `albedo_factor` of [`archetypes::Asset3D`][crate::archetypes::Asset3D] (if specified). + #[inline] + pub fn with_albedo_factor( + mut self, + albedo_factor: impl Into, + ) -> Self { + self.albedo_factor = Some(albedo_factor.into()); + self + } } diff --git a/crates/store/re_types/src/archetypes/asset3d_ext.rs b/crates/store/re_types/src/archetypes/asset3d_ext.rs index 78fd8a78f998..985ecfcb738f 100644 --- a/crates/store/re_types/src/archetypes/asset3d_ext.rs +++ b/crates/store/re_types/src/archetypes/asset3d_ext.rs @@ -36,6 +36,7 @@ impl Asset3D { Self { blob: contents.into(), media_type, + albedo_factor: None, } } } diff --git a/crates/store/re_types/tests/types/asset3d.rs b/crates/store/re_types/tests/types/asset3d.rs index 415ea3b81ea5..bfc4e64e8f38 100644 --- a/crates/store/re_types/tests/types/asset3d.rs +++ b/crates/store/re_types/tests/types/asset3d.rs @@ -1,7 +1,7 @@ use re_types::{ archetypes::Asset3D, components::{Blob, MediaType}, - datatypes::Utf8, + datatypes::{Rgba32, Utf8}, Archetype as _, AsComponents as _, }; @@ -12,9 +12,11 @@ fn roundtrip() { let expected = Asset3D { blob: Blob(BYTES.to_vec().into()), media_type: Some(MediaType(Utf8(MediaType::GLTF.into()))), + albedo_factor: Some(Rgba32::from_unmultiplied_rgba(0xEE, 0x11, 0x22, 0x33).into()), }; - let arch = Asset3D::from_file_contents(BYTES.to_vec(), Some(MediaType::gltf())); + let arch = Asset3D::from_file_contents(BYTES.to_vec(), Some(MediaType::gltf())) + .with_albedo_factor(0xEE112233); similar_asserts::assert_eq!(expected, arch); // let expected_extensions: HashMap<_, _> = [ diff --git a/crates/viewer/re_renderer/src/importer/stl.rs b/crates/viewer/re_renderer/src/importer/stl.rs index 51cd42c4f669..57d0b714c9d6 100644 --- a/crates/viewer/re_renderer/src/importer/stl.rs +++ b/crates/viewer/re_renderer/src/importer/stl.rs @@ -31,7 +31,7 @@ pub fn load_stl_from_buffer( let num_vertices = triangles.len() * 3; let material = mesh::Material { - label: "default material".into(), + label: name.clone().into(), index_range: 0..num_vertices as u32, albedo: ctx.texture_manager_2d.white_texture_unorm_handle().clone(), albedo_factor: crate::Rgba::WHITE, diff --git a/crates/viewer/re_space_view_spatial/src/mesh_cache.rs b/crates/viewer/re_space_view_spatial/src/mesh_cache.rs index 3aeb3d10d957..0baafbc4bb4d 100644 --- a/crates/viewer/re_space_view_spatial/src/mesh_cache.rs +++ b/crates/viewer/re_space_view_spatial/src/mesh_cache.rs @@ -36,7 +36,9 @@ pub struct MeshCache(HashMap /// Either a [`re_types::archetypes::Asset3D`] or [`re_types::archetypes::Mesh3D`] to be cached. #[derive(Debug, Clone, Copy)] pub enum AnyMesh<'a> { - Asset(&'a re_types::archetypes::Asset3D), + Asset { + asset: &'a re_types::archetypes::Asset3D, + }, Mesh { mesh: &'a re_types::archetypes::Mesh3D, diff --git a/crates/viewer/re_space_view_spatial/src/mesh_loader.rs b/crates/viewer/re_space_view_spatial/src/mesh_loader.rs index 39ac2757ca87..737ca00317ff 100644 --- a/crates/viewer/re_space_view_spatial/src/mesh_loader.rs +++ b/crates/viewer/re_space_view_spatial/src/mesh_loader.rs @@ -3,7 +3,7 @@ use re_chunk_store::RowId; use re_renderer::{mesh::GpuMesh, RenderContext, Rgba32Unmul}; use re_types::{ archetypes::{Asset3D, Mesh3D}, - components::MediaType, + components::{AlbedoFactor, MediaType}, }; use re_viewer_context::{gpu_bridge::texture_creation_desc_from_color_image, ImageInfo}; @@ -27,7 +27,7 @@ impl LoadedMesh { ) -> anyhow::Result { // TODO(emilk): load CpuMesh in background thread. match mesh { - AnyMesh::Asset(asset3d) => Self::load_asset3d(name, asset3d, render_ctx), + AnyMesh::Asset { asset } => Ok(Self::load_asset3d(name, asset, render_ctx)?), AnyMesh::Mesh { mesh, texture_key } => { Ok(Self::load_mesh3d(name, mesh, texture_key, render_ctx)?) } @@ -39,10 +39,11 @@ impl LoadedMesh { media_type: &MediaType, bytes: &[u8], render_ctx: &RenderContext, + albedo_factor: &Option, ) -> anyhow::Result { re_tracing::profile_function!(); - let cpu_model = match media_type.as_str() { + let mut cpu_model = match media_type.as_str() { MediaType::GLTF | MediaType::GLB => { re_renderer::importer::gltf::load_gltf_from_buffer(&name, bytes, render_ctx)? } @@ -51,6 +52,12 @@ impl LoadedMesh { _ => anyhow::bail!("{media_type} files are not supported"), }; + // Overwriting albedo_factor of CpuMesh in specified in the Asset3D + cpu_model.instances.iter().for_each(|instance| { + cpu_model.meshes[instance.mesh].materials[0].albedo_factor = + albedo_factor.map_or(re_renderer::Rgba::WHITE, |c| c.0.into()); + }); + let bbox = cpu_model.calculate_bounding_box(); let mesh_instances = cpu_model.into_gpu_meshes(render_ctx)?; @@ -68,11 +75,21 @@ impl LoadedMesh { ) -> anyhow::Result { re_tracing::profile_function!(); - let Asset3D { blob, media_type } = asset3d; + let Asset3D { + blob, + media_type, + albedo_factor, + } = asset3d; let media_type = MediaType::or_guess_from_data(media_type.clone(), blob.as_slice()) .ok_or_else(|| anyhow::anyhow!("couldn't guess media type"))?; - let slf = Self::load_asset3d_parts(name, &media_type, blob.as_slice(), render_ctx)?; + let slf = Self::load_asset3d_parts( + name, + &media_type, + blob.as_slice(), + render_ctx, + albedo_factor, + )?; Ok(slf) } diff --git a/crates/viewer/re_space_view_spatial/src/visualizers/assets3d.rs b/crates/viewer/re_space_view_spatial/src/visualizers/assets3d.rs index a80a38d8e112..50c72a4c07a1 100644 --- a/crates/viewer/re_space_view_spatial/src/visualizers/assets3d.rs +++ b/crates/viewer/re_space_view_spatial/src/visualizers/assets3d.rs @@ -4,7 +4,7 @@ use re_renderer::renderer::GpuMeshInstance; use re_renderer::RenderContext; use re_types::{ archetypes::Asset3D, - components::{Blob, MediaType}, + components::{AlbedoFactor, Blob, MediaType}, ArrowBuffer, ArrowString, Loggable as _, }; use re_viewer_context::{ @@ -32,32 +32,29 @@ impl Default for Asset3DVisualizer { } } -struct Asset3DComponentData { +struct Asset3DComponentData<'a> { index: (TimeInt, RowId), + query_result_hash: Hash64, blob: ArrowBuffer, media_type: Option, + albedo_factor: Option<&'a AlbedoFactor>, } // NOTE: Do not put profile scopes in these methods. They are called for all entities and all // timestamps within a time range -- it's _a lot_. impl Asset3DVisualizer { - fn process_data( + fn process_data<'a>( &mut self, ctx: &QueryContext<'_>, render_ctx: &RenderContext, instances: &mut Vec, ent_context: &SpatialSceneEntityContext<'_>, - data: impl Iterator, + data: impl Iterator>, ) { let entity_path = ctx.target_entity_path; for data in data { - let mesh = Asset3D { - blob: data.blob.clone().into(), - media_type: data.media_type.clone().map(Into::into), - }; - let primary_row_id = data.index.1; let picking_instance_hash = re_entity_db::InstancePathHash::entity_all(entity_path); let outline_mask_ids = ent_context.highlight.index_outline_mask(Instance::ALL); @@ -65,15 +62,22 @@ impl Asset3DVisualizer { // TODO(#5974): this is subtly wrong, the key should actually be a hash of everything that got // cached, which includes the media type… let mesh = ctx.viewer_ctx.cache.entry(|c: &mut MeshCache| { + let key = MeshCacheKey { + versioned_instance_path_hash: picking_instance_hash.versioned(primary_row_id), + query_result_hash: data.query_result_hash, + media_type: data.media_type.clone().map(Into::into), + }; + c.entry( &entity_path.to_string(), - MeshCacheKey { - versioned_instance_path_hash: picking_instance_hash - .versioned(primary_row_id), - query_result_hash: Hash64::ZERO, - media_type: data.media_type.clone().map(Into::into), + key.clone(), + AnyMesh::Asset { + asset: &Asset3D { + blob: data.blob.clone().into(), + media_type: data.media_type.clone().map(Into::into), + albedo_factor: data.albedo_factor.copied(), + }, }, - AnyMesh::Asset(&mesh), render_ctx, ) }); @@ -154,16 +158,29 @@ impl VisualizerSystem for Asset3DVisualizer { let timeline = ctx.query.timeline(); let all_blobs_indexed = iter_buffer::(&all_blob_chunks, timeline, Blob::name()); let all_media_types = results.iter_as(timeline, MediaType::name()); + let all_albedo_factors = results.iter_as(timeline, AlbedoFactor::name()); + + let query_result_hash = results.query_result_hash(); - let data = re_query::range_zip_1x1(all_blobs_indexed, all_media_types.string()) - .filter_map(|(index, blobs, media_types)| { - blobs.first().map(|blob| Asset3DComponentData { - index, - blob: blob.clone(), - media_type: media_types - .and_then(|media_types| media_types.first().cloned()), - }) - }); + let data = re_query::range_zip_1x2( + all_blobs_indexed, + all_media_types.string(), + all_albedo_factors.primitive::(), + ) + .filter_map(|(index, blobs, media_types, albedo_factors)| { + blobs.first().map(|blob| Asset3DComponentData { + index, + query_result_hash, + blob: blob.clone(), + media_type: media_types + .and_then(|media_types| media_types.first().cloned()), + albedo_factor: albedo_factors + .map_or(&[] as &[AlbedoFactor], |albedo_factors| { + bytemuck::cast_slice(albedo_factors) + }) + .first(), + }) + }); self.process_data(ctx, render_ctx, &mut instances, spatial_ctx, data); diff --git a/crates/viewer/re_viewer/src/reflection/mod.rs b/crates/viewer/re_viewer/src/reflection/mod.rs index 563b56f5f1de..245825156a81 100644 --- a/crates/viewer/re_viewer/src/reflection/mod.rs +++ b/crates/viewer/re_viewer/src/reflection/mod.rs @@ -808,6 +808,10 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { "rerun.components.MediaType".into(), display_name : "Media type", docstring_md : "The Media Type of the asset.\n\nSupported values:\n* `model/gltf-binary`\n* `model/gltf+json`\n* `model/obj` (.mtl material files are not supported yet, references are silently ignored)\n* `model/stl`\n\nIf omitted, the viewer will try to guess from the data blob.\nIf it cannot guess, it won't be able to render the asset.", + is_required : false, }, ArchetypeFieldReflection { component_name : + "rerun.components.AlbedoFactor".into(), display_name : + "Albedo factor", docstring_md : + "A color multiplier applied to the whole asset.\n\nFor mesh who already have `albedo_factor` in materials,\nit will be overwritten by actual `albedo_factor` of [`archetypes.Asset3D`](https://rerun.io/docs/reference/types/archetypes/asset3d) (if specified).", is_required : false, }, ], }, diff --git a/docs/content/reference/types/archetypes/asset3d.md b/docs/content/reference/types/archetypes/asset3d.md index 145fb8cfa15d..ed8b246601db 100644 --- a/docs/content/reference/types/archetypes/asset3d.md +++ b/docs/content/reference/types/archetypes/asset3d.md @@ -16,6 +16,8 @@ an instance of the mesh will be drawn for each transform. **Recommended**: [`MediaType`](../components/media_type.md) +**Optional**: [`AlbedoFactor`](../components/albedo_factor.md) + ## Shown in * [Spatial3DView](../views/spatial3d_view.md) * [Spatial2DView](../views/spatial2d_view.md) (if logged above active projection) diff --git a/docs/content/reference/types/components/albedo_factor.md b/docs/content/reference/types/components/albedo_factor.md index 179ee779f786..ff8200054a4f 100644 --- a/docs/content/reference/types/components/albedo_factor.md +++ b/docs/content/reference/types/components/albedo_factor.md @@ -17,4 +17,5 @@ A color multiplier, usually applied to a whole entity, e.g. a mesh. ## Used by +* [`Asset3D`](../archetypes/asset3d.md) * [`Mesh3D`](../archetypes/mesh3d.md) diff --git a/rerun_cpp/src/rerun/archetypes/asset3d.cpp b/rerun_cpp/src/rerun/archetypes/asset3d.cpp index 4466eb0eb17b..99f9341f8359 100644 --- a/rerun_cpp/src/rerun/archetypes/asset3d.cpp +++ b/rerun_cpp/src/rerun/archetypes/asset3d.cpp @@ -14,7 +14,7 @@ namespace rerun { ) { using namespace archetypes; std::vector cells; - cells.reserve(3); + cells.reserve(4); { auto result = ComponentBatch::from_loggable(archetype.blob); @@ -26,6 +26,11 @@ namespace rerun { RR_RETURN_NOT_OK(result.error); cells.push_back(std::move(result.value)); } + if (archetype.albedo_factor.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.albedo_factor.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } { auto indicator = Asset3D::IndicatorComponent(); auto result = ComponentBatch::from_loggable(indicator); diff --git a/rerun_cpp/src/rerun/archetypes/asset3d.hpp b/rerun_cpp/src/rerun/archetypes/asset3d.hpp index 812cf71eacb9..05ee83b58192 100644 --- a/rerun_cpp/src/rerun/archetypes/asset3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/asset3d.hpp @@ -6,6 +6,7 @@ #include "../collection.hpp" #include "../compiler_utils.hpp" #include "../component_batch.hpp" +#include "../components/albedo_factor.hpp" #include "../components/blob.hpp" #include "../components/media_type.hpp" #include "../indicator_component.hpp" @@ -66,6 +67,12 @@ namespace rerun::archetypes { /// If it cannot guess, it won't be able to render the asset. std::optional media_type; + /// A color multiplier applied to the whole asset. + /// + /// For mesh who already have `albedo_factor` in materials, + /// it will be overwritten by actual `albedo_factor` of `archetypes::Asset3D` (if specified). + std::optional albedo_factor; + public: static constexpr const char IndicatorComponentName[] = "rerun.components.Asset3DIndicator"; @@ -118,6 +125,16 @@ namespace rerun::archetypes { // See: https://github.com/rerun-io/rerun/issues/4027 RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) } + + /// A color multiplier applied to the whole asset. + /// + /// For mesh who already have `albedo_factor` in materials, + /// it will be overwritten by actual `albedo_factor` of `archetypes::Asset3D` (if specified). + Asset3D with_albedo_factor(rerun::components::AlbedoFactor _albedo_factor) && { + albedo_factor = std::move(_albedo_factor); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } }; } // namespace rerun::archetypes diff --git a/rerun_py/rerun_sdk/rerun/archetypes/asset3d.py b/rerun_py/rerun_sdk/rerun/archetypes/asset3d.py index 834a7cb2b4ed..2f0d74c87bcc 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/asset3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/asset3d.py @@ -62,6 +62,7 @@ def __attrs_clear__(self) -> None: self.__attrs_init__( blob=None, # type: ignore[arg-type] media_type=None, # type: ignore[arg-type] + albedo_factor=None, # type: ignore[arg-type] ) @classmethod @@ -97,5 +98,17 @@ def _clear(cls) -> Asset3D: # # (Docstring intentionally commented out to hide this field from the docs) + albedo_factor: components.AlbedoFactorBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.AlbedoFactorBatch._optional, # type: ignore[misc] + ) + # A color multiplier applied to the whole asset. + # + # For mesh who already have `albedo_factor` in materials, + # it will be overwritten by actual `albedo_factor` of [`archetypes.Asset3D`][rerun.archetypes.Asset3D] (if specified). + # + # (Docstring intentionally commented out to hide this field from the docs) + __str__ = Archetype.__str__ __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/archetypes/asset3d_ext.py b/rerun_py/rerun_sdk/rerun/archetypes/asset3d_ext.py index 390aeb25e719..9499c6dd6f04 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/asset3d_ext.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/asset3d_ext.py @@ -16,6 +16,7 @@ def __init__( path: str | pathlib.Path | None = None, contents: datatypes.BlobLike | None = None, media_type: datatypes.Utf8Like | None = None, + albedo_factor: datatypes.Rgba32Like | None = None, ): """ Create a new instance of the Asset3D archetype. @@ -43,6 +44,9 @@ def __init__( or the viewer will try to guess from the contents (magic header). If the media type cannot be guessed, the viewer won't be able to render the asset. + albedo_factor: + Optional color multiplier for the whole mesh + """ from ..components import MediaType @@ -58,7 +62,7 @@ def __init__( if media_type is None: media_type = MediaType.guess_from_path(path) - self.__attrs_init__(blob=blob, media_type=media_type) + self.__attrs_init__(blob=blob, media_type=media_type, albedo_factor=albedo_factor) return self.__attrs_clear__()