Skip to content

Commit 847c65f

Browse files
authored
Merge pull request #1 from lanxinger/claude/extend-meshdenoiser-formats-011CUpST8rYKaNpbw1ZrCaFm
Add USD format support (usd, usda, usdc, usdz) using tinyusdz
2 parents 1ed2b56 + 3aabda6 commit 847c65f

File tree

3 files changed

+211
-7
lines changed

3 files changed

+211
-7
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ Dependencies are handled per‑platform (OpenMesh sources are bundled by the ups
1313
MeshSDFilter requires:
1414
- **Eigen 3.3+** (header-only)
1515
- **OpenMP** (optional, if compiler supports it)
16-
- **OpenMesh** sources – the upstream MeshSDFilter CMake builds a static OpenMesh from its bundled `external/OpenMesh` folder.
17-
- **tinygltf** (header-only, fetched automatically) to allow loading `.gltf` and `.glb` meshes when running `MeshDenoiser`.
16+
- **OpenMesh 11.0** – fetched and built automatically from source
17+
- **tinygltf** v2.9.6 (header-only, fetched automatically) to allow loading `.gltf` and `.glb` meshes when running `MeshDenoiser`
18+
- **tinyusdz** (fetched automatically) to allow loading USD formats (`.usd`, `.usda`, `.usdc`, `.usdz`) when running `MeshDenoiser`
1819

1920
OpenMP is an open standard for shared-memory parallelism; compilers that support it (e.g. GCC, Clang with `libomp`, MSVC) let MeshSDFilter run heavy loops across multiple CPU cores.
2021

@@ -93,4 +94,9 @@ MeshSDFilter FilteringOptions.txt input_mesh.ply output_mesh.ply
9394
MeshDenoiser DenoisingOptions.txt input_mesh.ply output_mesh.ply
9495
```
9596
- A detail-preserving MeshDenoiser preset is in `MeshDenoiserDefaults.txt` (outer iterations 1, lambda 0.15, eta 2.2, mu 0.2, nu 0.25). Copy it to your working folder or pass it directly; raise lambda/eta or the iteration count only if you want stronger smoothing.
96-
- `MeshDenoiser` accepts OBJ, PLY, OFF, STL, and now `.gltf/.glb` inputs (thanks to tinygltf). If the glTF mesh contains multiple nodes, transforms are applied automatically before filtering.
97+
- `MeshDenoiser` accepts:
98+
- Traditional formats: OBJ, PLY, OFF, STL (via OpenMesh)
99+
- glTF formats: `.gltf`, `.glb` (via tinygltf)
100+
- USD formats: `.usd`, `.usda`, `.usdc`, `.usdz` (via tinyusdz)
101+
102+
For glTF and USD files with multiple meshes or transforms, all geometry is combined and transforms are applied automatically before filtering.

cmake/FetchMeshSDFilter.cmake

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,11 @@ if(NOT alicevision_src_POPULATED)
2525
FetchContent_Populate(alicevision_src)
2626
endif()
2727

28-
set(_OPENMESH_URL "https://www.graphics.rwth-aachen.de/media/openmesh_static/Releases/10.0/OpenMesh-10.0.0.tar.bz2")
28+
set(_OPENMESH_URL "https://www.graphics.rwth-aachen.de/media/openmesh_static/Releases/11.0/OpenMesh-11.0.0.tar.gz")
2929
FetchContent_Declare(
3030
openmesh
3131
URL "${_OPENMESH_URL}"
3232
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
33-
URL_HASH SHA256=af22520a474bb6a3b355eb0867449c6b995126f97632d1ee5ff9c7ebd322fedb
3433
)
3534
FetchContent_GetProperties(openmesh)
3635
if(NOT openmesh_POPULATED)
@@ -56,6 +55,23 @@ if(NOT tinygltf_POPULATED)
5655
FetchContent_Populate(tinygltf)
5756
endif()
5857

58+
set(_TINYUSDZ_URL "https://github.com/lighttransport/tinyusdz/archive/refs/heads/release.zip")
59+
60+
# TinyUSDZ build options must be set BEFORE FetchContent
61+
set(TINYUSDZ_PRODUCTION_BUILD ON CACHE BOOL "" FORCE)
62+
set(TINYUSDZ_WITH_OPENSUBDIV OFF CACHE BOOL "" FORCE)
63+
set(TINYUSDZ_WITH_AUDIO OFF CACHE BOOL "" FORCE)
64+
set(TINYUSDZ_WITH_EXR OFF CACHE BOOL "" FORCE)
65+
set(TINYUSDZ_BUILD_TESTS OFF CACHE BOOL "" FORCE)
66+
set(TINYUSDZ_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
67+
68+
FetchContent_Declare(
69+
tinyusdz
70+
URL "${_TINYUSDZ_URL}"
71+
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
72+
)
73+
FetchContent_MakeAvailable(tinyusdz)
74+
5975
set(MESHSD_DIR "${alicevision_src_SOURCE_DIR}/src/dependencies/MeshSDFilter")
6076

6177
if(NOT EXISTS "${MESHSD_DIR}/CMakeLists.txt")
@@ -73,10 +89,16 @@ if(EXISTS "${_override_dir}")
7389
endforeach()
7490
endif()
7591

92+
# Add include directories globally before building MeshSDFilter
93+
# This ensures tinygltf and tinyusdz headers are available during compilation
94+
include_directories("${tinygltf_SOURCE_DIR}")
95+
include_directories("${tinyusdz_SOURCE_DIR}/src")
96+
7697
add_subdirectory("${MESHSD_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/MeshSDFilter-build")
7798

99+
# Link tinyusdz to the targets after they're created
78100
foreach(_mesh_target MeshSDFilter MeshDenoiser)
79101
if(TARGET ${_mesh_target})
80-
target_include_directories(${_mesh_target} PRIVATE "${tinygltf_SOURCE_DIR}")
102+
target_link_libraries(${_mesh_target} tinyusdz_static)
81103
endif()
82104
endforeach()

overrides/MeshSDFilter/MeshDenoiser.cpp

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,15 @@
5252
#define TINYGLTF_USE_CPP14
5353
#include <tiny_gltf.h>
5454

55+
#include "tinyusdz.hh"
56+
#include "tydra/render-data.hh"
57+
#include "tydra/scene-access.hh"
58+
5559
namespace
5660
{
5761

62+
using namespace tinyusdz; // For tydra namespace access
63+
5864
std::string to_lower_copy(std::string str)
5965
{
6066
std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
@@ -388,14 +394,180 @@ bool load_gltf_mesh(const std::string &filename, TriMesh &mesh, std::string &war
388394
return true;
389395
}
390396

397+
bool load_usd_mesh(const std::string &filename, TriMesh &mesh, std::string &warning, std::string &error)
398+
{
399+
tinyusdz::Stage stage;
400+
std::string warn, err;
401+
402+
// Load USD file based on extension
403+
bool success = false;
404+
if(has_extension(filename, ".usdz")){
405+
success = tinyusdz::LoadUSDZFromFile(filename, &stage, &warn, &err);
406+
}
407+
else if(has_extension(filename, ".usdc")){
408+
success = tinyusdz::LoadUSDCFromFile(filename, &stage, &warn, &err);
409+
}
410+
else if(has_extension(filename, ".usda")){
411+
success = tinyusdz::LoadUSDAFromFile(filename, &stage, &warn, &err);
412+
}
413+
else if(has_extension(filename, ".usd")){
414+
// Generic .usd extension - auto-detect format (ASCII or binary)
415+
success = tinyusdz::LoadUSDFromFile(filename, &stage, &warn, &err);
416+
}
417+
else{
418+
error = "Unsupported USD file extension. Supported: .usd, .usda, .usdc, .usdz";
419+
return false;
420+
}
421+
422+
if(!warn.empty()){
423+
warning += warn;
424+
}
425+
if(!err.empty()){
426+
error += err;
427+
}
428+
if(!success){
429+
if(error.empty()){
430+
error = "Unable to parse USD file.";
431+
}
432+
return false;
433+
}
434+
435+
// Convert USD stage to RenderScene using Tydra
436+
tydra::RenderScene render_scene;
437+
tydra::RenderSceneConverter converter;
438+
tydra::RenderSceneConverterEnv env(stage);
439+
440+
if(!converter.ConvertToRenderScene(env, &render_scene)){
441+
error = "Failed to convert USD stage to render scene.";
442+
return false;
443+
}
444+
445+
if(render_scene.meshes.empty()){
446+
error = "No meshes found in USD file.";
447+
return false;
448+
}
449+
450+
// Clear and prepare the output mesh
451+
mesh.clear();
452+
mesh.request_face_normals();
453+
mesh.request_vertex_normals();
454+
455+
// Process all meshes in the render scene
456+
for(const auto &render_mesh : render_scene.meshes){
457+
if(render_mesh.points.empty()){
458+
continue;
459+
}
460+
461+
// Add vertices
462+
std::vector<TriMesh::VertexHandle> vertex_handles;
463+
vertex_handles.reserve(render_mesh.points.size());
464+
465+
for(const auto &point : render_mesh.points){
466+
TriMesh::Point p(point[0], point[1], point[2]);
467+
vertex_handles.push_back(mesh.add_vertex(p));
468+
}
469+
470+
// Determine which indices to use
471+
const std::vector<uint32_t> *face_indices = nullptr;
472+
const std::vector<uint32_t> *face_counts = nullptr;
473+
474+
if(!render_mesh.triangulatedFaceVertexIndices.empty()){
475+
// Use triangulated indices if available
476+
face_indices = &render_mesh.triangulatedFaceVertexIndices;
477+
face_counts = &render_mesh.triangulatedFaceVertexCounts;
478+
}
479+
else{
480+
// Use original indices
481+
face_indices = &render_mesh.usdFaceVertexIndices;
482+
face_counts = &render_mesh.usdFaceVertexCounts;
483+
}
484+
485+
if(face_indices->empty() || face_counts->empty()){
486+
continue;
487+
}
488+
489+
// Add faces
490+
size_t index_offset = 0;
491+
for(uint32_t count : *face_counts){
492+
if(count == 3){
493+
// Triangle - add directly
494+
std::vector<TriMesh::VertexHandle> face_verts;
495+
face_verts.reserve(3);
496+
497+
bool valid = true;
498+
for(uint32_t i = 0; i < 3; ++i){
499+
uint32_t idx = (*face_indices)[index_offset + i];
500+
if(idx >= vertex_handles.size()){
501+
valid = false;
502+
break;
503+
}
504+
face_verts.push_back(vertex_handles[idx]);
505+
}
506+
507+
if(valid){
508+
mesh.add_face(face_verts);
509+
}
510+
}
511+
else if(count > 3){
512+
// Polygon - triangulate by fan method
513+
std::vector<uint32_t> poly_indices;
514+
poly_indices.reserve(count);
515+
516+
bool valid = true;
517+
for(uint32_t i = 0; i < count; ++i){
518+
uint32_t idx = (*face_indices)[index_offset + i];
519+
if(idx >= vertex_handles.size()){
520+
valid = false;
521+
break;
522+
}
523+
poly_indices.push_back(idx);
524+
}
525+
526+
if(valid){
527+
// Simple fan triangulation from first vertex
528+
for(uint32_t i = 1; i + 1 < count; ++i){
529+
std::vector<TriMesh::VertexHandle> face_verts;
530+
face_verts.push_back(vertex_handles[poly_indices[0]]);
531+
face_verts.push_back(vertex_handles[poly_indices[i]]);
532+
face_verts.push_back(vertex_handles[poly_indices[i + 1]]);
533+
mesh.add_face(face_verts);
534+
}
535+
}
536+
}
537+
538+
index_offset += count;
539+
}
540+
}
541+
542+
if(mesh.n_faces() == 0){
543+
error = "No valid triangles found in USD mesh.";
544+
return false;
545+
}
546+
547+
mesh.garbage_collection();
548+
return true;
549+
}
550+
391551
} // anonymous namespace
392552

393553

394554
int main(int argc, char **argv)
395555
{
396556
if(argc != 4)
397557
{
398-
std::cout << "Usage:\tMeshDenoiser OPTION_FILE INPUT_MESH OUTPUT_MESH" << std::endl;
558+
std::cout << "MeshDenoiser - Mesh normal denoising filter" << std::endl;
559+
std::cout << std::endl;
560+
std::cout << "Usage: MeshDenoiser OPTION_FILE INPUT_MESH OUTPUT_MESH" << std::endl;
561+
std::cout << std::endl;
562+
std::cout << "Supported input formats:" << std::endl;
563+
std::cout << " - OBJ, PLY, OFF, STL (via OpenMesh)" << std::endl;
564+
std::cout << " - glTF (.gltf, .glb)" << std::endl;
565+
std::cout << " - USD (.usd, .usda, .usdc, .usdz)" << std::endl;
566+
std::cout << std::endl;
567+
std::cout << "Example:" << std::endl;
568+
std::cout << " MeshDenoiser options.txt input.obj output.obj" << std::endl;
569+
std::cout << " MeshDenoiser options.txt model.gltf denoised.obj" << std::endl;
570+
std::cout << " MeshDenoiser options.txt scene.usdz clean.ply" << std::endl;
399571
return 1;
400572
}
401573

@@ -411,6 +583,10 @@ int main(int argc, char **argv)
411583
if(has_extension(input_mesh_path, ".gltf") || has_extension(input_mesh_path, ".glb")){
412584
load_success = load_gltf_mesh(input_mesh_path, mesh, warning, error);
413585
}
586+
else if(has_extension(input_mesh_path, ".usd") || has_extension(input_mesh_path, ".usda") ||
587+
has_extension(input_mesh_path, ".usdc") || has_extension(input_mesh_path, ".usdz")){
588+
load_success = load_usd_mesh(input_mesh_path, mesh, warning, error);
589+
}
414590
else{
415591
load_success = OpenMesh::IO::read_mesh(mesh, input_mesh_path);
416592
if(!load_success){

0 commit comments

Comments
 (0)