chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
<!--[metadata]
|
||||
title = "Raw mesh"
|
||||
tags = ["Mesh"]
|
||||
thumbnail = "https://static.rerun.io/raw-mesh/7731418dda47e15dbfc0f9a2c32673909071cb40/480w.png"
|
||||
thumbnail_dimensions = [480, 480]
|
||||
channel = "release"
|
||||
include_in_manifest = true
|
||||
-->
|
||||
|
||||
Demonstrates logging of raw 3D mesh data (so-called "triangle soups") with simple material properties and their transform hierarchy.
|
||||
|
||||
Note that while this example loads GLTF meshes to illustrate [`Mesh3D`](https://rerun.io/docs/reference/types/archetypes/mesh3d)'s abilitites, you can also send various kinds of mesh assets
|
||||
directly via [`Asset3D`](https://rerun.io/docs/reference/types/archetypes/asset3d).
|
||||
|
||||
<!-- TODO(#1957): How about we load something elseto avoid confusion? -->
|
||||
|
||||
<picture data-inline-viewer="examples/raw_mesh">
|
||||
<img src="https://static.rerun.io/raw_mesh/d5d008b9f1b53753a86efe2580443a9265070b77/full.png" alt="">
|
||||
<source media="(max-width: 480px)" srcset="https://static.rerun.io/raw_mesh/d5d008b9f1b53753a86efe2580443a9265070b77/480w.png">
|
||||
<source media="(max-width: 768px)" srcset="https://static.rerun.io/raw_mesh/d5d008b9f1b53753a86efe2580443a9265070b77/768w.png">
|
||||
<source media="(max-width: 1024px)" srcset="https://static.rerun.io/raw_mesh/d5d008b9f1b53753a86efe2580443a9265070b77/1024w.png">
|
||||
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/raw_mesh/d5d008b9f1b53753a86efe2580443a9265070b77/1200w.png">
|
||||
</picture>
|
||||
|
||||
## Used Rerun types
|
||||
[`Transform3D`](https://www.rerun.io/docs/reference/types/archetypes/transform3d), [`Mesh3D`](https://www.rerun.io/docs/reference/types/archetypes/mesh3d)
|
||||
|
||||
## Background
|
||||
Raw 3D mesh data refers to the basic geometric representation of a three-dimensional object, typically composed of interconnected triangles.
|
||||
These triangles collectively form the surface of the object, defining its shape and structure in a digital environment.
|
||||
Rerun was employed to visualize and manage this raw mesh data, along with its associated simple material properties and transform hierarchy.
|
||||
|
||||
|
||||
## Logging and visualizing with Rerun
|
||||
|
||||
The visualizations in this example were created with the following Rerun code:
|
||||
|
||||
### 3D mesh data
|
||||
The raw 3D mesh data are logged as [`Mesh3D`](https://www.rerun.io/docs/reference/types/archetypes/mesh3d) objects, and includes details about vertex positions, colors, normals, texture coordinates, material properties, and face indices for an accurate reconstruction and visualization.
|
||||
|
||||
```python
|
||||
rr.log(
|
||||
path,
|
||||
rr.Mesh3D(
|
||||
vertex_positions=mesh.vertices,
|
||||
vertex_colors=vertex_colors,
|
||||
vertex_normals=mesh.vertex_normals,
|
||||
vertex_texcoords=vertex_texcoords,
|
||||
albedo_texture=albedo_texture,
|
||||
triangle_indices=mesh.faces,
|
||||
albedo_factor=albedo_factor,
|
||||
),
|
||||
)
|
||||
```
|
||||
Through Rerun's [`Transform3D`](https://www.rerun.io/docs/reference/types/archetypes/transform3d) archetype, essential details are captured to ensure precise positioning and orientation of meshes within the 3D scene.
|
||||
```python
|
||||
rr.log(
|
||||
path,
|
||||
rr.Transform3D(
|
||||
translation=trimesh.transformations.translation_from_matrix(world_from_mesh),
|
||||
mat3x3=world_from_mesh[0:3, 0:3],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## Run the code
|
||||
To run this example, make sure you have the Rerun repository checked out and the latest SDK installed:
|
||||
```bash
|
||||
pip install --upgrade rerun-sdk # install the latest Rerun SDK
|
||||
git clone git@github.com:rerun-io/rerun.git # Clone the repository
|
||||
cd rerun
|
||||
git checkout latest # Check out the commit matching the latest SDK release
|
||||
```
|
||||
Install the necessary libraries specified in the requirements file:
|
||||
```bash
|
||||
pip install -e examples/python/raw_mesh
|
||||
```
|
||||
To experiment with the provided example, simply execute the main Python script:
|
||||
```bash
|
||||
python -m raw_mesh # run the example
|
||||
```
|
||||
You can specify scene:
|
||||
```bash
|
||||
python -m raw_mesh --scene {lantern,avocado,buggy,brain_stem}
|
||||
```
|
||||
If you wish to customize it, explore additional features, or save it use the CLI with the `--help` option for guidance:
|
||||
```bash
|
||||
python -m raw_mesh --help
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "raw_mesh"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
dependencies = ["numpy", "requests>=2.31,<3", "rerun-sdk", "trimesh"]
|
||||
|
||||
[project.scripts]
|
||||
raw_mesh = "raw_mesh.__main__:main"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Shows how to use the Rerun SDK to log raw 3D meshes (so-called "triangle soups") and their transform hierarchy.
|
||||
|
||||
Note that while this example loads GLTF meshes to illustrate
|
||||
[`Mesh3D`](https://rerun.io/docs/reference/types/archetypes/mesh3d)'s abilitites,
|
||||
you can also send various kinds of mesh assets directly via
|
||||
[`Asset3D`](https://rerun.io/docs/reference/types/archetypes/asset3d).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import numpy as np
|
||||
import trimesh
|
||||
|
||||
import rerun as rr # pip install rerun-sdk
|
||||
import rerun.blueprint as rrb
|
||||
|
||||
from .download_dataset import AVAILABLE_MESHES, ensure_mesh_downloaded
|
||||
|
||||
DESCRIPTION = """
|
||||
# Raw meshes
|
||||
This example shows how you can log a hierarchical 3D mesh, including its transform hierarchy.
|
||||
|
||||
The full source code for this example is available [on GitHub](https://github.com/rerun-io/rerun/blob/latest/examples/python/raw_mesh).
|
||||
"""
|
||||
|
||||
|
||||
def load_scene(path: Path) -> trimesh.Scene:
|
||||
print(f"loading scene {path}…")
|
||||
mesh = trimesh.load(path, force="scene")
|
||||
return cast("trimesh.Scene", mesh)
|
||||
|
||||
|
||||
# NOTE: The scene hierarchy will look different compared to the Rust example, as this is using the
|
||||
# trimesh hierarchy, not the raw glTF hierarchy.
|
||||
def log_scene(scene: trimesh.Scene, node: str, path: str | None = None) -> None:
|
||||
path = path + "/" + node if path else node
|
||||
|
||||
parent = scene.graph.transforms.parents.get(node)
|
||||
children = scene.graph.transforms.children.get(node)
|
||||
|
||||
node_data = scene.graph.get(frame_to=node, frame_from=parent)
|
||||
if node_data:
|
||||
# Log the transform between this node and its direct parent (if it has one!).
|
||||
if parent:
|
||||
# TODO(#3559): We should support 4x4 matrices directly
|
||||
world_from_mesh = node_data[0]
|
||||
rr.log(
|
||||
path,
|
||||
rr.Transform3D(
|
||||
translation=trimesh.transformations.translation_from_matrix(world_from_mesh),
|
||||
mat3x3=world_from_mesh[0:3, 0:3],
|
||||
),
|
||||
)
|
||||
|
||||
# Log this node's mesh, if it has one.
|
||||
mesh = cast("trimesh.Trimesh", scene.geometry.get(node_data[1]))
|
||||
if mesh is not None:
|
||||
vertex_colors = None
|
||||
vertex_texcoords = None
|
||||
albedo_factor = None
|
||||
albedo_texture = None
|
||||
|
||||
try:
|
||||
vertex_texcoords = mesh.visual.uv # type: ignore[union-attr]
|
||||
# trimesh uses the OpenGL convention for UV coordinates, so we need to flip the V coordinate
|
||||
# since Rerun uses the Vulkan/Metal/DX12/WebGPU convention.
|
||||
vertex_texcoords[:, 1] = 1.0 - vertex_texcoords[:, 1]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
albedo_texture = mesh.visual.material.baseColorTexture # type: ignore[union-attr]
|
||||
if mesh.visual.material.baseColorTexture is None: # type: ignore[union-attr]
|
||||
raise ValueError()
|
||||
except Exception:
|
||||
# Try vertex colors instead.
|
||||
try:
|
||||
colors = mesh.visual.to_color().vertex_colors # type: ignore[union-attr]
|
||||
if len(colors) == 4:
|
||||
# If trimesh gives us a single vertex color for the entire mesh, we can interpret that
|
||||
# as an albedo factor for the whole primitive.
|
||||
albedo_factor = np.array(colors)
|
||||
else:
|
||||
vertex_colors = colors
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
rr.log(
|
||||
path,
|
||||
rr.Mesh3D(
|
||||
vertex_positions=mesh.vertices,
|
||||
vertex_colors=vertex_colors,
|
||||
vertex_normals=mesh.vertex_normals,
|
||||
vertex_texcoords=vertex_texcoords,
|
||||
albedo_texture=albedo_texture,
|
||||
triangle_indices=mesh.faces,
|
||||
albedo_factor=albedo_factor,
|
||||
),
|
||||
)
|
||||
|
||||
if children:
|
||||
for child in children:
|
||||
log_scene(scene, child, path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Logs raw 3D meshes and their transform hierarchy using the Rerun SDK.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scene",
|
||||
type=str,
|
||||
choices=AVAILABLE_MESHES,
|
||||
default=AVAILABLE_MESHES[0],
|
||||
help="The name of the scene to load",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scene-path",
|
||||
type=Path,
|
||||
help="Path to a scene to analyze. If set, overrides the `--scene` argument.",
|
||||
)
|
||||
rr.script_add_args(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
scene_path = args.scene_path
|
||||
if scene_path is None:
|
||||
scene_path = ensure_mesh_downloaded(args.scene)
|
||||
scene = load_scene(scene_path)
|
||||
|
||||
root = next(iter(scene.graph.nodes))
|
||||
|
||||
blueprint = rrb.Horizontal(
|
||||
rrb.Spatial3DView(name="Mesh", origin="/world"),
|
||||
rrb.TextDocumentView(name="Description", origin="/description"),
|
||||
column_shares=[3, 1],
|
||||
)
|
||||
|
||||
rr.script_setup(args, "rerun_example_raw_mesh", default_blueprint=blueprint)
|
||||
rr.log("description", rr.TextDocument(DESCRIPTION, media_type=rr.MediaType.MARKDOWN), static=True)
|
||||
|
||||
# glTF always uses a right-handed coordinate system when +Y is up and meshes face +Z.
|
||||
rr.log(root, rr.ViewCoordinates.RUB, static=True)
|
||||
log_scene(scene, root)
|
||||
|
||||
rr.script_teardown(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import requests
|
||||
|
||||
DATASET_URL_BASE: Final = "https://storage.googleapis.com/rerun-example-datasets/raw_mesh"
|
||||
|
||||
# TODO(cmc): re-enable obj meshes when we support those on the viewer's side
|
||||
AVAILABLE_MESHES: Final = [
|
||||
"lantern",
|
||||
"avocado",
|
||||
"buggy",
|
||||
"brain_stem",
|
||||
# "buddha",
|
||||
# "bunny",
|
||||
# "dragon",
|
||||
# "mori_knob",
|
||||
]
|
||||
|
||||
# Maps mesh name to list of (remote_filename, local_filename) pairs.
|
||||
MESH_FILES: Final = {
|
||||
"avocado": [("avocado.glb", "avocado.glb")],
|
||||
"brain_stem": [("brain_stem.glb", "brain_stem.glb")],
|
||||
"buddha": [("buddha.obj", "buddha.obj")],
|
||||
"buggy": [("buggy.glb", "buggy.glb")],
|
||||
"bunny": [("bunny.obj", "bunny.obj")],
|
||||
"dragon": [("dragon.obj", "dragon.obj")],
|
||||
"lantern": [("lantern.glb", "lantern.glb")],
|
||||
"mori_knob": [
|
||||
("testObj.obj", "testObj.obj"),
|
||||
("testObj.mtl", "testObj.mtl"),
|
||||
],
|
||||
}
|
||||
|
||||
DOWNLOADED_DIR: Final = Path(__file__).parent.parent / "dataset"
|
||||
|
||||
|
||||
def ensure_mesh_downloaded(mesh_name: str) -> Path:
|
||||
path = find_mesh_path_if_downloaded(mesh_name)
|
||||
if path is not None:
|
||||
return path
|
||||
return download_mesh(mesh_name)
|
||||
|
||||
|
||||
def find_mesh_path_if_downloaded(name: str) -> Path | None:
|
||||
for mesh_format in ("obj", "glb"):
|
||||
for path in DOWNLOADED_DIR.glob(f"{name}/**/*.{mesh_format}"):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def download_mesh(name: str) -> Path:
|
||||
"""Downloads a mesh from the Rerun example datasets bucket and returns the path to the main mesh file."""
|
||||
if name not in MESH_FILES:
|
||||
raise RuntimeError(f"Unknown mesh named: {name}")
|
||||
|
||||
mesh_dir = DOWNLOADED_DIR / name
|
||||
os.makedirs(mesh_dir, exist_ok=True)
|
||||
|
||||
main_path = None
|
||||
for remote_name, local_name in MESH_FILES[name]:
|
||||
local_path = mesh_dir / local_name
|
||||
if not local_path.exists():
|
||||
url = f"{DATASET_URL_BASE}/{name}/{remote_name}"
|
||||
print(f"downloading {url}…")
|
||||
resp = requests.get(url)
|
||||
resp.raise_for_status()
|
||||
with open(local_path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
|
||||
# The main mesh file is the first one listed (obj or glb).
|
||||
if main_path is None:
|
||||
main_path = local_path
|
||||
|
||||
assert main_path is not None
|
||||
return main_path
|
||||
Reference in New Issue
Block a user