Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add STL mesh support #5244

Merged
merged 4 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
105 changes: 105 additions & 0 deletions crates/re_renderer/src/importer/stl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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("Missing data in STL file.")]
MissingData,
Wumpf marked this conversation as resolved.
Show resolved Hide resolved

#[error("Unexpected data in STL file at line {0}.")]
Unexpected(usize),

#[error("Failed to parse line {0} (usually due to a malformed vertex).")]
Parse(usize),

#[error("Failed to convert the number of triangles to a 32bit unsigned integer (as required by the STL specification).")]
TooManyFacets,

#[error("Failed to parse integer.")]
TryFromInt,

#[error(transparent)]
Io(#[from] std::io::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 stl_data =
StlData::read_buffer(std::io::BufReader::new(cursor)).map_err(|err| match err {
tinystl::Error::MissingData => StlImportError::MissingData,
tinystl::Error::Unexpected(line) => StlImportError::Unexpected(line),
tinystl::Error::Parse(line) => StlImportError::Parse(line),
tinystl::Error::TooManyFacets(_) => StlImportError::TooManyFacets,
tinystl::Error::TryFromInt(_) => StlImportError::TryFromInt,
tinystl::Error::Io(err) => err.into(),
})?;

let StlData {
name,
triangles,
normals,
..
} = stl_data;

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
Loading
Loading