Skip to content

Commit

Permalink
Add STL mesh support (#5244)
Browse files Browse the repository at this point in the history
### What

Allows to log stl through `Asset3D` or load it directly into the viewer
via drag/drop or the CLI


https://github.com/rerun-io/rerun/assets/1220815/b3050b4d-8115-47fd-abb2-7f03285d322f


To test that the api works I ran
`python ./docs/code-examples/all/asset3d_simple.py ../buckle.stl` with
[this](https://github.com/rerun-io/rerun/files/14351841/buckle.stl.zip)
file.


### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using newly built examples:
[app.rerun.io](https://app.rerun.io/pr/5244/index.html)
* Using examples from latest `main` build:
[app.rerun.io](https://app.rerun.io/pr/5244/index.html?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[app.rerun.io](https://app.rerun.io/pr/5244/index.html?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/5244)
- [Docs
preview](https://rerun.io/preview/648d891e1f1f8ec715aacbb123a71792224e5ac1/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/648d891e1f1f8ec715aacbb123a71792224e5ac1/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)
  • Loading branch information
Wumpf authored Feb 21, 2024
1 parent a580ef3 commit 3c85348
Show file tree
Hide file tree
Showing 23 changed files with 203 additions and 28 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ thiserror = "1.0"
time = { version = "0.3", default-features = false, features = [
"wasm-bindgen",
] }
tinystl = { version = "0.0.3", default-features = false }
tinyvec = { version = "1.6", features = ["alloc", "rustc_1_55"] }
tobj = "4.0"
tokio = { version = "1.24", default-features = false }
Expand Down
2 changes: 1 addition & 1 deletion crates/re_data_source/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub const SUPPORTED_IMAGE_EXTENSIONS: &[&str] = &[
"pbm", "pgm", "png", "ppm", "tga", "tif", "tiff", "webp",
];

pub const SUPPORTED_MESH_EXTENSIONS: &[&str] = &["glb", "gltf", "obj"];
pub const SUPPORTED_MESH_EXTENSIONS: &[&str] = &["glb", "gltf", "obj", "stl"];

// TODO(#4532): `.ply` data loader should support 2D point cloud & meshes
pub const SUPPORTED_POINT_CLOUD_EXTENSIONS: &[&str] = &["ply"];
Expand Down
6 changes: 5 additions & 1 deletion crates/re_renderer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]


[features]
default = ["import-obj", "import-gltf"]
default = ["import-obj", "import-gltf", "import-stl"]

## Support for Arrow datatypes for end-to-end zero-copy.
arrow = ["dep:arrow2"]
Expand All @@ -35,6 +35,9 @@ import-obj = ["dep:tobj"]
## Support importing .gltf and .glb files
import-gltf = ["dep:gltf"]

## Support importing binary & ascii .stl files
import-stl = ["dep:tinystl"]

## Enable (de)serialization using serde.
serde = ["dep:serde"]

Expand Down Expand Up @@ -73,6 +76,7 @@ wgpu-core.workspace = true # Needed fo
# optional
arrow2 = { workspace = true, optional = true }
gltf = { workspace = true, optional = true }
tinystl = { workspace = true, features = ["bytemuck"], optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
tobj = { workspace = true, optional = true }

Expand Down
3 changes: 3 additions & 0 deletions crates/re_renderer/src/importer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ pub mod obj;
#[cfg(feature = "import-gltf")]
pub mod gltf;

#[cfg(feature = "import-stl")]
pub mod stl;

use macaw::Vec3Ext as _;

use crate::renderer::MeshInstance;
Expand Down
80 changes: 80 additions & 0 deletions crates/re_renderer/src/importer/stl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use itertools::Itertools;
use smallvec::smallvec;
use tinystl::StlData;

use crate::{mesh, renderer::MeshInstance, resource_managers::ResourceLifeTime, RenderContext};

#[derive(thiserror::Error, Debug)]
pub enum StlImportError {
#[error("Error loading STL mesh: {0}")]
TinyStl(tinystl::Error),

#[error(transparent)]
MeshError(#[from] mesh::MeshError),

#[error(transparent)]
ResourceManagerError(#[from] crate::resource_managers::ResourceManagerError),
}

/// Load a [STL .stl file](https://en.wikipedia.org/wiki/STL_(file_format)) into the mesh manager.
pub fn load_stl_from_buffer(
buffer: &[u8],
ctx: &RenderContext,
) -> Result<Vec<MeshInstance>, StlImportError> {
re_tracing::profile_function!();

let cursor = std::io::Cursor::new(buffer);
let StlData {
name,
triangles,
normals,
..
} = StlData::read_buffer(std::io::BufReader::new(cursor)).map_err(StlImportError::TinyStl)?;

let num_vertices = triangles.len() * 3;

let material = mesh::Material {
label: "default material".into(),
index_range: 0..num_vertices as u32,
albedo: ctx.texture_manager_2d.white_texture_unorm_handle().clone(),
albedo_multiplier: crate::Rgba::WHITE,
};

let mesh = mesh::Mesh {
label: name.into(),
triangle_indices: (0..num_vertices as u32)
.tuples::<(_, _, _)>()
.map(glam::UVec3::from)
.collect::<Vec<_>>(),
vertex_positions: bytemuck::cast_slice(&triangles).to_vec(),

// Normals on STL are per triangle, not per vertex.
// Yes, this makes STL always look faceted.
vertex_normals: normals
.into_iter()
.flat_map(|n| {
let n = glam::Vec3::from_array(n);
[n, n, n]
})
.collect(),

// STL has neither colors nor texcoords.
vertex_colors: vec![crate::Rgba32Unmul::WHITE; num_vertices],
vertex_texcoords: vec![glam::Vec2::ZERO; num_vertices],

materials: smallvec![material],
};

mesh.sanity_check()?;

let gpu_mesh = ctx
.mesh_manager
.write()
.create(ctx, &mesh, ResourceLifeTime::LongLived)?;

Ok(vec![MeshInstance {
gpu_mesh,
mesh: Some(std::sync::Arc::new(mesh)),
..Default::default()
}])
}
6 changes: 5 additions & 1 deletion crates/re_space_view_spatial/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ re_log_types.workspace = true
re_log.workspace = true
re_query.workspace = true
re_query_cache.workspace = true
re_renderer = { workspace = true, features = ["import-gltf", "import-obj"] }
re_renderer = { workspace = true, features = [
"import-gltf",
"import-obj",
"import-stl",
] }
re_types = { workspace = true, features = ["ecolor", "glam", "image"] }
re_tracing.workspace = true
re_ui.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/re_space_view_spatial/src/mesh_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ impl LoadedMesh {
ResourceLifeTime::LongLived,
render_ctx,
)?,
MediaType::STL => re_renderer::importer::stl::load_stl_from_buffer(bytes, render_ctx)?,
_ => anyhow::bail!("{media_type} files are not supported"),
};

Expand Down
4 changes: 3 additions & 1 deletion crates/re_types/definitions/rerun/archetypes/asset3d.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace rerun.archetypes;

// ---

/// A prepacked 3D asset (`.gltf`, `.glb`, `.obj`, etc.).
/// A prepacked 3D asset (`.gltf`, `.glb`, `.obj`, `.stl`, etc.).
///
/// \py See also [`Mesh3D`][rerun.archetypes.Mesh3D].
/// \rs See also [`Mesh3D`][crate::archetypes::Mesh3D].
Expand All @@ -28,7 +28,9 @@ table Asset3D (
///
/// Supported values:
/// * `model/gltf-binary`
/// * `model/gltf+json`
/// * `model/obj` (.mtl material files are not supported yet, references are silently ignored)
/// * `model/stl`
///
/// 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.
Expand Down
6 changes: 4 additions & 2 deletions crates/re_types/src/archetypes/asset3d.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 38 additions & 8 deletions crates/re_types/src/components/media_type_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ impl MediaType {
///
/// <https://www.iana.org/assignments/media-types/model/obj>
pub const OBJ: &'static str = "model/obj";

/// [Stereolithography Model `stl`](https://en.wikipedia.org/wiki/STL_(file_format)): `model/stl`.
///
/// Either binary or ASCII.
/// <https://www.iana.org/assignments/media-types/model/stl>
pub const STL: &'static str = "model/stl";
}

impl MediaType {
Expand Down Expand Up @@ -57,6 +63,12 @@ impl MediaType {
pub fn obj() -> Self {
Self(Self::OBJ.into())
}

/// `model/stl`
#[inline]
pub fn stl() -> Self {
Self(Self::STL.into())
}
}

impl MediaType {
Expand All @@ -71,15 +83,20 @@ impl MediaType {
#[inline]
pub fn guess_from_path(path: impl AsRef<std::path::Path>) -> Option<Self> {
let path = path.as_ref();

// `mime_guess2` considers `.obj` to be a tgif… but really it's way more likely to be an obj.
if path
let extension = path
.extension()
.and_then(|ext| ext.to_str().map(|s| s.to_lowercase()))
.as_deref()
== Some("obj")
{
return Some(Self::obj());
.and_then(|ext| ext.to_str().map(|s| s.to_lowercase()));

match extension.as_deref() {
// `mime_guess2` considers `.obj` to be a tgif… but really it's way more likely to be an obj.
Some("obj") => {
return Some(Self::obj());
}
// `mime_guess2` considers `.stl` to be a `application/vnd.ms-pki.stl`.
Some("stl") => {
return Some(Self::stl());
}
_ => {}
}

mime_guess2::from_path(path)
Expand All @@ -95,6 +112,18 @@ impl MediaType {
buf.len() >= 4 && buf[0] == b'g' && buf[1] == b'l' && buf[2] == b'T' && buf[3] == b'F'
}

fn stl_matcher(buf: &[u8]) -> bool {
// ASCII STL
buf.len() >= 5
&& buf[0] == b's'
&& buf[1] == b'o'
&& buf[2] == b'l'
&& buf[3] == b'i'
&& buf[3] == b'd'
// Binary STL is hard to infer since it starts with an 80 byte header that is commonly ignored, see
// https://en.wikipedia.org/wiki/STL_(file_format)#Binary
}

// NOTE:
// - gltf is simply json, so no magic byte
// (also most gltf files contain file:// links, so not much point in sending that to
Expand All @@ -103,6 +132,7 @@ impl MediaType {

let mut inferer = infer::Infer::new();
inferer.add(Self::GLB, "", glb_matcher);
inferer.add(Self::STL, "", stl_matcher);

inferer
.get(data)
Expand Down
2 changes: 1 addition & 1 deletion docs/code-examples/all/asset3d_simple.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <path_to_asset.[gltf|glb|obj]>" << std::endl;
std::cerr << "Usage: " << argv[0] << " <path_to_asset.[gltf|glb|obj|stl]>" << std::endl;
return 1;
}

Expand Down
2 changes: 1 addition & 1 deletion docs/code-examples/all/asset3d_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import rerun as rr

if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <path_to_asset.[gltf|glb|obj]>")
print(f"Usage: {sys.argv[0]} <path_to_asset.[gltf|glb|obj|stl]>")
sys.exit(1)

rr.init("rerun_example_asset3d", spawn=True)
Expand Down
2 changes: 1 addition & 1 deletion docs/code-examples/all/asset3d_simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use rerun::external::anyhow;
fn main() -> anyhow::Result<()> {
let args = std::env::args().collect::<Vec<_>>();
let Some(path) = args.get(1) else {
anyhow::bail!("Usage: {} <path_to_asset.[gltf|glb|obj]>", args[0]);
anyhow::bail!("Usage: {} <path_to_asset.[gltf|glb|obj|stl]>", args[0]);
};

let rec = rerun::RecordingStreamBuilder::new("rerun_example_asset3d").spawn()?;
Expand Down
2 changes: 1 addition & 1 deletion docs/content/reference/types/archetypes/asset3d.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@
"stacklevel",
"startswith",
"staticmethod",
"Stereolithography",
"struct",
"Struct",
"structs",
Expand Down
8 changes: 6 additions & 2 deletions rerun_cpp/src/rerun/archetypes/asset3d.hpp

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions rerun_cpp/src/rerun/archetypes/asset3d_ext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ namespace rerun::archetypes {
return rerun::components::MediaType::gltf();
} else if (ext == ".obj") {
return rerun::components::MediaType::obj();
} else if (ext == ".stl") {
return rerun::components::MediaType::stl();
} else {
return std::nullopt;
}
Expand Down
Loading

0 comments on commit 3c85348

Please sign in to comment.