diff --git a/.gitignore b/.gitignore index 771ea287..069db8b6 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,7 @@ test/assets/fbx/*.usd test/assets/gltf/*.usd test/assets/obj/*/*.usd test/assets/ply/*.usd -test/assets/stl/*.usd \ No newline at end of file +test/assets/stl/*.usd + +# Other +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index f6a24345..a6a7c73a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,10 +37,12 @@ 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_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) option(USD_FILEFORMATS_ENABLE_CXX11_ABI "Use the CXX 11 ABI on Linux" OFF) option(USD_FILEFORMATS_ENABLE_ASM "Enables ASM material representation" OFF) +option(USD_FILEFORMATS_FETCH_SPHERICAL_HARMONICS "Forces FetchContent for SphericalHarmonics" ON) # This is a future looking option which turns on/off writing MaterialX Shaders # when importing a file per the OpenPBR spec: diff --git a/README.md b/README.md index 9876af90..3046e878 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/windows-2022-2411-ALL.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-2408-ALL.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-2311-ALL.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-ALL.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-ALL.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-ALL.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-ALL.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-ALL.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-ALL.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-2405-ALL.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-ALL.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-2408-ALL.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-2405-ALL.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-2311-ALL.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-ALL.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) @@ -41,17 +41,19 @@ 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 | -| [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 | -| [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 | 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 | ## Build @@ -141,8 +143,6 @@ where: | -DUSD_FILEFORMATS_FETCH_FASTFLOAT | Forces FetchContent for FastFLoat | ON | usdobj | | -DUSD_FILEFORMATS_ENABLE_ASM | Generate a ASM based material network on layerwrite | OFF | -Note that `Dpxr_ROOT` is *not* an optional parameter - a reference to the OpenUSD installation path **has** to be passed. - ZLIB, Draco and OpenImageIO packages are hinted to search into the USD installation by default. Override this by setting their ROOT or their FETCH variables (no fetch for OIIO). The previous commands will place intermediate files into the folder `build` and install binaries into the folder `bin`. @@ -157,7 +157,7 @@ Also, make the plugins discoverable by USD to complete installation, by adding t ```bash python ./USD/build_scripts/build_usd.py ./usd-install --build-shared --usd-imaging --tools --generator --openimageio --build-variant release ``` - #### For Linux/Mac: + #### For Linux: ```bash python ./USD/build_scripts/build_usd.py ./usd-install --use-cxx11-abi=1 --build-shared --usd-imaging --tools --generator --openimageio --build-variant release ``` @@ -175,12 +175,19 @@ Environment Variables set PATH=%PATH%;.\USD-Fileformat-plugins\bin\bin;.\USD-Fileformat-plugins\bin\plugin\usd set PXR_PLUGINPATH_NAME=%PXR_PLUGINPATH_NAME%;.\USD-Fileformat-plugins\bin\plugin\usd ``` -#### For Linux/Mac +#### For Linux ```bash export PATH=$PATH:./USD-Fileformat-plugins/bin/bin:./USD-Fileformat-plugins/bin/plugin/usd export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./USD-Fileformat-plugins/bin/lib:./USD-Fileformat-plugins/bin/lib64 export PXR_PLUGINPATH_NAME=$PXR_PLUGINPATH_NAME:./USD-Fileformat-plugins/bin/plugin/usd ``` +#### For Mac + ```bash + export PATH=$PATH:./USD-Fileformat-plugins/bin/bin:./USD-Fileformat-plugins/bin/plugin/usd + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./USD-Fileformat-plugins/bin/lib + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./USD-Fileformat-plugins/bin/lib + export PXR_PLUGINPATH_NAME=$PXR_PLUGINPATH_NAME:./USD-Fileformat-plugins/bin/plugin/usd + ``` Or Copy plugins: * Copy the installed plugins and dependent shared libraries to the specified folder: @@ -259,4 +266,4 @@ To generate the documentation go to the project root folder and enter: ``` doxygen ``` -The resulting documentation will be placed at the `docs` folder. +The resulting documentation will be placed at the `docs` folder. \ No newline at end of file diff --git a/changelog.txt b/changelog.txt index 852bd8f9..fd0b34e3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,30 @@ +v1.1.0 January 31st, 2025 +fbx: + - add display name to USD to save imported names for export + - import specular roughness from autodesk standard surface + - interpolate diffuse color on export for intermediate metallic values + - avoid creating extra root nodes during import & export + - adjust mesh and mesh instancing setup +gltf: + - add display name to USD to save imported names for export + - ignore invalid indices on export + - adjust mesh and mesh instancing setup +ply: + - add spherical harmonics support + - detect nan values for gsplat opacity and convert to 0 + - fix for loading ply files with non-ascii characters in filename +sbsar + - updated the list of sbsar engines for mac x64 to include metal + - add emissive intensity of 1 when we find emissive color + - set scale and bias correctly for normal map reader nodes +utility: + - update required env vars for Mac in Readme + - print OIIO error on failure + - restrict XForm collapsing to DefaultPrim + - usd v24.11 cleanup + - refactor utility exporting, cmake cleanup, rename files, remove extra logs + + v1.0.10 November 19th, 2024 fbx: - small fbx spot light fixes diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 00000000..fb184674 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,33 @@ +set(CPM_DOWNLOAD_VERSION 0.40.2) + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +function(download_cpm) + message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") + file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} + ) +endfunction() + +if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) + download_cpm() +else() + # resume download if it previously failed + file(READ ${CPM_DOWNLOAD_LOCATION} check) + if("${check}" STREQUAL "") + download_cpm() + endif() + unset(check) +endif() + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/cmake/FindEigen3.cmake b/cmake/FindEigen3.cmake new file mode 100644 index 00000000..3a0bcb77 --- /dev/null +++ b/cmake/FindEigen3.cmake @@ -0,0 +1,63 @@ +if(TARGET Eigen3::Eigen) + return() +endif() + +option(EIGEN_WITH_MKL "Use Eigen with MKL" OFF) +option(EIGEN_DONT_VECTORIZE "Disable Eigen vectorization" OFF) + +if(EIGEN_ROOT) + message(STATUS "Third-party (external): creating target 'Eigen3::Eigen' for external path: ${EIGEN_ROOT}") + set(EIGEN_INCLUDE_DIRS ${EIGEN_ROOT}) +else() + message(STATUS "Third-party (external): creating target 'Eigen3::Eigen'") + + include(CPM) + CPMAddPackage( + NAME eigen + GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git + GIT_TAG 3.4.0 + DOWNLOAD_ONLY ON + ) + set(EIGEN_INCLUDE_DIRS ${eigen_SOURCE_DIR}) + + install(DIRECTORY ${EIGEN_INCLUDE_DIRS}/Eigen + DESTINATION include + ) +endif() + +add_library(Eigen3_Eigen INTERFACE) +add_library(Eigen3::Eigen ALIAS Eigen3_Eigen) + +include(GNUInstallDirs) +target_include_directories(Eigen3_Eigen SYSTEM INTERFACE + $ + $ +) +target_compile_definitions(Eigen3_Eigen INTERFACE EIGEN_MPL2_ONLY) + +if(EIGEN_DONT_VECTORIZE) + target_compile_definitions(Eigen3_Eigen INTERFACE EIGEN_DONT_VECTORIZE) +endif() + +if(EIGEN_WITH_MKL) + # TODO: Checks that, on 64bits systems, `mkl::mkl` is using the LP64 interface + # (by looking at the compile definition of the target) + include(mkl) + target_link_libraries(Eigen3_Eigen INTERFACE mkl::mkl) + target_compile_definitions(Eigen3_Eigen INTERFACE + EIGEN_USE_MKL_ALL + EIGEN_USE_LAPACKE_STRICT + ) +endif() + +# On Windows, enable natvis files to improve debugging experience +if(WIN32 AND eigen_SOURCE_DIR) + target_sources(Eigen3_Eigen INTERFACE $) +endif() + +# Install rules +set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME eigen) +set_target_properties(Eigen3_Eigen PROPERTIES EXPORT_NAME Eigen) +install(DIRECTORY ${EIGEN_INCLUDE_DIRS} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(TARGETS Eigen3_Eigen EXPORT Eigen_Targets) +install(EXPORT Eigen_Targets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/eigen NAMESPACE Eigen3::) diff --git a/cmake/FindSphericalHarmonics.cmake b/cmake/FindSphericalHarmonics.cmake new file mode 100644 index 00000000..a0b5adea --- /dev/null +++ b/cmake/FindSphericalHarmonics.cmake @@ -0,0 +1,100 @@ +#[=======================================================================[.rst: +---- + +Finds or fetches the spherical-harmonics library. +If USD_FILEFORMATS_FORCE_FETCHCONTENT or USD_FILEFORMATS_FETCH_SPHERICAL_HARMONICS are +TRUE, spherical-harmonics will be fetched. Otherwise it will be searched via find commands. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if fetched: + +``SphericalHarmonics::SphericalHarmonics`` + The SphericalHarmonics library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``SphericalHarmonics_FOUND`` + +Cache Variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``SH_INCLUDE_DIR`` + The directory containing ``sh/spherical_harmonics.h``. + +#]=======================================================================] + +if(TARGET SphericalHarmonics::SphericalHarmonics) + return() +endif() + +if (NOT TARGET Eigen3::Eigen) + find_package(Eigen3 REQUIRED) +endif() + +if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_SPHERICAL_HARMONICS) + message(STATUS "Fetching SphericalHarmonics") + include(FetchContent) + FetchContent_Declare( + spherical_harmonics_git + GIT_REPOSITORY "https://github.com/google/spherical-harmonics.git" + GIT_TAG "ccb6c7fec875a1cd5ce5eb1315a9fa7603e0919a" + ) + FetchContent_MakeAvailable(spherical_harmonics_git) + + if(spherical_harmonics_git_POPULATED) + set(SphericalHarmonics_FOUND TRUE) + set(SH_SRC_FILES + ${spherical_harmonics_git_SOURCE_DIR}/sh/spherical_harmonics.cc + ${spherical_harmonics_git_SOURCE_DIR}/sh/spherical_harmonics.h + ${spherical_harmonics_git_SOURCE_DIR}/sh/image.h + ) + add_library(SphericalHarmonics STATIC) + target_sources(SphericalHarmonics PRIVATE ${SH_SRC_FILES}) + set(SH_INCLUDE_DIR "${spherical_harmonics_git_SOURCE_DIR}") + target_include_directories(SphericalHarmonics PUBLIC ${SH_INCLUDE_DIR}) + target_link_libraries(SphericalHarmonics PUBLIC Eigen3::Eigen) + set_property(TARGET SphericalHarmonics PROPERTY POSITION_INDEPENDENT_CODE ON) + set_property(TARGET SphericalHarmonics PROPERTY CXX_STANDARD 17) + target_compile_definitions(SphericalHarmonics PRIVATE "_USE_MATH_DEFINES") + add_library(SphericalHarmonics::SphericalHarmonics ALIAS SphericalHarmonics) + endif() +else() + include(FindPackageHandleStandardArgs) + + find_path(SH_INCLUDE_DIR + NAMES sh/spherical_harmonics.h + ) + + find_package_handle_standard_args(SphericalHarmonics + REQUIRED_VARS SH_INCLUDE_DIR + ) + + if(SphericalHarmonics_FOUND) + set(SH_SRC_FILES + ${SH_INCLUDE_DIR}/sh/spherical_harmonics.cc + ${SH_INCLUDE_DIR}/sh/spherical_harmonics.h + ${SH_INCLUDE_DIR}/sh/image.h + ) + add_library(SphericalHarmonics STATIC) + target_sources(SphericalHarmonics PRIVATE ${SH_SRC_FILES}) + set(SH_INCLUDE_DIR "${SphericalHarmonics_SOURCE_DIR}") + target_include_directories(SphericalHarmonics PUBLIC ${SH_INCLUDE_DIR}) + target_link_libraries(SphericalHarmonics PUBLIC Eigen3::Eigen) + set_property(TARGET SphericalHarmonics PROPERTY POSITION_INDEPENDENT_CODE ON) + set_property(TARGET SphericalHarmonics PROPERTY CXX_STANDARD 17) + target_compile_definitions(SphericalHarmonics PRIVATE "_USE_MATH_DEFINES") + + add_library(SphericalHarmonics::SphericalHarmonics ALIAS SphericalHarmonics) + elseif(${SphericalHarmonics_FIND_REQUIRED}) + message(FATAL_ERROR "Could not find SphericalHarmonics") + endif() +endif() + + diff --git a/fbx/README.md b/fbx/README.md index 44591a10..5c0e9d17 100644 --- a/fbx/README.md +++ b/fbx/README.md @@ -38,7 +38,7 @@ |Skeletons |✅|✅| |Skeleton Animations |✅|⚠️| |||| -|Materials |✅|✅| +|Materials |✅|⚠️| @@ -74,6 +74,8 @@ opacity → phongSurface::TransparentColor ior → Not been used. displacement → phongSurface::DisplacementColor +Note that PBR materials are not supported on export, only Phong + - Only point, directional, and spot lights are imported. Other light types are exported as point lights. - **OBS: The image files used by the UsdPreviewShader node will be extracted from the USDZ file and saved as PNG files in the same folder as the generated fbx. If the source file is USD the files should also be copied from the USD folder into the FBX folder.** diff --git a/fbx/src/fbx.cpp b/fbx/src/fbx.cpp index a65c82be..d3f8e2c1 100644 --- a/fbx/src/fbx.cpp +++ b/fbx/src/fbx.cpp @@ -12,7 +12,7 @@ governing permissions and limitations under the License. #include "fbx.h" #include "debugCodes.h" #include -#include +#include #include #include #include diff --git a/fbx/src/fbx.h b/fbx/src/fbx.h index 24fe63be..32ce5100 100644 --- a/fbx/src/fbx.h +++ b/fbx/src/fbx.h @@ -16,7 +16,7 @@ governing permissions and limitations under the License. #include #include #include -#include +#include #include diff --git a/fbx/src/fbxExport.cpp b/fbx/src/fbxExport.cpp index d1b0a1f2..5a7123d2 100644 --- a/fbx/src/fbxExport.cpp +++ b/fbx/src/fbxExport.cpp @@ -11,12 +11,12 @@ governing permissions and limitations under the License. */ #include "fbxExport.h" #include "debugCodes.h" -#include "layerWriteShared.h" -#include #include -#include -#include -#include +#include +#include +#include +#include +#include #include @@ -245,7 +245,7 @@ exportFbxAnimationTracks(ExportFbxContext& ctx) // Create anim stack exportAnimStackData.animStack = FbxAnimStack::Create( - ctx.fbx->scene, ctx.usd->animationTracks[animationTrackIndex].name.c_str()); + ctx.fbx->scene, getNodeName(ctx.usd->animationTracks[animationTrackIndex]).c_str()); // Create anim layer std::string animLayerName = "AnimLayer" + std::to_string(animationTrackIndex); @@ -261,7 +261,7 @@ exportFbxTransform(ExportFbxContext& ctx, const Node& node, FbxNode* fbxNode) { if (!fbxNode) { TF_WARN("ExportFbxTransform: Cannot export node %s transform to null FBX node\n", - node.name.c_str()); + getNodeName(node).c_str()); return false; } @@ -350,7 +350,7 @@ exportFbxTransform(ExportFbxContext& ctx, const Node& node, FbxNode* fbxNode) &errorStr)) { TF_WARN("ExportFbxTransform: Failed to extract translation animation data for node " "%s: %s\n", - node.name.c_str(), + getNodeName(node).c_str(), errorStr.c_str()); } } @@ -435,7 +435,7 @@ exportFbxTransform(ExportFbxContext& ctx, const Node& node, FbxNode* fbxNode) &errorStr)) { TF_WARN( "ExportFbxTransform: Failed to extract rotation animation data for node %s: %s\n", - node.name.c_str(), + getNodeName(node).c_str(), errorStr.c_str()); } } @@ -491,7 +491,7 @@ exportFbxTransform(ExportFbxContext& ctx, const Node& node, FbxNode* fbxNode) &errorStr)) { TF_WARN( "ExportFbxTransform: Failed to extract scale animation data for node %s: %s\n", - node.name.c_str(), + getNodeName(node).c_str(), errorStr.c_str()); } } @@ -572,7 +572,7 @@ exportFbxMeshes(ExportFbxContext& ctx) ctx.meshes.resize(ctx.usd->meshes.size()); for (size_t i = 0; i < ctx.usd->meshes.size(); i++) { const Mesh& m = ctx.usd->meshes[i]; - FbxMesh* fbxMesh = FbxMesh::Create(ctx.fbx->scene, m.name.c_str()); + FbxMesh* fbxMesh = FbxMesh::Create(ctx.fbx->scene, getNodeName(m).c_str()); if (fbxMesh != nullptr) { ctx.meshes[i] = fbxMesh; createMeshMaterial(ctx, m, fbxMesh); @@ -699,7 +699,7 @@ exportFbxMeshes(ExportFbxContext& ctx) } } } else { - TF_WARN("Failed to create mesh %s\n", m.name.c_str()); + TF_WARN("Failed to create mesh %s\n", getNodeName(m).c_str()); } } return true; @@ -721,7 +721,7 @@ exportFbxCameras(ExportFbxContext& ctx) ? FbxCamera::EProjectionType::ePerspective : FbxCamera::EProjectionType::eOrthogonal; - fbxCamera->SetName(c.name.c_str()); + fbxCamera->SetName(getNodeName(c).c_str()); fbxCamera->ProjectionType.Set(p); fbxCamera->FocalLength.Set(c.f); fbxCamera->FieldOfView.Set(c.fov); @@ -822,7 +822,7 @@ exportFbxLights(ExportFbxContext& ctx) break; } - FbxLight* fbxLight = FbxLight::Create(ctx.fbx->scene, light.name.c_str()); + FbxLight* fbxLight = FbxLight::Create(ctx.fbx->scene, getNodeName(light).c_str()); fbxLight->LightType.Set(lightType); fbxLight->Color.Set(FbxDouble3(light.color[0], light.color[1], light.color[2])); @@ -838,7 +838,7 @@ exportFbxLights(ExportFbxContext& ctx) TF_DEBUG_MSG(FILE_FORMAT_FBX, "exportFbx: light[%d]{ %s } of type %s\n", (int)i, - light.name.c_str(), + getNodeName(light).c_str(), type.c_str()); } } @@ -934,10 +934,15 @@ exportFbxInput(ExportFbxContext& ctx, return false; } -// If metallic value is present do the following mapping -// 1. Disable diffuse color -// 2. Specular color is the old diffuse color -// 3. Set shininess which is calculated from roughness +/** If metallic value is present do the following mapping + * 1. Decrease the diffuse color based on how smooth and metallic the material is + * 2. Specular color is also the diffuse color, weighted opposite the diffuse color + * 3. Set shininess which is calculated from roughness + * + * "input" should be the metallic input + * + * Returns whether the material is at least somewhat metallic (if metallic > 0) + */ bool exportMetallicValueInput(ExportFbxContext& ctx, const InputTranslator& inputTranslator, @@ -945,33 +950,37 @@ exportMetallicValueInput(ExportFbxContext& ctx, float roughness, FbxSurfacePhong* phong) { - bool setDiffuseToZero = false; + float metallic = 0.0; if (!input.value.IsEmpty() && input.value.IsHolding()) { - float metallic = input.value.UncheckedGet(); - if (metallic > 0.0f) { - setDiffuseToZero = true; - } + metallic = input.value.UncheckedGet(); } - if (setDiffuseToZero) { + // The more metallic the surface is, the less diffuse should be present. But increasing + // roughness decreases the metal look of metallic surfaces, so we modulate the metallic factor + // based on roughness + float diffuseFactor = (1.0 - (metallic * (1.0 - roughness))); + if (metallic > 0) { + // If the material is more metallic (and less diffuse), it will be shinier with specular + // components + float specularFactor = 1.0 - diffuseFactor; + FbxFileTexture* diffuseTexture = FbxCast(phong->Diffuse.GetSrcObject()); if (diffuseTexture) { phong->Specular.ConnectSrcObject(diffuseTexture); - phong->Diffuse.DisconnectSrcObject(diffuseTexture); + phong->Diffuse.ConnectSrcObject(diffuseTexture); } else { FbxDouble3 oldBaseColor = phong->Diffuse.Get(); phong->Specular.Set(oldBaseColor); } - phong->Diffuse.Set(FbxDouble3(0.0, 0.0, 0.0)); - phong->DiffuseFactor.Set(0.0); float shininess = (1.0f - roughness) * MAX_FBX_SHININESS; if (shininess > 0.0f) { phong->Shininess.Set(shininess); } + phong->DiffuseFactor.Set(diffuseFactor); + phong->SpecularFactor.Set(specularFactor); } - exportFbxInput(ctx, inputTranslator, input, phong->ReflectionFactor, FbxTexture::eStandard); - return setDiffuseToZero; + return metallic > 0; } void @@ -981,7 +990,7 @@ exportFbxMaterials(ExportFbxContext& ctx) ctx.materials.resize(ctx.usd->materials.size()); for (size_t i = 0; i < ctx.usd->materials.size(); i++) { const Material& m = ctx.usd->materials[i]; - FbxSurfacePhong* phong = FbxSurfacePhong::Create(ctx.fbx->scene, m.name.c_str()); + FbxSurfacePhong* phong = FbxSurfacePhong::Create(ctx.fbx->scene, getNodeName(m).c_str()); ctx.materials[i] = phong; Input diffuseColor; @@ -1066,7 +1075,8 @@ exportSkeletons(ExportFbxContext& ctx) // Now that all joints are created, set up skeleton types and parenting relationships. // We use a non-skeleton node to act as a parent for all root bones - FbxNode* skeletonParentNode = FbxNode::Create(ctx.fbx->scene, skeleton.name.c_str()); + FbxNode* skeletonParentNode = + FbxNode::Create(ctx.fbx->scene, getNodeName(skeleton).c_str()); ctx.skeletons[i] = skeletonParentNode; for (size_t j = 0; j < jointCount; j++) { @@ -1074,7 +1084,7 @@ exportSkeletons(ExportFbxContext& ctx) FbxNode* fbxNode = fbxNodes[j]; FbxSkeleton* fbxSkeleton = - FbxSkeleton::Create(ctx.fbx->scene, skeleton.name.c_str()); + FbxSkeleton::Create(ctx.fbx->scene, getNodeName(skeleton).c_str()); fbxNode->AddNodeAttribute(fbxSkeleton); int parent = skeleton.jointParents[j]; if (parent < 0) { @@ -1267,7 +1277,7 @@ exportFbxNodes(ExportFbxContext& ctx) { std::function exportFbxNode; exportFbxNode = [&](const Node& node, FbxNode* parent) -> bool { - FbxNode* fbxNode = FbxNode::Create(ctx.fbx->scene, node.name.c_str()); + FbxNode* fbxNode = FbxNode::Create(ctx.fbx->scene, getNodeName(node).c_str()); if (fbxNode != nullptr) { // context.AddNodePath(primPath, node); @@ -1300,7 +1310,8 @@ exportFbxNodes(ExportFbxContext& ctx) const Skeleton& skeleton = ctx.usd->skeletons[skeletonIndex]; for (int skinningTargetIdx : skeleton.meshSkinningTargets) { const Mesh& mesh = ctx.usd->meshes[skinningTargetIdx]; - FbxNode* fbxMeshNode = FbxNode::Create(ctx.fbx->scene, mesh.name.c_str()); + FbxNode* fbxMeshNode = + FbxNode::Create(ctx.fbx->scene, getNodeName(mesh).c_str()); if (fbxMeshNode != nullptr) { fbxNode->AddChild(fbxMeshNode); @@ -1324,7 +1335,8 @@ exportFbxNodes(ExportFbxContext& ctx) const Mesh& m = ctx.usd->meshes[meshIndex]; FbxNode* container = fbxNode; if (node.staticMeshes.size() > 1) { - std::string containerName = node.name.c_str() + std::to_string(i); + + std::string containerName = getNodeName(node).c_str() + std::to_string(i); container = FbxNode::Create(ctx.fbx->scene, containerName.c_str()); fbxNode->AddChild(container); } diff --git a/fbx/src/fbxExport.h b/fbx/src/fbxExport.h index 9598333e..1ea6c311 100644 --- a/fbx/src/fbxExport.h +++ b/fbx/src/fbxExport.h @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ #pragma once #include "fbx.h" -#include +#include namespace adobe::usd { diff --git a/fbx/src/fbxImport.cpp b/fbx/src/fbxImport.cpp index f333453a..949d0f45 100644 --- a/fbx/src/fbxImport.cpp +++ b/fbx/src/fbxImport.cpp @@ -11,17 +11,17 @@ governing permissions and limitations under the License. */ #include "fbxImport.h" #include "debugCodes.h" -#include +#include +#include +#include +#include #include #include -#include #include -#include #include #include #include #include -#include using namespace PXR_NS; using namespace fbxsdk; @@ -163,7 +163,8 @@ importFbxTransform(ImportFbxContext& ctx, Node& node, GfVec3d& t, GfQuatf& r, - GfVec3f& s) + GfVec3f& s, + bool useGlobalTransform) { // Helper function to decompose the transformation matrix into translation, rotation, and scale auto decomposeTransformation = [](GfVec3f& translation, @@ -251,8 +252,12 @@ importFbxTransform(ImportFbxContext& ctx, GfQuatf rotation; GfVec3f scale; float time = keyFrameTime.GetSecondDouble(); - decomposeTransformation( - translation, rotation, scale, fbxNode->EvaluateLocalTransform(keyFrameTime)); + decomposeTransformation(translation, + rotation, + scale, + useGlobalTransform + ? fbxNode->EvaluateGlobalTransform(keyFrameTime) + : fbxNode->EvaluateLocalTransform(keyFrameTime)); nodeAnimation.translations.times.push_back(time); nodeAnimation.translations.values.push_back(translation); @@ -270,7 +275,11 @@ importFbxTransform(ImportFbxContext& ctx, GfVec3f scale; { ScopedAnimStackDisabler animStackDisabler(ctx); - decomposeTransformation(translation, rotation, scale, fbxNode->EvaluateLocalTransform()); + decomposeTransformation(translation, + rotation, + scale, + useGlobalTransform ? fbxNode->EvaluateGlobalTransform() + : fbxNode->EvaluateLocalTransform()); } node.translation = translation; node.rotation = rotation; @@ -873,7 +882,7 @@ _toCamelCase(std::string s) noexcept // between Maya and Max. But regardless we're able to use both of them in this utility by just // relying on the properties being present and treating them effectively the same here. // -// Returns true if the matieral was a standard surface shader and was successfully processed +// Returns true if the material was a standard surface shader and was successfully processed bool _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, ImportFbxContext& ctx, @@ -895,7 +904,7 @@ _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, { "specular_color", { usdMaterial.specularColor, FbxPropertyNumChannels::Three, AdobeTokens->sRGB } }, { "metalness", { usdMaterial.metallic, FbxPropertyNumChannels::One, AdobeTokens->raw } }, - { "diffuse_roughness", + { "specular_roughness", { usdMaterial.roughness, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "coat", { usdMaterial.clearcoat, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "coat_color", @@ -1998,7 +2007,12 @@ importFbxNodes(ImportFbxContext& ctx, FbxNode* fbxNode, int parent) GfVec3d t(0); GfQuatf r(0); GfVec3f s(1); - importFbxTransform(ctx, fbxNode, node, t, r, s); + + // It is rare for the root node to contain a transform, but in case it does, we use the global + // transform to import the transform of each child of the root, given that we skipped importing + // the root node itself. + const bool useGlobalTransform = (parent == -1); + importFbxTransform(ctx, fbxNode, node, t, r, s, useGlobalTransform); // Fbx nodes have additional 'Geometric TRS' data, which are applied to its node attributes // but not to its children nodes. So if these are found, we insert a subnode here. @@ -2088,6 +2102,28 @@ importFbxNodes(ImportFbxContext& ctx, FbxNode* fbxNode, int parent) } } +void +importFbxNodeHierarchy(ImportFbxContext& ctx) +{ + FbxNode* fbxNode = ctx.scene->GetRootNode(); + + // Call importFbxNodes once for each child of the root node. We do not import the root node + // itself, as this node is created by the FBX to act as a container for the other nodes. + // Skipping it will ensure each child ends up as a one of the rootNodes in the UsdData. The root + // node itself would not be expected to have any attributes, so it should be ok to skip the + // attribute import process on it. It is also rare for the root node to contain a transform, but + // in case it does, we use the global transform to import the transform of each child. + for (int i = 0; i < fbxNode->GetChildCount(); i++) { + FbxNode* childNode = fbxNode->GetChild(i); + if (childNode == nullptr) { + TF_WARN( + "Child node at index %d is null for node '%s'. Skipping.", i, fbxNode->GetName()); + continue; + } + importFbxNodes(ctx, childNode, -1); + } +} + // Before converting meshes from Fbx to USD, we first triangulate // any meshes that have edge information which defines a specific // triangulation (ie. the splitting of quads). We don't pre-triangulate @@ -2162,7 +2198,7 @@ importFbx(const ImportFbxOptions& options, Fbx& fbx, UsdData& usd) triangulateMeshes(ctx); loadAnimLayers(ctx); importFBXSkeletons(ctx); - importFbxNodes(ctx, ctx.scene->GetRootNode(), -1); + importFbxNodeHierarchy(ctx); setSkeletonParents(ctx); } diff --git a/fbx/src/fbxImport.h b/fbx/src/fbxImport.h index 57347696..181573fc 100644 --- a/fbx/src/fbxImport.h +++ b/fbx/src/fbxImport.h @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ #pragma once #include "fbx.h" -#include +#include namespace adobe::usd { diff --git a/fbx/src/fbxResolver.cpp b/fbx/src/fbxResolver.cpp index 7247622f..3a3832fd 100644 --- a/fbx/src/fbxResolver.cpp +++ b/fbx/src/fbxResolver.cpp @@ -13,7 +13,7 @@ governing permissions and limitations under the License. #include "debugCodes.h" #include "fbx.h" #include "fbxImport.h" -#include +#include #include #include #include diff --git a/fbx/src/fbxResolver.h b/fbx/src/fbxResolver.h index be4793f1..42a40f57 100644 --- a/fbx/src/fbxResolver.h +++ b/fbx/src/fbxResolver.h @@ -10,7 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once -#include +#include namespace adobe::usd { diff --git a/fbx/src/fileFormat.cpp b/fbx/src/fileFormat.cpp index 96b32b30..6adb5f4c 100644 --- a/fbx/src/fileFormat.cpp +++ b/fbx/src/fileFormat.cpp @@ -15,12 +15,14 @@ governing permissions and limitations under the License. #include "fbx.h" #include "fbxExport.h" #include "fbxImport.h" +#include -#include -#include -#include -#include -#include + +#include +#include +#include +#include +#include #include diff --git a/fbx/src/fileFormat.h b/fbx/src/fileFormat.h index 3122cde6..c852fda9 100644 --- a/fbx/src/fileFormat.h +++ b/fbx/src/fileFormat.h @@ -21,10 +21,10 @@ governing permissions and limitations under the License. #endif // _MSC_VER #include "api.h" #include +#include #include #include #include -#include #include #include diff --git a/fbx/tests/sanityTests.cpp b/fbx/tests/sanityTests.cpp index 25500db0..f707e3e7 100644 --- a/fbx/tests/sanityTests.cpp +++ b/fbx/tests/sanityTests.cpp @@ -24,6 +24,6 @@ TEST(Sanity, LoadCube) // Load an FBX UsdStageRefPtr stage = UsdStage::Open("SanityCube.fbx"); ASSERT_TRUE(stage); - UsdPrim mesh = stage->GetPrimAtPath(SdfPath("/SanityCube/RootNode/Cube")); + UsdPrim mesh = stage->GetPrimAtPath(SdfPath("/SanityCube/Cube")); ASSERT_TRUE(mesh); } diff --git a/gltf/src/fileFormat.cpp b/gltf/src/fileFormat.cpp index 45e5a470..07f4d9f6 100644 --- a/gltf/src/fileFormat.cpp +++ b/gltf/src/fileFormat.cpp @@ -16,11 +16,11 @@ governing permissions and limitations under the License. #include "gltfImport.h" // utils -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include // USD #include diff --git a/gltf/src/fileFormat.h b/gltf/src/fileFormat.h index 9bd6f422..4244d440 100644 --- a/gltf/src/fileFormat.h +++ b/gltf/src/fileFormat.h @@ -13,7 +13,7 @@ governing permissions and limitations under the License. #include "api.h" #include -#include +#include #include #include diff --git a/gltf/src/gltf.cpp b/gltf/src/gltf.cpp index 637e9e9f..c97e4348 100644 --- a/gltf/src/gltf.cpp +++ b/gltf/src/gltf.cpp @@ -11,10 +11,10 @@ governing permissions and limitations under the License. */ #include "gltf.h" #include "debugCodes.h" -#include "neuralAssetsHelper.h" -#include #include #include +#include +#include #include #include #include diff --git a/gltf/src/gltfAnisotropy.cpp b/gltf/src/gltfAnisotropy.cpp index 7664aba5..b6a0c623 100644 --- a/gltf/src/gltfAnisotropy.cpp +++ b/gltf/src/gltfAnisotropy.cpp @@ -13,10 +13,11 @@ governing permissions and limitations under the License. #include "debugCodes.h" #include "gltfImport.h" #include -#include #include #include +#include + using namespace PXR_NS; namespace adobe::usd { @@ -456,7 +457,9 @@ constructAnisotropyImage(const Material& m, roughness = static_cast(roughnessImage->image[i]) / MAX_COLOR_VALUE; } } else { - roughness = m.roughness.value.Get(); + if (m.roughness.value.IsHolding()) { + roughness = m.roughness.value.Get(); + } } // Set anisotropy level (blue channel) @@ -491,11 +494,10 @@ exportAnisotropyExtension(ExportGltfContext& ctx, tinygltf::ExtensionMap ext; if (m.anisotropyLevel.value.IsHolding()) { // Use default roughness if none is available - float roughness = m.roughness.value.IsHolding() - ? m.roughness.value.UncheckedGet() - : 0.0f; - reconstructedStrength = reverseASMLevel( - m.anisotropyLevel.value.UncheckedGet(), 1.0f, roughness); + float roughness = + m.roughness.value.IsHolding() ? m.roughness.value.UncheckedGet() : 0.0f; + reconstructedStrength = + reverseASMLevel(m.anisotropyLevel.value.UncheckedGet(), 1.0f, roughness); addFloatValueToExt(ext, "anisotropyStrength", reconstructedStrength); } diff --git a/gltf/src/gltfAnisotropy.h b/gltf/src/gltfAnisotropy.h index ccdf7dc2..c285694e 100644 --- a/gltf/src/gltfAnisotropy.h +++ b/gltf/src/gltfAnisotropy.h @@ -12,9 +12,9 @@ governing permissions and limitations under the License. #pragma once #include "gltf.h" #include "gltfExport.h" -#include "images.h" #include "importGltfContext.h" -#include "usdData.h" +#include +#include #include namespace adobe::usd { diff --git a/gltf/src/gltfExport.cpp b/gltf/src/gltfExport.cpp index 871496af..87c11ea1 100644 --- a/gltf/src/gltfExport.cpp +++ b/gltf/src/gltfExport.cpp @@ -12,10 +12,10 @@ governing permissions and limitations under the License. #include "gltfExport.h" #include "debugCodes.h" #include "gltfAnisotropy.h" -#include "neuralAssetsHelper.h" -#include -#include -#include +#include +#include +#include +#include #include #include #include @@ -102,7 +102,7 @@ exportAnimationTracks(ExportGltfContext& ctx) for (int animationTrackIndex = 0; animationTrackIndex < ctx.usd->animationTracks.size(); animationTrackIndex++) { const AnimationTrack& track = ctx.usd->animationTracks[animationTrackIndex]; - ctx.gltf->animations[animationTrackIndex].name = track.name; + ctx.gltf->animations[animationTrackIndex].name = getNodeName(track); } } } @@ -194,7 +194,7 @@ exportCamera(ExportGltfContext& ctx, int camera) int cameraIndex = ctx.gltf->cameras.size(); ctx.gltf->cameras.push_back(tinygltf::Camera()); tinygltf::Camera& gCamera = ctx.gltf->cameras[cameraIndex]; - gCamera.name = usdCamera.name; + gCamera.name = getNodeName(usdCamera); const GfCamera& uCamera = usdCamera.camera; float znear = usdCamera.nearZ; float zfar = usdCamera.farZ; @@ -305,7 +305,7 @@ exportLights(ExportGltfContext& ctx) break; } - gltfLight.name = light.name; + gltfLight.name = getNodeName(light); gltfLight.intensity = intensity; @@ -460,9 +460,11 @@ exportNode(ExportGltfContext& ctx, int usdNodeIndex, int offset) ctx.usdNodesToGltfNodes[usdNodeIndex] = gltfNodeIndex; - TF_DEBUG_MSG( - FILE_FORMAT_GLTF, "glTF::write node: { %s } path=%s\n", node.name.c_str(), node.path.c_str()); - gnode.name = node.name; + gnode.name = getNodeName(node); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, + "glTF::write node: { %s } path=%s\n", + gnode.name.c_str(), + node.path.c_str()); bool hasAnimation = false; for (const NodeAnimation& nodeAnimation : node.animations) { @@ -763,7 +765,7 @@ exportSkeletons(ExportGltfContext& ctx, int gltfRootNodeIndex) // XXX should these form a hierarchy as well? for (size_t j = 0; j < skeleton.meshSkinningTargets.size(); j++) { int usdMeshIndex = skeleton.meshSkinningTargets[j]; - const std::string& meshName = usd->meshes[usdMeshIndex].name; + const std::string& meshName = getNodeName(usd->meshes[usdMeshIndex]); int nodeIndex = ctx.gltf->nodes.size(); ctx.gltf->nodes.push_back(tinygltf::Node()); @@ -1374,7 +1376,7 @@ exportMaterials(ExportGltfContext& ctx) Material& m = ctx.usd->materials[i]; tinygltf::Material& gm = ctx.gltf->materials[i]; - gm.name = m.name; + gm.name = getNodeName(m); // If we're not exporting material extensions which can express transmission directly, we // map it to opacity since transmission is an important effect we want to capture, even if // approximated as opacity @@ -1397,7 +1399,7 @@ exportMaterials(ExportGltfContext& ctx) m.opacity.bias = GfVec4f(1.0f) - bias; TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write material %s, using transmission for opacity\n", - m.name.c_str()); + gm.name.c_str()); } if (m.opacity.image >= 0) { // Unwarranted opacity is expensive and leads to rendering errors, so we check the pixel @@ -1416,7 +1418,7 @@ exportMaterials(ExportGltfContext& ctx) if (minValue > maxValue) { // No texture data for opacity. We assume opacity from the texture is 1.0 TF_DEBUG_MSG( - FILE_FORMAT_GLTF, "Invalid opacity texture on material %s", m.name.c_str()); + FILE_FORMAT_GLTF, "Invalid opacity texture on material %s", gm.name.c_str()); texOpacity = 1.0f; } else { static const float eps = 0.001f; @@ -1448,7 +1450,7 @@ exportMaterials(ExportGltfContext& ctx) m.opacity.bias = VtValue(); TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write opacity for %s is a constant %f (texture omitted)\n", - m.name.c_str(), + gm.name.c_str(), opacityValue); } } @@ -1457,7 +1459,7 @@ exportMaterials(ExportGltfContext& ctx) (getInputValue(m.opacity, &constOpacity) && constOpacity != 1.0f)) { TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write material %s, opacity in use (image %d, const %f)\n", - m.name.c_str(), + gm.name.c_str(), m.opacity.image, constOpacity); gm.alphaMode = "BLEND"; @@ -1479,7 +1481,7 @@ exportMaterials(ExportGltfContext& ctx) // Create a texture that combines diffuse color and opacity in the alpha channel TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write material %s, generating baseColor and opacity texture\n", - m.name.c_str()); + gm.name.c_str()); // GLTF can't express the bias on a texture, so if a texture uses bias we need to // process the pixels and incorporate it into the texel data. Note, this always happens // when we turn transmission into opacity in the code above. @@ -1493,7 +1495,7 @@ exportMaterials(ExportGltfContext& ctx) TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write material %s, opacity uses bias -> affine transform " "image: %d %f %f\n", - m.name.c_str(), + gm.name.c_str(), chIdx, opacityScale, opacityBias); @@ -1555,7 +1557,7 @@ exportMaterials(ExportGltfContext& ctx) TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write material %s, generating occlusionRoughnessMetallic texture: " "%d %d %d %d\n", - m.name.c_str(), + gm.name.c_str(), needToPackOcclusion, needToPackRoughness, needToPackMetallic, @@ -1612,7 +1614,7 @@ exportMaterials(ExportGltfContext& ctx) TF_WARN("glTF::write material %s, roughness and metallic textures have different " "transforms but will be combined into a single texture\n", - m.name.c_str()); + gm.name.c_str()); } exportTexture(ctx, occlusion, gm.occlusionTexture.index, gm.occlusionTexture.texCoord); @@ -1758,7 +1760,7 @@ exportMaterials(ExportGltfContext& ctx) } } - TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write material { %s }\n", m.name.c_str()); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write material { %s }\n", gm.name.c_str()); } // cleanup any images we don't need to export @@ -1767,6 +1769,7 @@ exportMaterials(ExportGltfContext& ctx) for (size_t i = 0; i < images.size(); i++) { ImageAsset* ui = &images[i]; tinygltf::Image& gi = ctx.gltf->images[i]; + // Images do not have display names, so we don't need to use getNodeName() gi.name = ui->name; if (ui->format == ImageFormatWebp) { ctx.extensionsUsed.insert("EXT_texture_webp"); @@ -1888,7 +1891,7 @@ exportPrimitive(ExportGltfContext& ctx, "glTF::cache primitive[%d]: {\"%s\", TRIANGLES, indices: %lu, pos: %lu, norms: %lu, " "uvs: %lu, joints: %lu, weights: %lu, subset: %s}\n", usdMeshIndex, - mesh.name.c_str(), + getNodeName(mesh).c_str(), indices.size(), mesh.points.size(), mesh.normals.values.size(), diff --git a/gltf/src/gltfExport.h b/gltf/src/gltfExport.h index 7a683028..4892b3a0 100644 --- a/gltf/src/gltfExport.h +++ b/gltf/src/gltfExport.h @@ -11,9 +11,9 @@ governing permissions and limitations under the License. */ #pragma once #include "gltf.h" -#include "materials.h" #include -#include +#include +#include namespace adobe::usd { diff --git a/gltf/src/gltfImport.cpp b/gltf/src/gltfImport.cpp index db9c627a..32742f01 100644 --- a/gltf/src/gltfImport.cpp +++ b/gltf/src/gltfImport.cpp @@ -14,9 +14,9 @@ governing permissions and limitations under the License. #include "gltfAnisotropy.h" #include "gltfSpecGloss.h" #include "importGltfContext.h" -#include "neuralAssetsHelper.h" -#include -#include +#include +#include +#include #include #include @@ -99,7 +99,7 @@ importCameras(ImportGltfContext& ctx) const tinygltf::Camera& gCamera = ctx.gltf->cameras[i]; Camera& usdCamera = ctx.usd->cameras[i]; GfCamera& uCamera = usdCamera.camera; - usdCamera.name = gCamera.name; + usdCamera.displayName = gCamera.name; if (gCamera.type == "perspective") { uCamera.SetProjection(GfCamera::Perspective); uCamera.SetClippingRange( @@ -853,7 +853,7 @@ importMaterials(ImportGltfContext& ctx) // gm = glTF material, m = USD material const tinygltf::Material& gm = ctx.gltf->materials[i]; Material& m = ctx.usd->materials[i]; - m.name = gm.name.empty() ? "Material" + std::to_string(i) : gm.name; + m.displayName = gm.name.empty() ? "Material" + std::to_string(i) : gm.name; // KHR_materials_pbrSpecularGlossiness data, in extensions, requires some cherrypicking. auto it = gm.extensions.find("KHR_materials_pbrSpecularGlossiness"); @@ -892,7 +892,8 @@ importMaterials(ImportGltfContext& ctx) if (!readTextureInfo(diffuseTextureVal, diffuseTextureInfo)) diffuseTextureInfo.index = -1; if (diffuseTextureInfo.index >= 0) { - int imageIndex = importImage(ctx, diffuseTextureInfo.index, m.name, "diffuse"); + int imageIndex = + importImage(ctx, diffuseTextureInfo.index, m.displayName, "diffuse"); importTexture(ctx.gltf, imageIndex, diffuseTextureInfo.index, @@ -919,7 +920,8 @@ importMaterials(ImportGltfContext& ctx) if (!readTextureInfo(specGlossTextureVal, specularTextureInfo)) specularTextureInfo.index = -1; if (specularTextureInfo.index >= 0) { - int imageIndex = importImage(ctx, specularTextureInfo.index, m.name, "specGloss"); + int imageIndex = + importImage(ctx, specularTextureInfo.index, m.displayName, "specGloss"); importTexture(ctx.gltf, imageIndex, specularTextureInfo.index, @@ -947,7 +949,7 @@ importMaterials(ImportGltfContext& ctx) const std::vector& diffuse = gm.pbrMetallicRoughness.baseColorFactor; // Import pbrMetallicRoughness.baseColorTexture from glTF if (diffuseTexture >= 0) { - int imageIndex = importImage(ctx, diffuseTexture, m.name, "diffuse"); + int imageIndex = importImage(ctx, diffuseTexture, m.displayName, "diffuse"); importTexture(ctx.gltf, imageIndex, diffuseTexture, @@ -977,7 +979,7 @@ importMaterials(ImportGltfContext& ctx) } // Import pbrMetallicRoughness.metallicRoughnessTexture from glTF if (mrTexture >= 0) { - int imageIndex = importImage(ctx, mrTexture, m.name, "metallicRoughness"); + int imageIndex = importImage(ctx, mrTexture, m.displayName, "metallicRoughness"); importTexture(ctx.gltf, imageIndex, mrTexture, @@ -1013,7 +1015,7 @@ importMaterials(ImportGltfContext& ctx) Specular specular; if (importSpecular(gm.extensions, &specular)) { importInput(ctx, - m.name, + m.displayName, "specularLevel", m.specularLevel, specular.texture, @@ -1021,7 +1023,7 @@ importMaterials(ImportGltfContext& ctx) &specular.factor, 1.0); importColorInput(ctx, - m.name, + m.displayName, "specularColor", m.specularColor, specular.colorTexture, @@ -1057,28 +1059,31 @@ importMaterials(ImportGltfContext& ctx) Clearcoat clearcoat; if (importClearcoat(gm.extensions, &clearcoat)) { importInput(ctx, - m.name, + m.displayName, "clearcoat", m.clearcoat, clearcoat.texture, AdobeTokens->r, &clearcoat.factor); importInput(ctx, - m.name, + m.displayName, "clearcoatRoughness", m.clearcoatRoughness, clearcoat.roughnessTexture, AdobeTokens->g, &clearcoat.roughnessFactor); - importNormalInput( - ctx, m.name, "clearcoatNormal", m.clearcoatNormal, clearcoat.normalTexture); + importNormalInput(ctx, + m.displayName, + "clearcoatNormal", + m.clearcoatNormal, + clearcoat.normalTexture); } AdobeClearcoatSpecular clearcoatSpecular; if (importAdobeClearcoatSpecular(gm.extensions, &clearcoatSpecular)) { importValue1(m.clearcoatIor, clearcoatSpecular.ior); importInput(ctx, - m.name, + m.displayName, "clearcoatSpecular", m.clearcoatSpecular, clearcoatSpecular.texture, @@ -1090,7 +1095,7 @@ importMaterials(ImportGltfContext& ctx) AdobeClearcoatTint clearcoatTint; if (importAdobeClearcoatTint(gm.extensions, &clearcoatTint)) { importColorInput(ctx, - m.name, + m.displayName, "clearcoatColor", m.clearcoatColor, clearcoatTint.texture, @@ -1100,10 +1105,14 @@ importMaterials(ImportGltfContext& ctx) Sheen sheen; if (importSheen(gm.extensions, &sheen)) { - importColorInput( - ctx, m.name, "sheenColor", m.sheenColor, sheen.colorTexture, sheen.colorFactor); + importColorInput(ctx, + m.displayName, + "sheenColor", + m.sheenColor, + sheen.colorTexture, + sheen.colorFactor); importInput(ctx, - m.name, + m.displayName, "sheenRoughness", m.sheenRoughness, sheen.roughnessTexture, @@ -1115,7 +1124,7 @@ importMaterials(ImportGltfContext& ctx) bool hasTransmission = false; if (importTransmission(gm.extensions, &transmission)) { importInput(ctx, - m.name, + m.displayName, "transmission", m.transmission, transmission.texture, @@ -1145,13 +1154,13 @@ importMaterials(ImportGltfContext& ctx) } else { TF_WARN("Can't map baseColor to clearcoatColor for transmission, since " "clearcoatColor is in use, for material %s", - m.name.c_str()); + m.displayName.c_str()); } } else { TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Can't touch clearcoat lobe to enable " "transmission tinting on material %s\n", - m.name.c_str()); + m.displayName.c_str()); } } } @@ -1165,14 +1174,14 @@ importMaterials(ImportGltfContext& ctx) // specular, so we're not changing roughness. if (!hasTransmission) { importInput(ctx, - m.name, + m.displayName, "transmission", m.transmission, diffuseTransmission.texture, AdobeTokens->a, &diffuseTransmission.factor); importColorInput(ctx, - m.name, + m.displayName, "absorptionColor", m.absorptionColor, diffuseTransmission.colorTexture, @@ -1180,14 +1189,14 @@ importMaterials(ImportGltfContext& ctx) } else { TF_WARN("Material %s has both KHR_materials_transmission and " "KHR_materials_diffuse_transmission. Ignoring the latter.", - m.name.c_str()); + m.displayName.c_str()); } } Volume volume; if (importVolume(gm.extensions, &volume) && volume.thicknessFactor > 0.0) { importInput(ctx, - m.name, + m.displayName, "thickness", m.volumeThickness, volume.thicknessTexture, @@ -1213,7 +1222,7 @@ importMaterials(ImportGltfContext& ctx) double emissiveStrength = 1.0; importEmissionStrength(gm.extensions, &emissiveStrength); if (gm.emissiveTexture.index >= 0) { - int imageIndex = importImage(ctx, gm.emissiveTexture.index, m.name, "emissive"); + int imageIndex = importImage(ctx, gm.emissiveTexture.index, m.displayName, "emissive"); importTexture(ctx.gltf, imageIndex, gm.emissiveTexture.index, @@ -1239,7 +1248,7 @@ importMaterials(ImportGltfContext& ctx) // Import normal map if (gm.normalTexture.index >= 0) { - int imageIndex = importImage(ctx, gm.normalTexture.index, m.name, "normal"); + int imageIndex = importImage(ctx, gm.normalTexture.index, m.displayName, "normal"); // Normal maps should not get the sRGB treatment and hence should be read as "raw" // 8-bit channel data @@ -1263,7 +1272,8 @@ importMaterials(ImportGltfContext& ctx) importValue1(m.normalScale, gm.normalTexture.scale); } if (gm.occlusionTexture.index >= 0) { - int imageIndex = importImage(ctx, gm.occlusionTexture.index, m.name, "occlusion"); + int imageIndex = + importImage(ctx, gm.occlusionTexture.index, m.displayName, "occlusion"); importTexture(ctx.gltf, imageIndex, gm.occlusionTexture.index, @@ -1336,7 +1346,7 @@ importMeshJointWeights(const tinygltf::Model& model, for (int i = 0; i < numJointSets; ++i) { if (jointCounts[i] != weightCounts[i] || (i > 0 && jointCounts[i] != jointCounts[0])) { TF_WARN("Mismatch number of joint indices and weights for mesh '%s'", - mesh.name.c_str()); + mesh.displayName.c_str()); return; } } @@ -1413,6 +1423,7 @@ void importMeshes(ImportGltfContext& ctx) { ctx.meshes.resize(ctx.gltf->meshes.size()); + ctx.meshUseCount.resize(ctx.gltf->meshes.size(), 0); for (size_t i = 0; i < ctx.gltf->meshes.size(); i++) { const tinygltf::Mesh& gmesh = ctx.gltf->meshes[i]; ctx.meshes[i].resize(gmesh.primitives.size()); @@ -1425,8 +1436,12 @@ importMeshes(ImportGltfContext& ctx) const tinygltf::Primitive& primitive = gmesh.primitives[j]; auto [meshIndex, mesh] = ctx.usd->addMesh(); ctx.meshes[i][j] = meshIndex; - mesh.name = gmesh.name; - mesh.instanceable = true; + mesh.displayName = gmesh.name; + // When we have multiple GLTF primitives that we turn into meshes, we create names that + // are derived from the primitive index instead of just duplicating the name. + if (gmesh.primitives.size() > 1) { + mesh.displayName = mesh.displayName + "_primitive" + std::to_string(j); + } int positionsIndex = getPrimitiveAttribute(primitive, "POSITION"); int normalsIndex = getPrimitiveAttribute(primitive, "NORMAL"); int tangentsIndex = getPrimitiveAttribute(primitive, "TANGENT"); @@ -1594,7 +1609,7 @@ importSkeletons(ImportGltfContext& ctx) for (size_t i = 0; i < ctx.gltf->skins.size(); i++) { const tinygltf::Skin& skin = ctx.gltf->skins[i]; Skeleton& skeleton = ctx.usd->skeletons[i]; - skeleton.name = skin.name; + skeleton.displayName = skin.name; skeleton.joints = PXR_NS::VtTokenArray(skin.joints.size()); skeleton.jointNames = PXR_NS::VtTokenArray(skin.joints.size()); skeleton.restTransforms = PXR_NS::VtMatrix4dArray(skin.joints.size()); @@ -1669,7 +1684,7 @@ importAnimationTracks(ImportGltfContext& ctx) animationTrackIndex++) { const tinygltf::Animation& animation = ctx.gltf->animations[animationTrackIndex]; AnimationTrack& track = ctx.usd->animationTracks[animationTrackIndex]; - track.name = animation.name; + track.displayName = animation.name; } } @@ -1907,7 +1922,7 @@ importLights(ImportGltfContext& ctx) auto [lightIndex, light] = ctx.usd->addLight(); - light.name = gltfLight.name; + light.displayName = gltfLight.name; if (gltfLight.color.size() >= 3) { light.color[0] = gltfLight.color[0]; light.color[1] = gltfLight.color[1]; @@ -2079,7 +2094,7 @@ importNodes(ImportGltfContext& ctx) Node& n = ctx.usd->nodes[usdNodeIndex]; ctx.nodeMap[nodeIndex] = usdNodeIndex; ctx.parentMap[nodeIndex] = parentIndex; - n.name = node.name; + n.displayName = node.name; n.translation = !node.translation.empty() ? PXR_NS::GfVec3d(node.translation[0], node.translation[1], node.translation[2]) @@ -2103,6 +2118,7 @@ importNodes(ImportGltfContext& ctx) int usdParentIndex = (parentIndex != -1) ? ctx.nodeMap[parentIndex] : -1; n.parent = usdParentIndex; if (node.mesh >= 0) { + ctx.meshUseCount[node.mesh]++; // If the node has a skin, add the mesh to the root node of the skeleton held by the // skin. if (node.skin >= 0) { @@ -2128,6 +2144,8 @@ importNodes(ImportGltfContext& ctx) return usdNodeIndex; }; + // We do not preserve the original names of scenes we import, since scenes aren't preserved + // when we import to USD from glTF, and since we won't export multiple scenes back to glTF for (const tinygltf::Scene& scene : ctx.gltf->scenes) { for (int rootNodeIndex : scene.nodes) { int usdNodeIndex = traverse(-1, rootNodeIndex); @@ -2174,6 +2192,27 @@ importNodes(ImportGltfContext& ctx) return true; } +void +checkMeshInstancing(ImportGltfContext& ctx) +{ + // Visit all meshes and check if they are used by more than one node and if so mark them as + // instanceable + for (size_t meshIdx = 0; meshIdx < ctx.meshUseCount.size(); ++meshIdx) { + int useCount = ctx.meshUseCount[meshIdx]; + if (useCount > 1) { + const std::vector& meshPrimitiveIndices = ctx.meshes[meshIdx]; + for (int primitiveIdx : meshPrimitiveIndices) { + ctx.usd->meshes[primitiveIdx].instanceable = true; + } + } + + if (useCount == 0) { + const tinygltf::Mesh& gmesh = ctx.gltf->meshes[meshIdx]; + TF_WARN("Mesh %zu (%s) appears to be unused", meshIdx, gmesh.name.c_str()); + } + } +} + static const std::set supportedExtension = { // Ratified extensions "KHR_draco_mesh_compression", @@ -2293,6 +2332,7 @@ importGltf(const ImportGltfOptions& options, importAnimationTracks(ctx); importNodeAnimations(ctx); importSkeletonAnimations(ctx); + checkMeshInstancing(ctx); } usd.metadata.SetValueAtPath("filenames", VtValue(ctx.filenames)); diff --git a/gltf/src/gltfImport.h b/gltf/src/gltfImport.h index 3b843930..2e95fe5a 100644 --- a/gltf/src/gltfImport.h +++ b/gltf/src/gltfImport.h @@ -13,7 +13,7 @@ governing permissions and limitations under the License. #include "gltf.h" #include "importGltfContext.h" #include -#include +#include namespace adobe::usd { diff --git a/gltf/src/gltfResolver.cpp b/gltf/src/gltfResolver.cpp index 8e5c2186..8211f0bd 100644 --- a/gltf/src/gltfResolver.cpp +++ b/gltf/src/gltfResolver.cpp @@ -17,9 +17,6 @@ governing permissions and limitations under the License. #include -// from utils -#include - using namespace PXR_NS; namespace adobe::usd { diff --git a/gltf/src/gltfResolver.h b/gltf/src/gltfResolver.h index ebc159d4..6665219e 100644 --- a/gltf/src/gltfResolver.h +++ b/gltf/src/gltfResolver.h @@ -10,7 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once -#include +#include namespace adobe::usd { diff --git a/gltf/src/gltfSpecGloss.cpp b/gltf/src/gltfSpecGloss.cpp index 5d974eba..2513953e 100644 --- a/gltf/src/gltfSpecGloss.cpp +++ b/gltf/src/gltfSpecGloss.cpp @@ -13,8 +13,8 @@ governing permissions and limitations under the License. #include "debugCodes.h" #include "gltfImport.h" #include "importGltfContext.h" -#include -#include +#include +#include #include #include diff --git a/gltf/src/gltfSpecGloss.h b/gltf/src/gltfSpecGloss.h index b259cc24..6d3a95fb 100644 --- a/gltf/src/gltfSpecGloss.h +++ b/gltf/src/gltfSpecGloss.h @@ -12,7 +12,7 @@ governing permissions and limitations under the License. #pragma once #include "gltf.h" #include "importGltfContext.h" -#include "usdData.h" +#include #include diff --git a/gltf/src/importGltfContext.h b/gltf/src/importGltfContext.h index 647b3c42..4770ec74 100644 --- a/gltf/src/importGltfContext.h +++ b/gltf/src/importGltfContext.h @@ -10,7 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once -#include "usdData.h" +#include #include #include #include @@ -29,6 +29,7 @@ struct ImportGltfContext std::vector parentMap; std::vector skeletonNodeNames; std::vector> meshes; + std::vector meshUseCount; // paths to files loaded on import PXR_NS::VtArray filenames; diff --git a/obj/src/fileFormat.cpp b/obj/src/fileFormat.cpp index e6ab1e92..8aa921c7 100644 --- a/obj/src/fileFormat.cpp +++ b/obj/src/fileFormat.cpp @@ -16,13 +16,13 @@ governing permissions and limitations under the License. #include "objExport.h" #include "objImport.h" -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include diff --git a/obj/src/fileFormat.h b/obj/src/fileFormat.h index fae0991c..4e47a545 100644 --- a/obj/src/fileFormat.h +++ b/obj/src/fileFormat.h @@ -15,7 +15,7 @@ governing permissions and limitations under the License. #include #include #include -#include +#include #include #include diff --git a/obj/src/obj.cpp b/obj/src/obj.cpp index 6dcd83cc..ba42a01e 100644 --- a/obj/src/obj.cpp +++ b/obj/src/obj.cpp @@ -37,9 +37,9 @@ governing permissions and limitations under the License. #include "debugCodes.h" #include #include -#include #include #include +#include #include #include #include diff --git a/obj/src/obj.h b/obj/src/obj.h index 197d677b..c6eaede3 100644 --- a/obj/src/obj.h +++ b/obj/src/obj.h @@ -21,11 +21,11 @@ governing permissions and limitations under the License. #include #include +#include #include #include #include #include -#include #include namespace adobe::usd { diff --git a/obj/src/objExport.cpp b/obj/src/objExport.cpp index b4990659..24dc39a2 100644 --- a/obj/src/objExport.cpp +++ b/obj/src/objExport.cpp @@ -11,8 +11,8 @@ governing permissions and limitations under the License. */ #include "objExport.h" #include "debugCodes.h" -#include -#include +#include +#include #include #include #include @@ -125,7 +125,7 @@ exportMesh(Obj& obj, v = GfVec3f(worldTransform.Transform(v)); } if (m.colors.size()) { - const Primvar& color = m.colors[0]; // only export first color set + const Primvar& color = m.colors[0]; // only export first color set if (color.indices.size() == 0 && color.values.size() == m.points.size()) { g.colors = color.values; // Perform color space conversion if necessary @@ -201,11 +201,11 @@ bool exportObj(const ExportObjOptions& options, const UsdData& usd, Obj& obj) { GfMatrix4d correctionTransform(1); - if (usd.upAxis == PXR_NS::UsdGeomTokens->z) { + if (usd.upAxis == UsdGeomTokens->z) { correctionTransform.SetRotate(GfQuatd(0.7071068, -0.7071068, 0, 0)); // rotate -90 deg in x TF_DEBUG_MSG(FILE_FORMAT_OBJ, "obj::write correct rotation { rotX: %s }\n", - usd.upAxis == PXR_NS::UsdGeomTokens->z ? "-90deg" : "0deg"); + usd.upAxis == UsdGeomTokens->z ? "-90deg" : "0deg"); } obj.comments.push_back("# Meters per unit: " + TfStringify(usd.metersPerUnit)); diff --git a/obj/src/objExport.h b/obj/src/objExport.h index d310153f..ea3fea6b 100644 --- a/obj/src/objExport.h +++ b/obj/src/objExport.h @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ #pragma once #include "obj.h" -#include +#include namespace adobe::usd { diff --git a/obj/src/objImport.cpp b/obj/src/objImport.cpp index ac7c1041..3e3ac72a 100644 --- a/obj/src/objImport.cpp +++ b/obj/src/objImport.cpp @@ -11,9 +11,9 @@ governing permissions and limitations under the License. */ #include "objImport.h" #include "debugCodes.h" -#include -#include -#include +#include +#include +#include #include #include #include diff --git a/obj/src/objImport.h b/obj/src/objImport.h index d1086b12..f1afca61 100644 --- a/obj/src/objImport.h +++ b/obj/src/objImport.h @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ #pragma once #include "obj.h" -#include +#include namespace adobe::usd { diff --git a/obj/src/objResolver.cpp b/obj/src/objResolver.cpp index 1399cbc3..bc56c5ce 100644 --- a/obj/src/objResolver.cpp +++ b/obj/src/objResolver.cpp @@ -13,7 +13,7 @@ governing permissions and limitations under the License. #include "obj.h" #include "objImport.h" #include -#include +#include using namespace PXR_NS; namespace adobe::usd { diff --git a/obj/src/objResolver.h b/obj/src/objResolver.h index e731d172..0fe88f90 100644 --- a/obj/src/objResolver.h +++ b/obj/src/objResolver.h @@ -10,7 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once -#include +#include namespace adobe::usd { diff --git a/ply/README.md b/ply/README.md index 6dd3a14e..5b05cee5 100644 --- a/ply/README.md +++ b/ply/README.md @@ -65,7 +65,7 @@ and aggregated into a single mesh because Ply lacks support for multiple individ ## File Format Arguments **Import:** - +* `plyGsplatsClippingBox`: 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. * `plyPoints`: Forces importing UsdGeomMesh instances as points if true. The following imports UsdGeomMesh instances as points: ``` @@ -78,20 +78,15 @@ and aggregated into a single mesh because Ply lacks support for multiple individ UsdStageRefPtr stage = UsdStage::Open("cube.ply:SDF_FORMAT_ARGS:plyPoints=true&plyPointWidth=0.1") stage->Export("cube.usd") ``` -* `plyGsplatsWithZup`: 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 true. - The following imports UsdGeomPoints instances as Gaussian splats without rotation (if the PLY contains all the Gaussian-splat-related attributes). +* `plyWithUpAxisCorrection`: Whether the imported PLY will have its axis corrected based on its comment (either explicitly has + "Z-axis up" in the comment or is exported from a specific software that uses Z-axis up). + If so we apply a rotation to Y-up during importing. By default it is true. + The following imports UsdGeomPoints instances as Gaussian splats without axis correction (if the PLY contains all the Gaussian-splat-related attributes). ``` - UsdStageRefPtr stage = UsdStage::Open("gsplat.ply:SDF_FORMAT_ARGS:plyGsplatsWithZup=false") + UsdStageRefPtr stage = UsdStage::Open("gsplat.ply:SDF_FORMAT_ARGS:plyWithUpAxisCorrection=false") stage->Export("gsplat.usd") ``` -**Export:** - -* `plyGsplatsWithZup`: Whether the exported Gaussian splat is stored as a Z-up object. If so we apply a rotation - from Y-up during exporting. By default it is on. - ## Debug codes * `FILE_FORMAT_PLY`: Common debug messages. - diff --git a/ply/src/fileFormat.cpp b/ply/src/fileFormat.cpp index 9ece52c7..8613a168 100644 --- a/ply/src/fileFormat.cpp +++ b/ply/src/fileFormat.cpp @@ -16,26 +16,24 @@ governing permissions and limitations under the License. #include "plyExport.h" #include "plyImport.h" -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include +#include +#include + using namespace adobe::usd; using namespace happly; PXR_NAMESPACE_OPEN_SCOPE -const TfToken UsdPlyFileFormat::pointsToken("plyPoints", TfToken::Immortal); -const TfToken UsdPlyFileFormat::withUpAxisCorrectionToken("plyWithUpAxisCorrection", TfToken::Immortal); -const TfToken UsdPlyFileFormat::pointsGsplatWithClippingToken("plyGsplatsWithClipping", TfToken::Immortal); -const TfToken UsdPlyFileFormat::pointWidthToken("plyPointWidth", TfToken::Immortal); - TF_DEFINE_PUBLIC_TOKENS(UsdPlyFileFormatTokens, USDPLY_FILE_FORMAT_TOKENS); TF_REGISTRY_FUNCTION(TfType) @@ -63,10 +61,16 @@ UsdPlyFileFormat::InitData(const FileFormatArguments& args) const FILE_FORMAT_PLY, "FileFormatArg: %s = %s\n", arg.first.c_str(), arg.second.c_str()); } argReadBool(args, AdobeTokens->writeMaterialX.GetText(), pd->writeMaterialX, DEBUG_TAG); - argReadBool(args, pointsToken.GetText(), pd->points, DEBUG_TAG); - argReadFloat(args, pointWidthToken.GetText(), pd->pointWidth, DEBUG_TAG); - argReadBool(args, withUpAxisCorrectionToken.GetText(), pd->withUpAxisCorrection, DEBUG_TAG); - argReadBool(args, pointsGsplatWithClippingToken.GetText(), pd->gsplatsWithClipping, DEBUG_TAG); + argReadBool(args, UsdPlyFileFormatTokens->points.GetText(), pd->points, DEBUG_TAG); + argReadFloat(args, UsdPlyFileFormatTokens->pointWidth.GetText(), pd->pointWidth, DEBUG_TAG); + argReadBool(args, + UsdPlyFileFormatTokens->withUpAxisCorrection.GetText(), + pd->withUpAxisCorrection, + DEBUG_TAG); + argReadFloatArray(args, + UsdPlyFileFormatTokens->pointsGsplatClippingBox.GetText(), + pd->gsplatsClippingBox, + DEBUG_TAG); return pd; } @@ -76,10 +80,10 @@ UsdPlyFileFormat::ComposeFieldsForFileFormatArguments(const std::string& assetPa FileFormatArguments* args, VtValue* dependencyContextData) const { - argComposeBool(context, args, pointsToken, DEBUG_TAG); - argComposeFloat(context, args, pointWidthToken, DEBUG_TAG); - argComposeBool(context, args, withUpAxisCorrectionToken, DEBUG_TAG); - argComposeBool(context, args, pointsGsplatWithClippingToken, DEBUG_TAG); + argComposeBool(context, args, UsdPlyFileFormatTokens->points, DEBUG_TAG); + argComposeFloat(context, args, UsdPlyFileFormatTokens->pointWidth, DEBUG_TAG); + argComposeBool(context, args, UsdPlyFileFormatTokens->withUpAxisCorrection, DEBUG_TAG); + argComposeFloatArray(context, args, UsdPlyFileFormatTokens->pointsGsplatClippingBox, DEBUG_TAG); } bool @@ -114,13 +118,24 @@ UsdPlyFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool me options.importAsPoints = data->points; options.pointWidth = data->pointWidth; options.importWithUpAxisCorrection = data->withUpAxisCorrection; - options.importGsplatWithClipping = data->gsplatsWithClipping; + options.importGsplatClippingBox = data->gsplatsClippingBox; WriteLayerOptions layerOptions; layerOptions.writeMaterialX = data->writeMaterialX; - PLYData ply(resolvedPath); + + // Since the resolved path may contain non-ascii characters, we convert the string + // path to a filesystem path and then use it to create an ifstream we can then pass to + // the PLYData constructor. + const auto filePath = std::filesystem::u8path(resolvedPath); + std::ifstream inStream(filePath, std::ios::binary); + if (!inStream.is_open()) { + TF_DEBUG_MSG(FILE_FORMAT_PLY, "Failed to open %s\n", resolvedPath.c_str()); + } + PLYData ply(inStream); + GUARD(importPly(options, ply, usd), "Error translating PLY to USD\n"); GUARD( - writeLayer(layerOptions, usd, layer, layerData, fileType, DEBUG_TAG, SdfFileFormat::_SetLayerData), + 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_PLY, "Failed to open %s: %s\n", resolvedPath.c_str(), e.what()); diff --git a/ply/src/fileFormat.h b/ply/src/fileFormat.h index 7ee24bf2..481a9fb9 100644 --- a/ply/src/fileFormat.h +++ b/ply/src/fileFormat.h @@ -12,16 +12,25 @@ governing permissions and limitations under the License. #pragma once #include "api.h" #include +#include #include #include #include -#include #include #include PXR_NAMESPACE_OPEN_SCOPE -#define USDPLY_FILE_FORMAT_TOKENS ((Id, "ply"))((Version, FILE_FORMATS_VERSION))((Target, "usd")) +// clang-format off +#define USDPLY_FILE_FORMAT_TOKENS \ + ((Id, "ply")) \ + ((Version, FILE_FORMATS_VERSION)) \ + ((Target, "usd")) \ + ((points, "plyPoints")) \ + ((pointWidth, "plyPointWidth")) \ + ((withUpAxisCorrection, "plyWithUpAxisCorrection")) \ + ((pointsGsplatClippingBox, "plyGsplatsClippingBox")) +// clang-format on TF_DECLARE_PUBLIC_TOKENS(UsdPlyFileFormatTokens, USDPLY_FILE_FORMAT_TOKENS); TF_DECLARE_WEAK_AND_REF_PTRS(PlyData); TF_DECLARE_WEAK_AND_REF_PTRS(UsdPlyFileFormat); @@ -33,7 +42,7 @@ class PlyData : public FileFormatDataBase public: bool points = false; bool withUpAxisCorrection = true; - bool gsplatsWithClipping = true; + PXR_NS::VtFloatArray gsplatsClippingBox = { -2, -2, -2, 2, 2, 2 }; float pointWidth = 0.01f; static PlyDataRefPtr InitData(const SdfFileFormat::FileFormatArguments& args); }; diff --git a/ply/src/plugInfo.json.in b/ply/src/plugInfo.json.in index 1f55d920..886f306c 100644 --- a/ply/src/plugInfo.json.in +++ b/ply/src/plugInfo.json.in @@ -3,6 +3,12 @@ { "Info": { "SdfMetadata": { + "plyGsplatsClippingBox": { + "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" + }, "plyPoints": { "appliesTo": [ "prims" ], "displayGroup": "Core", @@ -15,10 +21,10 @@ "documentation:": "Point width. Default is 0.01f.", "type": "float" }, - "plyGsplatsWithZup": { + "plyWithUpAxisCorrection": { "appliesTo": [ "prims" ], "displayGroup": "Core", - "documentation:": "Should the imported Gaussian splat be treated as Z-up", + "documentation:": "Should the imported Gaussian splat be treated with axis correction", "type": "bool" } }, diff --git a/ply/src/plyExport.cpp b/ply/src/plyExport.cpp index ae127830..3ca1b8dd 100644 --- a/ply/src/plyExport.cpp +++ b/ply/src/plyExport.cpp @@ -11,9 +11,11 @@ governing permissions and limitations under the License. */ #include "plyExport.h" #include "debugCodes.h" -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include @@ -47,7 +49,6 @@ governing permissions and limitations under the License. #include #include #include -#include using namespace PXR_NS; @@ -190,58 +191,25 @@ aggregateMeshInstance(PlyTotalMesh& totalMesh, if (totalMesh.asGsplats) { // Aggregate Gsplat attributes - size_t widthsOffset = totalMesh.widths.size(); - size_t widths1Offset = totalMesh.widths1.size(); - size_t widths2Offset = totalMesh.widths2.size(); - size_t rotationsOffset = totalMesh.rotations.size(); - - // We need to use the number of points as the new size (and fill with default values) - // in case there's a mix of regular point cloud and Gsplats. - totalMesh.widths.resize(widthsOffset + currentMeshPointsSize, 0.0f); - totalMesh.rotations.resize(rotationsOffset + currentMeshPointsSize, GfQuatf::GetIdentity()); - totalMesh.widths1.resize(widths1Offset + currentMeshPointsSize, 0.0f); - totalMesh.widths2.resize(widths2Offset + currentMeshPointsSize, 0.0f); - - const size_t numPointWidths = std::min(currentMeshPointsSize, mesh.pointWidths.size()); - memcpy(totalMesh.widths.data() + widthsOffset, - mesh.pointWidths.data(), - numPointWidths * sizeof(mesh.pointWidths[0])); - if (mesh.pointExtraWidths.size() >= 2) { - const size_t numPointWidths1 = - std::min(currentMeshPointsSize, mesh.pointExtraWidths[0].values.size()); - const size_t numPointWidths2 = - std::min(currentMeshPointsSize, mesh.pointExtraWidths[1].values.size()); - memcpy(totalMesh.widths1.data() + widths1Offset, - mesh.pointExtraWidths[0].values.data(), - numPointWidths1 * sizeof(mesh.pointExtraWidths[0].values[0])); - memcpy(totalMesh.widths2.data() + widths2Offset, - mesh.pointExtraWidths[1].values.data(), - numPointWidths2 * sizeof(mesh.pointExtraWidths[1].values[0])); - } GfMatrix4f modelMatrixFloat(modelMatrix); - GfQuatf modelRotation = modelMatrixFloat.ExtractRotationQuat().GetNormalized(); - const size_t numPointRotations = - std::min(currentMeshPointsSize, mesh.pointRotations.values.size()); - for (size_t i = 0; i < numPointRotations; i++) { - totalMesh.rotations[rotationsOffset + i] = - mesh.pointRotations.values[i] * modelRotation; - } - - for (size_t shIndex = 0; shIndex < totalMesh.shCoeffs.size(); shIndex++) { - size_t shCoeffOffset = totalMesh.shCoeffs[shIndex].size(); - totalMesh.shCoeffs[shIndex].resize(shCoeffOffset + currentMeshPointsSize, 0.0f); - if (shIndex < mesh.pointSHCoeffs.size()) { - const size_t numPointSHCoeffs = - std::min(currentMeshPointsSize, mesh.pointSHCoeffs[shIndex].values.size()); - memcpy(totalMesh.shCoeffs[shIndex].data() + shCoeffOffset, - mesh.pointSHCoeffs[shIndex].values.data(), - numPointSHCoeffs * sizeof(mesh.pointSHCoeffs[shIndex].values[0])); - } - } + // An individual splat cannot be sheared and thus we extract a uniform scaling factor. + const float modelScaling = std::cbrt(std::abs(modelMatrixFloat.GetDeterminant())); + const 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_PLY, "ply::export aggregated mesh %s { faces: %lu, vIdx: %lu, v: %lu }\n", mesh.name.c_str(), @@ -357,7 +325,7 @@ exportPly(UsdData& usd, happly::PLYData& ply) expandIndexedValues(m.normals.indices.size() ? m.normals.indices : m.indices, m.normals.values); if (m.colors.size()) { // translate only first set of colors - Primvar& colorSet = m.colors[0]; + Primvar& colorSet = m.colors[0]; expandIndexedValues(colorSet.indices.size() ? colorSet.indices : m.indices, colorSet.values); } @@ -385,19 +353,9 @@ exportPly(UsdData& usd, happly::PLYData& ply) constexpr std::size_t numGsplatsSHCoeffs = 45; if (totalMesh.asGsplats) { totalMesh.shCoeffs.resize(numGsplatsSHCoeffs); - - // For now, we do not apply transform to Gsplats since they have spherical harmonics coefficients. - // TODO: Implement using Wigner D-matrices to correctly rotate the SH coefficients in Gsplats. - correctionTransform.SetIdentity(); - if (usd.upAxis == UsdGeomTokens->z) { - ply.comments.push_back("Gaussian Splats with Z-axis up"); - } else if (usd.upAxis == UsdGeomTokens->y) { - ply.comments.push_back("Gaussian Splats with Y-axis up"); - } - } else { - correctionTransform = - getTransformToMetersPositiveY(usd.metersPerUnit, usd.upAxis); + ply.comments.push_back("Gaussian Splats with Y-axis up"); } + correctionTransform = getTransformToMetersPositiveY(usd.metersPerUnit, usd.upAxis); for (size_t i = 0; i < usd.rootNodes.size(); i++) { traverseNodesAndAggregateMeshes( diff --git a/ply/src/plyExport.h b/ply/src/plyExport.h index 958575f4..33322632 100644 --- a/ply/src/plyExport.h +++ b/ply/src/plyExport.h @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ #pragma once #include -#include +#include namespace adobe::usd { diff --git a/ply/src/plyImport.cpp b/ply/src/plyImport.cpp index b2181319..739fbb5b 100644 --- a/ply/src/plyImport.cpp +++ b/ply/src/plyImport.cpp @@ -13,12 +13,12 @@ governing permissions and limitations under the License. #include "debugCodes.h" #include #include -#include -#include #include -#include +#include +#include +#include +#include #include -#include #include #include #include @@ -328,7 +328,9 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) opacity.interpolation = UsdGeomTokens->vertex; opacity.values.resize(gsOpacity->size()); for (size_t i = 0; i < opacity.values.size(); i++) { - opacity.values[i] = 1.0f / (1.0f + std::exp(-(*gsOpacity)[i])); + // when nan opacity is detected, set opacity to 0 + float op = (*gsOpacity)[i]; + opacity.values[i] = std::isfinite(op) ? 1.0f / (1.0f + std::exp(-op)) : 0.0f; } } else if (a && a->size()) { auto [opacityIndex, opacity] = usd.addOpacitySet(meshIndex); @@ -412,6 +414,7 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) auto [nodeIndex, node] = usd.addNode(-1); node.staticMeshes.push_back(meshIndex); + usd.metersPerUnit = 1.0f; if (options.importWithUpAxisCorrection) { // We filter out useful convention info from the comment. bool useZup = false; @@ -442,7 +445,7 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) usd.upAxis = UsdGeomTokens->y; } - if (mesh.asGsplats && options.importGsplatWithClipping) + if (mesh.asGsplats && options.importGsplatClippingBox.size() >= 6) { PXR_NS::GfVec3f minPos(std::numeric_limits::max()); PXR_NS::GfVec3f maxPos(-std::numeric_limits::max()); @@ -471,10 +474,14 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) // 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(-2.0f, minPos[0]), std::max(-2.0f, minPos[1]), std::max(-2.0f, minPos[2])); - mesh.clippingBox.values[1] = PXR_NS::GfVec3f( - std::min(2.0f, maxPos[0]), std::min(2.0f, maxPos[1]), std::min(2.0f, maxPos[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; diff --git a/ply/src/plyImport.h b/ply/src/plyImport.h index 1c7475d5..02c5bf47 100644 --- a/ply/src/plyImport.h +++ b/ply/src/plyImport.h @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ #pragma once #include -#include +#include namespace adobe::usd { @@ -19,7 +19,7 @@ struct ImportPlyOptions { bool importAsPoints = false; bool importWithUpAxisCorrection = true; - bool importGsplatWithClipping = true; + PXR_NS::VtFloatArray importGsplatClippingBox = { -2, -2, -2, 2, 2, 2 }; float pointWidth = 0.01f; }; diff --git a/ply/tests/CMakeLists.txt b/ply/tests/CMakeLists.txt index 640d37f7..8803cff7 100644 --- a/ply/tests/CMakeLists.txt +++ b/ply/tests/CMakeLists.txt @@ -13,3 +13,4 @@ PRIVATE gtest_add_tests(TARGET plySanityTests AUTO) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/SanityCube.ply" "${CMAKE_CURRENT_BINARY_DIR}/SanityCube.ply" COPYONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/貝殻ビューア Colored Cube.ply" "${CMAKE_CURRENT_BINARY_DIR}/貝殻ビューア Colored Cube.ply" COPYONLY) \ No newline at end of file diff --git a/ply/tests/sanityTests.cpp b/ply/tests/sanityTests.cpp index 27093ff9..715356e7 100644 --- a/ply/tests/sanityTests.cpp +++ b/ply/tests/sanityTests.cpp @@ -25,3 +25,12 @@ TEST(PLYSanityTests, LoadCube) UsdStageRefPtr stage = UsdStage::Open("SanityCube.ply"); ASSERT_TRUE(stage); } + +TEST(PLYSanityTests, LoadForeignCube) +{ + PXR_NAMESPACE_USING_DIRECTIVE + + // Load an FBX + UsdStageRefPtr stage = UsdStage::Open("貝殻ビューア Colored Cube.ply"); + ASSERT_TRUE(stage); +} diff --git "a/ply/tests/\350\262\235\346\256\273\343\203\223\343\203\245\343\203\274\343\202\242 Colored Cube.ply" "b/ply/tests/\350\262\235\346\256\273\343\203\223\343\203\245\343\203\274\343\202\242 Colored Cube.ply" new file mode 100644 index 00000000..db61d7bb --- /dev/null +++ "b/ply/tests/\350\262\235\346\256\273\343\203\223\343\203\245\343\203\274\343\202\242 Colored Cube.ply" @@ -0,0 +1,40 @@ +ply +format ascii 1.0 +comment author: Greg Turk +comment object: another cube +element vertex 8 +property float x +property float y +property float z +property uchar red +property uchar green +property uchar blue +element face 7 +property list uchar int vertex_index +element edge 5 +property int vertex1 +property int vertex2 +property uchar red +property uchar green +property uchar blue +end_header +0 0 0 255 0 0 +0 0 1 255 0 0 +0 1 1 255 0 0 +0 1 0 255 0 0 +1 0 0 0 0 255 +1 0 1 0 0 255 +1 1 1 0 0 255 +1 1 0 0 0 255 +3 0 1 2 +3 0 2 3 +4 7 6 5 4 +4 0 4 5 1 +4 1 5 6 2 +4 2 6 7 3 +4 3 7 4 0 +0 1 255 255 255 +1 2 255 255 255 +2 3 255 255 255 +3 0 255 255 255 +2 0 0 0 0 \ No newline at end of file diff --git a/sbsar/CMakeLists.txt b/sbsar/CMakeLists.txt index f2be0691..3251c416 100644 --- a/sbsar/CMakeLists.txt +++ b/sbsar/CMakeLists.txt @@ -30,7 +30,7 @@ elseif(USDSBSAR_BUILD_APPLE_SILICON) set(USDSBSAR_DEFAULT_SUBSTANCE_ENGINES mtl_blend ogl3_blend neon_blend) else() # Linux and Intel Mac - set(USDSBSAR_DEFAULT_SUBSTANCE_ENGINES ogl3_blend sse2_blend) + set(USDSBSAR_DEFAULT_SUBSTANCE_ENGINES mtl_blend ogl3_blend sse2_blend) endif() if(USDSBSAR_ENABLE_VULKAN) diff --git a/sbsar/src/assetResolver/sbsarImage.cpp b/sbsar/src/assetResolver/sbsarImage.cpp index a51a10fc..f4e01d33 100644 --- a/sbsar/src/assetResolver/sbsarImage.cpp +++ b/sbsar/src/assetResolver/sbsarImage.cpp @@ -311,13 +311,13 @@ SbsarImage::_OpenForReading(const std::string& filename, std::shared_ptr asset = PXR_NS::ArGetResolver().OpenAsset(PXR_NS::ArResolvedPath(filename)); if (!asset) { - TF_RUNTIME_ERROR("Fail to retrieve asset"); + TF_RUNTIME_ERROR("Fail to retrieve asset %s", filename.c_str()); return false; } mSbsarAsset = std::dynamic_pointer_cast(asset); if (!mSbsarAsset) { - TF_RUNTIME_ERROR("Fail to retrieve asset"); + TF_RUNTIME_ERROR("Fail to cast file %s to SbsarAsset", filename.c_str()); return false; } diff --git a/sbsar/src/sbsarfileformat.cpp b/sbsar/src/sbsarfileformat.cpp index dbec3f91..81faa028 100644 --- a/sbsar/src/sbsarfileformat.cpp +++ b/sbsar/src/sbsarfileformat.cpp @@ -32,8 +32,8 @@ governing permissions and limitations under the License. #include // File format utils -#include -#include +#include +#include #include #include diff --git a/sbsar/src/usdGeneration/sbsarAsm.cpp b/sbsar/src/usdGeneration/sbsarAsm.cpp index f2d5f2f5..fdfccfd9 100644 --- a/sbsar/src/usdGeneration/sbsarAsm.cpp +++ b/sbsar/src/usdGeneration/sbsarAsm.cpp @@ -14,9 +14,9 @@ governing permissions and limitations under the License. #include // File format utils -#include -#include -#include +#include +#include +#include #include @@ -81,11 +81,13 @@ bindTexture(SdfAbstractData* sdfData, const BindInfo& bindInfo, const SdfPath& uvOutputAttrPath, const SdfPath& textureAssetAttrPath, - const SdfPath& fallbackAttrPath) + const SdfPath& fallbackAttrPath, + const SdfPath& scaleAttrPath, + const SdfPath& biasAttrPath) { - TF_DEBUG(FILE_FORMAT_SBSAR) .Msg("bindTexture: Binding texture channel %s\n", bindInfo.name.c_str()); + SdfPath resultPath = createShader(sdfData, parentPath, TfToken("file" + bindInfo.name), @@ -96,7 +98,9 @@ bindTexture(SdfAbstractData* sdfData, { "wrapT", AdobeTokens->repeat } }, { { "st", uvOutputAttrPath }, { "file", textureAssetAttrPath }, - { "fallback", fallbackAttrPath } }); + { "fallback", fallbackAttrPath }, + { "scale", scaleAttrPath }, + { "bias", biasAttrPath } }); return resultPath; } @@ -146,6 +150,8 @@ addUsdAsmShaderImpl(SdfAbstractData* sdfData, // Create texture sampling nodes InputConnections inputConnections; + InputValues inputValues; + for (auto& usage : mapped_usages) { TF_DEBUG(FILE_FORMAT_SBSAR) .Msg("addUsdAsmShaderImpl: Looking for usage : '%s'\n", usage.c_str()); @@ -166,14 +172,26 @@ addUsdAsmShaderImpl(SdfAbstractData* sdfData, fallbackAttrPath = inputPath(materialPath, defaultName.first); } + SdfPath scaleAttrPath, biasAttrPath; + if (isNormal(usage)) { + const auto [scaleName, biasName] = getNormalMapScaleAndBiasNames(usage); + scaleAttrPath = inputPath(materialPath, scaleName); + biasAttrPath = inputPath(materialPath, biasName); + } + // Create the texture reader SdfPath texResultPath = bindTexture(sdfData, scopePath, bindInfo, uvOutputPath, textureAssetAttrPath, - fallbackAttrPath); + fallbackAttrPath, + scaleAttrPath, + biasAttrPath); + if (usage == "emissive") { + inputValues.emplace_back("emissiveIntensity", 1.0f); + } inputConnections.emplace_back(bindInfo.name, texResultPath); } } @@ -193,7 +211,7 @@ addUsdAsmShaderImpl(SdfAbstractData* sdfData, _tokens->AdobeStandardMaterial, AdobeTokens->adobeStandardMaterial, "surface", - {}, + inputValues, inputConnections); createShaderOutput( sdfData, materialPath, "adobe:surface", SdfValueTypeNames->Token, surfaceOutputPath); diff --git a/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp b/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp index 9ff5f3ee..66318b4d 100644 --- a/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp +++ b/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp @@ -19,8 +19,8 @@ governing permissions and limitations under the License. #include // File format utils -#include -#include +#include +#include PXR_NAMESPACE_USING_DIRECTIVE using namespace SubstanceAir; diff --git a/sbsar/src/usdGeneration/sbsarMaterial.cpp b/sbsar/src/usdGeneration/sbsarMaterial.cpp index 528d5d36..87d8e18a 100644 --- a/sbsar/src/usdGeneration/sbsarMaterial.cpp +++ b/sbsar/src/usdGeneration/sbsarMaterial.cpp @@ -23,8 +23,8 @@ governing permissions and limitations under the License. #include // File format utils -#include -#include +#include +#include PXR_NAMESPACE_USING_DIRECTIVE using namespace SubstanceAir; @@ -72,6 +72,9 @@ initDefaultMaterialInputs(SdfAbstractData* sdfData, size_t sbsarHash) { TF_DEBUG(FILE_FORMAT_SBSAR).Msg("initDefaultMaterialInputs: Creating material inputs\n"); + + NormalFormat normalFormat = getDefaultNormalFormat(graphDesc); + for (const auto& usage : mapped_usages) { if (hasUsage(usage, graphDesc)) { std::string textureAssetName = getTextureAssetName(usage); @@ -97,6 +100,19 @@ initDefaultMaterialInputs(SdfAbstractData* sdfData, setRangeMetadata(sdfData, textureBlendPath, { VtValue(0.0f), VtValue(1.0f) }); setAttributeMetadata(sdfData, textureBlendPath, SdfFieldKeys->Hidden, VtValue(true)); } + if (isNormal(usage)) { + const auto [scaleName, biasName] = getNormalMapScaleAndBiasNames(usage); + SdfPath scaleAttrPath = + createShaderInput(sdfData, materialPath, scaleName, SdfValueTypeNames->Float4); + SdfPath biasAttrPath = + createShaderInput(sdfData, materialPath, biasName, SdfValueTypeNames->Float4); + setAttributeMetadata(sdfData, scaleAttrPath, SdfFieldKeys->Hidden, VtValue(true)); + setAttributeMetadata(sdfData, biasAttrPath, SdfFieldKeys->Hidden, VtValue(true)); + + const auto [scale, bias] = getNormalMapScaleAndBias(normalFormat); + setAttributeDefaultValue(sdfData, scaleAttrPath, scale); + setAttributeDefaultValue(sdfData, biasAttrPath, bias); + } } } @@ -157,6 +173,41 @@ setMaterialValues(SdfAbstractData* sdfData, } } +void +setMaterialNormalScaleAndBias(SdfAbstractData* sdfData, + const SdfPath& materialPath, + const SubstanceAir::GraphDesc& graphDesc, + const JsValue& jsParams) +{ + // If we don't have concrete information on the normal format, we don't author an explict scale + // and bias to adjust for that and instead rely on the default that was authored with the + // default material inputs. + NormalFormat normalFormat = determineNormalFormat(jsParams); + if (normalFormat == NormalFormat::Unknown) { + return; + } + + // If the current format matches the default, there is nothing to be done + NormalFormat defaultNormalFormat = getDefaultNormalFormat(graphDesc); + if (normalFormat == defaultNormalFormat) { + return; + } + + // The scale and bias needs to be authored for each normal map usage + for (const auto& usage : normal_usages) { + if (hasUsage(usage, graphDesc)) { + const auto [scaleName, biasName] = getNormalMapScaleAndBiasNames(usage); + SdfPath scaleAttrPath = + createShaderInput(sdfData, materialPath, scaleName, SdfValueTypeNames->Float4); + SdfPath biasAttrPath = + createShaderInput(sdfData, materialPath, biasName, SdfValueTypeNames->Float4); + const auto [scale, bias] = getNormalMapScaleAndBias(normalFormat); + setAttributeDefaultValue(sdfData, scaleAttrPath, scale); + setAttributeDefaultValue(sdfData, biasAttrPath, bias); + } + } +} + //! \brief Add transform inputs to the given material. //! \param sdfData Sdf layer data. //! \param materialPath Path to the material @@ -310,6 +361,8 @@ addMaterialPrim(SdfAbstractData* sdfData, // Set procedural values for uniform usage setMaterialValues( sdfData, materialPath, graphDesc, graphName, sbsarHash, jsParams, packagePath); + // Set normal scale and bias depending on the normal format + setMaterialNormalScaleAndBias(sdfData, materialPath, graphDesc, jsParams); } return materialPath; diff --git a/sbsar/src/usdGeneration/sbsarMtlx.cpp b/sbsar/src/usdGeneration/sbsarMtlx.cpp index 46be0f7c..352a29d7 100644 --- a/sbsar/src/usdGeneration/sbsarMtlx.cpp +++ b/sbsar/src/usdGeneration/sbsarMtlx.cpp @@ -14,13 +14,12 @@ governing permissions and limitations under the License. #include // File format utils -#include -#include -#include +#include +#include +#include #include - using namespace SubstanceAir; PXR_NAMESPACE_USING_DIRECTIVE @@ -91,7 +90,7 @@ bindTexture(SdfAbstractData* sdfData, } // Note, there is currently no support for the color space choice. Also no support for a - // fallback value + // fallback value. Bias and scale are also not supported. SdfPath resultPath = createShader( sdfData, parentPath, @@ -173,11 +172,15 @@ addUsdMtlxShaderImpl(SdfAbstractData* sdfData, SdfPath texResultPath = bindTexture(sdfData, scopePath, bindInfo, uvOutputPath, textureAssetAttrPath); - if (usage == "normal") { + if (isNormal(usage)) { // Normal maps are disabled in MaterialX now since they // behave strangely in USD view // Route normal map through a normal map node + // TODO: When we reactivate this we need to make sure we can handle DirectX and + // OpenGL style normal maps. By default we can assume DirectX style maps, but + // we have a setup that uses scale and bias for the other networks to control + // how the texture maps are decoded to support both. // SdfPath wsNormalPath = createShader(sdfData, // scopePath, // _tokens->WsNormal, diff --git a/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.cpp b/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.cpp index 30384031..ea15682c 100644 --- a/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.cpp +++ b/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.cpp @@ -14,9 +14,9 @@ governing permissions and limitations under the License. #include // File format utils -#include -#include -#include +#include +#include +#include #include @@ -70,7 +70,9 @@ bindTexture(SdfAbstractData* sdfData, const BindInfo& bindInfo, const SdfPath& uvOutputAttrPath, const SdfPath& textureAssetAttrPath, - const SdfPath& fallbackAttrPath) + const SdfPath& fallbackAttrPath, + const SdfPath& scaleAttrPath, + const SdfPath& biasAttrPath) { TF_DEBUG(FILE_FORMAT_SBSAR) @@ -85,7 +87,9 @@ bindTexture(SdfAbstractData* sdfData, { "wrapT", AdobeTokens->repeat } }, { { "st", uvOutputAttrPath }, { "file", textureAssetAttrPath }, - { "fallback", fallbackAttrPath } }); + { "fallback", fallbackAttrPath }, + { "scale", scaleAttrPath }, + { "bias", biasAttrPath } }); return resultPath; } @@ -155,13 +159,22 @@ addUsdPreviewSurfaceImpl(SdfAbstractData* sdfData, fallbackAttrPath = inputPath(materialPath, defaultName.first); } + SdfPath scaleAttrPath, biasAttrPath; + if (isNormal(usage)) { + const auto [scaleName, biasName] = getNormalMapScaleAndBiasNames(usage); + scaleAttrPath = inputPath(materialPath, scaleName); + biasAttrPath = inputPath(materialPath, biasName); + } + // Create the texture reader SdfPath texResultPath = bindTexture(sdfData, scopePath, bindInfo, uvOutputPath, textureAssetAttrPath, - fallbackAttrPath); + fallbackAttrPath, + scaleAttrPath, + biasAttrPath); inputConnections.emplace_back(bindInfo.name, texResultPath); } diff --git a/sbsar/src/usdGeneration/usdGenerationHelpers.cpp b/sbsar/src/usdGeneration/usdGenerationHelpers.cpp index 62c07fd7..3fc9c912 100644 --- a/sbsar/src/usdGeneration/usdGenerationHelpers.cpp +++ b/sbsar/src/usdGeneration/usdGenerationHelpers.cpp @@ -28,8 +28,9 @@ governing permissions and limitations under the License. #include // File format utils -#include -#include +#include +#include +#include #include @@ -92,6 +93,8 @@ const std::vector uniform_usages = { "IOR", "heightScale", "normalScale" }; +const std::vector normal_usages = { "normal", "coatNormal" }; + const std::map reserved_label_map = { { "$time", "Time" }, { "$outputsize", "Output Size" }, { "$randomseed", "Random Seed" }, @@ -272,6 +275,7 @@ getGraphName(const GraphDesc& desc) size_t slashes_end = uri.find_first_not_of('/', 4); return uri.substr(slashes_end); } + bool hasUsage(const std::string& usage, const GraphDesc& graphDesc) { @@ -297,6 +301,17 @@ hasInput(const std::string& identifier, const GraphDesc& graphDesc) return false; } +bool +isNormal(const std::string& usage) +{ + for (const std::string& normal_usage : normal_usages) { + if (usage == normal_usage) { + return true; + } + } + return false; +} + JsValue convertSbsarParameters(const VtDictionary& sbsarParameters) { @@ -334,6 +349,100 @@ convertColorSRGBToLinear(VtValue& value) } } +std::pair +getNormalMapScaleAndBiasNames(const std::string& channelName) +{ + return { channelName + "_scale", channelName + "_bias" }; +} + +// This is the name that all SBSAR files from Substance Source use +static const char* normalFormatParamName = "normal_format"; +// We default to the OpenGL format, since that is the common format in USD +const NormalFormat defaultNormalFormat = NormalFormat::OpenGL; + +bool +adjustNormalFormatInput(const SubstanceAir::string& identifier, + SubstanceIOType inputType, + VtValue& defaultValue) +{ + if (inputType == Substance_IOType_Integer && identifier == normalFormatParamName) { + int currentValue = defaultValue.GetWithDefault(-1); + if ((currentValue == 0 ? NormalFormat::DirectX : NormalFormat::OpenGL) != + defaultNormalFormat) { + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("Detected normal format parameter %s with value %d. Changing to default %d.\n", + normalFormatParamName, + currentValue, + defaultNormalFormat == NormalFormat::DirectX ? 0 : 1); + defaultValue = VtValue(defaultNormalFormat == NormalFormat::DirectX ? 0 : 1); + return true; + } + } + + return false; +} + +NormalFormat +getDefaultNormalFormat(const SubstanceAir::GraphDesc& graphDesc) +{ + bool hasNormalFormatInput = hasInput(normalFormatParamName, graphDesc); + + // If the graph has a normal format input, we're adjusting the default input values to match the + // defaultNormalFormat. Otherwise we assume it's a DirectX normal map. + return hasNormalFormatInput ? defaultNormalFormat : NormalFormat::DirectX; +} + +NormalFormat +determineNormalFormat(const JsValue& jsParams) +{ + if (!jsParams.IsObject()) { + TF_WARN("JsParams not a JsObject"); + return NormalFormat::Unknown; + } + const JsObject& jsObject = jsParams.GetJsObject(); + const auto it = jsObject.find(normalFormatParamName); + if (it == jsObject.end()) { + // It's OK if this is missing. Not all SBSARs have this parameter + return NormalFormat::Unknown; + } + + if (!it->second.IsInt()) { + TF_WARN("%s parameter is not an int", normalFormatParamName); + return NormalFormat::Unknown; + } + int normalFormat = it->second.GetInt(); + + if (normalFormat == 0) { + return NormalFormat::DirectX; + } else if (normalFormat == 1) { + return NormalFormat::OpenGL; + } + + TF_WARN("%s parameter has value %d, which is not a supported value", + normalFormatParamName, + normalFormat); + + return NormalFormat::Unknown; +} + +std::pair +getNormalMapScaleAndBias(NormalFormat normalFormat) +{ + // By default we assume that SBSAR files generate DirectX normal maps + if (normalFormat == NormalFormat::Unknown || normalFormat == NormalFormat::DirectX) { + // USD usually expects OpenGL style normals maps. We express the conversion (flip of the + // green channel) via the scale and bias. + return { GfVec4f(2.0f, -2.0f, 2.0f, 1.0f), GfVec4f(-1.0f, 1.0f, -1.0f, 0.0f) }; + } else if (normalFormat == NormalFormat::OpenGL) { + // The `XYZ = 2 * RGB - 1` base equation is always needed to unpack [0, 1] RGB values into a + // XYZ vector in the [-1, 1] range. + return { GfVec4f(2.0f, 2.0f, 2.0f, 1.0f), GfVec4f(-1.0f, -1.0f, -1.0f, 0.0f) }; + } else { + TF_CODING_ERROR("Unsupported normalFormat"); + return { GfVec4f(1.0f), GfVec4f(0.0f) }; + } +} + std::string generateSbsarInfoPath(const std::string& usage, const MappedSymbol& graphName, @@ -369,20 +478,6 @@ getGraphCategory(const GraphDesc& graphDesc, SymbolMapper& symbolMapper) return symbolMapper.GetSymbol(graphDesc.mCategory.c_str()); } -std::vector -_split(const std::string& s, char delim) -{ - std::vector result; - std::stringstream ss(s); - std::string item; - - while (std::getline(ss, item, delim)) { - result.push_back(item); - } - - return result; -} - void setGraphMetadataOnPrim(SdfAbstractData* sdfData, const SdfPath& primPath, @@ -424,7 +519,7 @@ setGraphMetadataOnPrim(SdfAbstractData* sdfData, customData["category"] = graphDesc.mCategory.c_str(); } if (!graphDesc.mKeywords.empty()) { - auto keywords = _split(std::string(graphDesc.mKeywords), ';'); + auto keywords = split(std::string(graphDesc.mKeywords), ';'); customData["keywords"] = VtArray(keywords.begin(), keywords.end()); } if (!graphDesc.mAuthor.empty()) { @@ -683,6 +778,9 @@ setupProceduralParameters(SdfAbstractData* sdfData, } if (validProcParameter) { + // Special check for normal format inputs and their default values + adjustNormalFormatInput(input->mIdentifier, input->mType, defaultValue); + TfToken paramToken = getInputParamToken(symbolMapper, input->mIdentifier); SdfPath paramPath = createAttributeSpec(sdfData, primPath, paramToken, targetType); setAttributeDefaultValue(sdfData, paramPath, defaultValue); @@ -842,6 +940,9 @@ addPresetVariant(SdfAbstractData* sdfData, continue; } + // Special check for normal format inputs and their values within a preset + adjustNormalFormatInput(inputDesc->mIdentifier, inputDesc->mType, targetValue); + if (inputDesc->mGuiWidget == SubstanceAir::InputWidget::Input_Color) { convertColorSRGBToLinear(targetValue); } diff --git a/sbsar/src/usdGeneration/usdGenerationHelpers.h b/sbsar/src/usdGeneration/usdGenerationHelpers.h index f4839c7b..e009dc68 100644 --- a/sbsar/src/usdGeneration/usdGenerationHelpers.h +++ b/sbsar/src/usdGeneration/usdGenerationHelpers.h @@ -32,6 +32,7 @@ struct DefaultChannel }; extern const std::vector mapped_usages; extern const std::vector uniform_usages; +extern const std::vector normal_usages; extern const std::vector default_resolutions; extern const std::map default_channels; @@ -64,6 +65,8 @@ bool hasUsage(const std::string& usage, const SubstanceAir::GraphDesc& graphDesc); bool hasInput(const std::string& identifier, const SubstanceAir::GraphDesc& graphDesc); +bool +isNormal(const std::string& usage); USDSBSAR_API PXR_NS::JsValue convertSbsarParameters(const PXR_NS::VtDictionary& sbsarParmeters); @@ -73,6 +76,37 @@ convertColorLinearToSRGB(PXR_NS::VtValue& value); void convertColorSRGBToLinear(PXR_NS::VtValue& value); +//! Returns the name of the scale and bias interface attributes for a given normal channel. +std::pair +getNormalMapScaleAndBiasNames(const std::string& channelName); + +enum class NormalFormat +{ + Unknown, + DirectX, + OpenGL +}; + +//! If a graph has a "normal_format" input, this is the default we're using for USD +extern const NormalFormat defaultNormalFormat; + +//! Determines the normal format the graph uses by default. This is determined by checking if the +//! graph supports the "normal_format" input parameter. And if so to return the defaultNormalFormat. +//! If the graph doesn't support that input we assume a DirectX style normal map. +NormalFormat +getDefaultNormalFormat(const SubstanceAir::GraphDesc& graphDesc); + +//! This function is looking for the "normal_format" parameter in the current parameters. Not all +//! SBSAR files have this parameter, but all of the Substance Source materials have it. And if it +//! is available we can use it to determine the normal format that is being generated. +NormalFormat +determineNormalFormat(const PXR_NS::JsValue& jsParams); + +//! Returns the scale and bias for a texture reader that is appropriate for the respective normal +//! map format. +std::pair +getNormalMapScaleAndBias(NormalFormat normalFormat); + //! \brief Generate a texture path. //! An sbsar info path has several parts and look like this: //! Path[Graph?Usage=xxx#Hash=xxx#params={"name:value","name:value"}] diff --git a/stl/src/fileFormat.cpp b/stl/src/fileFormat.cpp index 0253423f..329828e6 100644 --- a/stl/src/fileFormat.cpp +++ b/stl/src/fileFormat.cpp @@ -16,9 +16,9 @@ governing permissions and limitations under the License. #include "stlImport.h" #include "stlModel.h" -#include -#include -#include +#include +#include +#include #include #include diff --git a/stl/src/fileFormat.h b/stl/src/fileFormat.h index d7db419d..a7cb90f4 100644 --- a/stl/src/fileFormat.h +++ b/stl/src/fileFormat.h @@ -12,10 +12,10 @@ governing permissions and limitations under the License. #pragma once #include "api.h" #include +#include #include #include #include -#include #include #include diff --git a/stl/src/stlExport.cpp b/stl/src/stlExport.cpp index ae424239..50d3f635 100644 --- a/stl/src/stlExport.cpp +++ b/stl/src/stlExport.cpp @@ -11,8 +11,8 @@ governing permissions and limitations under the License. */ #include "stlExport.h" -#include -#include +#include +#include #include @@ -68,8 +68,8 @@ exportStl(const ExportStlOptions& options, const UsdData& usd, StlModel& stl) for (int j = 0; j < 3; j++) { StlVec3f vertex; const int vertex_index = meshIndices[i + j]; - const PXR_NS::GfVec3f& vertex_data = mesh.points[vertex_index]; - const PXR_NS::GfVec3f transformedPoint = GfVec3f(worldTransform.Transform(vertex_data)); + const GfVec3f& vertex_data = mesh.points[vertex_index]; + const GfVec3f transformedPoint = GfVec3f(worldTransform.Transform(vertex_data)); vertex = { transformedPoint[0], transformedPoint[1], transformedPoint[2] }; facet.vertices[j] = vertex; } diff --git a/stl/src/stlExport.h b/stl/src/stlExport.h index 6df1f7e8..b95f2aa3 100644 --- a/stl/src/stlExport.h +++ b/stl/src/stlExport.h @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ #pragma once #include "stlModel.h" -#include +#include using namespace adobe::usd; diff --git a/stl/src/stlImport.cpp b/stl/src/stlImport.cpp index f75e5e18..6ea52d71 100644 --- a/stl/src/stlImport.cpp +++ b/stl/src/stlImport.cpp @@ -11,8 +11,7 @@ governing permissions and limitations under the License. */ #include "stlImport.h" #include "stlModel.h" -#include -#include +#include #include #include diff --git a/stl/src/stlImport.h b/stl/src/stlImport.h index 4eb5c157..877d0d91 100644 --- a/stl/src/stlImport.h +++ b/stl/src/stlImport.h @@ -13,7 +13,7 @@ governing permissions and limitations under the License. #include "stlModel.h" -#include +#include using namespace adobe::usd; diff --git a/test/baseline/Darwin/sbsar/cube.jpg b/test/baseline/Darwin/sbsar/cube.jpg index 747ac443..b645e8af 100644 Binary files a/test/baseline/Darwin/sbsar/cube.jpg and b/test/baseline/Darwin/sbsar/cube.jpg differ diff --git a/test/baseline/Darwin/sbsar/sphere.jpg b/test/baseline/Darwin/sbsar/sphere.jpg index d37c7d9a..33c69297 100644 Binary files a/test/baseline/Darwin/sbsar/sphere.jpg and b/test/baseline/Darwin/sbsar/sphere.jpg differ diff --git a/test/baseline/Linux/sbsar/cube.jpg b/test/baseline/Linux/sbsar/cube.jpg index a0c19cef..df47e44a 100644 Binary files a/test/baseline/Linux/sbsar/cube.jpg and b/test/baseline/Linux/sbsar/cube.jpg differ diff --git a/test/baseline/Linux/sbsar/sphere.jpg b/test/baseline/Linux/sbsar/sphere.jpg index e8ed994a..fa137ff5 100644 Binary files a/test/baseline/Linux/sbsar/sphere.jpg and b/test/baseline/Linux/sbsar/sphere.jpg differ diff --git a/test/baseline/Windows/sbsar/cube.jpg b/test/baseline/Windows/sbsar/cube.jpg index a0c19cef..df47e44a 100644 Binary files a/test/baseline/Windows/sbsar/cube.jpg and b/test/baseline/Windows/sbsar/cube.jpg differ diff --git a/test/baseline/Windows/sbsar/sphere.jpg b/test/baseline/Windows/sbsar/sphere.jpg index 28be62ad..fa137ff5 100644 Binary files a/test/baseline/Windows/sbsar/sphere.jpg and b/test/baseline/Windows/sbsar/sphere.jpg differ diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index f8f45d42..517c344b 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -7,54 +7,81 @@ find_package(ZLIB REQUIRED) if(USD_FILEFORMATS_BUILD_TESTS) find_package(GTest REQUIRED) endif() +if (USD_FILEFORMATS_ENABLE_PLY) + find_package(SphericalHarmonics REQUIRED) +endif() add_library(fileformatUtils SHARED) target_compile_definitions(fileformatUtils PRIVATE USDFFUTILS_EXPORTS) usd_plugin_compile_config(fileformatUtils) -target_sources(fileformatUtils -PRIVATE - "README.md" +set(HEADERS "assetresolver.h" - "assetresolver.cpp" "common.h" - "common.cpp" "debugCodes.h" "dictencoder.h" - "dictencoder.cpp" "geometry.h" - "geometry.cpp" "transforms.h" - "transforms.cpp" "images.h" - "images.cpp" "layerRead.h" - "layerRead.cpp" "layerWriteShared.h" - "layerWriteShared.cpp" "layerWriteMaterial.h" - "layerWriteMaterial.cpp" "layerWriteMaterialX.h" - "layerWriteMaterialX.cpp" "layerWriteSdfData.h" - "layerWriteSdfData.cpp" "materials.h" - "materials.cpp" "neuralAssetsHelper.h" - "neuralAssetsHelper.cpp" "resolver.h" - "resolver.cpp" "sdfMaterialUtils.h" - "sdfMaterialUtils.cpp" "sdfUtils.h" - "sdfUtils.cpp" "usdData.h" +) + +set(SOURCES + "assetresolver.cpp" + "common.cpp" + "dictencoder.cpp" + "geometry.cpp" + "transforms.cpp" + "images.cpp" + "layerRead.cpp" + "layerWriteShared.cpp" + "layerWriteMaterial.cpp" + "layerWriteMaterialX.cpp" + "layerWriteSdfData.cpp" + "materials.cpp" + "neuralAssetsHelper.cpp" + "resolver.cpp" + "sdfMaterialUtils.cpp" + "sdfUtils.cpp" "usdData.cpp" ) +if (USD_FILEFORMATS_ENABLE_PLY) + list(APPEND HEADERS + "gsplatHelper.h" + ) + list(APPEND SOURCES + "gsplatHelper.cpp" + ) +endif() + +# Prepend paths +list(TRANSFORM HEADERS PREPEND "include/fileformatutils/") +list(TRANSFORM SOURCES PREPEND "src/") + +# Add sources to target +target_sources(fileformatUtils +PRIVATE + "README.md" + ${HEADERS} + ${SOURCES} +) + target_include_directories(fileformatUtils - INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} - PRIVATE "${PROJECT_BINARY_DIR}" + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + PRIVATE + "${PROJECT_BINARY_DIR}" ) target_link_libraries(fileformatUtils @@ -70,15 +97,23 @@ PUBLIC usdShade usdUtils usdVol + hio arch ZLIB::ZLIB ) +if (USD_FILEFORMATS_ENABLE_PLY) + target_link_libraries(fileformatUtils + PRIVATE + SphericalHarmonics::SphericalHarmonics + ) +endif() + if(USD_FILEFORMATS_BUILD_TESTS) target_sources(fileformatUtils PRIVATE - "test.h" - "test.cpp" + "include/fileformatutils/test.h" + "src/test.cpp" ) target_link_libraries(fileformatUtils PUBLIC diff --git a/utils/api.h b/utils/include/fileformatutils/api.h similarity index 100% rename from utils/api.h rename to utils/include/fileformatutils/api.h diff --git a/utils/assetresolver.h b/utils/include/fileformatutils/assetresolver.h similarity index 100% rename from utils/assetresolver.h rename to utils/include/fileformatutils/assetresolver.h diff --git a/utils/common.h b/utils/include/fileformatutils/common.h similarity index 92% rename from utils/common.h rename to utils/include/fileformatutils/common.h index 2008cade..ea9bbc8b 100644 --- a/utils/common.h +++ b/utils/include/fileformatutils/common.h @@ -14,8 +14,10 @@ governing permissions and limitations under the License. #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. // XXX Split this list into categories for easier maintenance @@ -336,6 +338,12 @@ argComposeFloat(const PXR_NS::PcpDynamicFileFormatContext& context, const PXR_NS::TfToken& token, const std::string& debugTag); +void USDFFUTILS_API +argComposeFloatArray(const PXR_NS::PcpDynamicFileFormatContext& context, + PXR_NS::SdfFileFormat::FileFormatArguments* args, + const PXR_NS::TfToken& token, + const std::string& debugTag); + void USDFFUTILS_API argReadString(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, @@ -360,6 +368,12 @@ argReadFloat(const PXR_NS::SdfFileFormat::FileFormatArguments& args, float& target, const std::string& debugTag); +void USDFFUTILS_API +argReadFloatArray(const PXR_NS::SdfFileFormat::FileFormatArguments& args, + const std::string& arg, + PXR_NS::VtFloatArray& target, + const std::string& debugTag); + std::string USDFFUTILS_API getFileExtension(const std::string& filePath, const std::string& defaultValue); @@ -388,4 +402,16 @@ trim(std::string& s) ltrim(s); } +std::vector USDFFUTILS_API +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/debugCodes.h b/utils/include/fileformatutils/debugCodes.h similarity index 100% rename from utils/debugCodes.h rename to utils/include/fileformatutils/debugCodes.h diff --git a/utils/dictencoder.h b/utils/include/fileformatutils/dictencoder.h similarity index 100% rename from utils/dictencoder.h rename to utils/include/fileformatutils/dictencoder.h diff --git a/utils/geometry.h b/utils/include/fileformatutils/geometry.h similarity index 90% rename from utils/geometry.h rename to utils/include/fileformatutils/geometry.h index 50e4a5c4..e09be220 100644 --- a/utils/geometry.h +++ b/utils/include/fileformatutils/geometry.h @@ -48,33 +48,31 @@ struct MeshVerificationOptions /// \ingroup utils_geometry /// \brief Check a mesh for inconsistencies between the topology and the primvars. /// Returns true if no errors were detected. -/// If the issues pointer is valid, the issues are appended to the vector, which can include warnings -/// and hints, which won't fail the verification, but might still be issues. -/// Some of the checks can be controlled with the options struct. +/// If the issues pointer is valid, the issues are appended to the vector, which can include +/// warnings and hints, which won't fail the verification, but might still be issues. Some of the +/// checks can be controlled with the options struct. USDFFUTILS_API bool verifyMesh(const std::string& path, const Mesh& mesh, IssueVector* issues = nullptr, const MeshVerificationOptions& options = {}); - /// \ingroup utils_geometry -/// \brief Checks all meshes in the UsdData for issues and collects them in the options issues vector +/// \brief Checks all meshes in the UsdData for issues and collects them in the options issues +/// vector USDFFUTILS_API bool verifyMeshes(const UsdData& usdData, IssueVector* issues = nullptr, const MeshVerificationOptions& options = {}); - /// \ingroup utils_geometry /// \brief Prints the issues via the TfDebug::Helper and the provided debugTag USDFFUTILS_API void printIssues(const IssueVector& issues); - /// \ingroup utils_geometry -/// \brief If the TF_DEBUG flag for this module is set it will check all meshes in the UsdData and report -/// issues +/// \brief If the TF_DEBUG flag for this module is set it will check all meshes in the UsdData and +/// report issues USDFFUTILS_API void checkAndPrintMeshIssues(const UsdData& usdData); @@ -83,7 +81,6 @@ checkAndPrintMeshIssues(const UsdData& usdData); USDFFUTILS_API void createTriangulationIndices(Mesh& mesh); - /// \ingroup utils_geometry /// \brief Triangulate an existing mesh with all its primvars and subsets. // Note, the triangulation is done with a simple fan triangulation and hence @@ -91,7 +88,6 @@ createTriangulationIndices(Mesh& mesh); USDFFUTILS_API bool triangulateMesh(Mesh& mesh); - /// \ingroup utils_geometry /// \brief Some formats like GLTF can't handle the complex mesh representation that is // USD native, especially with regard to primvar interpolation. For these @@ -101,7 +97,6 @@ triangulateMesh(Mesh& mesh); USDFFUTILS_API void forceVertexInterpolation(Mesh& mesh); - /// \ingroup utils_geometry /// \brief Given the topology of a complete mesh and a subset of face indices into that // mesh, compute the corresponding face vertex indices @@ -111,7 +106,6 @@ computeFaceVertexIndicesForSubset(const PXR_NS::VtIntArray& faceVertexCounts, const PXR_NS::VtIntArray& subsetFaceIndices, PXR_NS::VtIntArray& subsetFaceVertexIndices); - /// \ingroup utils_geometry /// \brief Remove the indexing out of a set of values. template @@ -127,7 +121,12 @@ expandIndexedValues(const PXR_NS::VtIntArray& indices, PXR_NS::VtArray& value unsigned int size = indices.size(); values.resize(size); for (unsigned int i = 0; i < size; i++) { - values[i] = temp[indices[i]]; + int index = indices[i]; + if (index < 0 && index >= temp.size()) { + // set invalid indices to 0 + index = 0; + } + values[i] = temp[index]; } } } diff --git a/utils/include/fileformatutils/gsplatHelper.h b/utils/include/fileformatutils/gsplatHelper.h new file mode 100644 index 00000000..eab82436 --- /dev/null +++ b/utils/include/fileformatutils/gsplatHelper.h @@ -0,0 +1,45 @@ +/* +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 "usdData.h" +#include + +namespace adobe::usd { +USDFFUTILS_API size_t +numSHDegreesFromGsplat(size_t numCoefficients); + +USDFFUTILS_API size_t +numNonZeroSHBandsFromDegree(size_t numDegrees); + +USDFFUTILS_API void +rotatePointRotations(const Primvar& pointRotations, + const PXR_NS::GfQuatf& rotation, + size_t numPoints, + PXR_NS::VtQuatfArray& outPointRotations); + +USDFFUTILS_API void +rotatePointSphericalHarmonics(const std::vector>& inSH, + const PXR_NS::GfQuatf& rotation, + size_t numPoints, + std::vector& outSH); + +USDFFUTILS_API void +scalePointWidths(const PXR_NS::VtFloatArray& inWidths, + const std::vector>& inExtraWidths, + size_t numPoints, + float widthScale, + PXR_NS::VtFloatArray& outWidths, + PXR_NS::VtFloatArray& outWidths1, + PXR_NS::VtFloatArray& outWidths2); +} diff --git a/utils/images.h b/utils/include/fileformatutils/images.h similarity index 79% rename from utils/images.h rename to utils/include/fileformatutils/images.h index e243f32d..a3b344c3 100644 --- a/utils/images.h +++ b/utils/include/fileformatutils/images.h @@ -122,4 +122,36 @@ srgbToLinear(float s); USDFFUTILS_API float linearToSRGB(float s); +/// \ingroup utils_materials +/// \brief Is the resolved asset path a supported image file +bool USDFFUTILS_API +isImageFileSupported(const std::string& resolvedAssetPath); + +/// \ingroup utils_materials +/// \brief Is the uri a sbsar image +bool USDFFUTILS_API +isUriSbsarImage(const std::string& uri); + +/// \ingroup utils_materials +/// \brief Get the sbsar usage from the parameters string +std::string USDFFUTILS_API +getSbsarUsageFromParameters(const std::string& parametersStr); + +/// \ingroup utils_materials +/// \brief Get the sbsar image extension +std::string USDFFUTILS_API +getSbsarImageExtension(const std::string& resolvedAssetPath); + +/// \ingroup utils_materials +/// \brief Extract the file path from the asset path +std::string USDFFUTILS_API +extractFilePathFromAssetPath(const std::string& assetPath); + +/// \ingroup utils_materials +/// \brief Transcodes an image asset to memory +bool USDFFUTILS_API +transcodeImageAssetToMemory(const std::string& resolvedAssetPath, + const std::string& filename, + std::vector& outputPixelData); + } diff --git a/utils/layerRead.h b/utils/include/fileformatutils/layerRead.h similarity index 79% rename from utils/layerRead.h rename to utils/include/fileformatutils/layerRead.h index 2342d981..01d0862d 100644 --- a/utils/layerRead.h +++ b/utils/include/fileformatutils/layerRead.h @@ -29,6 +29,16 @@ struct USDFFUTILS_API ReadLayerOptions int maxMeshInfluenceCount = 4; }; +/// \ingroup utils_layer +/// \brief Takes a SBSAR texture parameterization. +USDFFUTILS_API std::string +getSbsarUsageFromParameters(const std::string& parametersStr); + +/// \ingroup utils_layer +/// \brief This function extracts a usable file path from an assetPath. +USDFFUTILS_API std::string +extractFilePathFromAssetPath(const std::string& assetPath); + /// \ingroup utils_layer /// \brief Reads data from a USD layer and dumps it into a UsdData structure. USDFFUTILS_API bool @@ -36,5 +46,4 @@ readLayer(const ReadLayerOptions& options, const PXR_NS::SdfLayer& layer, UsdData& data, const std::string& debugTag); - } \ No newline at end of file diff --git a/utils/layerWriteMaterial.h b/utils/include/fileformatutils/layerWriteMaterial.h similarity index 100% rename from utils/layerWriteMaterial.h rename to utils/include/fileformatutils/layerWriteMaterial.h diff --git a/utils/layerWriteMaterialX.h b/utils/include/fileformatutils/layerWriteMaterialX.h similarity index 100% rename from utils/layerWriteMaterialX.h rename to utils/include/fileformatutils/layerWriteMaterialX.h diff --git a/utils/layerWriteSdfData.h b/utils/include/fileformatutils/layerWriteSdfData.h similarity index 100% rename from utils/layerWriteSdfData.h rename to utils/include/fileformatutils/layerWriteSdfData.h diff --git a/utils/layerWriteShared.h b/utils/include/fileformatutils/layerWriteShared.h similarity index 100% rename from utils/layerWriteShared.h rename to utils/include/fileformatutils/layerWriteShared.h diff --git a/utils/materials.h b/utils/include/fileformatutils/materials.h similarity index 100% rename from utils/materials.h rename to utils/include/fileformatutils/materials.h diff --git a/utils/neuralAssetsHelper.h b/utils/include/fileformatutils/neuralAssetsHelper.h similarity index 100% rename from utils/neuralAssetsHelper.h rename to utils/include/fileformatutils/neuralAssetsHelper.h diff --git a/utils/resolver.h b/utils/include/fileformatutils/resolver.h similarity index 100% rename from utils/resolver.h rename to utils/include/fileformatutils/resolver.h diff --git a/utils/sdfMaterialUtils.h b/utils/include/fileformatutils/sdfMaterialUtils.h similarity index 100% rename from utils/sdfMaterialUtils.h rename to utils/include/fileformatutils/sdfMaterialUtils.h diff --git a/utils/sdfUtils.h b/utils/include/fileformatutils/sdfUtils.h similarity index 100% rename from utils/sdfUtils.h rename to utils/include/fileformatutils/sdfUtils.h diff --git a/utils/test.h b/utils/include/fileformatutils/test.h similarity index 97% rename from utils/test.h rename to utils/include/fileformatutils/test.h index 5df1dba7..d799adfa 100644 --- a/utils/test.h +++ b/utils/include/fileformatutils/test.h @@ -84,6 +84,7 @@ PXR_NAMESPACE_CLOSE_SCOPE #define ASSERT_ANIMATION(...) assertAnimation(__VA_ARGS__) #define ASSERT_CAMERA(...) assertCamera(__VA_ARGS__) #define ASSERT_LIGHT(...) assertLight(__VA_ARGS__) +#define ASSERT_DISPLAY_NAME(...) assertDisplayName(__VA_ARGS__) #ifdef DO_RENDER #define ASSERT_RENDER(...) assertRender(__VA_ARGS__) #else @@ -202,6 +203,10 @@ USDFFUTILS_API void assertCamera(PXR_NS::UsdStageRefPtr stage, const std::string& path, const CameraData& data); USDFFUTILS_API void assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightData& data); +USDFFUTILS_API void +assertDisplayName(PXR_NS::UsdStageRefPtr stage, + const std::string& primPath, + const std::string& displayName); USDFFUTILS_API void assertRender(const std::string& filename, const std::string& imageFilename); diff --git a/utils/transforms.h b/utils/include/fileformatutils/transforms.h similarity index 100% rename from utils/transforms.h rename to utils/include/fileformatutils/transforms.h diff --git a/utils/usdData.h b/utils/include/fileformatutils/usdData.h similarity index 94% rename from utils/usdData.h rename to utils/include/fileformatutils/usdData.h index 1396f809..71d05adc 100644 --- a/utils/usdData.h +++ b/utils/include/fileformatutils/usdData.h @@ -67,6 +67,8 @@ struct USDFFUTILS_API NodeAnimation struct USDFFUTILS_API Node { std::string name; + std::string displayName; + bool hasTransform = false; PXR_NS::GfMatrix4d transform = PXR_NS::GfMatrix4d(1); PXR_NS::GfMatrix4d worldTransform = PXR_NS::GfMatrix4d(1); @@ -95,6 +97,8 @@ struct USDFFUTILS_API Node struct USDFFUTILS_API Camera { std::string name; + std::string displayName; + PXR_NS::GfCamera::Projection projection; float f; float horizontalAperture; @@ -133,6 +137,8 @@ struct USDFFUTILS_API Primvar struct USDFFUTILS_API Mesh { std::string name; + std::string displayName; + PXR_NS::VtIntArray faces; PXR_NS::VtIntArray indices; PXR_NS::VtVec3fArray points; @@ -168,6 +174,8 @@ struct USDFFUTILS_API Mesh struct USDFFUTILS_API NurbData { std::string name; + std::string displayName; + int knotType; int surfaceForm; int uOrder; @@ -226,6 +234,8 @@ struct USDFFUTILS_API SkeletonAnimation struct USDFFUTILS_API Skeleton { std::string name; + std::string displayName; + int parent = -1; std::vector jointParents; std::vector meshSkinningTargets; @@ -248,6 +258,8 @@ struct USDFFUTILS_API Skeleton struct USDFFUTILS_API AnimationTrack { std::string name; + std::string displayName; + float minTime = std::numeric_limits::max(); float maxTime = 0; float offsetToJoinedTimeline = 0; @@ -270,6 +282,8 @@ enum USDFFUTILS_API ImageFormat struct USDFFUTILS_API ImageAsset { std::string name; + // Images are referenced differently than nodes, so they do not have display names + std::string uri; ImageFormat format = ImageFormatUnknown; std::vector image; @@ -291,6 +305,8 @@ enum USDFFUTILS_API LightType struct USDFFUTILS_API Light { std::string name; + std::string displayName; + LightType type; PXR_NS::GfVec3f color; PXR_NS::GfVec2f length; // Rect light dimensions. @@ -333,6 +349,7 @@ struct USDFFUTILS_API Input struct USDFFUTILS_API Material { std::string name; + std::string displayName; // Import of transmission from GLTF can activate the clearcoat lobe to model tinting of // transmission, which ASM doesn't do automatically. If this was activated on import, we do @@ -474,6 +491,24 @@ printSkeleton(const std::string& header, const Skeleton& skeleton, const std::string& debugTag); +/** + * Given a USD object that has both a name and a display name parameter, get the name that most + * closely matches the object's original name before being imported. If the original name had to + * be sanitized to be a valid USD identifier, the original name is stored in the displayName field + * and will be returned. If both are available, the display name is returned. + * + * @tparam T A USD object that must have properties named "name" and "displayName" + * @param usdObj The object whose name is queried + * + * @return The display name if one is present, otherwise the object's name + */ +template +const std::string& +getNodeName(const T& usdObj) +{ + return usdObj.displayName.empty() ? usdObj.name : usdObj.displayName; +} + // Makes sure that siblings in the hierarchy have unique names and that the names are valid USD prim // names USDFFUTILS_API void diff --git a/utils/assetresolver.cpp b/utils/src/assetresolver.cpp similarity index 98% rename from utils/assetresolver.cpp rename to utils/src/assetresolver.cpp index d225e180..1a4189b0 100644 --- a/utils/assetresolver.cpp +++ b/utils/src/assetresolver.cpp @@ -9,8 +9,8 @@ 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 "assetresolver.h" -#include "debugCodes.h" +#include +#include #include #include diff --git a/utils/common.cpp b/utils/src/common.cpp similarity index 59% rename from utils/common.cpp rename to utils/src/common.cpp index e0618743..1fb703e6 100644 --- a/utils/common.cpp +++ b/utils/src/common.cpp @@ -9,16 +9,22 @@ 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 "common.h" -#include "debugCodes.h" +#include +#include #include #include #include #include #include +#include #include +#include #include #include +#include +#include +#include +#include PXR_NAMESPACE_OPEN_SCOPE TF_DEFINE_PUBLIC_TOKENS(AdobeTokens, ADOBE_TOKENS); @@ -90,6 +96,34 @@ argComposeFloat(const PXR_NS::PcpDynamicFileFormatContext& context, } } +void +argComposeFloatArray(const PcpDynamicFileFormatContext& context, + SdfFileFormat::FileFormatArguments* args, + const TfToken& token, + const std::string& debugTag) +{ + VtValue value; + if (context.ComposeValue(token, &value) && value.IsHolding()) { + const auto& floatArray = value.UncheckedGet(); + std::ostringstream oss; + oss << "["; + for (size_t i = 0; i < floatArray.size(); ++i) { + if (i > 0) + oss << ","; + oss << floatArray[i]; + } + oss << "]"; + + std::string val = oss.str(); + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: ComposeFileFormatArg: %s = %s\n", + debugTag.c_str(), + token.GetText(), + val.c_str()); + (*args)[token.GetString()] = val; + } +} + void argReadString(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, @@ -149,6 +183,31 @@ argReadFloat(const PXR_NS::SdfFileFormat::FileFormatArguments& args, } } +void +argReadFloatArray(const SdfFileFormat::FileFormatArguments& args, + const std::string& arg, + VtFloatArray& target, + const std::string& debugTag) +{ + if (const auto& it = args.find(arg); it != args.end()) { + std::string value = it->second; + std::regex floatRegex(R"([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)"); + std::sregex_iterator begin(value.begin(), value.end(), floatRegex); + std::sregex_iterator end; + + target.clear(); + for (std::sregex_iterator i = begin; i != end; ++i) { + target.push_back(std::stof((*i).str())); + } + + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Read float array arg: \"%s\" = \"%s\"\n", + debugTag.c_str(), + arg.c_str(), + it->second.c_str()); + } +} + std::string getFileExtension(const std::string& filePath, const std::string& defaultValue = "") { // Find the last dot position @@ -168,4 +227,59 @@ getCurrentDate() { return ss.str(); } +// Splits the input string into a vector of substrings based on the specified delimiter. +std::vector +split(const std::string& str, char delimiter) +{ + std::vector pieces; + size_t start = 0; + size_t end = str.find(delimiter); + while (end != std::string::npos) { + pieces.push_back(str.substr(start, end - start)); + start = end + 1; + end = str.find(delimiter, start); + } + pieces.push_back(str.substr(start)); + + return pieces; +} + +// Creates a directory at the specified path, including any necessary parent directories. +// Returns true if the directory was created successfully or already exists, false otherwise. +bool +createDirectory(const std::filesystem::path& directoryPath) +{ + try { + std::filesystem::create_directories(directoryPath); + } catch (const std::filesystem::filesystem_error& e) { + TF_CODING_ERROR("Error creating directory:\n \"{}\"", e.what()); + return false; + } + + 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. +std::string +getLayerFilePath(const std::string& layerIdentifier) +{ + std::string layerPath; + SdfLayer::FileFormatArguments arguments; + SdfLayer::SplitIdentifier(layerIdentifier, &layerPath, &arguments); + const auto [outer, inner] = ArSplitPackageRelativePathInner(layerPath); + return inner.empty() ? outer : inner; +} + } \ No newline at end of file diff --git a/utils/dictencoder.cpp b/utils/src/dictencoder.cpp similarity index 98% rename from utils/dictencoder.cpp rename to utils/src/dictencoder.cpp index b9bb9858..4bc62868 100644 --- a/utils/dictencoder.cpp +++ b/utils/src/dictencoder.cpp @@ -9,7 +9,7 @@ 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 "dictencoder.h" +#include #include #include diff --git a/utils/geometry.cpp b/utils/src/geometry.cpp similarity index 99% rename from utils/geometry.cpp rename to utils/src/geometry.cpp index 8ec17163..5a37beeb 100644 --- a/utils/geometry.cpp +++ b/utils/src/geometry.cpp @@ -9,8 +9,8 @@ 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 "geometry.h" -#include "debugCodes.h" +#include +#include using namespace PXR_NS; diff --git a/utils/src/gsplatHelper.cpp b/utils/src/gsplatHelper.cpp new file mode 100644 index 00000000..a69385ff --- /dev/null +++ b/utils/src/gsplatHelper.cpp @@ -0,0 +1,166 @@ +/* +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 + +using namespace PXR_NS; + +namespace adobe::usd { +size_t +numSHDegreesFromGsplat(size_t numGsplatCoefficients) +{ + // Gsplat has R, G, B channels for each SH band, so we need to divide by 3. + const size_t numNonZeroSHBands = numGsplatCoefficients / 3; + return static_cast( + std::floor(std::sqrt(static_cast(numNonZeroSHBands) + 1.0f)) - 1.0f); +} + +size_t +numNonZeroSHBandsFromDegree(size_t numDegrees) +{ + // This is the number of bands without the zeroth order. + return numDegrees * (numDegrees + 2); +} + +void +rotatePointRotations(const Primvar& pointRotations, + const GfQuatf& rotation, + size_t numPoints, + VtQuatfArray& outPointRotations) +{ + size_t rotationsOffset = outPointRotations.size(); + outPointRotations.resize(rotationsOffset + numPoints, GfQuatf::GetIdentity()); + + // We need to find the minimum number of points that have rotations. This acts as a safe guard + // in case some input data is missing. + const size_t numPointsWithRotations = std::min(numPoints, pointRotations.values.size()); + + std::transform(pointRotations.values.begin(), + pointRotations.values.begin() + numPointsWithRotations, + outPointRotations.begin() + rotationsOffset, + [&rotation](const GfQuatf& quat) { return rotation * quat; }); +} + +void +rotatePointSphericalHarmonics(const std::vector>& inSH, + const PXR_NS::GfQuatf& rotation, + size_t numPoints, + std::vector& outSH) +{ + // We need to find the minimum number of points that have SH coefficients in all channels. + // This acts as a safe guard in case some input data is missing. + size_t numPointsCompleteSH = numPoints; + for (size_t shIndex = 0; shIndex < inSH.size(); shIndex++) { + numPointsCompleteSH = std::min(numPointsCompleteSH, inSH[shIndex].values.size()); + } + + if (1.0f - std::abs(rotation.GetReal()) > 1e-6f) { + // The rotation is not an identity, so we need to rotate the SH coefficients. + std::vector shDataOffset(outSH.size()); + + // If the input SH has less channels than the output, we need to fill these output SH + // with zeros. + for (size_t shIndex = 0; shIndex < outSH.size(); shIndex++) { + size_t shCoeffOffset = outSH[shIndex].size(); + shDataOffset[shIndex] = shCoeffOffset; + outSH[shIndex].resize(shCoeffOffset + numPoints, 0.0f); + } + + const size_t pointSHDegrees = numSHDegreesFromGsplat(std::min(inSH.size(), outSH.size())); + if (pointSHDegrees > 0) { + const size_t numSHCoeffsPerChannel = numNonZeroSHBandsFromDegree(pointSHDegrees); + Eigen::Quaterniond quatRot(rotation.GetReal(), + rotation.GetImaginary()[0], + rotation.GetImaginary()[1], + rotation.GetImaginary()[2]); + + // Need to renormalize due to floating precision differences. + quatRot.normalize(); + const auto shRot = sh::Rotation::Create(static_cast(pointSHDegrees), quatRot); + + std::vector shCoeffsBuffer(numSHCoeffsPerChannel + 1); + // We do not need to rotate the zeroth-order coefficient, while the + // SphericalHarmonics library needs coefficients from the zeroth to the highest + // order. Thus we just put a zero there. + shCoeffsBuffer[0] = 0.0f; + + for (size_t i = 0; i < numPointsCompleteSH; ++i) { + for (size_t iChannel = 0; iChannel < 3; ++iChannel) { + for (size_t iCoeff = 0; iCoeff < numSHCoeffsPerChannel; ++iCoeff) { + shCoeffsBuffer[iCoeff + 1] = + inSH[iCoeff + iChannel * numSHCoeffsPerChannel].values[i]; + } + shRot->Apply(shCoeffsBuffer, &shCoeffsBuffer); + for (size_t iCoeff = 0; iCoeff < numSHCoeffsPerChannel; ++iCoeff) { + const size_t coeffIndex = iCoeff + iChannel * numSHCoeffsPerChannel; + const size_t shOffset = shDataOffset[coeffIndex]; + outSH[coeffIndex][shOffset + i] = shCoeffsBuffer[iCoeff + 1]; + } + } + } + } + } else { + // The rotation is an identity, so we can skip the rotation and just copy the SH + // coefficients. + for (size_t shIndex = 0; shIndex < outSH.size(); shIndex++) { + size_t shCoeffOffset = outSH[shIndex].size(); + outSH[shIndex].resize(shCoeffOffset + numPoints, 0.0f); + if (shIndex < inSH.size()) { + memcpy(outSH[shIndex].data() + shCoeffOffset, + inSH[shIndex].values.data(), + numPointsCompleteSH * sizeof(inSH[shIndex].values[0])); + } + } + } +} + +void +scalePointWidths(const VtFloatArray& inWidths, + const std::vector>& inExtraWidths, + size_t numPoints, + float widthScale, + VtFloatArray& outWidths, + VtFloatArray& outWidths1, + VtFloatArray& outWidths2) +{ + size_t widthsOffset = outWidths.size(); + size_t widths1Offset = outWidths1.size(); + size_t widths2Offset = outWidths2.size(); + + // We need to use the number of points as the new size (and fill with default values) + // in case there's a mix of regular point cloud and Gsplats. + outWidths.resize(widthsOffset + numPoints, 0.0f); + outWidths1.resize(widths1Offset + numPoints, 0.0f); + outWidths2.resize(widths2Offset + numPoints, 0.0f); + + const size_t numPointWidths = std::min(numPoints, inWidths.size()); + std::transform(inWidths.begin(), + inWidths.begin() + numPointWidths, + outWidths.begin() + widthsOffset, + [widthScale](float width) { return width * widthScale; }); + if (inExtraWidths.size() >= 2) { + const size_t numPointWidths1 = std::min(numPoints, inExtraWidths[0].values.size()); + const size_t numPointWidths2 = std::min(numPoints, inExtraWidths[1].values.size()); + std::transform(inExtraWidths[0].values.begin(), + inExtraWidths[0].values.begin() + numPointWidths1, + outWidths1.begin() + widths1Offset, + [widthScale](float width) { return width * widthScale; }); + std::transform(inExtraWidths[1].values.begin(), + inExtraWidths[1].values.begin() + numPointWidths2, + outWidths2.begin() + widths2Offset, + [widthScale](float width) { return width * widthScale; }); + } +} +} diff --git a/utils/images.cpp b/utils/src/images.cpp similarity index 63% rename from utils/images.cpp rename to utils/src/images.cpp index 40259f03..5f1e23fc 100644 --- a/utils/images.cpp +++ b/utils/src/images.cpp @@ -9,14 +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 "images.h" -#include "common.h" -#include "debugCodes.h" #include #include #include +#include +#include +#include #include #include +#include +#include using namespace PXR_NS; @@ -65,6 +67,9 @@ Image::read(const ImageAsset& imageAsset, int forceChannels) std::string filename = "dummy." + extension; std::unique_ptr input = OIIO::ImageInput::open(filename, &config); if (!input) { + TF_WARN("Image::read() OpenImageIO failed to open ImageInput with URI=%s: %s\n", + imageAsset.uri.c_str(), + OIIO::geterror().c_str()); return false; } const OIIO::ImageSpec& spec = input->spec(); @@ -455,4 +460,212 @@ linearToSRGB(float s) return 1.055f * std::pow(s, (1.0f / 2.4f)) - 0.055f; } +bool +isImageFileSupported(const std::string& resolvedAssetPath) +{ + // Runtime cache for the supported file types. We don't expect the available plugins to change + // at run-time. The query to the HioImage::IsSupportedImageFile is quite expensive, so we cache + // it. + static std::unordered_map supportedExtensions; + // We want this to be multi-threading save, so we protect the cache + static std::mutex supportedExtensionsMutex; + + std::lock_guard lock(supportedExtensionsMutex); + + std::string ext = getSanitizedExtension(resolvedAssetPath); + auto [it, inserted] = supportedExtensions.emplace(ext, false); + if (inserted) { + it->second = HioImage::IsSupportedImageFile("filename." + ext); + } + return it->second; +} + +bool +isUriSbsarImage(const std::string& uri) +{ + size_t pos = uri.find_first_of('?'); + return uri.length() > 1 && pos != std::string::npos; +} + +// Takes a SBSAR texture parameterization like +// usage=ambientOcclusion#preset=Torn#packageHash=b427747e86441362#params={"$outputsize":\[4,4\]} +// and extracts the value for 'usage' +// --> ambientOcclusion +// Returns an empty string if the parsing fails +std::string +getSbsarUsageFromParameters(const std::string& parametersStr) +{ + auto params = split(parametersStr, '#'); + for (const auto& param : params) { + auto keyValue = split(param, '='); + if (keyValue.size() != 2) { + continue; + } + if (keyValue[0] == "usage") { + return keyValue[1]; + } + } + + return {}; +} + +std::string +getSbsarImageExtension(const std::string& resolvedAssetPath) +{ + if (!isImageFileSupported(resolvedAssetPath)) { + TF_WARN("Asset %s is not a supported image type", resolvedAssetPath.c_str()); + return {}; + } + + HioImageSharedPtr inputImage = HioImage::OpenForReading(resolvedAssetPath); + if (!inputImage) { + TF_WARN("Couldn't open image %s for reading", resolvedAssetPath.c_str()); + return {}; + } + + HioFormat hioFormat = inputImage->GetFormat(); + + // Floating point images are stored as exr files + if (hioFormat == HioFormatFloat16 || hioFormat == HioFormatFloat16Vec2 || + hioFormat == HioFormatFloat16Vec3 || hioFormat == HioFormatFloat16Vec4 || + hioFormat == HioFormatFloat32 || hioFormat == HioFormatFloat32Vec2 || + hioFormat == HioFormatFloat32Vec3 || hioFormat == HioFormatFloat32Vec4 || + hioFormat == HioFormatDouble64 || hioFormat == HioFormatDouble64Vec2 || + hioFormat == HioFormatDouble64Vec3 || hioFormat == HioFormatDouble64Vec4) { + return std::string("exr"); + } + + // All other images are most likely textures and we default to png + return std::string("png"); +} + +// This function extracts a usable file path from an assetPath, which might be a bit funky: +// Examples: +// Easy: some/path/to/texture.png -> some/path/to/texture.png +// With?: some/path/to/texture.png?param1=val1#param2=val2 -> some/path/to/texture.png +// SBSAR: +// graphs/CardBoard/images?usage=ambientOcclusion#preset=Torn#packageHash=b427747e86441362#params={"$outputsize":\[4,4\]}.png +// With some effort -> CardBoard_ambientOcclusion.png +std::string +extractFilePathFromAssetPath(const std::string& assetPath) +{ + // If there are no parameters after the file name we just take the whole path + auto q = assetPath.find_first_of('?'); + if (q == std::string::npos) { + return assetPath; + } + + // If the path contains a '?', take the first part as the subpath + std::string subpath = assetPath.substr(0, q); + // If this subpath has a file extension then we just take that path + if (std::filesystem::path(subpath).has_extension()) { + return subpath; + } + + // If the subpath does not have a file extension, check if the full asset path has an extension + // Note, the extension returned here does not have a '.', so we get 'png' + std::string ext = ArGetResolver().GetExtension(assetPath); + if (ext.empty()) { + TF_WARN("Could not find file extension for asset path %s", assetPath.c_str()); + } + + // Extract the parameters after the '?' and before the extension + // Note, we want to skip the '.' of the extension, which is where the + 1 for the extension + // length is coming from. + std::string parameters = assetPath.substr(q + 1, assetPath.size() - (q + 1) - (ext.size() + 1)); + + // Check if this is a SBSAR texture parameterization + // If so, we can do a better job than naming the file `images.png` + std::string usage = getSbsarUsageFromParameters(parameters); + if (!usage.empty()) { + // graphs/CardBoard/images -> CardBoard + std::string graphName = std::filesystem::path(subpath).parent_path().filename().u8string(); + subpath = graphName + "_" + usage; + } + + // Append the extension to create a complete path + subpath = subpath + "." + ext; + + return subpath; +} + +bool +transcodeImageAssetToMemory(const std::string& resolvedAssetPath, + const std::string& filename, + std::vector& outputPixelData) +{ + if (!isImageFileSupported(resolvedAssetPath)) { + TF_WARN("Asset %s is not a supported image type", resolvedAssetPath.c_str()); + return false; + } + + // Define the temporary directory path and create it if it doesn't exist + std::filesystem::path tempDir = std::filesystem::temp_directory_path() / "transcoded_images"; + if (!std::filesystem::exists(tempDir)) { + if (!createDirectory(tempDir)) { + TF_WARN("Failed to create temporary directory: %s", tempDir.string().c_str()); + return false; + } + } + + std::filesystem::path filePath = tempDir / filename; + if (!isImageFileSupported(filePath.string())) { + TF_WARN("Output %s is not a supported image type", filePath.c_str()); + return false; + } + + HioImageSharedPtr inputImage = HioImage::OpenForReading(resolvedAssetPath); + if (!inputImage) { + TF_WARN("Couldn't open image %s for reading", resolvedAssetPath.c_str()); + return false; + } + + HioImage::StorageSpec storage; + storage.width = inputImage->GetWidth(); + storage.height = inputImage->GetHeight(); + storage.format = inputImage->GetFormat(); + int bytesPerPixel = inputImage->GetBytesPerPixel(); + std::vector pixelData(storage.width * storage.height * bytesPerPixel); + storage.data = pixelData.data(); + + if (!inputImage->Read(storage)) { + TF_WARN("Reading of image %s failed", resolvedAssetPath.c_str()); + return false; + } + + HioImageSharedPtr outputImage = HioImage::OpenForWriting(filePath.string()); + if (!outputImage) { + TF_WARN("Couldn't open image %s for writing", filePath.string().c_str()); + return false; + } + + // Unfortunately, with the HioImage API and there is no way to write to memory + // and have it encoded to PNG or another format. We could refactor this in the + // future to use OIIO. + if (!outputImage->Write(storage)) { + TF_WARN("Writing of image %s failed", filePath.string().c_str()); + return false; + } + + // Read the output image file as binary data into outputPixelData + std::ifstream file(filePath, std::ios::binary | std::ios::ate); + if (!file) { + TF_WARN("Couldn't open outputImage %s for reading", filePath.string().c_str()); + return false; + } + + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + outputPixelData.resize(size); + if (!file.read(reinterpret_cast(outputPixelData.data()), size)) { + TF_WARN("Reading of transcoded image %s failed", filePath.string().c_str()); + return false; + } + + TF_STATUS("Transcoded image: %s -> %s and populated memory buffer", + resolvedAssetPath.c_str(), + filePath.string().c_str()); + return true; +} + } diff --git a/utils/layerRead.cpp b/utils/src/layerRead.cpp similarity index 92% rename from utils/layerRead.cpp rename to utils/src/layerRead.cpp index 06709a3e..aa25ddc1 100644 --- a/utils/layerRead.cpp +++ b/utils/src/layerRead.cpp @@ -9,20 +9,20 @@ 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 "layerRead.h" -#include "common.h" -#include "debugCodes.h" -#include "geometry.h" -#include "layerWriteShared.h" -#include "usdData.h" #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include #include -#include -#include #include #include #include @@ -120,6 +120,7 @@ getParentOrNewTransformParent(ReadLayerContext& ctx, if (transform != GfMatrix4d(0.0f) && transform != GfMatrix4d(1.0f)) { auto [nodeIndex, node] = ctx.usd->addNode(parent); node.name = newParentName; + node.displayName = prim.GetDisplayName(); node.transform = transform; node.hasTransform = true; GfMatrix4d parentWorldTransform = @@ -158,6 +159,7 @@ readScope(ReadLayerContext& ctx, const UsdPrim& prim, int parent) prim.GetPath().GetText()); auto [nodeIndex, node] = ctx.usd->addNode(parent); node.name = prim.GetName().GetString(); + node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); readTransform(ctx, prim, node, parent); UsdPrimSiblingRange children = @@ -176,23 +178,47 @@ readUnknown(ReadLayerContext& ctx, const UsdPrim& prim, int parent) ctx.debugTag.c_str(), prim.GetTypeName().GetText(), prim.GetName().GetText()); - auto [nodeIndex, node] = ctx.usd->addNode(parent); - node.name = prim.GetName().GetString(); - node.path = prim.GetPath().GetString(); - readTransform(ctx, prim, node, parent); + UsdPrimSiblingRange children = prim.GetFilteredChildren(UsdTraverseInstanceProxies(UsdPrimAllPrimsPredicate)); + + bool skipAddingNode = false; + if (prim.GetPrimTypeInfo() == UsdPrimTypeInfo::GetEmptyPrimType()) { + bool allChildrenAreMaterials = true; + for (const UsdPrim& p : children) { + if (!p.IsA()) { + allChildrenAreMaterials = false; + break; + } + } + + // If all children are materials, skip adding this node. + // This node does not need to be added to the node hierarchy, as materials don't live within + // the node hierarchy. + skipAddingNode = allChildrenAreMaterials; + } + + int parentIndexForChildren = parent; + if (!skipAddingNode) { + auto [nodeIndex, node] = ctx.usd->addNode(parent); + parentIndexForChildren = nodeIndex; + + node.name = prim.GetName().GetString(); + node.displayName = prim.GetDisplayName(); + node.path = prim.GetPath().GetString(); + readTransform(ctx, prim, node, parent); + } for (const UsdPrim& p : children) { - readPrim(ctx, p, nodeIndex); + readPrim(ctx, p, parentIndexForChildren); } return true; } -bool -readNode(ReadLayerContext& ctx, const UsdPrim& prim, int parent) +void +readXformInternal(ReadLayerContext& ctx, Node& node, const UsdPrim& prim, int parent) { - auto [nodeIndex, node] = ctx.usd->addNode(parent); node.name = prim.GetName().GetString(); + node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); readTransform(ctx, prim, node, parent); @@ -292,6 +318,34 @@ readNode(ReadLayerContext& ctx, const UsdPrim& prim, int parent) } } } +} + +bool +readXform(ReadLayerContext& ctx, const UsdPrim& prim, int parent) +{ + int nodeIndex = -1; + // Collapse this node if it is the default prim, and has an indentity transform. + // Often the default prim is present as a parent for all the root nodes of the scene. + // To preserve those root nodes during export, it is helpful to avoid creating a UsdData node + // for the default prim. + if (parent == -1 && ctx.stage->GetDefaultPrim() == prim) { + Node tempNode; + readXformInternal(ctx, tempNode, prim, parent); + + if (tempNode.hasTransform || !tempNode.animations.empty()) { + // Only add the node if it has a transform + std::pair pair = ctx.usd->addNode(parent); + + nodeIndex = pair.first; + pair.second = std::move(tempNode); + } + } else { + std::pair pair = ctx.usd->addNode(parent); + + nodeIndex = pair.first; + readXformInternal(ctx, pair.second, prim, parent); + } + UsdPrimSiblingRange children = prim.GetFilteredChildren(UsdTraverseInstanceProxies(UsdPrimAllPrimsPredicate)); for (const UsdPrim& p : children) { @@ -379,6 +433,7 @@ readMeshOrPointsData(ReadLayerContext& ctx, Mesh& mesh, int meshIndex, const Usd ctx.subsetMaterialBindings.push_back({}); mesh.name = prim.GetName(); + mesh.displayName = prim.GetDisplayName(); UsdGeomPrimvarsAPI primvarsAPI(prim); if (prim.IsA()) { @@ -648,6 +703,7 @@ readSkelRoot(ReadLayerContext& ctx, const UsdPrim& prim, int parent) prim.GetPath().GetText()); auto [nodeIndex, node] = ctx.usd->addNode(parent); node.name = prim.GetName().GetString(); + node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); UsdSkelCache skelCache; // to hoist later to see performance improvement @@ -683,6 +739,7 @@ readSkelRoot(ReadLayerContext& ctx, const UsdPrim& prim, int parent) const UsdPrim& skeletonPrim = skelSkeleton.GetPrim(); skeleton.name = skeletonPrim.GetName().GetString(); + skeleton.displayName = skeletonPrim.GetDisplayName(); printSkeleton("layer::read", skeletonPrim.GetPath(), skeleton, ctx.debugTag); // Process skinning targets @@ -798,6 +855,7 @@ readPointInstancer(ReadLayerContext& ctx, const UsdPrim& prim, int parent) prim.GetName().GetText()); auto [nodeIndex, node] = ctx.usd->addNode(parent); node.name = prim.GetName().GetString(); + node.displayName = prim.GetDisplayName(); node.path = prim.GetPath().GetString(); readTransform(ctx, prim, node, parent); @@ -922,56 +980,87 @@ readVolume(ReadLayerContext& ctx, const UsdPrim& prim, int parent) return true; } +// 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, + 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)); +} + bool readImage(ReadLayerContext& ctx, const SdfAssetPath& path, int& index) { - const std::string& uri = path.GetAssetPath(); - std::string name = TfStringGetBeforeSuffix(TfGetBaseName(uri)); - std::string extension = TfGetExtension(uri); - // If asset path originates from a custom resolver, fix name and extension: - size_t pos = name.find_first_of('['); - if (name.length() > 1 && pos != std::string::npos) { - name = name.substr(pos + 1, name.size()); - } - if (extension.length() > 1 && extension.back() == ']') { - extension = extension.substr(0, extension.size() - 1); - } - std::string absPath = path.GetResolvedPath().empty() - ? ArGetResolver().Resolve(path.GetAssetPath()) - : path.GetResolvedPath(); - if (const auto& it = ctx.images.find(uri); it != ctx.images.end()) { - index = it->second; - TF_DEBUG_MSG( - FILE_FORMAT_UTIL, "%s: Image (cached): %s\n", ctx.debugTag.c_str(), uri.c_str()); - } else { + std::string absPath, extension, name, uri; - // Deduplicate name - if (const auto& itName = ctx.imageNames.find(name); itName != ctx.imageNames.end()) { - itName->second++; - name = name + "_" + std::to_string(itName->second); - TF_DEBUG_MSG(FILE_FORMAT_UTIL, - "%s: Deduplicated image name: %s\n", - ctx.debugTag.c_str(), - name.c_str()); - } else { - ctx.imageNames[name] = 1; + // 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); } + } + + if (const auto& it = ctx.images.find(uri); it != ctx.images.end()) { + index = it->second; + TF_WARN("%s: Image (cached): %s\n", ctx.debugTag.c_str(), uri.c_str()); + return true; + } + // deduplicate 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()); + } 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); + } else { ArResolver& ar = ArGetResolver(); - std::shared_ptr asset = ar.OpenAsset(ArResolvedPath(absPath)); - if (!asset) + auto resolvedPath = ArResolvedPath(absPath); + auto asset = ar.OpenAsset(resolvedPath); + if (!asset) { + TF_WARN("%s: Unable to open asset: %s\n", + ctx.debugTag.c_str(), + resolvedPath.GetPathString().c_str()); return false; - int length = asset->GetSize(); - auto [imageIndex, image] = ctx.usd->addImage(); - image.name = name; + } image.uri = name + "." + extension; - image.format = getFormat(extension); - image.image.resize(length); - memcpy(image.image.data(), asset->GetBuffer().get(), length); - ctx.images[uri] = imageIndex; - index = imageIndex; - TF_DEBUG_MSG(FILE_FORMAT_UTIL, "%s: Image (new): %s\n", ctx.debugTag.c_str(), uri.c_str()); + image.image.resize(asset->GetSize()); + memcpy(image.image.data(), asset->GetBuffer().get(), asset->GetSize()); } + + image.name = name; + image.format = getFormat(extension); + ctx.images[uri] = imageIndex; + index = imageIndex; + TF_WARN("%s: Image (new): index: %d uri: %s\n", ctx.debugTag.c_str(), imageIndex, uri.c_str()); return true; } @@ -1317,6 +1406,7 @@ readMaterial(ReadLayerContext& ctx, const UsdPrim& prim, int parent) auto [materialIndex, material] = ctx.usd->addMaterial(); ctx.materials[prim.GetPath().GetString()] = materialIndex; material.name = prim.GetPath().GetName(); + material.displayName = prim.GetDisplayName(); UsdShadeMaterial usdMaterial(prim); // We give preference to the Adobe ASM surface, if present, and fallback to the standard @@ -1345,6 +1435,7 @@ readCamera(ReadLayerContext& ctx, const UsdPrim& prim, int parent) const auto& usdCamera = UsdGeomCamera(prim); camera.name = prim.GetName(); + camera.displayName = prim.GetDisplayName(); GfCamera gfCamera = usdCamera.GetCamera(0); camera.projection = gfCamera.GetProjection(); camera.f = gfCamera.GetFocalLength(); // f in mm @@ -1409,6 +1500,7 @@ readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) parentNode.light = lightIndex; light.name = prim.GetName(); + light.displayName = prim.GetDisplayName(); // Light type specific attributes @@ -1544,7 +1636,7 @@ readPrim(ReadLayerContext& ctx, const UsdPrim& prim, int parent) if (prim.IsA()) f = readScope; else if (prim.IsA()) - f = readNode; + f = readXform; else if (prim.IsA() || prim.IsA()) f = readMeshOrPoints; else if (prim.IsA()) @@ -1643,6 +1735,7 @@ readAnimationTracks(UsdData& usd) const std::string minTimeKey("minTime"); const std::string maxTimeKey("maxTime"); const std::string offsetKey("offset"); + const std::string displayNameKey("displayName"); for (auto i : tracksDictionary) { if (!i.second.IsHolding()) { @@ -1660,6 +1753,9 @@ readAnimationTracks(UsdData& usd) AnimationTrack track; track.name = name; + if (VtDictionaryIsHolding(dict, displayNameKey)) { + track.displayName = VtDictionaryGet(dict, displayNameKey); + } track.minTime = VtDictionaryGet(dict, minTimeKey); track.maxTime = VtDictionaryGet(dict, maxTimeKey); track.offsetToJoinedTimeline = VtDictionaryGet(dict, offsetKey); @@ -1844,5 +1940,4 @@ readLayer(const ReadLayerOptions& options, return true; } - } diff --git a/utils/layerWriteMaterial.cpp b/utils/src/layerWriteMaterial.cpp similarity index 98% rename from utils/layerWriteMaterial.cpp rename to utils/src/layerWriteMaterial.cpp index b2054055..3d4055a8 100644 --- a/utils/layerWriteMaterial.cpp +++ b/utils/src/layerWriteMaterial.cpp @@ -9,13 +9,13 @@ 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 "layerWriteMaterial.h" +#include -#include "common.h" -#include "debugCodes.h" -#include "layerRead.h" -#include "sdfMaterialUtils.h" -#include "sdfUtils.h" +#include +#include +#include +#include +#include #include diff --git a/utils/layerWriteMaterialX.cpp b/utils/src/layerWriteMaterialX.cpp similarity index 99% rename from utils/layerWriteMaterialX.cpp rename to utils/src/layerWriteMaterialX.cpp index 5a8b15dc..05b824d7 100644 --- a/utils/layerWriteMaterialX.cpp +++ b/utils/src/layerWriteMaterialX.cpp @@ -9,12 +9,12 @@ 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 "layerWriteMaterialX.h" +#include -#include "common.h" -#include "debugCodes.h" -#include "sdfMaterialUtils.h" -#include "sdfUtils.h" +#include +#include +#include +#include #include diff --git a/utils/layerWriteSdfData.cpp b/utils/src/layerWriteSdfData.cpp similarity index 87% rename from utils/layerWriteSdfData.cpp rename to utils/src/layerWriteSdfData.cpp index 5144aef3..e61315a8 100644 --- a/utils/layerWriteSdfData.cpp +++ b/utils/src/layerWriteSdfData.cpp @@ -9,17 +9,17 @@ 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 "layerWriteSdfData.h" - -#include "common.h" -#include "debugCodes.h" -#include "geometry.h" -#include "layerWriteMaterial.h" -#include "layerWriteMaterialX.h" -#include "sdfMaterialUtils.h" -#include "sdfUtils.h" -#include "usdData.h" -#include "version.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -133,6 +133,10 @@ _writeCamera(SdfAbstractData* sdfData, const SdfPath& parentPath, const Camera& SdfPath primPath = createPrimSpec(sdfData, parentPath, TfToken(camera.name), UsdGeomTokens->Camera); + if (!camera.displayName.empty()) { + setPrimMetadata(sdfData, primPath, SdfFieldKeys->DisplayName, VtValue(camera.displayName)); + } + auto createAttr = [&](const TfToken& name, const SdfValueTypeName& type, const auto& value) { SdfPath p = createAttributeSpec(sdfData, primPath, name, type); setAttributeDefaultValue(sdfData, p, value); @@ -260,6 +264,12 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li case LightType::Disk: { lightPath = createPrimSpec(sdfData, parentPath, TfToken(light.name), UsdLuxTokens->DiskLight); + + if (!light.displayName.empty()) { + setPrimMetadata( + sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); createAttr(UsdLuxTokens->inputsColor, SdfValueTypeNames->Color3f, light.color); createAttr(UsdLuxTokens->inputsRadius, SdfValueTypeNames->Float, light.radius); @@ -273,6 +283,12 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li case LightType::Rectangle: { lightPath = createPrimSpec(sdfData, parentPath, TfToken(light.name), UsdLuxTokens->RectLight); + + if (!light.displayName.empty()) { + setPrimMetadata( + sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); createAttr(UsdLuxTokens->inputsColor, SdfValueTypeNames->Color3f, light.color); createAttr(UsdLuxTokens->inputsWidth, SdfValueTypeNames->Float, light.length[0]); @@ -281,6 +297,12 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li case LightType::Sphere: { lightPath = createPrimSpec(sdfData, parentPath, TfToken(light.name), UsdLuxTokens->SphereLight); + + if (!light.displayName.empty()) { + setPrimMetadata( + sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); createAttr(UsdLuxTokens->inputsColor, SdfValueTypeNames->Color3f, light.color); createAttr(UsdLuxTokens->inputsRadius, SdfValueTypeNames->Float, light.radius); @@ -288,6 +310,12 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li case LightType::Environment: { lightPath = createPrimSpec(sdfData, parentPath, TfToken(light.name), UsdLuxTokens->DomeLight); + + if (!light.displayName.empty()) { + setPrimMetadata( + sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); SdfAssetPath texturePath(light.texture.uri); createAttr(UsdLuxTokens->inputsTextureFile, SdfValueTypeNames->Asset, texturePath); @@ -295,6 +323,12 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li case LightType::Sun: { lightPath = createPrimSpec(sdfData, parentPath, TfToken(light.name), UsdLuxTokens->DistantLight); + + if (!light.displayName.empty()) { + setPrimMetadata( + sdfData, lightPath, SdfFieldKeys->DisplayName, VtValue(light.displayName)); + } + createAttr(UsdLuxTokens->inputsIntensity, SdfValueTypeNames->Float, light.intensity); createAttr(UsdLuxTokens->inputsColor, SdfValueTypeNames->Color3f, light.color); // Some renderers can't handle an input angle of 0 @@ -541,6 +575,10 @@ _writePoints(SdfAbstractData* sdfData, const SdfPath& parentPath, const Mesh& me SdfPath primPath = createPrimSpec(sdfData, parentPath, TfToken(mesh.name), UsdGeomTokens->Points); + if (!mesh.displayName.empty()) { + setPrimMetadata(sdfData, primPath, SdfFieldKeys->DisplayName, VtValue(mesh.displayName)); + } + auto createAttr = [&](const TfToken& name, const SdfValueTypeName& type, const auto& value) { SdfPath p = createAttributeSpec(sdfData, primPath, name, type); setAttributeDefaultValue(sdfData, p, value); @@ -558,16 +596,32 @@ _writePoints(SdfAbstractData* sdfData, const SdfPath& parentPath, const Mesh& me return primPath; } +void +_bindMeshMaterial(SdfAbstractData* sdfData, + const SdfPath& primPath, + const SdfPathVector& materialMap, + const Mesh& mesh) +{ + if (mesh.material >= 0) { + _bindMaterial(sdfData, primPath, materialMap[mesh.material]); + } +} + SdfPath _writeMesh(SdfAbstractData* sdfData, const SdfPath& parentPath, const SdfPathVector& materialMap, const Mesh& mesh, - const SdfPath& skeletonPath) + const std::string& meshName, + const SdfPath& skeletonPath = SdfPath::EmptyPath()) { - SdfPath primPath = createPrimSpec(sdfData, parentPath, TfToken(mesh.name), UsdGeomTokens->Mesh); + SdfPath primPath = createPrimSpec(sdfData, parentPath, TfToken(meshName), UsdGeomTokens->Mesh); TF_DEBUG_MSG(FILE_FORMAT_UTIL, "write mesh: path=%s\n", primPath.GetString().c_str()); + if (!mesh.displayName.empty()) { + setPrimMetadata(sdfData, primPath, SdfFieldKeys->DisplayName, VtValue(mesh.displayName)); + } + if (!skeletonPath.IsEmpty()) { SdfPath bindingPath = createRelationshipSpec(sdfData, primPath, UsdSkelTokens->skelSkeleton); @@ -632,16 +686,11 @@ _writeMesh(SdfAbstractData* sdfData, mesh.geomBindTransform); } - // Material binding - if (mesh.material >= 0) { - _bindMaterial(sdfData, primPath, materialMap[mesh.material]); - } - // Subsets if (mesh.subsets.size()) { for (size_t i = 0; i < mesh.subsets.size(); i++) { const Subset& subset = mesh.subsets[i]; - TfToken subsetName = TfToken(mesh.name + "_sub" + std::to_string(i)); + TfToken subsetName = TfToken(meshName + "_sub" + std::to_string(i)); SdfPath subsetPath = _createGeomSubset(sdfData, primPath, subsetName, subset); if (subset.material >= 0) { @@ -653,46 +702,78 @@ _writeMesh(SdfAbstractData* sdfData, return primPath; } -bool -_writePointsOrInstancedMesh(WriteSdfContext& ctx, - const SdfPath& parentPath, - const Mesh& mesh, - int meshIdx, - int childIdx, - const SdfPath& skeletonPath) +void +_writePointsOrMesh(WriteSdfContext& ctx, + const SdfPath& parentPath, + const Mesh& mesh, + const SdfPath& skeletonPath = SdfPath::EmptyPath()) { if (mesh.asPoints) { _writePoints(ctx.sdfData, parentPath, mesh); - } else if (mesh.instanceable) { - // XXX Note, slightly awkward name generator to match old behavior. Once the old code has - // been removed, this can be cleaned up to any scheme that produces unique names - std::string scopeNameStr = - "GeomScope" + (childIdx == 0 ? "" : std::to_string(childIdx - 1)); - - SdfPath scopePath = - createPrimSpec(ctx.sdfData, parentPath, TfToken(scopeNameStr), UsdGeomTokens->Scope); - - const SdfPath& prototypePath = ctx.meshPrototypeMap[meshIdx]; - if (!prototypePath.IsEmpty()) { - addPrimReference(ctx.sdfData, scopePath, SdfReference("", prototypePath)); - setPrimMetadata(ctx.sdfData, scopePath, SdfFieldKeys->Instanceable, VtValue(true)); - TF_DEBUG_MSG(FILE_FORMAT_UTIL, - "layer::write gScope %s, Instance of %s\n", - scopePath.GetText(), - prototypePath.GetText()); - } else { - _writeMesh(ctx.sdfData, scopePath, ctx.materialMap, mesh, skeletonPath); - // Add this first instance to the list of prototypes - ctx.meshPrototypeMap[meshIdx] = scopePath; - TF_DEBUG_MSG(FILE_FORMAT_UTIL, - "layer::write gScope %s (add new prototype)\n", - scopePath.GetText()); - } } else { - _writeMesh(ctx.sdfData, parentPath, ctx.materialMap, mesh, skeletonPath); + SdfPath meshPath = + _writeMesh(ctx.sdfData, parentPath, ctx.materialMap, mesh, mesh.name, skeletonPath); + _bindMeshMaterial(ctx.sdfData, meshPath, ctx.materialMap, mesh); } +} - return true; +void +_writeInstancedMesh(WriteSdfContext& ctx, + const SdfPath& parentPath, + const Mesh& mesh, + int meshIdx, + const std::string& meshName) +{ + if (!mesh.instanceable) { + TF_CODING_ERROR("Trying to write instanced mesh %s/%s that is not instanceable", + parentPath.GetText(), + meshName.c_str()); + return; + } + if (mesh.asPoints) { + TF_CODING_ERROR("Trying to write instanced points %s/%s, which is not supported", + parentPath.GetText(), + meshName.c_str()); + return; + } + + SdfPath prototypePath = ctx.meshPrototypeMap[meshIdx]; + if (prototypePath.IsEmpty()) { + TfToken meshPrototypeName = TfToken("_MeshPrototype_" + meshName); + + // For Hydra to be happy with the prototype mesh, it needs to be under a transformable + // prim, hence we nest it here under a typeless `over` prim spec, so that it does not + // appear in the scene by itself. Only by being referenced onto an instancer. + prototypePath = + createPrimSpec(ctx.sdfData, parentPath, meshPrototypeName, TfToken(), SdfSpecifierOver); + + _writeMesh(ctx.sdfData, prototypePath, ctx.materialMap, mesh, meshName); + + // Add this to the list of prototypes + ctx.meshPrototypeMap[meshIdx] = prototypePath; + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "layer::write prototype %s for mesh %s\n", + prototypePath.GetText(), + meshName.c_str()); + } + + // We instantiate the mesh via a Xform prim that references the untyped prototype prim + SdfPath meshPath = + createPrimSpec(ctx.sdfData, parentPath, TfToken(meshName), UsdGeomTokens->Xform); + addPrimReference(ctx.sdfData, meshPath, SdfReference("", prototypePath)); + setPrimMetadata(ctx.sdfData, meshPath, SdfFieldKeys->Instanceable, VtValue(true)); + + // The material is bound on the instance and not on the actual prototype mesh, so that it + // becomes easy to override later. + // XXX if the instanced mesh has subsets, those material bindings are currently authored + // in the prototype, which makes it hard to manipulate after the fact. + _bindMeshMaterial(ctx.sdfData, meshPath, ctx.materialMap, mesh); + + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "layer::write mesh xform %s, instance of %s (%s)\n", + meshPath.GetText(), + prototypePath.GetText(), + meshName.c_str()); } // Layout of control points in USD is: row-major with U considered rows, and V columns. @@ -705,6 +786,10 @@ _writeNurb(SdfAbstractData* sdfData, const SdfPath& parentPath, NurbData& nurb) SdfPath primPath = createPrimSpec(sdfData, parentPath, TfToken(nurb.name), UsdGeomTokens->NurbsPatch); + if (!nurb.displayName.empty()) { + setPrimMetadata(sdfData, primPath, SdfFieldKeys->DisplayName, VtValue(nurb.displayName)); + } + auto createAttr = [&](const TfToken& name, const SdfValueTypeName& type, const auto& value) { SdfPath p = createAttributeSpec(sdfData, primPath, name, type); setAttributeDefaultValue(sdfData, p, value); @@ -792,6 +877,11 @@ _createNode(WriteSdfContext& ctx, PXR_NS::SdfSpecifier::SdfSpecifierDef, /* append = */ false); + if (!node.displayName.empty()) { + setPrimMetadata( + ctx.sdfData, primPath, SdfFieldKeys->DisplayName, VtValue(node.displayName)); + } + int nodeIndex = std::distance(ctx.usdData->nodes.data(), &node); ctx.nodeMap[nodeIndex] = primPath; @@ -807,6 +897,13 @@ _writeNode(WriteSdfContext& ctx, const SdfPath& primPath, const Node& node) { _writeXformAttributes(ctx.sdfData, primPath, node); + // The display name will have already been set by uniquifyNames() if the original node name + // was sanitized, so we don't need to check the node name + if (!node.displayName.empty()) { + setPrimMetadata( + ctx.sdfData, primPath, SdfFieldKeys->DisplayName, VtValue(node.displayName)); + } + if (node.camera >= 0) { _writeCamera(ctx.sdfData, primPath, ctx.usdData->cameras[node.camera]); } @@ -819,10 +916,24 @@ _writeNode(WriteSdfContext& ctx, const SdfPath& primPath, const Node& node) _writeLight(ctx.sdfData, primPath, ctx.usdData->lights[node.light]); } + // Uninstanced meshes first + for (int meshIndex : node.staticMeshes) { + const Mesh& mesh = ctx.usdData->meshes[meshIndex]; + if (!mesh.instanceable) { + _writePointsOrMesh(ctx, primPath, mesh); + } + } + + // Instanced meshes second. They need a name resolution to make sure they are unique + UniqueNameEnforcer enforcer; int i = 0; for (int meshIndex : node.staticMeshes) { const Mesh& mesh = ctx.usdData->meshes[meshIndex]; - _writePointsOrInstancedMesh(ctx, primPath, mesh, meshIndex, i++, SdfPath::EmptyPath()); + if (mesh.instanceable) { + std::string meshName = mesh.name; + enforcer.enforceUniqueness(meshName); + _writeInstancedMesh(ctx, primPath, mesh, meshIndex, meshName); + } } _writeNodes(ctx, primPath, node.children); @@ -917,6 +1028,11 @@ _writeSkeleton(SdfAbstractData* sdfData, const SdfPath& parentPath, const Skelet createPrimSpec(sdfData, parentPath, TfToken(skeleton.name), UsdSkelTokens->Skeleton); prependApiSchema(sdfData, primPath, UsdSkelTokens->SkelBindingAPI); + if (!skeleton.displayName.empty()) { + setPrimMetadata( + sdfData, primPath, SdfFieldKeys->DisplayName, VtValue(skeleton.displayName)); + } + auto createAttr = [&](const TfToken& name, const SdfValueTypeName& type, const auto& value, @@ -1005,6 +1121,11 @@ _writeMaterial(WriteSdfContext& ctx, const SdfPath& parentPath, const Material& { SdfPath materialPath = createMaterialPrimSpec(ctx.sdfData, parentPath, TfToken(material.name)); + if (!material.displayName.empty()) { + setPrimMetadata( + ctx.sdfData, materialPath, SdfFieldKeys->DisplayName, VtValue(material.displayName)); + } + printMaterial("layer::write", materialPath, material, ctx.debugTag); TF_DEBUG_MSG(FILE_FORMAT_UTIL, @@ -1050,15 +1171,15 @@ _writeAnimationTracks(const WriteLayerOptions& options, UsdData& data) } // Ignore all animation tracks beyond the first one if we aren't importing multiple tracks - int fistTrackWithTimepoints = -1; + int firstTrackWithTimepoints = -1; for (int trackIndex = 0; trackIndex < data.animationTracks.size(); trackIndex++) { AnimationTrack& track = data.animationTracks[trackIndex]; if (!track.hasTimepoints) { continue; } - if (fistTrackWithTimepoints < 0) { - fistTrackWithTimepoints = trackIndex; + if (firstTrackWithTimepoints < 0) { + firstTrackWithTimepoints = trackIndex; } else if (!options.animationTracks) { // Ignore all but the first non-empty track by pretending that it has no timepoints track.hasTimepoints = false; @@ -1095,6 +1216,7 @@ _writeAnimationTracks(const WriteLayerOptions& options, UsdData& data) dict["minTime"] = track.minTime; dict["maxTime"] = track.maxTime; dict["offset"] = track.offsetToJoinedTimeline; + dict["displayName"] = track.displayName; animationTracks[track.name] = dict; } @@ -1105,9 +1227,12 @@ _writeAnimationTracks(const WriteLayerOptions& options, UsdData& data) // Add animation name to metadata // This may be redundant with the "animationTracks" dict first entry, but that's ok - if (fistTrackWithTimepoints >= 0) { - data.metadata.SetValueAtPath("defaultAnimationTrack", - VtValue(data.animationTracks[fistTrackWithTimepoints].name)); + if (firstTrackWithTimepoints >= 0) { + // Since this meta data is not used by USD, it doesn't have to be the unique, sanitized + // name, and instead is safe to be the original display name + data.metadata.SetValueAtPath( + "defaultAnimationTrack", + VtValue(data.animationTracks[firstTrackWithTimepoints].displayName)); } // Join all NodeAnimations into the first track @@ -1266,6 +1391,13 @@ _writeLayerSdfData(const WriteLayerOptions& options, sdfData, skelParentPath, TfToken(skelRootName), UsdSkelTokens->SkelRoot); prependApiSchema(sdfData, skelRootPath, UsdSkelTokens->SkelBindingAPI); + if (!skeleton.displayName.empty()) { + setPrimMetadata(ctx.sdfData, + skelRootPath, + SdfFieldKeys->DisplayName, + VtValue(skeleton.displayName)); + } + SdfPath skeletonPath = _writeSkeleton(sdfData, skelRootPath, skeleton); ctx.skeletonMap[i++] = skeletonPath; @@ -1273,8 +1405,8 @@ _writeLayerSdfData(const WriteLayerOptions& options, for (int meshIndex : skeleton.meshSkinningTargets) { const Mesh& mesh = ctx.usdData->meshes[meshIndex]; - _writePointsOrInstancedMesh( - ctx, skelRootPath, mesh, meshIndex, meshChildIndex, skeletonPath); + // Note, skinned meshes are never emitted as instanced + _writePointsOrMesh(ctx, skelRootPath, mesh, skeletonPath); meshChildIndex++; } @@ -1325,7 +1457,8 @@ writeLayer(const WriteLayerOptions& options, // Add file names to metadata if (!data.importedFileNames.empty()) { - PXR_NS::VtArray filenames(data.importedFileNames.begin(), data.importedFileNames.end()); + PXR_NS::VtArray filenames(data.importedFileNames.begin(), + data.importedFileNames.end()); data.metadata.SetValueAtPath("filenames", VtValue(filenames)); } diff --git a/utils/layerWriteShared.cpp b/utils/src/layerWriteShared.cpp similarity index 97% rename from utils/layerWriteShared.cpp rename to utils/src/layerWriteShared.cpp index d11d1055..dadc1aba 100644 --- a/utils/layerWriteShared.cpp +++ b/utils/src/layerWriteShared.cpp @@ -9,9 +9,9 @@ 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 "layerWriteShared.h" +#include -#include "common.h" +#include #include #include diff --git a/utils/materials.cpp b/utils/src/materials.cpp similarity index 99% rename from utils/materials.cpp rename to utils/src/materials.cpp index 94c5bae4..23d501f0 100644 --- a/utils/materials.cpp +++ b/utils/src/materials.cpp @@ -9,10 +9,10 @@ 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 "materials.h" -#include "common.h" -#include "debugCodes.h" -#include "images.h" +#include +#include +#include +#include #include #include @@ -264,7 +264,6 @@ InputTranslator::translateDirectInternal(int imageIdx, Input& out) newAsset.name = asset.name; newAsset.format = asset.format; newAsset.image = asset.image; // create a copy - TF_DEBUG_MSG(FILE_FORMAT_UTIL, "key: %s\n", key.c_str()); mCache[key] = imageIndex; } out.image = imageIndex; @@ -768,7 +767,8 @@ InputTranslator::translatePhong2PBR(const Input& diffuseIn, GfVec3f diffuseValue = diffuseIn.value.Get(); GfVec3f specularValue = !specularIn.value.IsEmpty() ? specularIn.value.Get() : GfVec3f(.5); - float shininessValue = !glosinessIn.value.IsEmpty() ? glosinessIn.value.Get() : .5; + float shininessValue = + glosinessIn.value.IsHolding() ? glosinessIn.value.UncheckedGet() : 0.5f; // float shininess = m.ns == -1 ? 1 : m.ns; GfVec3f albedo; float roughness; @@ -991,7 +991,6 @@ InputTranslator::translateMix(const std::string& name, newImage.format = ImageFormatPng; mixed.write(newImage); } - TF_DEBUG_MSG(FILE_FORMAT_UTIL, "key: %s\n", key.c_str()); mCache[key] = imageIndex; } out.image = imageIndex; diff --git a/utils/neuralAssetsHelper.cpp b/utils/src/neuralAssetsHelper.cpp similarity index 99% rename from utils/neuralAssetsHelper.cpp rename to utils/src/neuralAssetsHelper.cpp index 0cf85bac..436a273e 100644 --- a/utils/neuralAssetsHelper.cpp +++ b/utils/src/neuralAssetsHelper.cpp @@ -9,7 +9,7 @@ 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 "neuralAssetsHelper.h" +#include #include #include diff --git a/utils/resolver.cpp b/utils/src/resolver.cpp similarity index 94% rename from utils/resolver.cpp rename to utils/src/resolver.cpp index e85c5541..03d91a21 100644 --- a/utils/resolver.cpp +++ b/utils/src/resolver.cpp @@ -9,11 +9,11 @@ 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 "resolver.h" -#include "assetresolver.h" -#include "common.h" -#include "debugCodes.h" -#include "images.h" +#include +#include +#include +#include +#include #include #include #include diff --git a/utils/sdfMaterialUtils.cpp b/utils/src/sdfMaterialUtils.cpp similarity index 99% rename from utils/sdfMaterialUtils.cpp rename to utils/src/sdfMaterialUtils.cpp index efd9e583..bf7e256c 100644 --- a/utils/sdfMaterialUtils.cpp +++ b/utils/src/sdfMaterialUtils.cpp @@ -9,7 +9,7 @@ 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 "sdfMaterialUtils.h" +#include PXR_NAMESPACE_USING_DIRECTIVE diff --git a/utils/sdfUtils.cpp b/utils/src/sdfUtils.cpp similarity index 99% rename from utils/sdfUtils.cpp rename to utils/src/sdfUtils.cpp index e9c9bfef..703632a3 100644 --- a/utils/sdfUtils.cpp +++ b/utils/src/sdfUtils.cpp @@ -9,10 +9,10 @@ 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 "sdfUtils.h" +#include -#include "common.h" -#include "debugCodes.h" +#include +#include #include #include diff --git a/utils/test.cpp b/utils/src/test.cpp similarity index 98% rename from utils/test.cpp rename to utils/src/test.cpp index ae29f11d..b808a789 100644 --- a/utils/test.cpp +++ b/utils/src/test.cpp @@ -9,7 +9,7 @@ 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 "test.h" +#include #include #include #include @@ -573,6 +573,18 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa } } +void +assertDisplayName(PXR_NS::UsdStageRefPtr stage, + const std::string& primPath, + const std::string& displayName) +{ + UsdPrim prim = stage->GetPrimAtPath(SdfPath(primPath)); + ASSERT_TRUE(prim) << primPath << " not found when verifying prim had proper display name\n"; + ASSERT_EQ(prim.GetDisplayName(), displayName) + << primPath << " has incorrect display name; expected \"" << displayName << "\" but got \"" + << prim.GetDisplayName() << "\"\n "; +} + void assertPoints(PXR_NS::UsdStageRefPtr stage, const std::string& path, const PointsData& data) { diff --git a/utils/transforms.cpp b/utils/src/transforms.cpp similarity index 94% rename from utils/transforms.cpp rename to utils/src/transforms.cpp index 6179f261..f300c972 100644 --- a/utils/transforms.cpp +++ b/utils/src/transforms.cpp @@ -9,9 +9,9 @@ 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 "transforms.h" -#include "common.h" -#include "debugCodes.h" +#include +#include +#include #include using namespace PXR_NS; diff --git a/utils/usdData.cpp b/utils/src/usdData.cpp similarity index 83% rename from utils/usdData.cpp rename to utils/src/usdData.cpp index e5f55944..7efe62be 100644 --- a/utils/usdData.cpp +++ b/utils/src/usdData.cpp @@ -9,10 +9,10 @@ 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 "usdData.h" -#include "common.h" -#include "debugCodes.h" #include +#include +#include +#include #include #include @@ -468,8 +468,71 @@ _makeValidPrimName(const std::string& name, const std::string& defaultName) return name.empty() ? defaultName : TfMakeValidIdentifier(name); } +/** + * Given the name of a node and/or its display name, create a valid USD identifier while + * preserving the original name for export. + * + * If there is no display name input, then the node name will be sanitized and used for the name. + * If the name has to be sanitized, the original will be preserved as the display name. + * + * If there is no node name input, then the display name will be sanitized and used for the name. + * If the display name has to be sanitized, the original will remain the display name, otherwise + * the display name will be empty. + * + * If there is both a node name and a display name, then the node name will be sanitized and used + * for the USD identifier and the display name will be passed through. + * + * @param nodeName The name of the node, as used for identifying the node. Not all formats have + * one; for instance, glTF references nodes by index. + * @param displayName The display name of the node, which may not necessarily be unique. The glTF + * "name" is an example of this + * @param defaultName A string that will be used as the name if both nodeName and displayName are + * empty strings. + * + * @return A pair of strings, the first being a valid USD identifier and the second being a + * display name for the USD. + */ +std::pair +_makeValidPrimName(const std::string& nodeName, + const std::string& displayName, + const std::string& defaultName) +{ + std::string newNodeName; + std::string newDisplayName; + + if (displayName.empty()) { + newNodeName = _makeValidPrimName(nodeName, defaultName); + newDisplayName = (newNodeName == nodeName ? "" : nodeName); + } else if (nodeName.empty()) { + newNodeName = _makeValidPrimName(displayName, defaultName); + newDisplayName = (newNodeName == displayName ? "" : displayName); + } else { + newNodeName = TfMakeValidIdentifier(nodeName); + newDisplayName = displayName; + } + + return std::make_pair(std::move(newNodeName), std::move(newDisplayName)); +} + +/** + * Makes the name unique among its siblings by appending a number to the end of the name if + * necessary. + * + * @param siblingNames A map of names to occurences of the name. + * @param primName The name to make unique. (This string may be modified) + * @param displayName The display name of the node, which may not necessarily be unique. It will + * be set to the original primName if the following conditions are met: + * 1. it is a valid pointer + * 2. the display name it points to is empty (to not override display names) + * 3. primName is modified (and the original name would be lost) + * + * Note that this is a pointer rather than a reference since this function can + * be called when there are no display names. By default, this is null + */ void -_makeUniqueAndAdd(std::unordered_map& siblingNames, std::string& primName) +_makeUniqueAndAdd(std::unordered_map& siblingNames, + std::string& primName, + std::string* displayName = nullptr) { // siblingNames is a map of names to occurences of the name. // This will retrieve the occurance count for the name or insert the name into the @@ -483,6 +546,14 @@ _makeUniqueAndAdd(std::unordered_map& siblingNames, std::strin } else { // The name has been seen before so append the occurence count to the original name std::string newName = primName + std::to_string(count); + + // Save the original name if there is a display name is present, and if there is not + // already an existing one, to preserve non-unique names without overriding a display + // name that has already been set + if (displayName != nullptr && displayName->empty()) { + *displayName = primName; + } + while (1) { // add the new name to the map. If the count is 0, it's an unused name // so we can use it. @@ -491,7 +562,7 @@ _makeUniqueAndAdd(std::unordered_map& siblingNames, std::strin count++; newNameCount++; primName = std::move(newName); - return; + break; } // The new proposed name is also taken so append the count of it // to create another proposed name and then loop again to check the @@ -499,6 +570,7 @@ _makeUniqueAndAdd(std::unordered_map& siblingNames, std::strin newName = newName + std::to_string(newNameCount); } } + return; } // Assumes type T has a std::string `name` field @@ -508,8 +580,11 @@ _uniquifySiblings(std::vector& siblings, const std::string& defaultName) { std::unordered_map siblingNames; for (T& sibling : siblings) { - sibling.name = _makeValidPrimName(sibling.name, defaultName); - _makeUniqueAndAdd(siblingNames, sibling.name); + auto [nodeName, displayName] = + _makeValidPrimName(sibling.name, sibling.displayName, defaultName); + sibling.name = nodeName; + sibling.displayName = displayName; + _makeUniqueAndAdd(siblingNames, sibling.name, &sibling.displayName); } } @@ -523,8 +598,11 @@ _uniquifySiblings(std::vector& all, std::unordered_map siblingNames; for (int idx : siblingIndices) { T& sibling = all[idx]; - sibling.name = _makeValidPrimName(sibling.name, defaultName); - _makeUniqueAndAdd(siblingNames, sibling.name); + auto [nodeName, displayName] = + _makeValidPrimName(sibling.name, sibling.displayName, defaultName); + sibling.name = nodeName; + sibling.displayName = displayName; + _makeUniqueAndAdd(siblingNames, sibling.name, &sibling.displayName); } } @@ -538,8 +616,16 @@ _uniquifySiblingMeshes(std::vector& all, const std::vector& siblingIn for (int idx : siblingIndices) { Mesh& sibling = all[idx]; - sibling.name = _makeValidPrimName(sibling.name, sibling.asPoints ? pointsStr : meshStr); - _makeUniqueAndAdd(siblingNames, sibling.name); + auto [nodeName, displayName] = _makeValidPrimName( + sibling.name, sibling.displayName, sibling.asPoints ? pointsStr : meshStr); + sibling.name = nodeName; + sibling.displayName = displayName; + // We skip uniquifying the names of meshes that will become prototypes, since the unique + // name belongs to the node that instances the mesh and not the mesh itself. + if (sibling.instanceable) { + continue; + } + _makeUniqueAndAdd(siblingNames, sibling.name, &sibling.displayName); } } @@ -562,11 +648,16 @@ uniquifyNames(UsdData& data) // Cameras are (currently) always children of a node with a unique name, hence they don't need // unique names, just valid prim names for (Camera& camera : data.cameras) { - camera.name = _makeValidPrimName(camera.name, "Camera"); + auto [nodeName, displayName] = + _makeValidPrimName(camera.name, camera.displayName, "Camera"); + camera.name = nodeName; + camera.displayName = displayName; } for (Light& light : data.lights) { - light.name = _makeValidPrimName(light.name, "Light"); + auto [nodeName, displayName] = _makeValidPrimName(light.name, light.displayName, "Light"); + light.name = nodeName; + light.displayName = displayName; } _uniquifySiblings(data.materials, "Material"); _uniquifySiblings(data.skeletons, "Skeleton"); @@ -592,7 +683,7 @@ uniquifyNames(UsdData& data) } } - _uniquifySiblings(data.animationTracks, "AnmimationTrack"); + _uniquifySiblings(data.animationTracks, "AnimationTrack"); } bool diff --git a/version b/version index 437d26b1..1cc5f657 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.0.10 \ No newline at end of file +1.1.0 \ No newline at end of file