diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89c0942..10a269b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: fi JSON=$(jq -c . < "$MATRIX_FILE") MODIFIED_JSON='{"include":[]}' - PLUGINS=("GLTF" "PLY" "OBJ" "STL" "FBX") + PLUGINS=("GLTF" "PLY" "SPZ" "OBJ" "STL" "FBX") for row in $(echo "${JSON}" | jq -c '.include[]'); do for plugin in "${PLUGINS[@]}"; do @@ -195,6 +195,7 @@ jobs: "-Dpxr_ROOT=${{ github.workspace }}/usd_build" "-DUSD_FILEFORMATS_ENABLE_GLTF=$([[ "${{ matrix.config }}" == "ALL" || "${{ matrix.config }}" == "GLTF" ]] && echo "ON" || echo "OFF")" "-DUSD_FILEFORMATS_ENABLE_PLY=$([[ "${{ matrix.config }}" == "ALL" || "${{ matrix.config }}" == "PLY" ]] && echo "ON" || echo "OFF")" + "-DUSD_FILEFORMATS_ENABLE_SPZ=$([[ "${{ matrix.config }}" == "ALL" || "${{ matrix.config }}" == "SPZ" ]] && echo "ON" || echo "OFF")" "-DUSD_FILEFORMATS_ENABLE_OBJ=$([[ "${{ matrix.config }}" == "ALL" || "${{ matrix.config }}" == "OBJ" ]] && echo "ON" || echo "OFF")" "-DUSD_FILEFORMATS_ENABLE_STL=$([[ "${{ matrix.config }}" == "ALL" || "${{ matrix.config }}" == "STL" ]] && echo "ON" || echo "OFF")" "-DUSD_FILEFORMATS_ENABLE_FBX=$([[ "${{ matrix.config }}" == "ALL" || "${{ matrix.config }}" == "FBX" ]] && echo "ON" || echo "OFF")" diff --git a/CMakeLists.txt b/CMakeLists.txt index a6a7c73..e91c703 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ option(USD_FILEFORMATS_BUILD_TESTS "Build the unit tests" ON) option(USD_FILEFORMATS_ENABLE_FBX "Enables fbx plugin" ON) option(USD_FILEFORMATS_ENABLE_GLTF "Enables gltf plugin" ON) option(USD_FILEFORMATS_ENABLE_OBJ "Enables obj plugin" ON) +option(USD_FILEFORMATS_ENABLE_SPZ "Enables spz plugin" ON) option(USD_FILEFORMATS_ENABLE_PLY "Enables ply plugin" ON) option(USD_FILEFORMATS_ENABLE_STL "Enables stl plugin" ON) option(USD_FILEFORMATS_ENABLE_SBSAR "Enables sbsar plugin" OFF) @@ -37,6 +38,7 @@ option(USD_FILEFORMATS_FETCH_DRACO "Forces FetchContent for Draco" OFF) option(USD_FILEFORMATS_FETCH_ZLIB "Forces FetchContent for Zlib" OFF) option(USD_FILEFORMATS_FETCH_LIBXML2 "Forces FetchContent for LibXml2" ON) option(USD_FILEFORMATS_FETCH_HAPPLY "Forces FetchContent for Happly" ON) +option(USD_FILEFORMATS_FETCH_SPZ "Forces FetchContent for spz" ON) option(USD_FILEFORMATS_FETCH_SPHERICAL_HARMONICS "Forces FetchContent for SphericalHarmonics" ON) option(USD_FILEFORMATS_FETCH_FMT "Forces FetchContent for Fmt" ON) option(USD_FILEFORMATS_FETCH_FASTFLOAT "Forces FetchContent for FastFLoat" ON) @@ -110,6 +112,9 @@ endif() if (USD_FILEFORMATS_ENABLE_SBSAR) add_subdirectory(sbsar) endif() +if (USD_FILEFORMATS_ENABLE_SPZ) + add_subdirectory(spz) +endif() if (USD_FILEFORMATS_ENABLE_STL) add_subdirectory(stl) endif() diff --git a/README.md b/README.md index 3046e87..c4ba991 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ These [USD file-format-plugins](https://graphics.pixar.com/usd/release/plugins.h | [usdobj](obj/README.md) | [Wavefront's obj](https://en.wikipedia.org/wiki/Wavefront_.obj_file) | `.obj` | | [usdply](ply/README.md) | [Polygon File Format](https://en.wikipedia.org/wiki/PLY_(file_format)) | `.ply` | | [usdsbsar](sbsar/README.md) | [SBSAR file format](https://developer.adobe.com/console/servicesandapis#) | `.sbsar` | +| [usdspz](spz/README.md) | [Niantic Labs SPZ](https://scaniverse.com/news/spz-gaussian-splat-open-source-file-format) | `.spz` | | [usdstl](stl/README.md) | [STL file format](https://en.wikipedia.org/wiki/STL_(file_format)) | `.stl` | @@ -41,19 +42,20 @@ The following tools are needed: The following dependencies are needed: |Dependency|Version|Affects|Optional| |--|--|--|--| -| [Pixar USD](https://github.com/PixarAnimationStudios/USD) | 23.08 | all | no | -| [GTest](https://github.com/google/googletest.git) | 1.11.0 | all tests | yes | -| [Eigen](https://gitlab.com/libeigen/eigen) | 3.4.0 | usdply | no | -| [FBX SDK](https://aps.autodesk.com/developer/overview/fbx-sdk) | 2020.3.7 | usdfbx | no | -| [LibXml2](https://gitlab.gnome.org/GNOME/libxml2) | 2.10.0 | usdfbx | no | -| [Zlib](https://github.com/madler/zlib.git) | 1.2.11 | usdfbx | no | -| [TinyGltf](https://github.com/syoyo/tinygltf) | 2.8.21 | usdgltf | no | -| [Draco](https://github.com/google/draco.git) | 1.56 | usdgltf | yes | -| [Fmt](https://github.com/fmtlib/fmt.git) | 10.1.1 | usdobj | no | -| [FastFloat](https://github.com/lemire/fast_float.git) | 1.1.2 | usdobj | no | -| [Happly](https://github.com/nmwsharp/happly.git) | cfa2611 | usdply | no | -| [Spherical Harmonics](https://github.com/google/spherical-harmonics) | ccb6c7f | usdply | no | -| [Substance](https://developer.adobe.com/substance3d-sdk/) | 9.1.2 | usdsbsar | no | +| [Pixar USD](https://github.com/PixarAnimationStudios/USD) | 23.08 | all | no | +| [GTest](https://github.com/google/googletest.git) | 1.11.0 | all tests | yes | +| [Eigen](https://gitlab.com/libeigen/eigen) | 3.4.0 | usdply, usdspz | no | +| [FBX SDK](https://aps.autodesk.com/developer/overview/fbx-sdk) | 2020.3.7 | usdfbx | no | +| [LibXml2](https://gitlab.gnome.org/GNOME/libxml2) | 2.10.0 | usdfbx | no | +| [Zlib](https://github.com/madler/zlib.git) | 1.2.11 | usdfbx, usdgltf | no | +| [TinyGltf](https://github.com/syoyo/tinygltf) | 2.8.21 | usdgltf | no | +| [Draco](https://github.com/google/draco.git) | 1.56 | usdgltf | yes | +| [Fmt](https://github.com/fmtlib/fmt.git) | 10.1.1 | usdobj | no | +| [FastFloat](https://github.com/lemire/fast_float.git) | 1.1.2 | usdobj | no | +| [Happly](https://github.com/nmwsharp/happly.git) | cfa2611 | usdply | no | +| [Spherical Harmonics](https://github.com/google/spherical-harmonics) | ccb6c7f | usdply, usdspz | no | +| [Spz](https://github.com/nianticlabs/spz) | fd4e2a5 | usdspz | no | +| [Substance](https://developer.adobe.com/substance3d-sdk/) | 9.1.2 | usdsbsar | no | ## Build @@ -130,8 +132,9 @@ where: | -DUSD_FILEFORMATS_ENABLE_GLTF | Enables gltf plugin | ON | usdgltf | | -DUSD_FILEFORMATS_ENABLE_OBJ | Enables obj plugin | ON | usdobj | | -DUSD_FILEFORMATS_ENABLE_PLY | Enables ply plugin | ON | usdply | -| -DUSD_FILEFORMATS_ENABLE_STL | Enables stl plugin | ON | usdstl | -| -DUSD_FILEFORMATS_ENABLE_SBSAR | Enables sbsar plugin | OFF | usdsbsar | +| -DUSD_FILEFORMATS_ENABLE_SPZ | Enables spz plugin | ON | usdspz | +| -DUSD_FILEFORMATS_ENABLE_STL | Enables stl plugin | ON | usdstl | +| -DUSD_FILEFORMATS_ENABLE_SBSAR | Enables sbsar plugin | OFF | usdsbsar | | -DUSD_FILEFORMATS_ENABLE_DRACO | Enables draco in usdgltf | OFF | usdgltf | | -DUSD_FILEFORMATS_FORCE_FETCHCONTENT | Forces FetchContent for various packages | OFF | all | | -DUSD_FILEFORMATS_FETCH_GTEST | Forces FetchContent for GTest | ON | all tests | diff --git a/changelog.txt b/changelog.txt index fd0b34e..81fa6e7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,28 @@ +v1.1.1 March 10th, 2025 +fbx: + - added null and index checks + - properly write images when import images arg is invoked + - add support for invisibility +gltf: + - export normal scale + - ignore invisible nodes on export +obj: + - fix parser + - ignore invisible nodes on export +ply: + - ignore invisible nodes on export +sbsar: + - fix for isImageFileSupported() [fixes nested sbsarimages] +spz: + - ignore invisible nodes on export + - initial +stl: + - ignore invisible nodes on export +utility: + - don't create subdivisionRule attribute with value none + - fix for isImageFileSupported() [fixes nested usdz images] + - prevent bad access in utils + v1.1.0 January 31st, 2025 fbx: - add display name to USD to save imported names for export diff --git a/cmake/Findspz.cmake b/cmake/Findspz.cmake new file mode 100644 index 0000000..d36b288 --- /dev/null +++ b/cmake/Findspz.cmake @@ -0,0 +1,107 @@ +#[=======================================================================[.rst: +---- + +Finds or fetches the spz library. +If USD_FILEFORMATS_FORCE_FETCHCONTENT or USD_FILEFORMATS_FETCH_SPZ are +TRUE, spz will be fetched. Otherwise it will be searched via find commands. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if fetched: + +``spz::spz`` + The spz library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``spz_FOUND`` + +Cache Variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``SPZ_INCLUDE_DIR`` + The directory containing ``splat-types.h``. + +#]=======================================================================] + +if(TARGET spz::spz) + return() +endif() + +if(NOT TARGET ZLIB::ZLIB) + find_package(ZLIB REQUIRED) +endif() + +if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_SPZ) + message(STATUS "Fetching spz") + include(FetchContent) + FetchContent_Declare( + spz + GIT_REPOSITORY "https://github.com/raymondyfei/spz.git" + GIT_TAG "fd4e2a57bd6b7462657d41eebda330eca0f35159" + OVERRIDE_FIND_PACKAGE + ) + FetchContent_MakeAvailable(spz) + if (spz_POPULATED) + set(spz_FOUND TRUE) + file(GLOB SPZ_SRC_FILES + ${spz_SOURCE_DIR}/src/cc/*.cc + ${spz_SOURCE_DIR}/src/cc/*.h + ) + add_library(spz STATIC) + target_sources(spz PRIVATE ${SPZ_SRC_FILES}) + set(SPZ_INCLUDE_DIR "${spz_SOURCE_DIR}/src/cc") + target_include_directories(spz PUBLIC ${SPZ_INCLUDE_DIR}) + target_link_libraries(spz PRIVATE ZLIB::ZLIB) + set_property(TARGET spz PROPERTY POSITION_INDEPENDENT_CODE ON) + set_property(TARGET spz PROPERTY CXX_STANDARD 17) + target_compile_definitions(spz PRIVATE "_USE_MATH_DEFINES") + if (NOT MSVC) + target_compile_options(spz PRIVATE "-Wno-shorten-64-to-32") + endif() + + add_library(spz::spz ALIAS spz) + elseif(${spz_FIND_REQUIRED}) + message(FATAL_ERROR "Could not fetch spz") + endif() +else() + include(FindPackageHandleStandardArgs) + + find_path(SPZ_INCLUDE_DIR + NAMES splat-types.h + ) + + find_package_handle_standard_args(spz + REQUIRED_VARS SPZ_INCLUDE_DIR + ) + + if(spz_FOUND) + file(GLOB SPZ_SRC_FILES + ${SPZ_INCLUDE_DIR}/*.cc + ${SPZ_INCLUDE_DIR}/*.h + ) + add_library(spz STATIC) + target_sources(spz PRIVATE ${SPZ_SRC_FILES}) + target_include_directories(spz PUBLIC ${SPZ_INCLUDE_DIR}) + target_link_libraries(spz PRIVATE ZLIB::ZLIB) + set_property(TARGET spz PROPERTY POSITION_INDEPENDENT_CODE ON) + set_property(TARGET spz PROPERTY CXX_STANDARD 17) + target_compile_definitions(spz PRIVATE "_USE_MATH_DEFINES") + + if (NOT MSVC) + target_compile_options(spz PRIVATE "-Wno-shorten-64-to-32") + endif() + + add_library(spz::spz ALIAS spz) + elseif(${spz_FIND_REQUIRED}) + message(FATAL_ERROR "Could not find spz") + endif() +endif() + + diff --git a/fbx/src/fbx.cpp b/fbx/src/fbx.cpp index d3f8e2c..4a02965 100644 --- a/fbx/src/fbx.cpp +++ b/fbx/src/fbx.cpp @@ -209,7 +209,7 @@ EmbedReadCBFunction(void* pUserData, } bool -readFbx(Fbx& fbx, const std::string& filename, bool onlyMaterials) +readFbx(Fbx& fbx, const std::string& filename, bool importImages, bool onlyMaterials) { GUARD(fbx.manager != nullptr, "Invalid fbx manager"); @@ -228,7 +228,7 @@ readFbx(Fbx& fbx, const std::string& filename, bool onlyMaterials) ios->SetBoolProp(IMP_FBX_TEXTURE, true); ios->SetBoolProp(IMP_FBX_ANIMATION, !onlyMaterials); ios->SetBoolProp(IMP_FBX_MODEL, !onlyMaterials); - fbx.loadImages = onlyMaterials; + fbx.loadImages = importImages; if (!importer->Initialize(filename.c_str(), -1, ios)) { FbxString error = importer->GetStatus().GetErrorString(); diff --git a/fbx/src/fbx.h b/fbx/src/fbx.h index 32ce510..cb81acb 100644 --- a/fbx/src/fbx.h +++ b/fbx/src/fbx.h @@ -71,8 +71,15 @@ struct Fbx ~Fbx(); }; +/* + * importImages Indicates whether the fbx should be set to load image data. It should be true if + * the images are being written out, and false otherwise + * + * onlyMaterials Indicates whether the fbx should only load materials. It should only be true if + * the file is being loaded just to separately load image textures, and nothing else is being used + */ bool -readFbx(Fbx& fbx, const std::string& filename, bool onlyMaterials); +readFbx(Fbx& fbx, const std::string& filename, bool importImages, bool onlyMaterials); bool writeFbx(const ExportFbxOptions& options, const Fbx& fbx, const std::string& filename); diff --git a/fbx/src/fbxExport.cpp b/fbx/src/fbxExport.cpp index 5a7123d..45bd0cf 100644 --- a/fbx/src/fbxExport.cpp +++ b/fbx/src/fbxExport.cpp @@ -1295,13 +1295,23 @@ exportFbxNodes(ExportFbxContext& ctx) parent->AddChild(fbxNode); exportFbxTransform(ctx, node, fbxNode); + if (node.markedInvisible) { + fbxNode->SetVisibility(false); + } if (node.camera >= 0) { + // Ignore camera invisibility, since it isn't important enough to add a new node FbxCamera* fbxCamera = ctx.cameras[node.camera]; fbxNode->AddNodeAttribute(fbxCamera); } if (node.light >= 0) { FbxLight* fbxLight = ctx.lights[node.light]; - fbxNode->AddNodeAttribute(fbxLight); + FbxNode* container = fbxNode; + if (ctx.usd->lights[node.light].markedInvisible) { + container = FbxNode::Create(ctx.fbx->scene, "light_visibility"); + container->SetVisibility(false); + fbxNode->AddChild(container); + } + container->AddNodeAttribute(fbxLight); } for (const auto& [skeletonIndex, meshIndices] : node.skinnedMeshes) { @@ -1334,10 +1344,18 @@ exportFbxNodes(ExportFbxContext& ctx) } const Mesh& m = ctx.usd->meshes[meshIndex]; FbxNode* container = fbxNode; - if (node.staticMeshes.size() > 1) { - - std::string containerName = getNodeName(node).c_str() + std::to_string(i); + if (node.staticMeshes.size() > 1 || m.markedInvisible) { + // Name the node based on the child index, unless there is only one child, in + // which case the node is only present to preserve visibility + std::string containerName = + node.staticMeshes.size() > 1 + ? getNodeName(node).c_str() + std::to_string(i) + : getNodeName(node).c_str() + std::string("_visibility"); container = FbxNode::Create(ctx.fbx->scene, containerName.c_str()); + + if (m.markedInvisible) { + container->SetVisibility(false); + } fbxNode->AddChild(container); } FbxMesh* fbxMesh = ctx.meshes[meshIndex]; diff --git a/fbx/src/fbxImport.cpp b/fbx/src/fbxImport.cpp index 949d0f4..2aa9155 100644 --- a/fbx/src/fbxImport.cpp +++ b/fbx/src/fbxImport.cpp @@ -486,7 +486,15 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) if (clusterCount > 0) { isSkinnedMesh = true; FbxCluster* firstCluster = skin->GetCluster(0); + if (firstCluster == nullptr) { + TF_WARN("Skin: %d does not have a first cluster.\n", i); + continue; + } FbxNode* firstlink = firstCluster->GetLink(); + if (firstlink == nullptr) { + TF_WARN("Skin: %d first cluster does not have a first link.\n", i); + continue; + } size_t skeletonIndex = ctx.skeletonsMap[firstlink]; ctx.meshSkinsMap[meshIndex] = skeletonIndex; @@ -503,7 +511,15 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) for (int j = 0; j < clusterCount; j++) { FbxCluster* cluster = skin->GetCluster(j); + if (cluster == nullptr) { + TF_WARN("No cluster at skin index %d.\n", j); + continue; + } FbxNode* link = cluster->GetLink(); + if (link == nullptr) { + TF_WARN("No link at skin index %d.\n", j); + continue; + } size_t jointIndex = ctx.bonesMap[link]; @@ -527,11 +543,28 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) int clusterControlPointIndicesCount = cluster->GetControlPointIndicesCount(); int* clusterControlPointIndices = cluster->GetControlPointIndices(); double* pointsWeights = cluster->GetControlPointWeights(); + if (clusterControlPointIndices == nullptr) { + TF_WARN("No cluster control point indices for skin cluster: %d.\n", j); + continue; + } + if (pointsWeights == nullptr) { + TF_WARN("No point weights for skin cluster: %d.\n", j); + continue; + } for (int k = 0; k < clusterControlPointIndicesCount; k++) { int controlPointIndex = clusterControlPointIndices[k]; - double influenceWeight = pointsWeights[k]; - indexes[controlPointIndex].push_back(jointIndex); - weights[controlPointIndex].push_back(influenceWeight); + if (controlPointIndex > indexes.size() || controlPointIndex > weights.size()) { + TF_WARN("Control Point Index outside of index or weight bounds. index: %d " + " Index Size: %d Weight Size: %d", + controlPointIndex, + indexes.size(), + weights.size()); + continue; + } else { + double influenceWeight = pointsWeights[k]; + indexes[controlPointIndex].push_back(jointIndex); + weights[controlPointIndex].push_back(influenceWeight); + } } } } @@ -2001,6 +2034,16 @@ importFbxNodes(ImportFbxContext& ctx, FbxNode* fbxNode, int parent) auto [nodeIndex, node] = ctx.usd->addNode(parent); node.name = fbxNode->GetName(); + if (!fbxNode->GetVisibility()) { + node.markedInvisible = true; + } + if (!fbxNode->VisibilityInheritance.Get()) { // True by default + TF_WARN("importFbxNodes: Node %s does not inherit visibility (VisibilityInheritance = " + "false). This is currently unsupported. The node is set as %s", + fbxNode->GetName(), + node.markedInvisible ? "invisible" : "visible"); + } + ctx.nodeMap[fbxNode] = nodeIndex; TF_DEBUG_MSG(FILE_FORMAT_FBX, "importFbx: node %s\n", node.name.c_str()); diff --git a/fbx/src/fbxResolver.cpp b/fbx/src/fbxResolver.cpp index 3a3832f..d834368 100644 --- a/fbx/src/fbxResolver.cpp +++ b/fbx/src/fbxResolver.cpp @@ -38,7 +38,7 @@ FbxResolver::readCache(const std::string& filename, std::vector& ima TfStopwatch watch; TF_DEBUG_MSG(FBX_PACKAGE_RESOLVER, "START TOTAL: %ld\n", static_cast(watch.GetMilliseconds())); watch.Start(); - VOID_GUARD(readFbx(fbx, filename, true), "Error reading FBX from %s\n", filename.c_str()); + VOID_GUARD(readFbx(fbx, filename, true, true), "Error reading FBX from %s\n", filename.c_str()); watch.Stop(); TF_DEBUG_MSG(FBX_PACKAGE_RESOLVER, "STOP TOTAL: %ld\n", static_cast(watch.GetMilliseconds())); ImportFbxOptions options; diff --git a/fbx/src/fileFormat.cpp b/fbx/src/fileFormat.cpp index 6adb5f4..0159322 100644 --- a/fbx/src/fileFormat.cpp +++ b/fbx/src/fileFormat.cpp @@ -119,8 +119,9 @@ UsdFbxFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool me { const std::lock_guard lock(mutex); // FBX SDK is not thread safe Fbx fbx; - GUARD( - readFbx(fbx, resolvedPath, false), "Error reading FBX from %s\n", resolvedPath.c_str()); + GUARD(readFbx(fbx, resolvedPath, options.importImages, false), + "Error reading FBX from %s\n", + resolvedPath.c_str()); GUARD(importFbx(options, fbx, usd), "Error translating FBX to USD\n"); } GUARD(writeLayer( diff --git a/gltf/src/fileFormat.cpp b/gltf/src/fileFormat.cpp index 07f4d9f..27bf414 100644 --- a/gltf/src/fileFormat.cpp +++ b/gltf/src/fileFormat.cpp @@ -254,6 +254,9 @@ UsdGltfFileFormat::WriteToFile(const SdfLayer& layer, // don't set a max on exported joints and weights when reading the USD data options.maxMeshInfluenceCount = -1; + // glTF doesn't support invisible primitives, so we filter them out here + options.ignoreInvisible = true; + UsdData usd; GUARD(readLayer(options, layer, usd, DEBUG_TAG), "Error reading USD file\n"); diff --git a/gltf/src/gltfExport.cpp b/gltf/src/gltfExport.cpp index 87c11ea..c215b23 100644 --- a/gltf/src/gltfExport.cpp +++ b/gltf/src/gltfExport.cpp @@ -1540,6 +1540,10 @@ exportMaterials(ExportGltfContext& ctx) exportTextureTransform(ctx, emissive, gm.emissiveTexture.extensions); exportTexture(ctx, normal, gm.normalTexture.index, gm.normalTexture.texCoord); + // Get the normal scale from the normal scale input if it is holding a single value + if (m.normalScale.value.IsHolding()) { + gm.normalTexture.scale = m.normalScale.value.UncheckedGet(); + } exportTextureTransform(ctx, normal, gm.normalTexture.extensions); // Occlusion texture needs to be in the r channel diff --git a/obj/src/fileFormat.cpp b/obj/src/fileFormat.cpp index 8aa921c..b6f3a62 100644 --- a/obj/src/fileFormat.cpp +++ b/obj/src/fileFormat.cpp @@ -163,6 +163,8 @@ UsdObjFileFormat::WriteToFile(const SdfLayer& layer, Obj obj; ReadLayerOptions layerOptions; layerOptions.flatten = true; + // OBJ doesn't support invisible primitives, so we filter them out here + layerOptions.ignoreInvisible = true; argReadString(args, "outputColorSpace", obj.outputColorSpace, DEBUG_TAG); ExportObjOptions options; options.filename = filename; diff --git a/obj/src/obj.cpp b/obj/src/obj.cpp index ba42a01..58abc5a 100644 --- a/obj/src/obj.cpp +++ b/obj/src/obj.cpp @@ -107,6 +107,7 @@ struct ObjIntermediate { int index = 0; const char* data = nullptr; + size_t dataSize = 0; const char* begin = nullptr; const char* end = nullptr; bool error = false; @@ -125,8 +126,73 @@ struct ObjIntermediate std::vector mdllibs; std::vector comments; std::vector entries; + int lineNum; }; +void +warnFromIntermediateAndCalculateLine(const ObjIntermediate& inter, const char* p) +{ + // If the data is empty, can't calculate the line + if (inter.dataSize == 0) { + TF_WARN("Error parsing OBJ: error calculating line number of empty data"); + return; + } + + const char* dataEnd = inter.data + (sizeof(char) * (inter.dataSize)); + + // Ensure p points to within the data block + if (p >= dataEnd || p < inter.data) { + TF_WARN("Error parsing OBJ: error calculating line number of invalid character"); + return; + } + + size_t lineNum = 1; + bool pFound = false; + + const char* lineBegin = inter.data; + const char* it = inter.data; + while (it < dataEnd) { + // Convert the iterator to a pointer to compare with p, but get benefits of using iters + if (it >= p) { + // Found the line p is on, but keep parsing until we have the complete line for the + // error message. + pFound = true; + } + + // Handle line breaks. Reads "\r" as a new line, but "\r\n" as only one new line + // + // \r may be a line break in legacy systems, or erroneous files where a \n is corrupted + // but there is a \r will still count as a line break, so the line number in the error + // message is consistent with the behavior of many other text editors + if (*it == '\n' || *it == '\r') { + if (pFound) { + // Found the error char and got to the end of the line, so we can break + break; + } else { + lineNum++; + + const char prevChar = *it; + + // The current line begins at the char after \n or \r + lineBegin = ++it; + + // Don't count \r\n as two new lines, skip an extra character forward + if (it < dataEnd && prevChar == '\r' && *it == '\n') { + lineBegin = ++it; + } + } + } else { + // Read the next char + ++it; + } + } + + size_t lineSize = it - lineBegin; + std::string line(lineBegin, lineSize); + + TF_WARN("Error parsing OBJ: Failed parsing line %zu:\n%s", lineNum, line.c_str()); +} + /// Read an entire file to a buffer. bool readFileContents(const std::string& filename, std::vector& buffer) @@ -137,12 +203,17 @@ readFileContents(const std::string& filename, std::vector& buffer) } fseek(file, 0, SEEK_END); int length = ftell(file); - fseek(file, 0, SEEK_SET); - buffer.resize(length + 1); - fread(buffer.data(), length, 1, file); - buffer[length] = '\0'; - fclose(file); - return true; + if (length < 0) { + TF_WARN("Unable to read file %s"); + return false; + } else { + fseek(file, 0, SEEK_SET); + buffer.resize(length + 1); + fread(buffer.data(), length, 1, file); + buffer[length] = '\0'; + fclose(file); + return true; + } } /// Helper parsing function. `p` is the moving pointer into the data. @@ -362,21 +433,13 @@ nextIndex(const char*& p, const char* end, bool& endOfLine, int& x) endOfLine = q >= end || *q == '\n' || (q + 1 < end && *(q + 1) == '\r'); if (p == q) return; // this is the case for an empty index -// XXX this could lead to subtle parsing differences between Windows and the other platforms -// can we just use one or the other in all cases? Also, std::from_chars is C++17 only -#ifdef _WIN32 - std::from_chars_result result = std::from_chars(p, q, x); - if (result.ec != std::errc()) - return; -#else + // strtol returns 0 on error. Coincidentally, we never expect an integer with value 0. char* qq; x = std::strtol(p, &qq, 10); if (x == 0) return; - q = qq; -#endif - p = q; + p = qq; }; /// Helper parsing function. Add an entry to the intermediate's entries. @@ -423,6 +486,7 @@ splitObjIntermediates(const std::vector& data, // filePointer++; intermediates[i].index = i; intermediates[i].data = data.data(); + intermediates[i].dataSize = data.size(); intermediates[i].begin = data.data() + begin; intermediates[i].end = data.data() + end; } @@ -469,6 +533,7 @@ readObjIntermediate(ObjIntermediate& inter) inter.vertices.push_back(GfVec3f(f0, f1, f2)); } else { inter.error = true; + warnFromIntermediateAndCalculateLine(inter, p); return; } } else if (c0 == 'v' && c1 == 't') { @@ -478,6 +543,7 @@ readObjIntermediate(ObjIntermediate& inter) inter.uvs.push_back(GfVec2f(f0, f1)); } else { inter.error = true; + warnFromIntermediateAndCalculateLine(inter, p); return; } } else if (c0 == 'v' && c1 == 'n') { @@ -487,6 +553,7 @@ readObjIntermediate(ObjIntermediate& inter) inter.normals.push_back(GfVec3f(f0, f1, f2)); } else { inter.error = true; + warnFromIntermediateAndCalculateLine(inter, p); return; } } else if (c0 == 'f' && c1 == ' ') { @@ -507,6 +574,7 @@ readObjIntermediate(ObjIntermediate& inter) inter.points.push_back(GfVec3i(vIndex, vtIndex, vnIndex)); } else { // can't have all of them fail or being zero inter.error = true; + warnFromIntermediateAndCalculateLine(inter, p); return; } @@ -519,6 +587,7 @@ readObjIntermediate(ObjIntermediate& inter) } else if (c0 == 'u' && c1 == 's') { if (!checkWord(p, end, "usemtl")) { inter.error = true; + warnFromIntermediateAndCalculateLine(inter, p); return; } inter.usemtls.push_back(std::string()); @@ -527,6 +596,7 @@ readObjIntermediate(ObjIntermediate& inter) } else if (c0 == 'm' && c1 == 't') { if (!checkWord(p, end, "mtllib")) { inter.error = true; + warnFromIntermediateAndCalculateLine(inter, p); return; } std::string temp; @@ -536,6 +606,7 @@ readObjIntermediate(ObjIntermediate& inter) } else if (c0 == 'a' && c1 == 'd') { if (!checkWord(p, end, "adobe_mdllib")) { inter.error = true; + warnFromIntermediateAndCalculateLine(inter, p); return; } std::string temp; diff --git a/ply/src/fileFormat.cpp b/ply/src/fileFormat.cpp index 8613a16..e3d58c6 100644 --- a/ply/src/fileFormat.cpp +++ b/ply/src/fileFormat.cpp @@ -172,6 +172,8 @@ UsdPlyFileFormat::WriteToFile(const SdfLayer& layer, PLYData ply; ReadLayerOptions layerOptions; layerOptions.flatten = true; + // PLY doesn't support invisible primitives, so we filter them out here + layerOptions.ignoreInvisible = true; SdfAbstractDataRefPtr layerData = InitData(layer.GetFileFormatArguments()); PlyDataConstPtr data = TfDynamic_cast(layerData); GUARD(readLayer(layerOptions, layer, usd, DEBUG_TAG), "Error reading USD\n"); diff --git a/spz/CMakeLists.txt b/spz/CMakeLists.txt new file mode 100644 index 0000000..657b82f --- /dev/null +++ b/spz/CMakeLists.txt @@ -0,0 +1,23 @@ +option(NO_UNDEFINED "Active no-undefined compile options" ON) +option(USD_FILEFORMATS_ENABLE_ASSET_TESTS "Build the more in depth unit tests using downloaded assets." OFF) +option(USDSPZ_ENABLE_INSTALL "Enable installation of plugin artifacts" ON) + +if (NOT TARGET usd) + find_package(pxr REQUIRED) +endif() +if(USD_FILEFORMATS_BUILD_TESTS) + find_package(GTest REQUIRED) +endif() +find_package(spz REQUIRED) +find_package(SphericalHarmonics REQUIRED) + + +add_subdirectory(src) +if (USD_FILEFORMATS_BUILD_TESTS) + add_subdirectory(tests) +endif () + +# We only want PLY in our package +set(CPACK_INSTALL_CMAKE_PROJECTS "src;usdSpz;ALL;/") + +include(CPack) diff --git a/spz/README.md b/spz/README.md new file mode 100644 index 0000000..b28c9a5 --- /dev/null +++ b/spz/README.md @@ -0,0 +1,33 @@ +# USDSPZ + +[![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/windows-2022-2411-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/windows-2022-2308-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) + +[![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/macOS-14-2411-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/macOS-14-2408-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) + +[![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/macOS-13-2411-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/macOS-13-2308-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) + +[![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/ubuntu-22.04-2411-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/ubuntu-22.04-2308-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) + +## Supported features + +|Feature|Import|Export| +|--|--|--| +|Gaussian splats |✅|✅| + +## File Format Arguments + +**Import:** + +* `spzGsplatsClippingBox`: imported Gaussian splats will be clipped with the range specified by this box, where the value is a string in the form of `[-X, -Y, -Z, X, Y, Z]`, by default it is -2 to 2 on each axis. +* `spzGsplatsWithZup`: Whether the imported Gaussian splat is treated as a Z-up object. If so we apply a rotation + to Y-up during importing. By default it is false. + The following imports UsdGeomPoints instances as Gaussian splats without rotation (if the SPZ contains all the Gaussian-splat-related attributes). + ``` + UsdStageRefPtr stage = UsdStage::Open("gsplat.spz:SDF_FORMAT_ARGS:spzGsplatsWithZup=false") + stage->Export("gsplat.usd") + ``` + +## Debug codes +* `FILE_FORMAT_SPZ`: Common debug messages. + + diff --git a/spz/src/CMakeLists.txt b/spz/src/CMakeLists.txt new file mode 100644 index 0000000..55f428f --- /dev/null +++ b/spz/src/CMakeLists.txt @@ -0,0 +1,124 @@ +add_library(usdSpz SHARED) + +usd_plugin_compile_config(usdSpz) +target_compile_definitions(usdSpz PRIVATE USDSPZ_EXPORTS) + +target_sources(usdSpz +PRIVATE + "api.h" + "debugCodes.h" + "debugCodes.cpp" + "fileFormat.h" + "fileFormat.cpp" + "spzImport.h" + "spzImport.cpp" + "spzExport.h" + "spzExport.cpp" +) + +target_include_directories(usdSpz +PRIVATE + "${PROJECT_BINARY_DIR}" +) + +target_link_libraries(usdSpz +PRIVATE + tf + sdf + usd + usdGeom + usdSkel + usdShade + spz::spz + SphericalHarmonics::SphericalHarmonics + fileformatUtils +) + +target_precompile_headers(usdSpz +PRIVATE + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" +) + +# Installation of plugin files mimics the file structure that USD has for plugins, +# so it is easy to deploy it in a pre-existing USD build, if one chooses to do so. + +# Allow an option for deferring the path replacement to install time +if(USD_PLUGIN_DEFER_LIBRARY_PATH_REPLACEMENT) + set(PLUG_INFO_LIBRARY_PATH "\$\{PLUG_INFO_LIBRARY_PATH\}") +else() + set(PLUG_INFO_LIBRARY_PATH "../${CMAKE_SHARED_LIBRARY_PREFIX}usdSpz${CMAKE_SHARED_LIBRARY_SUFFIX}") +endif() +configure_file(plugInfo.json.in plugInfo.json) +set_target_properties(usdSpz PROPERTIES RESOURCE ${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json) + +set_target_properties(usdSpz PROPERTIES RESOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json:plugInfo.json") + +if(USDSPZ_ENABLE_INSTALL) + install( + TARGETS usdSpz + RUNTIME DESTINATION plugin/usd COMPONENT Runtime + LIBRARY DESTINATION plugin/usd COMPONENT Runtime + RESOURCE DESTINATION plugin/usd/usdSpz/resources COMPONENT Runtime + ) + + install( + FILES plugInfo.root.json + DESTINATION plugin/usd + RENAME plugInfo.json + COMPONENT Runtime + ) +endif() diff --git a/spz/src/api.h b/spz/src/api.h new file mode 100644 index 0000000..163eb23 --- /dev/null +++ b/spz/src/api.h @@ -0,0 +1,32 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#pragma once + +#include "pxr/base/arch/export.h" + +#if defined(PXR_STATIC) +# define USDSPZ_API +# define USDSPZ_API_TEMPLATE_CLASS(...) +# define USDSPZ_API_TEMPLATE_STRUCT(...) +# define USDSPZ_LOCAL +#else +# if defined(USDSPZ_EXPORTS) +# define USDSPZ_API ARCH_EXPORT +# define USDSPZ_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +# define USDSPZ_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +# else +# define USDSPZ_API ARCH_IMPORT +# define USDSPZ_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +# define USDSPZ_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +# endif +# define USDSPZ_LOCAL ARCH_HIDDEN +#endif \ No newline at end of file diff --git a/spz/src/debugCodes.cpp b/spz/src/debugCodes.cpp new file mode 100644 index 0000000..17df714 --- /dev/null +++ b/spz/src/debugCodes.cpp @@ -0,0 +1,15 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#include "debugCodes.h" +#include + +const std::string DEBUG_TAG = "SPZ"; \ No newline at end of file diff --git a/spz/src/debugCodes.h b/spz/src/debugCodes.h new file mode 100644 index 0000000..6a6aa91 --- /dev/null +++ b/spz/src/debugCodes.h @@ -0,0 +1,20 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#pragma once +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE +TF_DEBUG_CODES(FILE_FORMAT_SPZ, SPZ_PACKAGE_RESOLVER) +PXR_NAMESPACE_CLOSE_SCOPE + +extern const std::string DEBUG_TAG; \ No newline at end of file diff --git a/spz/src/fileFormat.cpp b/spz/src/fileFormat.cpp new file mode 100644 index 0000000..698f862 --- /dev/null +++ b/spz/src/fileFormat.cpp @@ -0,0 +1,173 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#include "fileFormat.h" + +#include "debugCodes.h" +#include "spzExport.h" +#include "spzImport.h" + +#include +#include +#include +#include + +#include + +#include +#include + +using namespace adobe::usd; +using namespace spz; + +PXR_NAMESPACE_OPEN_SCOPE + +TF_DEFINE_PUBLIC_TOKENS(UsdSpzFileFormatTokens, USDSPZ_FILE_FORMAT_TOKENS); + +TF_REGISTRY_FUNCTION(TfType) +{ + SDF_DEFINE_FILE_FORMAT(UsdSpzFileFormat, SdfFileFormat); +} + +UsdSpzFileFormat::UsdSpzFileFormat() + : SdfFileFormat(UsdSpzFileFormatTokens->Id, + UsdSpzFileFormatTokens->Version, + UsdSpzFileFormatTokens->Target, + UsdSpzFileFormatTokens->Id) +{ + TF_DEBUG_MSG(FILE_FORMAT_SPZ, "usdspz %s\n", FILE_FORMATS_VERSION); +} + +UsdSpzFileFormat::~UsdSpzFileFormat() {} + +SdfAbstractDataRefPtr +UsdSpzFileFormat::InitData(const FileFormatArguments& args) const +{ + SpzDataRefPtr pd(new SpzData()); + for (auto arg : args) { + TF_DEBUG_MSG( + FILE_FORMAT_SPZ, "FileFormatArg: %s = %s\n", arg.first.c_str(), arg.second.c_str()); + } + argReadBool( + args, UsdSpzFileFormatTokens->gsplatsWithZup.GetText(), pd->gsplatsWithZup, DEBUG_TAG); + argReadFloatArray( + args, UsdSpzFileFormatTokens->gsplatsClippingBox.GetText(), pd->gsplatsClippingBox, DEBUG_TAG); + return pd; +} + +void +UsdSpzFileFormat::ComposeFieldsForFileFormatArguments(const std::string& assetPath, + const PcpDynamicFileFormatContext& context, + FileFormatArguments* args, + VtValue* dependencyContextData) const +{ + argComposeBool(context, args, UsdSpzFileFormatTokens->gsplatsWithZup, DEBUG_TAG); + argComposeFloatArray(context, args, UsdSpzFileFormatTokens->gsplatsClippingBox, DEBUG_TAG); +} + +bool +UsdSpzFileFormat::CanFieldChangeAffectFileFormatArguments( + const TfToken& field, + const VtValue& oldValue, + const VtValue& newValue, + const VtValue& dependencyContextData) const +{ + return true; +} + +bool +UsdSpzFileFormat::CanRead(const std::string& filePath) const +{ + // Could check to see if it looks like valid spz data... + return true; +} + +bool +UsdSpzFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool metadataOnly) const +{ + TfStopwatch w; + w.Start(); + TF_DEBUG_MSG(FILE_FORMAT_SPZ, "Read: %s\n", resolvedPath.c_str()); + std::string fileType = getFileExtension(resolvedPath, DEBUG_TAG); + SdfAbstractDataRefPtr layerData = InitData(layer->GetFileFormatArguments()); + SpzDataConstPtr data = TfDynamic_cast(layerData); + UsdData usd; + try { + WriteLayerOptions layerOptions; + ImportSpzOptions options; + options.importGsplatWithZup = data->gsplatsWithZup; + options.importGsplatClippingBox = data->gsplatsClippingBox; + GaussianCloud gaussianCloud = loadSpz(resolvedPath); + GUARD(importSpz(options, gaussianCloud, usd), "Error translating SPZ to USD\n"); + GUARD( + writeLayer(layerOptions, usd, layer, layerData, fileType, DEBUG_TAG, SdfFileFormat::_SetLayerData), + "Error writing to the USD layer\n"); + } catch (std::exception& e) { + TF_DEBUG_MSG(FILE_FORMAT_SPZ, "Failed to open %s: %s\n", resolvedPath.c_str(), e.what()); + } + w.Stop(); + TF_DEBUG_MSG(FILE_FORMAT_SPZ, "Total time: %ld\n", static_cast(w.GetMilliseconds())); + return true; +} + +bool +UsdSpzFileFormat::ReadFromString(SdfLayer* layer, const std::string& input) const +{ + return true; +} + +bool +UsdSpzFileFormat::WriteToFile(const SdfLayer& layer, + const std::string& filename, + const std::string& comment, + const FileFormatArguments& args) const +{ + TfStopwatch w; + w.Start(); + UsdData usd; + GaussianCloud gaussianCloud; + ReadLayerOptions layerOptions; + layerOptions.flatten = true; + // SPZ doesn't support invisible primitives, so we filter them out here + layerOptions.ignoreInvisible = true; + SdfAbstractDataRefPtr layerData = InitData(layer.GetFileFormatArguments()); + SpzDataConstPtr data = TfDynamic_cast(layerData); + GUARD(readLayer(layerOptions, layer, usd, DEBUG_TAG), "Error reading USD\n"); + GUARD(exportSpz(usd, gaussianCloud), "Error translating USD to SPZ\n"); + try { + const std::string parentPath = TfGetPathName(filename); + TfMakeDirs(parentPath, -1, true); + saveSpz(gaussianCloud, filename); + } catch (std::exception& e) { + TF_DEBUG_MSG(FILE_FORMAT_SPZ, "Error writing SPZ to %s: %s\n", filename.c_str(), e.what()); + } + w.Stop(); + TF_DEBUG_MSG(FILE_FORMAT_SPZ, "Total time: %ld\n", static_cast(w.GetMilliseconds())); + return true; +} + +bool +UsdSpzFileFormat::WriteToString(const SdfLayer& layer, + std::string* str, + const std::string& comment) const +{ + // Write USD as SPZ: Defer to the usda file format for now. + return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToString(layer, str, comment); +} + +bool +UsdSpzFileFormat::WriteToStream(const SdfSpecHandle& spec, std::ostream& out, size_t indent) const +{ + // Write USD as SPZ: Defer to the usda file format for now. + return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToStream(spec, out, indent); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/spz/src/fileFormat.h b/spz/src/fileFormat.h new file mode 100644 index 0000000..f201e4b --- /dev/null +++ b/spz/src/fileFormat.h @@ -0,0 +1,107 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#pragma once +#include "api.h" +#include +#include +#include +#include +#include +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +// clang-format off +#define USDSPZ_FILE_FORMAT_TOKENS \ + ((Id, "spz")) \ + ((Version, FILE_FORMATS_VERSION)) \ + ((Target, "usd")) \ + ((gsplatsWithZup, "spzGsplatsWithZup")) \ + ((gsplatsClippingBox, "spzGsplatsClippingBox")) +// clang-format on + +TF_DECLARE_PUBLIC_TOKENS(UsdSpzFileFormatTokens, USDSPZ_FILE_FORMAT_TOKENS); +TF_DECLARE_WEAK_AND_REF_PTRS(SpzData); +TF_DECLARE_WEAK_AND_REF_PTRS(UsdSpzFileFormat); + +/// \ingroup usdspz +/// \brief SdfData specialization for working with spz files. +class SpzData : public FileFormatDataBase +{ + public: + bool gsplatsWithZup = false; + PXR_NS::VtFloatArray gsplatsClippingBox = { -2.0, -2.0, -2.0, 2.0, 2.0, 2.0 }; + static SpzDataRefPtr InitData(const SdfFileFormat::FileFormatArguments& args); +}; + +/// \ingroup usdspz +/// \brief SdfFileFormat specialization for working with spz files. +class USDSPZ_API UsdSpzFileFormat + : public SdfFileFormat + , public PcpDynamicFileFormatInterface +{ + public: + friend class SpzData; + + virtual SdfAbstractDataRefPtr InitData(const FileFormatArguments& args) const override; + + virtual void ComposeFieldsForFileFormatArguments(const std::string& assetPath, + const PcpDynamicFileFormatContext& context, + FileFormatArguments* args, + VtValue* dependencyContextData) const override; + + virtual bool CanFieldChangeAffectFileFormatArguments( + const TfToken& field, + const VtValue& oldValue, + const VtValue& newValue, + const VtValue& dependencyContextData) const override; + + virtual bool CanRead(const std::string& file) const override; + + virtual bool Read(SdfLayer* layer, + const std::string& resolvedPath, + bool metadataOnly) const override; + + virtual bool ReadFromString(SdfLayer* layer, const std::string& input) const override; + + virtual bool WriteToFile( + const SdfLayer& layer, + const std::string& filePath, + const std::string& comment = std::string(), + const FileFormatArguments& args = FileFormatArguments()) const override; + + virtual bool WriteToStream(const SdfSpecHandle& spec, + std::ostream& out, + size_t indent) const override; + + virtual bool WriteToString(const SdfLayer& layer, + std::string* str, + const std::string& comment = std::string()) const override; + + protected: + static const TfToken gsplatsWithZupToken; + static const TfToken gsplatsWithClippingToken; + + SDF_FILE_FORMAT_FACTORY_ACCESS; + virtual ~UsdSpzFileFormat(); + UsdSpzFileFormat(); + + private: + bool ReadFromStream(SdfLayer* layer, + std::istream& input, + bool metadataOnly, + std::string* outError, + std::istream& mtlinput) const; +}; + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/spz/src/plugInfo.json.in b/spz/src/plugInfo.json.in new file mode 100644 index 0000000..bbaf612 --- /dev/null +++ b/spz/src/plugInfo.json.in @@ -0,0 +1,37 @@ +{ + "Plugins": [ + { + "Info": { + "SdfMetadata": { + "spzGsplatsClippingBox": { + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "The clipping box for the imported Gaussian splat, in the order of [-X, -Y, -Z, X, Y, Z]", + "type": "string" + }, + "spzGsplatsWithZup": { + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Should the imported Gaussian splat be treated as Z-up", + "type": "bool" + } + }, + "Types": { + "UsdSpzFileFormat": { + "bases": ["SdfFileFormat"], + "displayName": "SPZ (Polygon File Format)", + "extensions": ["spz"], + "formatId": "spz", + "primary": true, + "target": "usd" + } + } + }, + "LibraryPath": "${PLUG_INFO_LIBRARY_PATH}", + "Name": "usdSpz_plugin", + "ResourcePath": "resources", + "Root": "..", + "Type": "library" + } + ] +} diff --git a/spz/src/plugInfo.root.json b/spz/src/plugInfo.root.json new file mode 100644 index 0000000..1276914 --- /dev/null +++ b/spz/src/plugInfo.root.json @@ -0,0 +1,5 @@ +{ + "Includes": [ + "*/resources/" + ] +} diff --git a/spz/src/spzExport.cpp b/spz/src/spzExport.cpp new file mode 100644 index 0000000..d438dea --- /dev/null +++ b/spz/src/spzExport.cpp @@ -0,0 +1,258 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#include "spzExport.h" +#include "debugCodes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace PXR_NS; + +namespace adobe::usd { + +struct SpzTotalMesh +{ + VtVec3fArray points; + VtVec3fArray color; + VtFloatArray opacity; + + VtFloatArray widths; + VtFloatArray widths1; + VtFloatArray widths2; + VtQuatfArray rotations; + std::vector shCoeffs; +}; + +std::size_t +findMaxSHCoeffSize(const UsdData& usd, int nodeIndex) +{ + std::size_t maxSHCoeffSize = 0; + const Node& node = usd.nodes[nodeIndex]; + for (int meshIndex : node.staticMeshes) { + const Mesh& mesh = usd.meshes[meshIndex]; + if (!mesh.asGsplats) + continue; + maxSHCoeffSize = std::max(maxSHCoeffSize, mesh.pointSHCoeffs.size()); + } + for (size_t i = 0; i < node.children.size(); ++i) { + maxSHCoeffSize = std::max(maxSHCoeffSize, findMaxSHCoeffSize(usd, node.children[i])); + } + return maxSHCoeffSize; +} + +void +aggregateMeshInstance(SpzTotalMesh& totalMesh, + const Mesh& mesh, + const GfMatrix4d& modelMatrix) +{ + size_t currentMeshPointsSize = mesh.points.size(); + size_t offset = totalMesh.points.size(); + totalMesh.points.resize(offset + currentMeshPointsSize); + totalMesh.opacity.resize(offset + mesh.points.size(), 1.0f); + totalMesh.color.resize(offset + mesh.points.size(), GfVec3f(0.0f, 0.0f, 0.0f)); + + for (size_t i = 0; i < currentMeshPointsSize; ++i) { + totalMesh.points[offset + i] = GfVec3f(modelMatrix.Transform(mesh.points[i])); + } + + const size_t numPointOpacities = std::min(currentMeshPointsSize, mesh.opacities[0].values.size()); + memcpy(totalMesh.opacity.data() + offset, + mesh.opacities[0].values.data(), + numPointOpacities * sizeof(mesh.opacities[0].values[0])); + + const size_t numPointColors = std::min(currentMeshPointsSize, mesh.colors[0].values.size()); + memcpy(totalMesh.color.data() + offset, + mesh.colors[0].values.data(), + numPointColors * sizeof(mesh.colors[0].values[0])); + + GfMatrix4f modelMatrixFloat(modelMatrix); + const float modelScaling = std::cbrt(std::abs(modelMatrixFloat.GetDeterminant())); + GfQuatf modelRotation = modelMatrixFloat.ExtractRotationQuat().GetNormalized(); + + scalePointWidths(mesh.pointWidths, + mesh.pointExtraWidths, + currentMeshPointsSize, + modelScaling, + totalMesh.widths, + totalMesh.widths1, + totalMesh.widths2); + rotatePointRotations( + mesh.pointRotations, modelRotation, currentMeshPointsSize, totalMesh.rotations); + rotatePointSphericalHarmonics( + mesh.pointSHCoeffs, modelRotation, currentMeshPointsSize, totalMesh.shCoeffs); + + TF_DEBUG_MSG(FILE_FORMAT_SPZ, + "spz::export aggregated mesh %s { v: %lu }\n", + mesh.name.c_str(), + currentMeshPointsSize); +} + +void +traverseNodesAndAggregateMeshes(const UsdData& usd, + SpzTotalMesh& totalMesh, + const GfMatrix4d& correctionTransform, + int nodeIndex) +{ + const Node& node = usd.nodes[nodeIndex]; + GfMatrix4d modelMatrix = node.worldTransform * correctionTransform; + + for (int meshIndex : node.staticMeshes) { + const Mesh& mesh = usd.meshes[meshIndex]; + if (!mesh.asGsplats) + continue; + aggregateMeshInstance(totalMesh, mesh, modelMatrix); + } + + for (size_t i = 0; i < node.children.size(); ++i) { + traverseNodesAndAggregateMeshes( + usd, totalMesh, correctionTransform, node.children[i]); + } +} + +float +encodeGsplatOpacity(float opacity) +{ + // Make sure the inversed sigmoid function doesn't cause + // infinite result. + const float clampedOpacity = std::clamp( + opacity, std::numeric_limits::min(), 1.0f - std::numeric_limits::epsilon()); + return -log(1.0f / clampedOpacity - 1.0f); +} + +float +encodeGsplatWidth(float width) +{ + // Make sure the log function doesn't cause + // infinite result. + const float clamped_half_width = std::max(std::numeric_limits::min(), width * 0.5f); + return log(clamped_half_width); +} + +bool +exportSpz(const UsdData& usd, spz::GaussianCloud& gaussianCloud) +{ + if (usd.meshes.size() <= 0) { + TF_DEBUG_MSG(FILE_FORMAT_SPZ, + "spz::export no instances of UsdGeomMesh, nothing will be exported\n"); + return true; + } + + // Because Spz does not support multiple individual meshes, we need to aggregate all meshes into + // a single mesh and apply their local to world transforms, together with the system's + // correction transform. + SpzTotalMesh totalMesh; + GfMatrix4d correctionTransform = getTransformToMetersPositiveY(usd.metersPerUnit, usd.upAxis); + + std::size_t numGsplatsSHCoeffs = 0; + for (size_t i = 0; i < usd.rootNodes.size(); ++i) { + numGsplatsSHCoeffs = std::max(numGsplatsSHCoeffs, findMaxSHCoeffSize(usd, usd.rootNodes[i])); + } + + // We only store SH coefficients up to the degree with complete bands (i.e., 0, 9, 24, or 45 + // coefficients). + const std::size_t numSHDegrees = numSHDegreesFromGsplat(numGsplatsSHCoeffs); + const std::size_t numNonZeroSHBands = numNonZeroSHBandsFromDegree(numSHDegrees); + numGsplatsSHCoeffs = numNonZeroSHBands * 3; + + totalMesh.shCoeffs.resize(numGsplatsSHCoeffs); + + for (size_t i = 0; i < usd.rootNodes.size(); ++i) { + traverseNodesAndAggregateMeshes( + usd, totalMesh, correctionTransform, usd.rootNodes[i]); + } + + gaussianCloud.numPoints = totalMesh.points.size(); + gaussianCloud.shDegree = numSHDegrees; + gaussianCloud.positions.resize(totalMesh.points.size() * 3); + memcpy(gaussianCloud.positions.data(), + totalMesh.points.data(), + totalMesh.points.size() * sizeof(totalMesh.points[0])); + + // Zeroth coefficient of SH, inversed as 2sqrt(pi) + constexpr float invShC0 = 3.5449077018f; + gaussianCloud.colors.resize(totalMesh.color.size() * 3); + for (size_t i = 0; i < totalMesh.color.size(); ++i) { + gaussianCloud.colors[i * 3 + 0] = (totalMesh.color[i][0] - 0.5f) * invShC0; + gaussianCloud.colors[i * 3 + 1] = (totalMesh.color[i][1] - 0.5f) * invShC0; + gaussianCloud.colors[i * 3 + 2] = (totalMesh.color[i][2] - 0.5f) * invShC0; + } + + gaussianCloud.alphas.resize(totalMesh.opacity.size()); + for (size_t i = 0; i < totalMesh.opacity.size(); ++i) { + gaussianCloud.alphas[i] = encodeGsplatOpacity(totalMesh.opacity[i]); + } + + gaussianCloud.scales.resize(totalMesh.widths.size() * 3); + for (size_t i = 0; i < totalMesh.widths.size(); ++i) { + gaussianCloud.scales[i * 3 + 0] = encodeGsplatWidth(totalMesh.widths[i]); + gaussianCloud.scales[i * 3 + 1] = encodeGsplatWidth(totalMesh.widths1[i]); + gaussianCloud.scales[i * 3 + 2] = encodeGsplatWidth(totalMesh.widths2[i]); + } + + gaussianCloud.rotations.resize(totalMesh.rotations.size() * 4); + memcpy(gaussianCloud.rotations.data(), + totalMesh.rotations.data(), + totalMesh.rotations.size() * sizeof(totalMesh.rotations[0])); + + gaussianCloud.sh.resize(numGsplatsSHCoeffs * totalMesh.points.size()); + // SPZ stores SH coefficients in row-major order, different than USD's column-major order. + for (size_t shRowIndex = 0; shRowIndex < numNonZeroSHBands; ++shRowIndex) { + for (size_t shColIndex = 0; shColIndex < 3; ++shColIndex) { + const std::size_t spzSHIndex = shRowIndex * 3 + shColIndex; + const std::size_t usdSHIndex = shColIndex * numNonZeroSHBands + shRowIndex; + const std::size_t spzShCoeffOffset = spzSHIndex * totalMesh.points.size(); + + for (size_t i = 0; i < totalMesh.points.size(); ++i) { + gaussianCloud.sh[spzShCoeffOffset + i] = totalMesh.shCoeffs[usdSHIndex][i]; + } + } + } + + return true; +} + +} diff --git a/spz/src/spzExport.h b/spz/src/spzExport.h new file mode 100644 index 0000000..30fd743 --- /dev/null +++ b/spz/src/spzExport.h @@ -0,0 +1,23 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#pragma once +#include +#include + +namespace adobe::usd { + +/// \ingroup usdspz +/// \brief Export USD data to a spz model. +bool +exportSpz(const UsdData& data, spz::GaussianCloud& spz); + +} \ No newline at end of file diff --git a/spz/src/spzImport.cpp b/spz/src/spzImport.cpp new file mode 100644 index 0000000..35f8627 --- /dev/null +++ b/spz/src/spzImport.cpp @@ -0,0 +1,187 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#include "spzImport.h" +#include "debugCodes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace PXR_NS; +using namespace spz; + +namespace adobe::usd { +bool +importSpz(const ImportSpzOptions& options, const spz::GaussianCloud& gaussianCloud, UsdData& usd) +{ + auto [meshIndex, mesh] = usd.addMesh(); + + // SPZ always stores Gsplat only. + mesh.asPoints = true; + mesh.asGsplats = true; + + try { + mesh.points.resize(gaussianCloud.numPoints); + if (gaussianCloud.positions.size() < static_cast(gaussianCloud.numPoints * 3)) + throw std::runtime_error("Invalid position data size"); + for (size_t i = 0; i < mesh.points.size(); i++) { + mesh.points[i][0] = gaussianCloud.positions[i * 3 + 0]; + mesh.points[i][1] = gaussianCloud.positions[i * 3 + 1]; + mesh.points[i][2] = gaussianCloud.positions[i * 3 + 2]; + } + + // The 0th-order coefficient of spherical harmonics, which + // is 1/sqrt(4*pi). + constexpr float shC0 = 0.28209479177387814f; + auto [colorIndex, colors] = usd.addColorSet(meshIndex); + colors.interpolation = UsdGeomTokens->vertex; + colors.values.resize(gaussianCloud.numPoints); + if (gaussianCloud.colors.size() < static_cast(gaussianCloud.numPoints * 3)) + throw std::runtime_error("Invalid color data size"); + for (size_t i = 0; i < colors.values.size(); i++) { + colors.values[i][0] = + std::clamp(gaussianCloud.colors[i * 3 + 0] * shC0 + 0.5f, 0.0f, 1.0f); + colors.values[i][1] = + std::clamp(gaussianCloud.colors[i * 3 + 1] * shC0 + 0.5f, 0.0f, 1.0f); + colors.values[i][2] = + std::clamp(gaussianCloud.colors[i * 3 + 2] * shC0 + 0.5f, 0.0f, 1.0f); + } + + auto [opacityIndex, opacity] = usd.addOpacitySet(meshIndex); + opacity.interpolation = UsdGeomTokens->vertex; + opacity.values.resize(gaussianCloud.numPoints); + if (gaussianCloud.alphas.size() < static_cast(gaussianCloud.numPoints)) + throw std::runtime_error("Invalid opacity data size"); + for (size_t i = 0; i < opacity.values.size(); i++) { + opacity.values[i] = 1.0f / (1.0f + std::exp(-gaussianCloud.alphas[i])); + } + + if (gaussianCloud.scales.size() < static_cast(gaussianCloud.numPoints * 3)) + throw std::runtime_error("Invalid scale data size"); + mesh.pointWidths.resize(gaussianCloud.numPoints); + for (size_t i = 0; i < mesh.pointWidths.size(); i++) + mesh.pointWidths[i] = std::exp(gaussianCloud.scales[i * 3 + 0]) * 2.0f; + + auto [width1Index, widths1] = usd.addExtraPointWidthSet(meshIndex); + widths1.interpolation = UsdGeomTokens->vertex; + widths1.values.resize(gaussianCloud.numPoints); + for (size_t i = 0; i < mesh.pointWidths.size(); i++) + widths1.values[i] = std::exp(gaussianCloud.scales[i * 3 + 1]) * 2.0f; + + auto [width2Index, widths2] = usd.addExtraPointWidthSet(meshIndex); + widths2.interpolation = UsdGeomTokens->vertex; + widths2.values.resize(gaussianCloud.numPoints); + for (size_t i = 0; i < mesh.pointWidths.size(); i++) + widths2.values[i] = std::exp(gaussianCloud.scales[i * 3 + 2]) * 2.0f; + + mesh.pointRotations.interpolation = UsdGeomTokens->vertex; + mesh.pointRotations.values.resize(gaussianCloud.numPoints); + if (gaussianCloud.rotations.size() < static_cast(gaussianCloud.numPoints * 4)) + throw std::runtime_error("Invalid rotation data size"); + for (size_t i = 0; i < mesh.pointRotations.values.size(); i++) { + mesh.pointRotations.values[i].SetReal(gaussianCloud.rotations[i * 4 + 3]); + mesh.pointRotations.values[i].SetImaginary(gaussianCloud.rotations[i * 4 + 0], + gaussianCloud.rotations[i * 4 + 1], + gaussianCloud.rotations[i * 4 + 2]); + mesh.pointRotations.values[i] = mesh.pointRotations.values[i].GetNormalized(); + } + + const size_t shDim = gaussianCloud.shDegree * (gaussianCloud.shDegree + 2); + if (gaussianCloud.sh.size() < static_cast(gaussianCloud.numPoints * shDim * 3)) + throw std::runtime_error("Invalid SH coefficient data size"); + for (std::size_t shColIndex = 0; shColIndex < 3; ++shColIndex) { + for (std::size_t shRowIndex = 0; shRowIndex < shDim; ++shRowIndex) { + auto [shCoeffIndex, shCoeffs] = usd.addPointSHCoeffSet(meshIndex); + shCoeffs.interpolation = UsdGeomTokens->vertex; + shCoeffs.values.resize(gaussianCloud.numPoints); + + // SPZ stores SH coefficients in a row-major order, where + // we need to convert it to a column-major order that we + // use for USD. + const size_t spzShIndex = shRowIndex * 3 + shColIndex; + const size_t spzShBase = spzShIndex * gaussianCloud.numPoints; + for (size_t i = 0; i < shCoeffs.values.size(); i++) { + shCoeffs.values[i] = gaussianCloud.sh[spzShBase + i]; + } + } + } + } catch (std::exception& e) { + TF_DEBUG_MSG(FILE_FORMAT_SPZ, "Cannot load SPZ: %s\n", e.what()); + return false; + } + + auto [nodeIndex, node] = usd.addNode(-1); + node.staticMeshes.push_back(meshIndex); + + usd.metersPerUnit = 1.0f; + usd.upAxis = options.importGsplatWithZup ? UsdGeomTokens->z : UsdGeomTokens->y; + + if (options.importGsplatClippingBox.size() >= 6) { + PXR_NS::GfVec3f minPos(std::numeric_limits::max()); + PXR_NS::GfVec3f maxPos(-std::numeric_limits::max()); + for (size_t i = 0; i < mesh.points.size(); i++) { + minPos[0] = std::min(mesh.points[i][0], minPos[0]); + minPos[1] = std::min(mesh.points[i][1], minPos[1]); + minPos[2] = std::min(mesh.points[i][2], minPos[2]); + maxPos[0] = std::max(mesh.points[i][0], maxPos[0]); + maxPos[1] = std::max(mesh.points[i][1], maxPos[1]); + maxPos[2] = std::max(mesh.points[i][2], maxPos[2]); + } + if (maxPos[0] < minPos[0] || maxPos[1] < minPos[1] || maxPos[2] < minPos[2]) { + TF_DEBUG_MSG(FILE_FORMAT_SPZ, + "Invalid bounding box: (%f, %f, %f) - (%f, %f, %f)\n", + minPos[0], + minPos[1], + minPos[2], + maxPos[0], + maxPos[1], + maxPos[2]); + return false; + } + + // We apply a clipping box for Gsplat and limit its maximal size, to avoid rendering the low + // quality splats far from the reconstruction center. This range will be part of the USD + // asset and can be adjusted on-the-fly. + mesh.clippingBox.values.resize(2); + mesh.clippingBox.values[0] = + PXR_NS::GfVec3f(std::max(options.importGsplatClippingBox[0], minPos[0]), + std::max(options.importGsplatClippingBox[1], minPos[1]), + std::max(options.importGsplatClippingBox[2], minPos[2])); + mesh.clippingBox.values[1] = + PXR_NS::GfVec3f(std::min(options.importGsplatClippingBox[3], maxPos[0]), + std::min(options.importGsplatClippingBox[4], maxPos[1]), + std::min(options.importGsplatClippingBox[5], maxPos[2])); + mesh.clippingBox.interpolation = UsdGeomTokens->constant; + } + return true; +} + +} \ No newline at end of file diff --git a/spz/src/spzImport.h b/spz/src/spzImport.h new file mode 100644 index 0000000..e4dc8d6 --- /dev/null +++ b/spz/src/spzImport.h @@ -0,0 +1,29 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#pragma once +#include +#include + +namespace adobe::usd { + +struct ImportSpzOptions +{ + bool importGsplatWithZup = false; + PXR_NS::VtFloatArray importGsplatClippingBox = { -2.0, -2.0, -2.0, 2.0, 2.0, 2.0 }; +}; + +/// \ingroup usdspz +/// \brief Import spz data into a USD data cache. +bool +importSpz(const ImportSpzOptions& options, const spz::GaussianCloud& spz, UsdData& data); + +} \ No newline at end of file diff --git a/spz/tests/CMakeLists.txt b/spz/tests/CMakeLists.txt new file mode 100644 index 0000000..f09fe8c --- /dev/null +++ b/spz/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +include(GoogleTest) + +add_executable(spzSanityTests sanityTests.cpp) + +usd_plugin_compile_config(spzSanityTests) + +target_link_libraries(spzSanityTests +PRIVATE + usd + GTest::gtest + GTest::gtest_main +) + +gtest_add_tests(TARGET spzSanityTests AUTO) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/SanitySplats.spz" "${CMAKE_CURRENT_BINARY_DIR}/SanitySplats.spz" COPYONLY) \ No newline at end of file diff --git a/spz/tests/SanitySplats.spz b/spz/tests/SanitySplats.spz new file mode 100644 index 0000000..3536e26 Binary files /dev/null and b/spz/tests/SanitySplats.spz differ diff --git a/spz/tests/sanityTests.cpp b/spz/tests/sanityTests.cpp new file mode 100644 index 0000000..b1b7daf --- /dev/null +++ b/spz/tests/sanityTests.cpp @@ -0,0 +1,27 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#include +#include +#include +#include +#include +#include +#include + +TEST(SPZSanityTests, LoadCube) +{ + PXR_NAMESPACE_USING_DIRECTIVE + + // Load an FBX + UsdStageRefPtr stage = UsdStage::Open("SanitySplats.spz"); + ASSERT_TRUE(stage); +} diff --git a/stl/src/fileFormat.cpp b/stl/src/fileFormat.cpp index 329828e..b6f246a 100644 --- a/stl/src/fileFormat.cpp +++ b/stl/src/fileFormat.cpp @@ -100,6 +100,8 @@ UsdStlFileFormat::WriteToFile(const SdfLayer& layer, StlModel stl; ReadLayerOptions layerOptions; layerOptions.triangulate = true; + // STL doesn't support invisible primitives, so we filter them out here + layerOptions.ignoreInvisible = true; GUARD(readLayer(layerOptions, layer, usd, DEBUG_TAG), "Error reading USD\n"); ExportStlOptions options; GUARD(exportStl(options, usd, stl), "Error translating USD to STL\n"); diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 517c344..a2e81bc 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -7,7 +7,7 @@ find_package(ZLIB REQUIRED) if(USD_FILEFORMATS_BUILD_TESTS) find_package(GTest REQUIRED) endif() -if (USD_FILEFORMATS_ENABLE_PLY) +if (USD_FILEFORMATS_ENABLE_PLY OR USD_FILEFORMATS_ENABLE_SPZ) find_package(SphericalHarmonics REQUIRED) endif() @@ -56,7 +56,7 @@ set(SOURCES "usdData.cpp" ) -if (USD_FILEFORMATS_ENABLE_PLY) +if (USD_FILEFORMATS_ENABLE_PLY OR USD_FILEFORMATS_ENABLE_SPZ) list(APPEND HEADERS "gsplatHelper.h" ) @@ -102,7 +102,7 @@ PUBLIC ZLIB::ZLIB ) -if (USD_FILEFORMATS_ENABLE_PLY) +if (USD_FILEFORMATS_ENABLE_PLY OR USD_FILEFORMATS_ENABLE_SPZ) target_link_libraries(fileformatUtils PRIVATE SphericalHarmonics::SphericalHarmonics diff --git a/utils/include/fileformatutils/common.h b/utils/include/fileformatutils/common.h index ea9bbc8..b947ada 100644 --- a/utils/include/fileformatutils/common.h +++ b/utils/include/fileformatutils/common.h @@ -11,12 +11,14 @@ governing permissions and limitations under the License. */ #pragma once #include "api.h" + #include "pxr/base/tf/staticTokens.h" #include #include #include #include #include + #include /// We defined these tokens to skip linking to usd imaging, which is heavy. @@ -408,9 +410,6 @@ split(const std::string& str, char delimiter); bool USDFFUTILS_API createDirectory(const std::filesystem::path& directoryPath); -std::string USDFFUTILS_API -getSanitizedExtension(const std::string& file); - std::string USDFFUTILS_API getLayerFilePath(const std::string& layerIdentifier); diff --git a/utils/include/fileformatutils/layerRead.h b/utils/include/fileformatutils/layerRead.h index 01d0862..fc98913 100644 --- a/utils/include/fileformatutils/layerRead.h +++ b/utils/include/fileformatutils/layerRead.h @@ -22,6 +22,7 @@ struct USDFFUTILS_API ReadLayerOptions { bool triangulate = false; bool flatten = false; + bool ignoreInvisible = false; // The default max for the number of mesh joints indices and weights is 4. Specific file // format exporters can modify this prior to export. Setting the value to -1 means the max is diff --git a/utils/include/fileformatutils/test.h b/utils/include/fileformatutils/test.h index d799adf..1e6089c 100644 --- a/utils/include/fileformatutils/test.h +++ b/utils/include/fileformatutils/test.h @@ -85,6 +85,7 @@ PXR_NAMESPACE_CLOSE_SCOPE #define ASSERT_CAMERA(...) assertCamera(__VA_ARGS__) #define ASSERT_LIGHT(...) assertLight(__VA_ARGS__) #define ASSERT_DISPLAY_NAME(...) assertDisplayName(__VA_ARGS__) +#define ASSERT_VISIBILITY(...) assertVisibility(__VA_ARGS__) #ifdef DO_RENDER #define ASSERT_RENDER(...) assertRender(__VA_ARGS__) #else @@ -192,6 +193,7 @@ struct USDFFUTILS_API LightData // PXR_NS::GfVec2f length // ImageAsset texture }; + USDFFUTILS_API void assertPrim(PXR_NS::UsdStageRefPtr stage, const std::string& path); USDFFUTILS_API void assertNode(PXR_NS::UsdStageRefPtr stage, const std::string& path); USDFFUTILS_API void assertMesh(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MeshData& data); @@ -207,8 +209,24 @@ USDFFUTILS_API void assertDisplayName(PXR_NS::UsdStageRefPtr stage, const std::string& primPath, const std::string& displayName); +/** + * Assert that a prim has a visibility attribute and that it is set to the expected value + * + * @param stage The stage containing the prim + * @param path The path to the prim + * @param expectedVisibilityAttr If the prim is expected to be set as inherited or invisible, when + * the visibility attribute is checked with UsdGeomImageable::GetVisibilityAttr() + * @param expectedActualVisibility If the prim is expected to be visible or invisible, when the + * effective visibility is computed with UsdGeomImageable::ComputeVisibility() + */ +USDFFUTILS_API void +assertVisibility(PXR_NS::UsdStageRefPtr stage, + const std::string& path, + bool expectedVisibilityAttr, + bool expectedActualVisibility); -USDFFUTILS_API void assertRender(const std::string& filename, const std::string& imageFilename); +USDFFUTILS_API void +assertRender(const std::string& filename, const std::string& imageFilename); template bool diff --git a/utils/include/fileformatutils/usdData.h b/utils/include/fileformatutils/usdData.h index 71d05ad..b6b2c92 100644 --- a/utils/include/fileformatutils/usdData.h +++ b/utils/include/fileformatutils/usdData.h @@ -68,6 +68,7 @@ struct USDFFUTILS_API Node { std::string name; std::string displayName; + bool markedInvisible = false; bool hasTransform = false; PXR_NS::GfMatrix4d transform = PXR_NS::GfMatrix4d(1); @@ -86,6 +87,7 @@ struct USDFFUTILS_API Node std::vector nurbs = {}; std::vector staticMeshes = {}; std::vector>> skinnedMeshes = {}; // Only used during export + std::vector curves = {}; std::vector children = {}; std::string path; @@ -98,6 +100,7 @@ struct USDFFUTILS_API Camera { std::string name; std::string displayName; + bool markedInvisible = false; PXR_NS::GfCamera::Projection projection; float f; @@ -138,6 +141,7 @@ struct USDFFUTILS_API Mesh { std::string name; std::string displayName; + bool markedInvisible = false; PXR_NS::VtIntArray faces; PXR_NS::VtIntArray indices; @@ -194,6 +198,16 @@ struct USDFFUTILS_API NurbData PXR_NS::VtArray trimCurveVertexCounts; }; +/// \ingroup utils_geometry +/// \brief Cubic Bezier curve data +struct USDFFUTILS_API Curve +{ + std::string name; + bool periodic; // closed? + bool piecewise; // consists of multiple 1-segment curves? + PXR_NS::VtVec3fArray points; +}; + /// \ingroup utils_geometry /// \brief Ngp data struct USDFFUTILS_API NgpData @@ -306,6 +320,7 @@ struct USDFFUTILS_API Light { std::string name; std::string displayName; + bool markedInvisible = false; LightType type; PXR_NS::GfVec3f color; @@ -416,6 +431,7 @@ struct USDFFUTILS_API UsdData std::vector rootNodes; std::vector nodes; std::vector meshes; + std::vector curves; std::vector cameras; std::vector nurbs; std::vector images; @@ -432,6 +448,7 @@ struct USDFFUTILS_API UsdData std::pair&> addOpacitySet(int meshIndex); std::pair&> addExtraPointWidthSet(int meshIndex); std::pair&> addPointSHCoeffSet(int meshIndex); + std::pair addCurve(); std::pair addMaterial(); void reserveImages(size_t count); std::pair addImage(); @@ -484,6 +501,8 @@ printMaterial(const std::string& header, const std::string& debugTag); USDFFUTILS_API void printMesh(const std::string& header, const Mesh& mesh, const std::string& debugTag); +USDFFUTILS_API void +printCurve(const std::string& header, const Curve& curve, const std::string& debugTag); // void printImage(const std::string& header, const SdfPath& path, const ImageAsset& image); USDFFUTILS_API void printSkeleton(const std::string& header, diff --git a/utils/src/common.cpp b/utils/src/common.cpp index 1fb703e..474719a 100644 --- a/utils/src/common.cpp +++ b/utils/src/common.cpp @@ -10,7 +10,14 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #include + #include + +//#include +#include +#include +#include + #include #include #include @@ -21,10 +28,6 @@ governing permissions and limitations under the License. #include #include #include -#include -#include -#include -#include PXR_NAMESPACE_OPEN_SCOPE TF_DEFINE_PUBLIC_TOKENS(AdobeTokens, ADOBE_TOKENS); @@ -209,7 +212,8 @@ argReadFloatArray(const SdfFileFormat::FileFormatArguments& args, } std::string -getFileExtension(const std::string& filePath, const std::string& defaultValue = "") { +getFileExtension(const std::string& filePath, const std::string& defaultValue = "") +{ // Find the last dot position std::size_t dotPos = filePath.rfind('.'); if (dotPos != std::string::npos && dotPos + 1 < filePath.size()) { @@ -219,7 +223,8 @@ getFileExtension(const std::string& filePath, const std::string& defaultValue = } std::string -getCurrentDate() { +getCurrentDate() +{ auto now = std::chrono::system_clock::now(); auto in_time_t = std::chrono::system_clock::to_time_t(now); std::stringstream ss; @@ -259,16 +264,6 @@ createDirectory(const std::filesystem::path& directoryPath) return true; } -// Retrieves the sanitized file extension from the given filename by removing any trailing ']' character. -std::string -getSanitizedExtension(const std::string& file) { - std::string ext = TfGetExtension(file); - if (ext.length() > 1 && ext.back() == ']') { - ext.pop_back(); - } - return ext; -} - // Retrieves the file path associated with a given layer identifier. // Parses the layer identifier to extract the outer and inner paths, // and returns the inner path if available; otherwise, returns the outer path. diff --git a/utils/src/geometry.cpp b/utils/src/geometry.cpp index 5a37bee..9473b86 100644 --- a/utils/src/geometry.cpp +++ b/utils/src/geometry.cpp @@ -663,8 +663,20 @@ mapPrimvarWithReverseIndex(const ReverseIndex& reverseIndices, // If we have indices we just remap the indices, the values referenced by the indices // stay the same VtIntArray newIndices(numElements); + int numIndices = primvar.indices.size(); for (size_t i = 0; i < numElements; ++i) { - newIndices[i] = primvar.indices[reverseIndices[i]]; + int idx = reverseIndices[i]; + if (idx >= numIndices) { + TF_WARN("error trying to remap primvar '%s' with interpolation '%s', " + "remapping index at %zu references index %d >= %d primvar indices", + primvarName.c_str(), + primvar.interpolation.GetText(), + i, + idx, + numIndices); + return; + } + newIndices[i] = primvar.indices[idx]; } primvar.indices = std::move(newIndices); } diff --git a/utils/src/images.cpp b/utils/src/images.cpp index 5f1e23f..a1d75be 100644 --- a/utils/src/images.cpp +++ b/utils/src/images.cpp @@ -460,6 +460,12 @@ linearToSRGB(float s) return 1.055f * std::pow(s, (1.0f / 2.4f)) - 0.055f; } +static std::string +_getAssetFileExtension(const std::string& resolvedAssetPath) +{ + return TfStringToLower(ArGetResolver().GetExtension(resolvedAssetPath)); +} + bool isImageFileSupported(const std::string& resolvedAssetPath) { @@ -472,10 +478,15 @@ isImageFileSupported(const std::string& resolvedAssetPath) std::lock_guard lock(supportedExtensionsMutex); - std::string ext = getSanitizedExtension(resolvedAssetPath); + std::string ext = _getAssetFileExtension(resolvedAssetPath); auto [it, inserted] = supportedExtensions.emplace(ext, false); if (inserted) { it->second = HioImage::IsSupportedImageFile("filename." + ext); + if (!it->second) { + TF_WARN("Image file with extension '%s' at path '%s' is not supported", + ext.c_str(), + resolvedAssetPath.c_str()); + } } return it->second; } diff --git a/utils/src/layerRead.cpp b/utils/src/layerRead.cpp index aa25ddc..12a4543 100644 --- a/utils/src/layerRead.cpp +++ b/utils/src/layerRead.cpp @@ -9,20 +9,16 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#include -#include +#include + #include #include #include #include -#include #include #include -#include -#include -#include -#include -#include + +#include #include #include #include @@ -79,6 +75,14 @@ governing permissions and limitations under the License. #include #include #include + +#include +#include +#include +#include +#include +#include +#include #include #include @@ -102,6 +106,30 @@ struct ReadLayerContext std::string debugTag; }; +/** + * Check whether the given prim is explicitly marked invisible. For this to be the case, it must: + * 1. Be a UsdGeomImageable + * 2. Have a visibility attribute + * 3. The visibility attribute must be set to UsdGeomTokens->invisible + * + * If not all of these are the case, false is returned. + * + * Note that false doesn't mean that the prim is visible, it means that the prim is not explicitly + * marked invisible. It may still inherit invisibility from a parent. + */ +bool +isMarkedInvisible(ReadLayerContext& ctx, const UsdPrim& prim) +{ + UsdGeomImageable imageable(prim); + if (imageable && imageable.GetVisibilityAttr().HasValue()) { + TfToken visibility; + imageable.GetVisibilityAttr().Get(&visibility); + return visibility == UsdGeomTokens->invisible; + // visibility will otherwise be UsdGeomTokens->inherited + } + return false; +} + // Gets the UsdData parent node with index 'parent', with the condition that if 'prim' has a // transform, like a UsdGeomMesh or a UsdCamera, then we extract that transform and put it // in a child node of the original parent. This is so native file formats which cannot put @@ -161,6 +189,7 @@ readScope(ReadLayerContext& ctx, const UsdPrim& prim, int parent) node.name = prim.GetName().GetString(); node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); + node.markedInvisible = isMarkedInvisible(ctx, prim); readTransform(ctx, prim, node, parent); UsdPrimSiblingRange children = prim.GetFilteredChildren(UsdTraverseInstanceProxies(UsdPrimAllPrimsPredicate)); @@ -206,6 +235,7 @@ readUnknown(ReadLayerContext& ctx, const UsdPrim& prim, int parent) node.name = prim.GetName().GetString(); node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); + node.markedInvisible = isMarkedInvisible(ctx, prim); readTransform(ctx, prim, node, parent); } for (const UsdPrim& p : children) { @@ -220,6 +250,7 @@ readXformInternal(ReadLayerContext& ctx, Node& node, const UsdPrim& prim, int pa node.name = prim.GetName().GetString(); node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); + node.markedInvisible = isMarkedInvisible(ctx, prim); readTransform(ctx, prim, node, parent); UsdGeomXformable xformable{ prim }; @@ -434,6 +465,7 @@ readMeshOrPointsData(ReadLayerContext& ctx, Mesh& mesh, int meshIndex, const Usd mesh.name = prim.GetName(); mesh.displayName = prim.GetDisplayName(); + mesh.markedInvisible = isMarkedInvisible(ctx, prim); UsdGeomPrimvarsAPI primvarsAPI(prim); if (prim.IsA()) { @@ -705,6 +737,7 @@ readSkelRoot(ReadLayerContext& ctx, const UsdPrim& prim, int parent) node.name = prim.GetName().GetString(); node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); + node.markedInvisible = isMarkedInvisible(ctx, prim); UsdSkelCache skelCache; // to hoist later to see performance improvement UsdSkelRoot skelRoot(prim); @@ -857,6 +890,7 @@ readPointInstancer(ReadLayerContext& ctx, const UsdPrim& prim, int parent) node.name = prim.GetName().GetString(); node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); + node.markedInvisible = isMarkedInvisible(ctx, prim); readTransform(ctx, prim, node, parent); UsdTimeCode time = UsdTimeCode::EarliestTime(); @@ -958,6 +992,8 @@ readVolume(ReadLayerContext& ctx, const UsdPrim& prim, int parent) ctx.debugTag.c_str(), prim.GetName().GetText()); + // TODO: read volume visibility + // Currently, we only support NGP volume. if (UsdRelationship rNgp = prim.GetRelationship(AdobeNgpTokens->fieldNgp)) { SdfPathVector relToNgps; @@ -983,72 +1019,68 @@ readVolume(ReadLayerContext& ctx, const UsdPrim& prim, int parent) // Populates the absolute path, base name, and sanitized extension for an SBSAR asset by resolving // the absolute path from the provided URI. void -populateSbsarPathNameExtension(const SdfAssetPath& path, - std::string& absPath, +populatePathPartsFromAssetPath(const SdfAssetPath& path, + std::string& resolvedAssetPath, std::string& name, std::string& extension) { - absPath = ArGetResolver().Resolve(path.GetResolvedPath()); - std::string layerPath = getLayerFilePath(path.GetResolvedPath()); - std::string filePath = extractFilePathFromAssetPath(layerPath); - name = TfStringGetBeforeSuffix(TfGetBaseName(filePath)); - extension = getSanitizedExtension(TfGetBaseName(filePath)); + // Make sure we have a resolved path, either coming from SdfAssetPath value or by running it + // throught the resolver. + resolvedAssetPath = path.GetResolvedPath().empty() + ? ArGetResolver().Resolve(path.GetAssetPath()) + : path.GetResolvedPath(); + // This will extract the inner most path to the asset: + // path/to/package.usdz[path/to/image.png] -> path/to/image.png + std::string innerAssetPath = getLayerFilePath(resolvedAssetPath); + // This helper function will detect "funky" paths, like those to SBSAR images and convert them + // to good usable file paths + std::string filePath = extractFilePathFromAssetPath(innerAssetPath); + // Strip the path part since we only want the filename and the extension + std::string baseName = TfGetBaseName(filePath); + name = TfStringGetBeforeSuffix(baseName); + extension = TfGetExtension(baseName); } bool -readImage(ReadLayerContext& ctx, const SdfAssetPath& path, int& index) +readImage(ReadLayerContext& ctx, const SdfAssetPath& assetPath, int& index) { - std::string absPath, extension, name, uri; - - // SBSAR images are special cases where the URI must be resolved - if (isUriSbsarImage(path.GetAssetPath())) { - uri = path.GetResolvedPath(); - populateSbsarPathNameExtension(path, absPath, name, extension); - } else { - uri = path.GetAssetPath(); - absPath = path.GetResolvedPath().empty() ? ArGetResolver().Resolve(path.GetAssetPath()) - : path.GetResolvedPath(); - name = TfStringGetBeforeSuffix(TfGetBaseName(uri)); - extension = getSanitizedExtension(uri); - size_t pos = name.find_first_of('['); - if (pos != std::string::npos) { - name = name.substr(pos + 1); - } - } + std::string resolvedAssetPath, name, extension; + populatePathPartsFromAssetPath(assetPath, resolvedAssetPath, name, extension); - if (const auto& it = ctx.images.find(uri); it != ctx.images.end()) { + // Check in the cache if we've processed this image before + if (const auto& it = ctx.images.find(resolvedAssetPath); it != ctx.images.end()) { index = it->second; - TF_WARN("%s: Image (cached): %s\n", ctx.debugTag.c_str(), uri.c_str()); + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Image (cached): %s\n", + ctx.debugTag.c_str(), + resolvedAssetPath.c_str()); return true; } - // deduplicate name + // The image is new. Make sure we don't get name collisions in the short name if (const auto& itName = ctx.imageNames.find(name); itName != ctx.imageNames.end()) { itName->second++; name += "_" + std::to_string(itName->second); - TF_WARN("%s: Deduplicated image name: %s\n", ctx.debugTag.c_str(), name.c_str()); + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Deduplicated image name: %s\n", + ctx.debugTag.c_str(), + name.c_str()); } else { ctx.imageNames[name] = 1; } - std::string assetPath; - SdfLayer::FileFormatArguments arguments; - SdfLayer::SplitIdentifier(uri, &assetPath, &arguments); - extension = getSanitizedExtension(assetPath); auto [imageIndex, image] = ctx.usd->addImage(); if (extension == "sbsarimage") { - // SBSAR images are special cases where the data is stored raw must be transcoded to memory - extension = getSbsarImageExtension(assetPath); - uri = image.uri = name + "." + extension; - transcodeImageAssetToMemory(assetPath, image.uri, image.image); + // SBSAR images are a special cases where the data is stored raw and must be transcoded to a + // different image in memory + extension = getSbsarImageExtension(resolvedAssetPath); + image.uri = name + "." + extension; + transcodeImageAssetToMemory(resolvedAssetPath, image.uri, image.image); } else { - ArResolver& ar = ArGetResolver(); - auto resolvedPath = ArResolvedPath(absPath); - auto asset = ar.OpenAsset(resolvedPath); + auto asset = ArGetResolver().OpenAsset(ArResolvedPath(resolvedAssetPath)); if (!asset) { - TF_WARN("%s: Unable to open asset: %s\n", - ctx.debugTag.c_str(), - resolvedPath.GetPathString().c_str()); + TF_WARN( + "%s: Unable to open asset: %s\n", ctx.debugTag.c_str(), resolvedAssetPath.c_str()); return false; } image.uri = name + "." + extension; @@ -1058,9 +1090,15 @@ readImage(ReadLayerContext& ctx, const SdfAssetPath& path, int& index) image.name = name; image.format = getFormat(extension); - ctx.images[uri] = imageIndex; + ctx.images[resolvedAssetPath] = imageIndex; index = imageIndex; - TF_WARN("%s: Image (new): index: %d uri: %s\n", ctx.debugTag.c_str(), imageIndex, uri.c_str()); + + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Image (new): index: %d uri: %s\n", + ctx.debugTag.c_str(), + imageIndex, + resolvedAssetPath.c_str()); + return true; } @@ -1436,6 +1474,7 @@ readCamera(ReadLayerContext& ctx, const UsdPrim& prim, int parent) const auto& usdCamera = UsdGeomCamera(prim); camera.name = prim.GetName(); camera.displayName = prim.GetDisplayName(); + camera.markedInvisible = isMarkedInvisible(ctx, prim); GfCamera gfCamera = usdCamera.GetCamera(0); camera.projection = gfCamera.GetProjection(); camera.f = gfCamera.GetFocalLength(); // f in mm @@ -1501,6 +1540,7 @@ readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) light.name = prim.GetName(); light.displayName = prim.GetDisplayName(); + light.markedInvisible = isMarkedInvisible(ctx, prim); // Light type specific attributes @@ -1627,6 +1667,21 @@ readPrim(ReadLayerContext& ctx, const UsdPrim& prim, int parent) FILE_FORMAT_UTIL, "%s: layer::read prim: invalid prim\n", ctx.debugTag.c_str()); return false; } + if (ctx.options->ignoreInvisible && prim.IsA()) { + UsdGeomImageable imageable(prim); + if (imageable && imageable.GetVisibilityAttr().HasValue()) { + TfToken visibility; + imageable.GetVisibilityAttr().Get(&visibility); + if (visibility == UsdGeomTokens->invisible) { + // visibility will otherwise be UsdGeomTokens->inherited + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: layer::read prim: ignoring invisible prim \"%s\"\n", + ctx.debugTag.c_str(), + prim.GetName().GetString().c_str()); + return false; + } + } + } TF_DEBUG_MSG(FILE_FORMAT_UTIL, "%s: layer::read %-10s %s\n", ctx.debugTag.c_str(), diff --git a/utils/src/layerWriteSdfData.cpp b/utils/src/layerWriteSdfData.cpp index e61315a..4d07d3b 100644 --- a/utils/src/layerWriteSdfData.cpp +++ b/utils/src/layerWriteSdfData.cpp @@ -142,6 +142,10 @@ _writeCamera(SdfAbstractData* sdfData, const SdfPath& parentPath, const Camera& setAttributeDefaultValue(sdfData, p, value); }; + if (camera.markedInvisible) { + createAttr(UsdGeomTokens->visibility, SdfValueTypeNames->Token, UsdGeomTokens->invisible); + } + const TfToken& proj = camera.projection == GfCamera::Perspective ? UsdGeomTokens->perspective : UsdGeomTokens->orthographic; createAttr(UsdGeomTokens->projection, SdfValueTypeNames->Token, proj); @@ -270,6 +274,11 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); } + if (light.markedInvisible) { + createAttr( + UsdGeomTokens->visibility, SdfValueTypeNames->Token, UsdGeomTokens->invisible); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); createAttr(UsdLuxTokens->inputsColor, SdfValueTypeNames->Color3f, light.color); createAttr(UsdLuxTokens->inputsRadius, SdfValueTypeNames->Float, light.radius); @@ -289,6 +298,11 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); } + if (light.markedInvisible) { + createAttr( + UsdGeomTokens->visibility, SdfValueTypeNames->Token, UsdGeomTokens->invisible); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); createAttr(UsdLuxTokens->inputsColor, SdfValueTypeNames->Color3f, light.color); createAttr(UsdLuxTokens->inputsWidth, SdfValueTypeNames->Float, light.length[0]); @@ -303,6 +317,11 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); } + if (light.markedInvisible) { + createAttr( + UsdGeomTokens->visibility, SdfValueTypeNames->Token, UsdGeomTokens->invisible); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); createAttr(UsdLuxTokens->inputsColor, SdfValueTypeNames->Color3f, light.color); createAttr(UsdLuxTokens->inputsRadius, SdfValueTypeNames->Float, light.radius); @@ -316,6 +335,11 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); } + if (light.markedInvisible) { + createAttr( + UsdGeomTokens->visibility, SdfValueTypeNames->Token, UsdGeomTokens->invisible); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); SdfAssetPath texturePath(light.texture.uri); createAttr(UsdLuxTokens->inputsTextureFile, SdfValueTypeNames->Asset, texturePath); @@ -329,6 +353,11 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); } + if (light.markedInvisible) { + createAttr( + UsdGeomTokens->visibility, SdfValueTypeNames->Token, UsdGeomTokens->invisible); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); createAttr(UsdLuxTokens->inputsColor, SdfValueTypeNames->Color3f, light.color); // Some renderers can't handle an input angle of 0 @@ -383,6 +412,12 @@ _writeXformAttributes(SdfAbstractData* sdfData, const SdfPath& primPath, const N const NodeAnimation& nodeAnimation = node.animations.empty() ? emptyNodeAnimation : node.animations.front(); + if (node.markedInvisible) { + SdfPath p = createAttributeSpec( + sdfData, primPath, UsdGeomTokens->visibility, SdfValueTypeNames->Token); + setAttributeDefaultValue(sdfData, p, UsdGeomTokens->invisible); + } + VtArray xformOpOrder; xformOpOrder.reserve(3); bool hasTranslation = node.translation != GfVec3d(0); @@ -638,6 +673,10 @@ _writeMesh(SdfAbstractData* sdfData, setAttributeDefaultValue(sdfData, p, value); }; + if (mesh.markedInvisible) { + createAttr(UsdGeomTokens->visibility, SdfValueTypeNames->Token, UsdGeomTokens->invisible); + } + // UsdMesh basics createAttr(UsdGeomTokens->points, SdfValueTypeNames->Point3fArray, mesh.points); createAttr(UsdGeomTokens->faceVertexCounts, SdfValueTypeNames->IntArray, mesh.faces); @@ -645,8 +684,6 @@ _writeMesh(SdfAbstractData* sdfData, // Subdivision rules createAttr( UsdGeomTokens->subdivisionScheme, SdfValueTypeNames->Token, UsdGeomTokens->none, true); - createAttr( - UsdGeomTokens->triangleSubdivisionRule, SdfValueTypeNames->Token, UsdGeomTokens->none); // Double sided createAttr(UsdGeomTokens->doubleSided, SdfValueTypeNames->Bool, mesh.doubleSided, true); @@ -852,6 +889,66 @@ _writeNurb(SdfAbstractData* sdfData, const SdfPath& parentPath, NurbData& nurb) return primPath; } +SdfPath +_writeCurve(WriteSdfContext& ctx, + const SdfPath& parentPath, + const Curve& curve) +{ + SdfPath primPath = createPrimSpec(ctx.sdfData, parentPath, TfToken(curve.name), UsdGeomTokens->BasisCurves); + TF_DEBUG_MSG(FILE_FORMAT_UTIL, "write curve: path=%s\n", primPath.GetString().c_str()); + + auto createAttr = [&](const TfToken& name, + const SdfValueTypeName& type, + const auto& value, + bool uniform = true) { + SdfVariability variability = + uniform ? PXR_NS::SdfVariabilityUniform : PXR_NS::SdfVariabilityVarying; + SdfPath p = createAttributeSpec(ctx.sdfData, primPath, name, type, variability); + setAttributeDefaultValue(ctx.sdfData, p, value); + return p; + }; + + // Basis and type + createAttr(UsdGeomTokens->basis, SdfValueTypeNames->Token, UsdGeomTokens->bezier); + createAttr(UsdGeomTokens->type, SdfValueTypeNames->Token, UsdGeomTokens->cubic); + + // Wrap (periodicity, open/closed) + // We don't support pinned curves for now + TfToken periodicity = curve.periodic ? UsdGeomTokens->periodic : UsdGeomTokens->nonperiodic; + createAttr(UsdGeomTokens->wrap, SdfValueTypeNames->Token, periodicity); + + // Points (static) + createAttr(UsdGeomTokens->points, SdfValueTypeNames->Point3fArray, curve.points); + + // Curve vertex counts. Primarily needed for curves that consist + // of multiple (sub)curves (which as far as USD is concerned can + // be discontinuous or even disjointed). But usdview won't render + // even a single continuous curve unless this attribute exists and + // is set ot the total number of vertices. + int npts = (int)curve.points.size(); + if (curve.piecewise) { + int nSegments = npts / 4; + PXR_NS::VtArray cvc; + cvc.resize(nSegments); + for (int i = 0; i < nSegments; i++) { + cvc[i] = 4; + } + createAttr(UsdGeomTokens->curveVertexCounts, SdfValueTypeNames->IntArray, cvc); + } + else { + createAttr(UsdGeomTokens->curveVertexCounts, SdfValueTypeNames->IntArray, PXR_NS::VtArray{npts}); + } + +#if 0 + // Curve width. This matches the default export from Maya. Skip + // for now, until we find a renderer that actually supports it. + SdfPath widthsAttrPath = createAttr(UsdGeomTokens->widths, SdfValueTypeNames->FloatArray, PXR_NS::VtArray{1.0}); + setAttributeMetadata(ctx.sdfData, widthsAttrPath, UsdGeomTokens->interpolation, VtValue(UsdGeomTokens->constant)); +#endif + + return primPath; +} + // forward declaration void _writeNodes(WriteSdfContext& ctx, @@ -936,6 +1033,12 @@ _writeNode(WriteSdfContext& ctx, const SdfPath& primPath, const Node& node) } } + // Curves + for (int curveIndex : node.curves) { + const Curve& curve = ctx.usdData->curves[curveIndex]; + _writeCurve(ctx, primPath, curve); + } + _writeNodes(ctx, primPath, node.children); return true; diff --git a/utils/src/test.cpp b/utils/src/test.cpp index b808a78..39d9397 100644 --- a/utils/src/test.cpp +++ b/utils/src/test.cpp @@ -585,6 +585,36 @@ assertDisplayName(PXR_NS::UsdStageRefPtr stage, << prim.GetDisplayName() << "\"\n "; } +void +assertVisibility(PXR_NS::UsdStageRefPtr stage, + const std::string& path, + bool expectedVisibilityAttr, + bool expectedActualVisibility) +{ + UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); + + UsdGeomImageable imageable(prim); + ASSERT_TRUE(imageable) << "Test setup error: " << path << " is not an imageable prim"; + + // Visibility attribute should always be present, even if it's not written explicitly + ASSERT_TRUE(imageable.GetVisibilityAttr().HasValue()) + << "Unexpected error: " << path << " missing visibility attribute"; + + // Check visibility attribute + TfToken visibility; + imageable.GetVisibilityAttr().Get(&visibility); + ASSERT_EQ(expectedVisibilityAttr, visibility == UsdGeomTokens->inherited) + << path << " has visibility attribute " + << (expectedVisibilityAttr ? "inherited" : "invisible") << " that is expected to be " + << visibility.GetString(); + + // Check actual visibility + visibility = imageable.ComputeVisibility(); + ASSERT_EQ(expectedActualVisibility, visibility == UsdGeomTokens->inherited) + << path << " is computed as " << visibility.GetString() << " but is expected to be " + << (expectedVisibilityAttr ? "visible" : "invisible"); +} + void assertPoints(PXR_NS::UsdStageRefPtr stage, const std::string& path, const PointsData& data) { diff --git a/utils/src/usdData.cpp b/utils/src/usdData.cpp index 7efe62b..39b8e06 100644 --- a/utils/src/usdData.cpp +++ b/utils/src/usdData.cpp @@ -257,6 +257,19 @@ printMesh(const std::string& header, const Mesh& mesh, const std::string& debugT mesh.material); } +void +printCurve(const std::string& header, const Curve& curve, const std::string& debugTag) +{ + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: %s curve { name: %s, periodic: %s, , piecewise: %s, pos: %zu}\n", + debugTag.c_str(), + header.c_str(), + curve.name.c_str(), + curve.periodic ? "yes" : "no", + curve.piecewise ? "yes" : "no", + curve.points.size()); +} + void printSkeleton(const std::string& header, const SdfPath& path, @@ -408,6 +421,14 @@ UsdData::addPointSHCoeffSet(int meshIndex) return { index, m.pointSHCoeffs[index] }; } +std::pair +UsdData::addCurve() +{ + int index = curves.size(); + curves.push_back(Curve()); + return { index, curves[index] }; +} + std::pair UsdData::addMaterial() { diff --git a/version b/version index 1cc5f65..8cfbc90 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.1.0 \ No newline at end of file +1.1.1 \ No newline at end of file