diff --git a/.gitignore b/.gitignore index 069db8b6..71ce32ce 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ build-meta # Python /.venv* +/venv* # ide folder .vscode/* @@ -55,5 +56,5 @@ test/assets/obj/*/*.usd test/assets/ply/*.usd test/assets/stl/*.usd -# Other +# .DS_Store .DS_Store diff --git a/README.md b/README.md index c4ba991c..d585dcab 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,11 @@ The following dependencies are needed: * Install python and the following pip components: `pyside6`, `pyopengl`. * Build and install USD entering in a terminal (in windows a x64 Native Tools Command prompt): ``` - python /build_scripts/build_usd.py --draco --openimageio --build-variant release + python /build_scripts/build_usd.py --onetbb --no-examples --draco --openimageio --build-variant release ``` + `--no-examples` is needed to omit the example plugin usdObj that ships with USD from the install, as it will conflict with our usdObj plugin. + Add `--build-target universal` for universal binaries in macos. If adding `--openimageio` you may need these fixes: @@ -269,4 +271,4 @@ To generate the documentation go to the project root folder and enter: ``` doxygen ``` -The resulting documentation will be placed at the `docs` folder. \ No newline at end of file +The resulting documentation will be placed at the `docs` folder. diff --git a/changelog.txt b/changelog.txt index 81fa6e7c..8fd407fe 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,56 @@ +v1.1.2 October 22nd, 2025 +fbx: + - fix mesh import when fbx mesh is a root node + - do not require `gtest` if tests are disabled + - bind meshes that have materials but no elementmaterials + - add 'triangulatemeshes' import option to allow control of whether triangulation should be performed + - add generator metadata to USD + - support bitangents/tangents during import/emport + - fix material property mapping for non lambert/phong shader models +gltf: + - fix material index lookup when material is missing + - support khr_materials_volume_scatter extension + - improved support for gltf scattering extension + - add support for textures with brackets in their file names + - support EXT_materials_specular_edge_color & EXT_materials_clearcoat_color + - add generator metadata to USD + - fix various crashes + - support bitangents/tangents during import/export + - fix inverted normal maps +obj: + - replace backslash with slash in texture filepath + - fix crash on loading a file > 2gb + - allow single value for ke material setting +ply: + - fix reading gsplat sh coefficients + - fix export issues when not all meshes have uvs or normals + - remove clipping to SH0 for Gsplat + - fix GSplat import and export and add support to SH4 +spz: + - remove clipping to SH0 for Gsplat + - fix GSplat import and export and add support to SH4 +sbsar: + - expose uv texture repeat controls + - panorama support + - refactor of sbsar for MaterialX support + - allow for unlimited cache + - switching the default normal format to sbsar +stl: + - reverse normals on export + - support empty normals on import + - calculate geometric normals on import +utility: + - increase MaterialX OpenPBR support + - improve shared file format args + - refactor input struct & material processing + - fix for crash in smooth normals computation + - update test baseline images for 24.11 renderer changes +cmake: + - switch from cmake FetchContent to CPM + - updating openimageio cmake +usd: + - adding support for usd 25.05.01 + v1.1.1 March 10th, 2025 fbx: - added null and index checks @@ -262,4 +315,4 @@ utility: - asset resolver fix v0.9.0 November 10, 2023 -- Initial release of fbx, gltf, obj, ply and stl USD fileformat plugins. \ No newline at end of file +- Initial release of fbx, gltf, obj, ply and stl USD fileformat plugins. diff --git a/cmake/FindFastFloat.cmake b/cmake/FindFastFloat.cmake index 51331c9d..4a697e58 100644 --- a/cmake/FindFastFloat.cmake +++ b/cmake/FindFastFloat.cmake @@ -30,20 +30,14 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_FASTFLOAT) message(STATUS "Fetching FastFloat") - include(FetchContent) - FetchContent_Declare( - FastFloat + include(CPM) + CPMAddPackage( + NAME FastFloat GIT_REPOSITORY "https://github.com/lemire/fast_float.git" GIT_TAG "v1.1.2" # 8159e8bcf63c1b92f5a51fb550f966e56624b209 - OVERRIDE_FIND_PACKAGE ) - FetchContent_MakeAvailable(FastFloat) - if (fastfloat_POPULATED) - set(FastFloat_FOUND TRUE) - add_library(FastFloat::fast_float ALIAS fast_float) - elseif(${FastFloat_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch FastFloat") - endif() + set(FastFloat_FOUND TRUE) + add_library(FastFloat::fast_float ALIAS fast_float) else() if(${FastFloat_FIND_REQUIRED}) find_package(FastFloat CONFIG REQUIRED) diff --git a/cmake/FindGTest.cmake b/cmake/FindGTest.cmake index 4ea1a880..f79a635f 100644 --- a/cmake/FindGTest.cmake +++ b/cmake/FindGTest.cmake @@ -30,24 +30,19 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_GTEST) message(STATUS "Fetching GTest") - include(FetchContent) - FetchContent_Declare( - googletest # using GTest here triggers errors - GIT_REPOSITORY "https://github.com/google/googletest.git" - GIT_TAG "release-1.11.0" - OVERRIDE_FIND_PACKAGE - ) + include(CPM) set(BUILD_SHARED_LIBS OFF) set(gtest_force_shared_crt ON) set(BUILD_GMOCK OFF) set(BUILD_GTEST ON) - FetchContent_MakeAvailable(googletest) + CPMAddPackage( + NAME googletest # using GTest here triggers errors + GIT_REPOSITORY "https://github.com/google/googletest.git" + GIT_TAG "release-1.11.0" + ) set(BUILD_SHARED_LIBS ON) - if(googletest_POPULATED) - set(GTest_FOUND TRUE) - elseif(${GTest_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch GTest") - endif() + + set(GTest_FOUND TRUE) else() if(${GTest_FIND_REQUIRED}) find_package(GTest CONFIG REQUIRED) diff --git a/cmake/FindHapply.cmake b/cmake/FindHapply.cmake index 37bbfeec..847a37b3 100644 --- a/cmake/FindHapply.cmake +++ b/cmake/FindHapply.cmake @@ -36,21 +36,15 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_HAPPLY) message(STATUS "Fetching Happly") - include(FetchContent) - FetchContent_Declare( - Happly + include(CPM) + CPMAddPackage( + NAME happly GIT_REPOSITORY "https://github.com/nmwsharp/happly.git" GIT_TAG "cfa2611550bc7da65855a78af0574b65deb81766" - OVERRIDE_FIND_PACKAGE ) - FetchContent_MakeAvailable(Happly) - if (happly_POPULATED) - set(Happly_FOUND TRUE) - add_library(happly::happly INTERFACE IMPORTED) - target_include_directories(happly::happly INTERFACE ${happly_SOURCE_DIR}) - elseif(${Happly_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch Happly") - endif() + set(Happly_FOUND TRUE) + add_library(happly::happly INTERFACE IMPORTED) + target_include_directories(happly::happly INTERFACE ${happly_SOURCE_DIR}) else() include(FindPackageHandleStandardArgs) diff --git a/cmake/FindLibXml2.cmake b/cmake/FindLibXml2.cmake index 106e82e3..447e20dc 100644 --- a/cmake/FindLibXml2.cmake +++ b/cmake/FindLibXml2.cmake @@ -30,25 +30,19 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_LIBXML2) message(STATUS "Fetching libxml2") - include(FetchContent) - FetchContent_Declare( - LibXml2 - GIT_REPOSITORY "https://github.com/GNOME/libxml2.git" - GIT_TAG "ae383bdb74523ddaf831d7db0690173c25e483b3" # Release v2.10.0 - OVERRIDE_FIND_PACKAGE - ) + include(CPM) set(BUILD_SHARED_LIBS OFF) # otherwise fails set(LIBXML2_WITH_ICONV OFF) set(LIBXML2_WITH_LZMA OFF) set(LIBXML2_WITH_PYTHON OFF) set(LIBXML2_WITH_ZLIB ON) set(LIBXML2_WITH_TESTS OFF) - FetchContent_MakeAvailable(LibXml2) - if(libxml2_POPULATED) - set(LibXml2_FOUND TRUE) - elseif(${LibXml2_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch LibXml2") - endif() + CPMAddPackage( + NAME LibXml2 + GIT_REPOSITORY "https://github.com/GNOME/libxml2.git" + GIT_TAG "ae383bdb74523ddaf831d7db0690173c25e483b3" # Release v2.10.0 + ) + set(LibXml2_FOUND TRUE) else() message(STATUS "Find LibXml2 ${LibXml2_ROOT}") if(${LibXml2_FIND_REQUIRED}) diff --git a/cmake/FindOpenImageIO.cmake b/cmake/FindOpenImageIO.cmake index 094d7370..4a74f0ad 100644 --- a/cmake/FindOpenImageIO.cmake +++ b/cmake/FindOpenImageIO.cmake @@ -3,7 +3,7 @@ Finds the OpenImageIO library. This find module will simply redirect to a find_package(CONFIG) hinted to look -into the pxr_DIR. +into the pxr_ROOT. Imported Targets @@ -25,12 +25,33 @@ This will define the following variables: #]=======================================================================] -if (TARGET OpenImageIO::OpenImageIO) +if (TARGET OpenImageIO::OpenImageIO AND TARGET OpenImageIO::OpenImageIO_Util) return() endif() if(${OpenImageIO_FIND_REQUIRED}) - find_package(OpenImageIO PATHS ${pxr_DIR} ${pxr_DIR}/lib64 REQUIRED) + find_package(OpenImageIO PATHS ${pxr_ROOT} ${pxr_ROOT}/lib64 REQUIRED) else() - find_package(OpenImageIO PATHS ${pxr_DIR} ${pxr_DIR}/lib64) + find_package(OpenImageIO PATHS ${pxr_ROOT} ${pxr_ROOT}/lib64) +endif() + +# Ensure both OpenImageIO and OpenImageIO_Util targets are available +if(NOT TARGET OpenImageIO::OpenImageIO_Util AND TARGET OpenImageIO::OpenImageIO) + # Sometimes the Util library is named differently, try to find it + get_target_property(OIIO_LOCATION OpenImageIO::OpenImageIO LOCATION) + get_filename_component(OIIO_DIR ${OIIO_LOCATION} DIRECTORY) + + # Look for the util library in the same directory + find_library(OIIO_UTIL_LIB + NAMES OpenImageIO_Util libOpenImageIO_Util + PATHS ${OIIO_DIR} + NO_DEFAULT_PATH + ) + + if(OIIO_UTIL_LIB) + add_library(OpenImageIO::OpenImageIO_Util UNKNOWN IMPORTED) + set_target_properties(OpenImageIO::OpenImageIO_Util PROPERTIES + IMPORTED_LOCATION ${OIIO_UTIL_LIB} + ) + endif() endif() diff --git a/cmake/FindSphericalHarmonics.cmake b/cmake/FindSphericalHarmonics.cmake index a0b5adea..04ced4ff 100644 --- a/cmake/FindSphericalHarmonics.cmake +++ b/cmake/FindSphericalHarmonics.cmake @@ -40,31 +40,28 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_SPHERICAL_HARMONICS) message(STATUS "Fetching SphericalHarmonics") - include(FetchContent) - FetchContent_Declare( - spherical_harmonics_git + include(CPM) + CPMAddPackage( + NAME 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() + 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) else() include(FindPackageHandleStandardArgs) diff --git a/cmake/FindTinyGLTF.cmake b/cmake/FindTinyGLTF.cmake index 4468b5d7..664f0fca 100644 --- a/cmake/FindTinyGLTF.cmake +++ b/cmake/FindTinyGLTF.cmake @@ -30,23 +30,17 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_TINYGLTF) message(STATUS "Fetching TinyGLTF") - include(FetchContent) - FetchContent_Declare( - TinyGLTF - GIT_REPOSITORY "https://github.com/syoyo/tinygltf.git" - GIT_TAG "v2.8.21" # 4bfc1fc1807e2e2cf3d3111f67d6ebd957514c80 - OVERRIDE_FIND_PACKAGE - ) + include(CPM) set(TINYGLTF_BUILD_LOADER_EXAMPLE OFF) set(TINYGLTF_INSTALL OFF) set(TINYGLTF_HEADER_ONLY ON) - FetchContent_MakeAvailable(TinyGLTF) - if(tinygltf_POPULATED) - set(TinyGLTF_FOUND TRUE) - add_library(tinygltf::tinygltf ALIAS tinygltf) - elseif(${TinyGLTF_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch TinyGLTF") - endif() + CPMAddPackage( + NAME TinyGLTF + GIT_REPOSITORY "https://github.com/syoyo/tinygltf.git" + GIT_TAG "v2.8.21" # 4bfc1fc1807e2e2cf3d3111f67d6ebd957514c80 + ) + set(TinyGLTF_FOUND TRUE) + add_library(tinygltf::tinygltf ALIAS tinygltf) else() if (${TinyGLTF_FIND_REQUIRED}) find_package(TinyGLTF CONFIG REQUIRED) diff --git a/cmake/FindZLIB.cmake b/cmake/FindZLIB.cmake index cb16895f..87c31dfd 100644 --- a/cmake/FindZLIB.cmake +++ b/cmake/FindZLIB.cmake @@ -4,7 +4,7 @@ Finds or fetches the ZLIB library. If USD_FILEFORMATS_FORCE_FETCHCONTENT or USD_FILEFORMATS_FETCH_ZLIB are TRUE, ZLIB will be fetched. Otherwise it will be searched via find commands. -The search is hinted to look into pxr_DIR, but can be overriden by setting +The search is hinted to look into pxr_ROOT, but can be overriden by setting ZLIB_ROOT. @@ -42,50 +42,54 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_ZLIB) message(STATUS "Fetching ZLIB") - include(FetchContent) - FetchContent_Declare( - ZLIB + include(CPM) + CPMAddPackage( + NAME ZLIB GIT_REPOSITORY "https://github.com/madler/zlib.git" GIT_TAG "cacf7f1d4e3d44d871b605da3b647f07d718623f" # /tag/v1.2.11 - OVERRIDE_FIND_PACKAGE ) - FetchContent_MakeAvailable(ZLIB) - if(zlib_POPULATED) - set(ZLIB_FOUND TRUE) - add_library(ZLIB::ZLIB ALIAS zlib) - elseif(${ZLIB_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch ZLIB") - endif() + set(ZLIB_FOUND TRUE) + add_library(ZLIB::ZLIB ALIAS zlib) + # Create an export set to make sure the libxml2 export set + # doesn't complain + install(TARGETS zlib + EXPORT zlib_export + DESTINATION lib + ) + install(EXPORT zlib_export + DESTINATION lib + FILE zlib.cmake + ) else() include(SelectLibraryConfigurations) include(FindPackageHandleStandardArgs) - if (UNIX AND NOT APPLE) + if (UNIX OR APPLE) find_path(ZLIB_INCLUDE_DIR - HINTS ${pxr_DIR}/include + HINTS ${pxr_ROOT}/include NAMES zlib.h ) find_library(ZLIB_LIBRARY_DEBUG - HINTS ${pxr_DIR}/lib + HINTS ${pxr_ROOT}/lib NAMES z zlib zlibd ) find_library(ZLIB_LIBRARY_RELEASE - HINTS ${pxr_DIR}/lib + HINTS ${pxr_ROOT}/lib NAMES z zlib ) else() find_path(ZLIB_INCLUDE_DIR - HINTS ${pxr_DIR}/include + HINTS ${pxr_ROOT}/include NO_CMAKE_SYSTEM_PATH # otherwise it always finds the system one NAMES zlib.h ) find_library(ZLIB_LIBRARY_DEBUG - HINTS ${pxr_DIR}/lib + HINTS ${pxr_ROOT}/lib NO_CMAKE_SYSTEM_PATH # otherwise it always finds the system one NAMES z zlib zlibd ) find_library(ZLIB_LIBRARY_RELEASE - HINTS ${pxr_DIR}/lib + HINTS ${pxr_ROOT}/lib NO_CMAKE_SYSTEM_PATH # otherwise it always finds the system one NAMES z zlib ) diff --git a/cmake/Finddraco.cmake b/cmake/Finddraco.cmake index 30e3d7fa..5f3e7df1 100644 --- a/cmake/Finddraco.cmake +++ b/cmake/Finddraco.cmake @@ -5,7 +5,7 @@ Finds or fetches the Draco library. If USD_FILEFORMATS_FORCE_FETCHCONTENT or USD_FILEFORMATS_FETCH_DRACO are TRUE, Draco will be fetched. Otherwise it will be searched via find commands, since draco 1.3.6 (the one bundled in USD) does not have a good config module. -The search is hinted to look into pxr_DIR, but can be overriden by setting +The search is hinted to look into pxr_ROOT, but can be overriden by setting draco_ROOT. @@ -44,35 +44,29 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_DRACO) message(STATUS "Fetching draco") - include(FetchContent) - FetchContent_Declare( - draco + include(CPM) + CPMAddPackage( + NAME draco GIT_REPOSITORY "https://github.com/google/draco.git" GIT_TAG "9f856abaafb4b39f1f013763ff061522e0261c6f" # 1.56 - OVERRIDE_FIND_PACKAGE ) - FetchContent_MakeAvailable(draco) - if(draco_POPULATED) - set(draco_FOUND TRUE) - elseif(${draco_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch draco") - endif() + set(draco_FOUND TRUE) else() include(SelectLibraryConfigurations) include(FindPackageHandleStandardArgs) find_path(draco_INCLUDE_DIR - HINTS ${pxr_DIR}/include + HINTS ${pxr_ROOT}/include NAMES draco/core/draco_version.h ) find_library(draco_LIBRARY_DEBUG - HINTS ${pxr_DIR}/lib + HINTS ${pxr_ROOT}/lib NAMES draco ) find_library(draco_LIBRARY_RELEASE - HINTS ${pxr_DIR}/lib + HINTS ${pxr_ROOT}/lib NAMES draco ) diff --git a/cmake/Findfmt.cmake b/cmake/Findfmt.cmake index be86a51c..7d1ed4b3 100644 --- a/cmake/Findfmt.cmake +++ b/cmake/Findfmt.cmake @@ -30,19 +30,13 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_FMT) message(STATUS "Fetching fmt") - include(FetchContent) - FetchContent_Declare( - fmt + include(CPM) + CPMAddPackage( + NAME fmt GIT_REPOSITORY "https://github.com/fmtlib/fmt.git" GIT_TAG "10.1.1" # f5e54359df4c26b6230fc61d38aa294581393084 - OVERRIDE_FIND_PACKAGE ) - FetchContent_MakeAvailable(fmt) - if(fmt_POPULATED) - set(fmt_FOUND TRUE) - elseif(${fmt_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch fmt") - endif() + set(fmt_FOUND TRUE) else() if(${fmt_FIND_REQUIRED}) find_package(fmt CONFIG REQUIRED) diff --git a/cmake/Findspz.cmake b/cmake/Findspz.cmake index d36b2884..84290d8b 100644 --- a/cmake/Findspz.cmake +++ b/cmake/Findspz.cmake @@ -40,35 +40,16 @@ endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_SPZ) message(STATUS "Fetching spz") - include(FetchContent) - FetchContent_Declare( - spz - GIT_REPOSITORY "https://github.com/raymondyfei/spz.git" - GIT_TAG "fd4e2a57bd6b7462657d41eebda330eca0f35159" - OVERRIDE_FIND_PACKAGE + include(CPM) + CPMAddPackage( + NAME spz + GIT_REPOSITORY "https://github.com/nianticlabs/spz.git" + GIT_TAG "v1.1.0+adobe.4" + OPTIONS "BUILD_SHARED_LIBS OFF" ) - FetchContent_MakeAvailable(spz) - if (spz_POPULATED) - set(spz_FOUND TRUE) - file(GLOB SPZ_SRC_FILES - ${spz_SOURCE_DIR}/src/cc/*.cc - ${spz_SOURCE_DIR}/src/cc/*.h - ) - add_library(spz STATIC) - target_sources(spz PRIVATE ${SPZ_SRC_FILES}) - set(SPZ_INCLUDE_DIR "${spz_SOURCE_DIR}/src/cc") - target_include_directories(spz PUBLIC ${SPZ_INCLUDE_DIR}) - target_link_libraries(spz PRIVATE ZLIB::ZLIB) - set_property(TARGET spz PROPERTY POSITION_INDEPENDENT_CODE ON) - set_property(TARGET spz PROPERTY CXX_STANDARD 17) - target_compile_definitions(spz PRIVATE "_USE_MATH_DEFINES") - if (NOT MSVC) - target_compile_options(spz PRIVATE "-Wno-shorten-64-to-32") - endif() - - add_library(spz::spz ALIAS spz) - elseif(${spz_FIND_REQUIRED}) - message(FATAL_ERROR "Could not fetch spz") + set(spz_FOUND TRUE) + if (NOT MSVC) + target_compile_options(spz PRIVATE "-Wno-shorten-64-to-32") endif() else() include(FindPackageHandleStandardArgs) diff --git a/fbx/CMakeLists.txt b/fbx/CMakeLists.txt index 7d7f5129..ee74fa74 100644 --- a/fbx/CMakeLists.txt +++ b/fbx/CMakeLists.txt @@ -8,7 +8,9 @@ endif() find_package(ZLIB REQUIRED) find_package(LibXml2 REQUIRED) find_package(FBXSDK REQUIRED) -find_package(GTest REQUIRED) +if(USD_FILEFORMATS_BUILD_TESTS) + find_package(GTest REQUIRED) +endif() add_subdirectory(src) diff --git a/fbx/README.md b/fbx/README.md index 5c0e9d17..6cefd8ce 100644 --- a/fbx/README.md +++ b/fbx/README.md @@ -76,76 +76,126 @@ 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. +- Only point, directional, and spot lights are exported. 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.** ## File Format Arguments + **Import:** -* `fbxAssetsPath`: Filesystem path where image assets are saved to. - By default image assets are not copied, but the generated usd file will resolve them from the original file. - The following saves images to the path `myPath` during `UsdStage::Open` and then exports the stage to that same path. +* `assetsPath`: Filesystem path where image assets are saved to during import. Default is `""` + + By default image textures used by the asset are not copied during import, but are kept in memory and are available + via an associated `ArResolver` plugin. By specifying a filesystem location via `assetsPath`, the import process will + copy the image textures to that location and provide asset paths to those locations in the generated USD data. This + file format argument allows an easy way to export associated images textures to disk when converting an asset to USD. + + This snippet saves image textures to the path at `exportPath` during `Usd.Stage.Open` and then also exports the stage + to that same location, so that the USD data and the used images a co-located. ``` - UsdStageRefPtr stage = UsdStage::Open("cube.fbx:SDF_FORMAT_ARGS:fbxAssetsPath=myPath") - stage->Export("myPath/cube.usd") + from pxr import Usd + stage = Usd.Stage.Open("asset.fbx:SDF_FORMAT_ARGS:assetsPath=exportPath") + stage.Export("exportPath/asset.usd") ``` +* `fbxAssetsPath`: Deprecated in favor of `assetsPath`. + +* `writeUsdPreviewSurface`: Generate a UsdPreviewSurface based network for each material. Default is `true` + + UsdPreviewSurface and its associated nodes are a universally understood USD material description + and all application should support them. The PBR capabilities are limited. + +* `writeASM`: Generate a ASM (Adobe Standard Material) based network for each material. Default is `true` + + ASM is a standard supported by many Adobe applications with richer support for PBR capabilities. + It will be superseded by OpenPBR in the near future. + +* `writeOpenPBR`: Generate a OpenPBR based material network for each material. Default is `false` + + OpenPBR is a new industry standard that will have wide spread support, but is still in its infancy. + The material network uses `MaterialX` nodes to express individual operations and has an `OpenPBR` surface, + which has rich support for PBR oriented materials. + * `fbxPhong`: Forces phong to PBR material conversion. By default turned off: the plugin imports the diffuse component only, without specularities. The following converts PBR to phong. ``` - UsdStageRefPtr stage = UsdStage::Open("cube.fbx:SDF_FORMAT_ARGS:fbxPhong=true") + from pxr import Usd + stage = Usd.Stage.Open("cube.fbx:SDF_FORMAT_ARGS:fbxPhong=true") stage.Export("cube.usd") ``` - The phong to PBR conversion follows https://docs.microsoft.com/en-us/azure/remote-rendering/reference/material-mapping. Keep in mind it is a lossy conversion. + The phong to PBR conversion follows https://docs.microsoft.com/en-us/azure/remote-rendering/reference/material-mapping. + Keep in mind it is a lossy conversion. -* `fbxOriginalColorSpace`: USD uses linear colorspace, however, FBX colorspace could be either linear or sRGB. - The user can set which one the data was in during import. If the data is in sRGB it will be converted to linear while in USD. Exporting will also consider the original color space. See Export -> outputColorSpace for details. +* `fbxOriginalColorSpace`: Convert colors from sRGB to linear. Default: `""` + + USD uses a linear colorspace, however, FBX colorspace could be either linear or sRGB. + The user can set which one the data was in during import. If the data is in `sRGB` it will be converted to linear + for USD. Exporting will also consider the original color space. See Export -> `outputColorSpace` for details. ``` - UsdStageRefPtr stage = UsdStage::Open("cube.fbx:SDF_FORMAT_ARGS:fbxOriginalColorSpace=sRGB") + from pxr import Usd + stage = Usd.Stage.Open("cube.fbx:SDF_FORMAT_ARGS:fbxOriginalColorSpace=sRGB") ``` -* `fbxAnimationTracks`: Import multiple animation stacks. Default is `false` - The default is that only the first animation stack is imported. - It is only recommended to use this parameter in order to convert from FBX to another format, such as fbx. - It is not recommended to export a .usd file after importing a file with this parameter set. +* `fbxAnimationStacks`: Import multiple animation stacks. Default is `false` + + By default only the first animation stack is imported. + It is only recommended to use this parameter in order to convert from FBX to another format that supports multiple + animation tracks, such as GLTF. It is not recommended to export a .usd file after importing a file with this parameter + set, as there is no standard way to encode this information. + + The following allows additional animation stacks to be imported, and adds metadata to USD to encode where each stack + begins and ends. The exporter can then read this metadata to export the stacks properly. ``` - The following allows additional animation stacks to be imported, and adds metadata to USD to encode where - each stack begins and ends. The exporter can then read this metadata to export the stacks properly. + from pxr import Usd + stage = Usd.Stage.Open("cube.fbx:SDF_FORMAT_ARGS:fbxAnimationStacks=true") + stage.Export("myPath/cube.gltf") ``` - UsdStageRefPtr stage = UsdStage::Open("cube.fbx:SDF_FORMAT_ARGS:fbxAnimationStacks=true") - stage->Export("myPath/cube.fbx") +* `triangulateMeshes`: Use edge information if present to triangulate quads. Default is `true` + + FBX supports quad meshes and there may be additional edge information that can be used to guide the triangulation + on import. The flag controls whether the triangulation should be done at all. + + ``` + from pxr import Usd + stage = Usd.Stage.Open("cube.fbx:SDF_FORMAT_ARGS:triangulateMeshes=false") ``` **Export:** -* `embedImages` Embed images in the exported fbx file instead of as separate files. Default is `false`. - The following exports to `fbx` and embeds images: +* `embedImages`: Embed images in the exported FBX file instead of as separate files. Default is `false`. + + The following exports to FBX and embeds images: ``` - UsdStageRefPtr stage = UsdStage::Open("cube.usd"); - SdfLayer::FileFormatArguments args = { {"embedImages", "true"} }; - stage->Export("cube.fbx", false, args); + from pxr import Usd + stage = Usd.Stage.Open("cube.usd"); + stage.Export("cube.fbx", args={ "embedImages": "true" }); ``` -* `outputColorSpace`: USD uses linear colorspace, however, the original FBX colorspace could be either linear or sRGB. - If fbxOriginalColorSpace was set the fileformat plugin will use it when exporting unless outputColorSpace is specified. - Order or precendence on export (Note: the plugin assumes usd data is linear) - 1. If outputColorSpace=linear, the usd color data is exported as is. - 2. If outputColorSpace=sRGB, the usd color data is converted to sRGB on export - 3. If outputColorSpace is not set and fbxOriginalColorSpace is known, it will export the color data in the original format - 4. If outputColorSpace is not set and fbxOriginalColorSpace is not known, it will export the color data as is. +* `outputColorSpace`: Convert colors from linear to sRGB. Default: `""` + + USD uses linear colorspace, however, the original FBX colorspace could be either linear or sRGB. + If `fbxOriginalColorSpace` was set the fileformat plugin will use it when exporting unless outputColorSpace is specified. + + Order or precendence on export (Note: the plugin assumes USD data is linear) + 1. If `outputColorSpace=linear`, the USD color data is exported as is. + 2. If `outputColorSpace=sRGB`, the USD color data is converted to sRGB on export + 3. If `outputColorSpace` is not set and `fbxOriginalColorSpace` is known, it will export the color data in the original format + 4. If `outputColorSpace` is not set and `fbxOriginalColorSpace` is not known, it will export the color data as is. Example: ``` - UsdStageRefPtr stage = UsdStage::Open("cube.fbx:SDF_FORMAT_ARGS:fbxOriginalColorSpace=sRGB") + from pxr import Usd + stage = Usd.Stage.Open("cube.fbx:SDF_FORMAT_ARGS:fbxOriginalColorSpace=sRGB") # round trip the asset using the original colorspace stage.Export("round_trip_original_cube_srgb.fbx") // exported file will have sRGB colorspace # round trip the asset overriding the original colorspace - stage.Export("round_trip_original_cube_linear.fbx:SDF_FORMAT_ARGS:outputColorSpace=linear") // exported file will have linear colorspace + # the exported file will have a linear colorspace + stage.Export("round_trip_original_cube_linear.fbx", args={ "outputColorSpace": "linear" } ) ``` ## Debug codes diff --git a/fbx/src/fbxExport.cpp b/fbx/src/fbxExport.cpp index 45bd0cf7..94df7316 100644 --- a/fbx/src/fbxExport.cpp +++ b/fbx/src/fbxExport.cpp @@ -12,9 +12,9 @@ governing permissions and limitations under the License. #include "fbxExport.h" #include "debugCodes.h" #include -#include #include #include +#include #include #include @@ -619,6 +619,60 @@ exportFbxMeshes(ExportFbxContext& ctx) } } + // Tangents + if (m.tangents.values.size()) { + FbxGeometryElement::EMappingMode tangentMapping; + if (!exportFbxMapping(m.tangents.interpolation, tangentMapping)) { + TF_WARN( + "Tangents interpolation: %s not supported, defaulting to byControlPoint\n", + m.tangents.interpolation.GetText()); + } + + FbxGeometryElementTangent* elementTangent = fbxMesh->CreateElementTangent(); + elementTangent->SetMappingMode(tangentMapping); + for (size_t j = 0; j < m.tangents.values.size(); j++) { + GfVec4f t = m.tangents.values[j]; + FbxVector4 tangent = FbxVector4(t[0], t[1], t[2], t[3]); + elementTangent->GetDirectArray().Add(tangent); + } + if (m.tangents.indices.size()) { + elementTangent->SetReferenceMode( + FbxGeometryElement::EReferenceMode::eIndexToDirect); + for (size_t j = 0; j < m.tangents.indices.size(); j++) { + elementTangent->GetIndexArray().Add(m.tangents.indices[j]); + } + } else { + elementTangent->SetReferenceMode(FbxGeometryElement::EReferenceMode::eDirect); + } + } + + // Bitangents (export as FBX binormals) + if (m.bitangents.values.size()) { + FbxGeometryElement::EMappingMode binormalMapping; + if (!exportFbxMapping(m.bitangents.interpolation, binormalMapping)) { + TF_WARN( + "Bitangents interpolation: %s not supported, defaulting to byControlPoint\n", + m.bitangents.interpolation.GetText()); + } + + FbxGeometryElementBinormal* elementBinormal = fbxMesh->CreateElementBinormal(); + elementBinormal->SetMappingMode(binormalMapping); + for (size_t j = 0; j < m.bitangents.values.size(); j++) { + GfVec3f b = m.bitangents.values[j]; + FbxVector4 binormal = FbxVector4(b[0], b[1], b[2]); + elementBinormal->GetDirectArray().Add(binormal); + } + if (m.bitangents.indices.size()) { + elementBinormal->SetReferenceMode( + FbxGeometryElement::EReferenceMode::eIndexToDirect); + for (size_t j = 0; j < m.bitangents.indices.size(); j++) { + elementBinormal->GetIndexArray().Add(m.bitangents.indices[j]); + } + } else { + elementBinormal->SetReferenceMode(FbxGeometryElement::EReferenceMode::eDirect); + } + } + // Uvs if (m.uvs.values.size()) { FbxGeometryElementUV* elementUvs = @@ -907,17 +961,14 @@ exportFbxInput(ExportFbxContext& ctx, fbxTexture->SetSwapUV(false); fbxTexture->UVSet.Set(FbxString(getSTPrimvarAttrToken(input.uvIndex).GetText())); - if (input.transformScale.IsHolding()) { - GfVec2f scale = input.transformScale.UncheckedGet(); - fbxTexture->SetScale(scale[0], scale[1]); + if (input.uvScale != kDefaultUvScale) { + fbxTexture->SetScale(input.uvScale[0], input.uvScale[1]); } - if (input.transformRotation.IsHolding()) { - float rot = input.transformRotation.UncheckedGet(); - fbxTexture->SetRotation(0.0, 0.0, rot); + if (input.uvRotation != kDefaultUvRotation) { + fbxTexture->SetRotation(0.0, 0.0, input.uvRotation); } - if (input.transformTranslation.IsHolding()) { - GfVec2f trans = input.transformTranslation.UncheckedGet(); - fbxTexture->SetTranslation(trans[0], trans[1]); + if (input.uvTranslation != kDefaultUvTranslation) { + fbxTexture->SetTranslation(input.uvTranslation[0], input.uvTranslation[1]); } property.ConnectSrcObject(fbxTexture); diff --git a/fbx/src/fbxImport.cpp b/fbx/src/fbxImport.cpp index 2aa91557..eaad37e8 100644 --- a/fbx/src/fbxImport.cpp +++ b/fbx/src/fbxImport.cpp @@ -85,7 +85,20 @@ struct ImportFbxContext void importMetadata(ImportFbxContext& ctx) { - ctx.usd->metadata.SetValueAtPath("generator", PXR_NS::VtValue("Adobe usdFbx 1.0")); + std::string generator = "Adobe usdFbx 1.0"; + + FbxDocumentInfo* docInfo = ctx.fbx->scene->GetDocumentInfo(); + if (docInfo) { + FbxString origAppName = docInfo->Original_ApplicationName.Get(); + + // If the FBX specified a generator, add it to the USD generator string + if (!origAppName.IsEmpty()) { + generator += "; FBX generator: "; + generator += origAppName.Buffer(); + } + } + + ctx.usd->metadata.SetValueAtPath("generator", PXR_NS::VtValue(generator)); if (!ctx.options->originalColorSpace.IsEmpty()) { ctx.usd->metadata.SetValueAtPath(AdobeTokens->originalColorSpace, PXR_NS::VtValue(ctx.options->originalColorSpace)); @@ -399,6 +412,60 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) } } + // Tangents + FbxGeometryElementTangent* tangentElement = fbxMesh->GetElementTangent(); + if (tangentElement != nullptr) { + mesh.tangents.interpolation = fbxGetInterpolation(tangentElement->GetMappingMode()); + if (tangentElement->GetReferenceMode() == FbxGeometryElement::eDirect) { + size_t tangentCount = tangentElement->GetDirectArray().GetCount(); + mesh.tangents.values.resize(tangentCount); + for (size_t i = 0; i < tangentCount; i++) { + FbxVector4 tangent = tangentElement->GetDirectArray().GetAt(i); + mesh.tangents.values[i] = GfVec4f{ static_cast(tangent[0]), + static_cast(tangent[1]), + static_cast(tangent[2]), + static_cast(tangent[3]) }; + } + } else { // FbxGeometryElement::eIndexToDirect + size_t tangentCount = tangentElement->GetIndexArray().GetCount(); + mesh.tangents.values.resize(tangentCount); + for (size_t i = 0; i < tangentCount; i++) { + int tangentIndex = tangentElement->GetIndexArray().GetAt(i); + FbxVector4 tangent = tangentElement->GetDirectArray().GetAt(tangentIndex); + mesh.tangents.values[i] = GfVec4f{ static_cast(tangent[0]), + static_cast(tangent[1]), + static_cast(tangent[2]), + static_cast(tangent[3]) }; + } + } + } + + // Bitangents (read from FBX binormals) + FbxGeometryElementBinormal* binormalElement = fbxMesh->GetElementBinormal(); + if (binormalElement != nullptr) { + mesh.bitangents.interpolation = fbxGetInterpolation(binormalElement->GetMappingMode()); + if (binormalElement->GetReferenceMode() == FbxGeometryElement::eDirect) { + size_t binormalCount = binormalElement->GetDirectArray().GetCount(); + mesh.bitangents.values.resize(binormalCount); + for (size_t i = 0; i < binormalCount; i++) { + FbxVector4 binormal = binormalElement->GetDirectArray().GetAt(i); + mesh.bitangents.values[i] = GfVec3f{ static_cast(binormal[0]), + static_cast(binormal[1]), + static_cast(binormal[2]) }; + } + } else { // FbxGeometryElement::eIndexToDirect + size_t binormalCount = binormalElement->GetIndexArray().GetCount(); + mesh.bitangents.values.resize(binormalCount); + for (size_t i = 0; i < binormalCount; i++) { + int binormalIndex = binormalElement->GetIndexArray().GetAt(i); + FbxVector4 binormal = binormalElement->GetDirectArray().GetAt(binormalIndex); + mesh.bitangents.values[i] = GfVec3f{ static_cast(binormal[0]), + static_cast(binormal[1]), + static_cast(binormal[2]) }; + } + } + } + // Uvs size_t elementUVsCount = fbxMesh->GetElementUVCount(); size_t numUVsets = 0; @@ -621,46 +688,64 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) if (fbxNode != nullptr) { int materialCount = fbxNode->GetMaterialCount(); int elementMaterialCount = fbxMesh->GetElementMaterialCount(); - for (int i = 0; i < elementMaterialCount; i++) { - if (i >= 1) { - TF_WARN("Mesh[%s].material[%d] Multiple material layers not supported\n", - mesh.name.c_str(), - i); - break; + if (elementMaterialCount == 0 && materialCount > 0) { + // If there are no element materials, FBX defaults to using the first material for the + // whole mesh + TF_DEBUG_MSG(FILE_FORMAT_FBX, + "Mesh[%s] has no material elements. Defaulting to use first material\n", + mesh.name.c_str()); + + FbxSurfaceMaterial* fbxMaterial = fbxNode->GetMaterial(0); + const auto& it = ctx.materials.find(fbxMaterial); + if (it != ctx.materials.end()) { + mesh.material = it->second; + } else { + TF_WARN("Mesh[%s] has no material element, and the (default) first material " + "could not be found. No materials will be linked\n", + mesh.name.c_str()); } - FbxGeometryElementMaterial* material = fbxMesh->GetElementMaterial(i); - FbxLayerElement::EMappingMode mappingMode = material->GetMappingMode(); - if (mappingMode == FbxLayerElement::EMappingMode::eNone) { - TF_DEBUG_MSG(FILE_FORMAT_FBX, "None material mapping mode found\n"); - } else if (mappingMode == FbxLayerElement::EMappingMode::eByControlPoint) { - TF_DEBUG_MSG(FILE_FORMAT_FBX, - "byControlPoint material mapping mode not supported\n"); - } else if (mappingMode == FbxLayerElement::EMappingMode::eByPolygonVertex) { - TF_DEBUG_MSG(FILE_FORMAT_FBX, - "byPolygonVertex material mapping mode not supported\n"); - } else if (mappingMode == FbxLayerElement::EMappingMode::eByPolygon) { - for (int i = 0; i < materialCount; i++) { - auto [subsetIndex, subset] = ctx.usd->addSubset(meshIndex); + } else { + for (int i = 0; i < elementMaterialCount; i++) { + if (i >= 1) { + TF_WARN("Mesh[%s].material[%d] Multiple material layers not supported\n", + mesh.name.c_str(), + i); + break; + } + FbxGeometryElementMaterial* material = fbxMesh->GetElementMaterial(i); + FbxLayerElement::EMappingMode mappingMode = material->GetMappingMode(); + if (mappingMode == FbxLayerElement::EMappingMode::eNone) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, "None material mapping mode found\n"); + } else if (mappingMode == FbxLayerElement::EMappingMode::eByControlPoint) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, + "byControlPoint material mapping mode not supported\n"); + } else if (mappingMode == FbxLayerElement::EMappingMode::eByPolygonVertex) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, + "byPolygonVertex material mapping mode not supported\n"); + } else if (mappingMode == FbxLayerElement::EMappingMode::eByPolygon) { + for (int i = 0; i < materialCount; i++) { + auto [subsetIndex, subset] = ctx.usd->addSubset(meshIndex); + FbxSurfaceMaterial* fbxMaterial = fbxNode->GetMaterial(i); + const auto& it = ctx.materials.find(fbxMaterial); + if (it != ctx.materials.end()) { + subset.material = it->second; + } + for (int j = 0; j < material->GetIndexArray().GetCount(); j++) { + int index = material->GetIndexArray().GetAt(j); + if (index == i) { + subset.faces.push_back(j); + } + } + } + } else if (mappingMode == FbxLayerElement::EMappingMode::eByEdge) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, "byEdge material mapping mode not supported\n"); + } else if (mappingMode == FbxLayerElement::EMappingMode::eAllSame) { FbxSurfaceMaterial* fbxMaterial = fbxNode->GetMaterial(i); const auto& it = ctx.materials.find(fbxMaterial); if (it != ctx.materials.end()) { - subset.material = it->second; - } - for (int j = 0; j < material->GetIndexArray().GetCount(); j++) { - int index = material->GetIndexArray().GetAt(j); - if (index == i) { - subset.faces.push_back(j); - } + mesh.material = it->second; } } - } else if (mappingMode == FbxLayerElement::EMappingMode::eByEdge) { - TF_DEBUG_MSG(FILE_FORMAT_FBX, "byEdge material mapping mode not supported\n"); - } else if (mappingMode == FbxLayerElement::EMappingMode::eAllSame) { - FbxSurfaceMaterial* fbxMaterial = fbxNode->GetMaterial(i); - const auto& it = ctx.materials.find(fbxMaterial); - if (it != ctx.materials.end()) { - mesh.material = it->second; - } } } printMesh("importFbx:", mesh, DEBUG_TAG); @@ -708,17 +793,17 @@ importPropFileTexture(ImportFbxContext& ctx, double su = texture->GetScaleU(); double sv = texture->GetScaleV(); - if (su != 1 || sv != 1) { - input.transformScale = GfVec2f(su, sv); + if (su != 1.0 || sv != 1.0) { + input.uvScale = GfVec2f(su, sv); } double rot = texture->GetRotationW(); - if (rot != 0) { - input.transformRotation = rot; + if (rot != 0.0) { + input.uvRotation = rot; } double tu = texture->GetTranslationU(); double tv = texture->GetTranslationV(); - if (tu != 0 || tv != 0) { - input.transformTranslation = GfVec2f(tu, tv); + if (tu != 0.0 || tv != 0.0) { + input.uvTranslation = GfVec2f(tu, tv); } } } @@ -1085,6 +1170,200 @@ _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, return true; } +bool +_processHardwareShaderMaterial(const FbxSurfaceMaterial* fbxMaterial, + ImportFbxContext& ctx, + const std::unordered_map& textures, + Material& usdMaterial, + InputTranslator& inputTranslator) +{ + TF_DEBUG_MSG(FILE_FORMAT_FBX, "Attempting hardware shader material processing for '%s'\n", + fbxMaterial->GetName()); + + bool foundAnyProperties = false; + + FbxProperty prop = fbxMaterial->GetFirstProperty(); + int propertyIndex = 0; + + while (prop.IsValid()) { + auto propType = prop.GetPropertyDataType(); + + // Check for ColorAndAlpha properties (typical for 3ds Max materials) + if (propType.GetType() == eFbxDouble4) { + auto typedProperty = static_cast>(prop); + FbxDouble4 colorWithAlpha = typedProperty.Get(); + GfVec3f colorValue(colorWithAlpha[0], colorWithAlpha[1], colorWithAlpha[2]); + + // (3ds Max Physical Material stores base_color as the first ColorAndAlpha property) + if (colorValue != GfVec3f(0, 0, 0) && !usdMaterial.diffuseColor.value.IsHolding()) { + usdMaterial.diffuseColor.value = colorValue; + usdMaterial.diffuseColor.colorspace = AdobeTokens->sRGB; + TF_DEBUG_MSG(FILE_FORMAT_FBX, " Found color at property index %d: (%f, %f, %f)\n", + propertyIndex, colorValue[0], colorValue[1], colorValue[2]); + foundAnyProperties = true; + } + } + + prop = fbxMaterial->GetNextProperty(prop); + propertyIndex++; + } + + return foundAnyProperties; +} + +// Fallback processor for materials with unknown ShadingModel that fail Lambert/Phong casting. +// This uses property-based detection to extract common material properties regardless of +// FBX material type classification. +// Returns true if the material was successfully processed as a property-based material +bool +_processUnknownShadingModel(const FbxSurfaceMaterial* fbxMaterial, + ImportFbxContext& ctx, + const std::unordered_map& textures, + Material& usdMaterial, + InputTranslator& inputTranslator) +{ + TF_DEBUG_MSG(FILE_FORMAT_FBX, + "Processing material '%s' with unknown ShadingModel using property-based approach\n", + fbxMaterial->GetName()); + + bool foundAnyProperties = false; + + // Helper to safely extract color properties with multiple naming conventions + auto extractColorProperty = [&](const std::vector& names, Input& targetInput) -> bool { + for (const std::string& propName : names) { + auto property = FbxSurfaceMaterialUtils::GetProperty(propName.c_str(), fbxMaterial); + if (property.IsValid()) { + // Check for both FbxDouble3DT and ColorRGB types (more flexible) + auto propType = property.GetPropertyDataType(); + TF_DEBUG_MSG(FILE_FORMAT_FBX, " Checking property '%s' (type: %s)\n", + propName.c_str(), propType.GetName()); + + // Check if property is compatible with FbxDouble3 or FbxDouble4 (ColorAndAlpha) + if (propType.GetType() == eFbxDouble3) { + auto typedProperty = static_cast>(property); + GfVec3f colorValue = readPropValue(typedProperty); + if (colorValue != GfVec3f(0, 0, 0)) { // Skip if all zeros + targetInput.value = colorValue; + targetInput.colorspace = AdobeTokens->sRGB; + TF_DEBUG_MSG(FILE_FORMAT_FBX, " Found color property '%s': (%f, %f, %f)\n", + propName.c_str(), colorValue[0], colorValue[1], colorValue[2]); + return true; + } + } else if (propType.GetType() == eFbxDouble4) { + // Handle ColorAndAlpha (FbxDouble4) - extract RGB, ignore alpha + auto typedProperty = static_cast>(property); + FbxDouble4 colorWithAlpha = typedProperty.Get(); + GfVec3f colorValue(colorWithAlpha[0], colorWithAlpha[1], colorWithAlpha[2]); + if (colorValue != GfVec3f(0, 0, 0)) { // Skip if all zeros + targetInput.value = colorValue; + targetInput.colorspace = AdobeTokens->sRGB; + TF_DEBUG_MSG(FILE_FORMAT_FBX, " Found ColorAndAlpha property '%s': (%f, %f, %f, alpha=%f)\n", + propName.c_str(), colorValue[0], colorValue[1], colorValue[2], colorWithAlpha[3]); + return true; + } + } else { + TF_DEBUG_MSG(FILE_FORMAT_FBX, " Property '%s' has incompatible type '%s', expected FbxDouble3 or FbxDouble4\n", + propName.c_str(), propType.GetName()); + } + } + } + return false; + }; + + // Helper to safely extract scalar properties with multiple naming conventions + auto extractScalarProperty = [&](const std::vector& names, Input& targetInput) -> bool { + for (const std::string& propName : names) { + auto property = FbxSurfaceMaterialUtils::GetProperty(propName.c_str(), fbxMaterial); + if (property.IsValid() && property.GetPropertyDataType() == FbxDoubleDT) { + double scalarValue = property.Get(); + if (scalarValue > 0.0) { // Skip if zero or negative + targetInput.value = static_cast(scalarValue); + targetInput.colorspace = AdobeTokens->raw; + TF_DEBUG_MSG(FILE_FORMAT_FBX, " Found scalar property '%s': %f\n", + propName.c_str(), scalarValue); + return true; + } + } + } + return false; + }; + + // Debug: List all properties available on this material + if (TfDebug::IsEnabled(FILE_FORMAT_FBX)) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, "Debugging properties for material '%s':\n", fbxMaterial->GetName()); + FbxProperty prop = fbxMaterial->GetFirstProperty(); + int propertyCount = 0; + while (prop.IsValid()) { + const char* propName = prop.GetName(); + auto propType = prop.GetPropertyDataType(); + TF_DEBUG_MSG(FILE_FORMAT_FBX, " Property[%d]: '%s' (type: %s)\n", + propertyCount++, propName, propType.GetName()); + prop = fbxMaterial->GetNextProperty(prop); + } + } + + // Try to extract diffuse/base color with the exact property names from FBX ASCII analysis + const std::vector colorNames = { + // 3ds Max Physical Material properties + "3dsMax|Parameters|base_color", + // PRIMARY: Properties confirmed in FBX ASCII + "DiffuseColor", // All materials have this exact property + "AmbientColor", // Fallback color property + // SECONDARY: Common variations + "base_color", "baseColor", "BaseColor", + "diffuseColor", "diffuse_color", + "Color", "color", "Diffuse", "diffuse" + }; + + if (extractColorProperty(colorNames, usdMaterial.diffuseColor)) { + foundAnyProperties = true; + } + + // Try to extract metallic with various naming conventions + const std::vector metallicNames = { + "3dsMax|Parameters|metalness", + "metallic", "Metallic", "metalness", "Metalness", + "metal", "Metal" + }; + + if (extractScalarProperty(metallicNames, usdMaterial.metallic)) { + foundAnyProperties = true; + } + + // Try to extract roughness with various naming conventions + const std::vector roughnessNames = { + "3dsMax|Parameters|roughness", + "roughness", "Roughness", "specular_roughness", "SpecularRoughness", + "surface_roughness", "SurfaceRoughness" + }; + + if (extractScalarProperty(roughnessNames, usdMaterial.roughness)) { + foundAnyProperties = true; + } + + // Try to extract emissive color with various naming conventions + const std::vector emissiveNames = { + "emissive", "Emissive", "emissive_color", "EmissiveColor", + "emission", "Emission", "emission_color", "EmissionColor" + }; + + if (extractColorProperty(emissiveNames, usdMaterial.emissiveColor)) { + foundAnyProperties = true; + } + + if (foundAnyProperties) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, + "Successfully processed material '%s' using property-based approach\n", + fbxMaterial->GetName()); + return true; + } + + TF_DEBUG_MSG(FILE_FORMAT_FBX, + "No recognizable properties found for material '%s'\n", + fbxMaterial->GetName()); + return false; +} + // Returns a normalized path, using '/' as the separator. Note that if a component of the file path // has a '\' on POSIX systems, this would misinterpret that '\' as a directory separator. But // given that we don't know what OS the original path came from, this replacement is helpful @@ -1258,39 +1537,30 @@ importFbxMaterials(ImportFbxContext& ctx) continue; } - const FbxImplementation* imp = LookForNonSupportedImplementation(material); - if (imp) { // This is a hardware shader - TF_WARN("Hardware shader not supported\n"); - TF_DEBUG_MSG(FILE_FORMAT_FBX, " Language: %s\n", imp->Language.Get().Buffer()); - TF_DEBUG_MSG( - FILE_FORMAT_FBX, " LanguageVersion: %s\n", imp->LanguageVersion.Get().Buffer()); - TF_DEBUG_MSG(FILE_FORMAT_FBX, " RenderName: %s\n", imp->RenderName.Buffer()); - TF_DEBUG_MSG(FILE_FORMAT_FBX, " RenderAPI: %s\n", imp->RenderAPI.Get().Buffer()); - TF_DEBUG_MSG( - FILE_FORMAT_FBX, " RenderAPIVersion: %s\n", imp->RenderAPIVersion.Get().Buffer()); - const FbxBindingTable* lRootTable = imp->GetRootTable(); - FbxString filename = lRootTable->DescAbsoluteURL.Get(); - FbxString techniqueName = lRootTable->DescTAG.Get(); - continue; - } - - Input ambientFactor; - Input diffuse; - Input diffuseFactor; - Input emissive; - Input emissiveFactor; - Input normal; - Input bump; - Input transparentColor; - Input transparencyFactor; - Input shininess; - Input specular; - Input specularFactor; - Input reflectionFactor; - + // Try traditional Lambert/Phong casting FbxSurfaceLambert* lambert = FbxCast(material); FbxSurfacePhong* phong = FbxCast(material); - if (lambert) { + + if (lambert || phong) { + // Traditional material processing - MOVED UP from below + TF_DEBUG_MSG(FILE_FORMAT_FBX, "Processing '%s' as Lambert/Phong material\n", + material->GetName()); + + Input ambientFactor; + Input diffuse; + Input diffuseFactor; + Input emissive; + Input emissiveFactor; + Input normal; + Input bump; + Input transparentColor; + Input transparencyFactor; + Input shininess; + Input specular; + Input specularFactor; + Input reflectionFactor; + + if (lambert) { importPropTexture(ctx, textures, material, @@ -1404,7 +1674,45 @@ importFbxMaterials(ImportFbxContext& ctx) } } - inputTranslator.translateNormals(bump, normal, um.normal); + inputTranslator.translateNormals(bump, normal, um.normal); + } else { + // Elegant fallback: Try property-based processing for materials that failed Lambert/Phong casting + TF_DEBUG_MSG(FILE_FORMAT_FBX, "Lambert/Phong casting failed for '%s', trying fallback approaches\n", + material->GetName()); + + // First check if it's a hardware shader + const FbxImplementation* imp = LookForNonSupportedImplementation(material); + if (imp) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, "Detected hardware shader for '%s'\n", material->GetName()); + TF_DEBUG_MSG(FILE_FORMAT_FBX, " Language: %s\n", imp->Language.Get().Buffer()); + TF_DEBUG_MSG(FILE_FORMAT_FBX, " LanguageVersion: %s\n", imp->LanguageVersion.Get().Buffer()); + TF_DEBUG_MSG(FILE_FORMAT_FBX, " RenderName: %s\n", imp->RenderName.Buffer()); + TF_DEBUG_MSG(FILE_FORMAT_FBX, " RenderAPI: %s\n", imp->RenderAPI.Get().Buffer()); + TF_DEBUG_MSG(FILE_FORMAT_FBX, " RenderAPIVersion: %s\n", imp->RenderAPIVersion.Get().Buffer()); + + // Try to extract properties from hardware shader + if (_processHardwareShaderMaterial(material, ctx, textures, um, inputTranslator)) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, "Successfully processed hardware shader '%s'\n", + material->GetName()); + continue; + } + + TF_WARN("Hardware shader '%s' detected but no properties could be extracted\n", + material->GetName()); + continue; + } + + // Try standard property-based fallback for non-hardware shader materials + if (_processUnknownShadingModel(material, ctx, textures, um, inputTranslator)) { + TF_DEBUG_MSG(FILE_FORMAT_FBX, "Successfully processed '%s' using property-based fallback\n", + material->GetName()); + continue; + } + + // If we get here, the material couldn't be processed by any method + TF_WARN("Unable to process material '%s' - no recognizable properties found\n", + material->GetName()); + } } ctx.usd->images = std::move(inputTranslator.getImages()); } @@ -2010,7 +2318,14 @@ setSkeletonParents(ImportFbxContext& ctx) { int skeletonIndex = 0; for (const ImportedFbxSkeleton& skeleton : ctx.skeletons) { - int parentIndex = ctx.nodeMap[skeleton.fbxParent]; + // Note: - if the skeleton was parented to FBX's artificial root node (which we do not + // represent in USD) we will not find it in the nodeMap. In that case it is better to parent + // it to the root node- we can use a parent index of -1 to do this. + int parentIndex = -1; + if (ctx.nodeMap.find(skeleton.fbxParent) != ctx.nodeMap.end()) { + parentIndex = ctx.nodeMap[skeleton.fbxParent]; + } + ctx.usd->skeletons[skeletonIndex].parent = parentIndex; skeletonIndex++; } @@ -2238,7 +2553,8 @@ importFbx(const ImportFbxOptions& options, Fbx& fbx, UsdData& usd) importFbxMaterials(ctx); } if (options.importGeometry) { - triangulateMeshes(ctx); + if (options.triangulateMeshes) + triangulateMeshes(ctx); loadAnimLayers(ctx); importFBXSkeletons(ctx); importFbxNodeHierarchy(ctx); diff --git a/fbx/src/fbxImport.h b/fbx/src/fbxImport.h index 181573fc..440f3549 100644 --- a/fbx/src/fbxImport.h +++ b/fbx/src/fbxImport.h @@ -22,6 +22,7 @@ struct ImportFbxOptions bool importImages = true; bool importPhong = false; bool importAnimationStacks = false; + bool triangulateMeshes = true; PXR_NS::TfToken originalColorSpace; }; @@ -56,4 +57,4 @@ enum class FbxPropertyNumChannels bool importFbx(const ImportFbxOptions& options, Fbx& fbx, UsdData& usd); -} \ No newline at end of file +} diff --git a/fbx/src/fileFormat.cpp b/fbx/src/fileFormat.cpp index 0159322c..52b9b1d0 100644 --- a/fbx/src/fileFormat.cpp +++ b/fbx/src/fileFormat.cpp @@ -17,7 +17,6 @@ governing permissions and limitations under the License. #include "fbxImport.h" #include - #include #include #include @@ -31,10 +30,11 @@ using namespace adobe::usd; PXR_NAMESPACE_OPEN_SCOPE static std::mutex mutex; +const TfToken UsdFbxFileFormat::animationStacksToken("fbxAnimationStacks", TfToken::Immortal); const TfToken UsdFbxFileFormat::assetsPathToken("fbxAssetsPath", TfToken::Immortal); -const TfToken UsdFbxFileFormat::phongToken("fbxPhong", TfToken::Immortal); const TfToken UsdFbxFileFormat::originalColorSpaceToken("fbxOriginalColorSpace", TfToken::Immortal); -const TfToken UsdFbxFileFormat::animationStacksToken("fbxAnimationStacks", TfToken::Immortal); +const TfToken UsdFbxFileFormat::phongToken("fbxPhong", TfToken::Immortal); +const TfToken UsdFbxFileFormat::triangulateMeshesToken("triangulateMeshes", TfToken::Immortal); TF_DEFINE_PUBLIC_TOKENS(UsdFbxFileFormatTokens, USDFBX_FILE_FORMAT_TOKENS); @@ -62,11 +62,17 @@ UsdFbxFileFormat::InitData(const FileFormatArguments& args) const TF_DEBUG_MSG( FILE_FORMAT_FBX, "FileFormatArg: %s = %s\n", arg.first.c_str(), arg.second.c_str()); } - argReadBool(args, AdobeTokens->writeMaterialX.GetString(), pd->writeMaterialX, DEBUG_TAG); + pd->parseFromFileFormatArgs(args, DEBUG_TAG); + + // "fbxAssetsPath" is deprecated in favor of the universal "assetsPath" argument - 2025-3-18 + // If both are present, "fbxAssetsPath" is stronger. argReadString(args, assetsPathToken.GetString(), pd->assetsPath, DEBUG_TAG); + argWarnDeprecatedArg(args, assetsPathToken.GetString(), DEBUG_TAG); + + argReadBool(args, animationStacksToken.GetString(), pd->animationStacks, DEBUG_TAG); argReadBool(args, phongToken.GetString(), pd->phong, DEBUG_TAG); + argReadBool(args, triangulateMeshesToken.GetString(), pd->triangulateMeshes, DEBUG_TAG); argReadString(args, originalColorSpaceToken.GetString(), pd->originalColorSpace, DEBUG_TAG); - argReadBool(args, animationStacksToken.GetString(), pd->animationStacks, DEBUG_TAG); return pd; } void @@ -75,8 +81,10 @@ UsdFbxFileFormat::ComposeFieldsForFileFormatArguments(const std::string& assetPa FileFormatArguments* args, VtValue* dependencyContextData) const { + argComposeBool(context, args, animationStacksToken, DEBUG_TAG); argComposeString(context, args, assetsPathToken, DEBUG_TAG); argComposeBool(context, args, phongToken, DEBUG_TAG); + argComposeBool(context, args, triangulateMeshesToken, DEBUG_TAG); argComposeString(context, args, originalColorSpaceToken, DEBUG_TAG); } @@ -112,9 +120,8 @@ UsdFbxFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool me options.importImages = !data->assetsPath.empty(); options.importPhong = data->phong; options.originalColorSpace = data->originalColorSpace; - WriteLayerOptions layerOptions; - layerOptions.writeMaterialX = data->writeMaterialX; - layerOptions.assetsPath = data->assetsPath; + options.triangulateMeshes = data->triangulateMeshes; + WriteLayerOptions layerOptions(*data); layerOptions.animationTracks = data->animationStacks; { const std::lock_guard lock(mutex); // FBX SDK is not thread safe diff --git a/fbx/src/fileFormat.h b/fbx/src/fileFormat.h index c852fda9..8fc518cc 100644 --- a/fbx/src/fileFormat.h +++ b/fbx/src/fileFormat.h @@ -10,6 +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 + #ifdef _MSC_VER // Disable warnings in the pxr headers. // conversion from 'double' to 'float', possible loss of data @@ -19,12 +20,15 @@ governing permissions and limitations under the License. // truncation from 'double' to 'float' # pragma warning(disable : 4305) #endif // _MSC_VER + #include "api.h" -#include #include + #include #include #include + +#include #include #include @@ -43,9 +47,9 @@ TF_DECLARE_WEAK_AND_REF_PTRS(FbxData); class FbxData : public FileFormatDataBase { public: - std::string assetsPath; - bool phong = false; bool animationStacks = false; + bool phong = false; + bool triangulateMeshes = true; TfToken originalColorSpace; static FbxDataRefPtr InitData(const SdfFileFormat::FileFormatArguments& args); }; @@ -95,14 +99,15 @@ class USDFBX_API UsdFbxFileFormat const FileFormatArguments& args = FileFormatArguments()) const override; protected: + static const TfToken animationStacksToken; static const TfToken assetsPathToken; - static const TfToken phongToken; static const TfToken originalColorSpaceToken; - static const TfToken animationStacksToken; + static const TfToken phongToken; + static const TfToken triangulateMeshesToken; SDF_FILE_FORMAT_FACTORY_ACCESS; ~UsdFbxFileFormat() override; UsdFbxFileFormat(); }; -PXR_NAMESPACE_CLOSE_SCOPE \ No newline at end of file +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/fbx/src/plugInfo.json.in b/fbx/src/plugInfo.json.in index d3c3c5ca..e7952d0d 100644 --- a/fbx/src/plugInfo.json.in +++ b/fbx/src/plugInfo.json.in @@ -4,21 +4,21 @@ "Info": { "SdfMetadata": { "fbxAssetsPath": { - "appliesTo": [ "prims" ], - "displayGroup": "Core", - "documentation:": "Path to store assets to, instead of resolving from the source file", + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Path to store assets to, instead of resolving from the source file", "type": "string" }, "fbxPhong": { - "appliesTo": [ "prims" ], - "displayGroup": "Core", - "documentation:": "Whether to import phong (by default conversion only keeps the diffuse component)", + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Whether to import phong (by default conversion only keeps the diffuse component)", "type": "bool" }, "fbxAnimationStacks": { - "appliesTo": [ "prims" ], - "displayGroup": "Core", - "documentation:": "Whether to import multiple animation stacks (by default only the first stack is imported)", + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Whether to import multiple animation stacks (by default only the first stack is imported)", "type": "bool" }, "fbxOriginalColorSpace": { @@ -26,6 +26,12 @@ "displayGroup": "Core", "documentation:": "Is the original colorspace in linear or sRGB", "type": "string" + }, + "triangulateMeshes": { + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Whether to perform mesh triangulation at import", + "type": "bool" } }, "Types": { diff --git a/fbx/tests/CMakeLists.txt b/fbx/tests/CMakeLists.txt index fe745cab..70843b4a 100644 --- a/fbx/tests/CMakeLists.txt +++ b/fbx/tests/CMakeLists.txt @@ -1,6 +1,6 @@ include(GoogleTest) -add_executable(fbxSanityTests sanityTests.cpp) +add_executable(fbxSanityTests sanityTests.cpp util.cpp) usd_plugin_compile_config(fbxSanityTests) @@ -10,6 +10,7 @@ PRIVATE usd GTest::gtest GTest::gtest_main + fbxsdk::fbxsdk ) gtest_add_tests(TARGET fbxSanityTests AUTO) diff --git a/fbx/tests/sanityTests.cpp b/fbx/tests/sanityTests.cpp index f707e3e7..cc7de75a 100644 --- a/fbx/tests/sanityTests.cpp +++ b/fbx/tests/sanityTests.cpp @@ -17,6 +17,10 @@ governing permissions and limitations under the License. #include #include +#include + +#include "util.h" + TEST(Sanity, LoadCube) { PXR_NAMESPACE_USING_DIRECTIVE @@ -27,3 +31,20 @@ TEST(Sanity, LoadCube) UsdPrim mesh = stage->GetPrimAtPath(SdfPath("/SanityCube/Cube")); ASSERT_TRUE(mesh); } + +TEST(Sanity, ExportCube) +{ + PXR_NAMESPACE_USING_DIRECTIVE + + FbxScene* scene = getFbxSceneFromUsd("cube.usd"); + ASSERT_TRUE(scene); + + // Start the recursive traversal from the root node + std::vector paths = getFbxNodePaths(scene); + + // Check if the expected path exist in the FBX scene + ASSERT_TRUE(std::find(paths.begin(), paths.end(), "/RootNode/Cube") != paths.end()); + + // Cleanup + scene->Destroy(); +} \ No newline at end of file diff --git a/fbx/tests/util.cpp b/fbx/tests/util.cpp new file mode 100644 index 00000000..c05bcf7c --- /dev/null +++ b/fbx/tests/util.cpp @@ -0,0 +1,259 @@ +/* +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 "util.h" + +#include + +#include +#include + +using namespace PXR_NS; + +FbxLoaderSingleton::~FbxLoaderSingleton() +{ + // TODO: add callback to load images + // if (readCallback) { + // readCallback->Destroy(); + // } + if (manager) { + manager->Destroy(); + } +} + +FbxLoaderSingleton& +FbxLoaderSingleton::getInstance() +{ + static FbxLoaderSingleton instance; + return instance; +} + +FbxScene* +FbxLoaderSingleton::loadScene(std::string filename) +{ + std::lock_guard lock(mFbxLoaderMutex); + if (!manager) { + TF_WARN("ERROR: FBX manager not initialized\n"); + return nullptr; + } + + FbxImporter* importer = FbxImporter::Create(manager, IOSROOT); + if (!importer) { + TF_WARN("ERROR: FBX importer could not be initialized\n"); + return nullptr; + } + + FbxIOSettings* ios = FbxIOSettings::Create(manager, IOSROOT); + if (!ios) { + TF_WARN("Failed to create FbxIOSettings\n"); + importer->Destroy(); + return nullptr; + } + + FbxScene* scene = FbxScene::Create(manager, "root"); + + bool onlyMaterials = false; + bool importImages = true; // TODO: use this when adding callback below + ios->SetBoolProp(IMP_FBX_MATERIAL, true); + ios->SetBoolProp(IMP_FBX_TEXTURE, true); + ios->SetBoolProp(IMP_FBX_ANIMATION, !onlyMaterials); + ios->SetBoolProp(IMP_FBX_MODEL, !onlyMaterials); + + if (!importer->Initialize(filename.c_str(), -1, ios)) { + FbxString error = importer->GetStatus().GetErrorString(); + TF_WARN("Call to FbxImporter::Initialize() failed on opening file %s\n", filename.c_str()); + TF_WARN("Error returned: %s\n", error.Buffer()); + importer->Destroy(); + ios->Destroy(); + if (scene) { + scene->Destroy(); + } + return nullptr; + } + + // TODO: add callback. The following code has been copied from fbx.cpp as an example + + // // Create the read callback to handle loading embedded data (ie images) + // FbxEmbeddedFileCallback* readCallback = + // FbxEmbeddedFileCallback::Create(fbx.manager, "EmbeddedFileReadCallback"); + + // if (!readCallback) { + // TF_RUNTIME_ERROR(FILE_FORMAT_FBX, "Failed to create FbxEmbeddedFileCallback"); + // importer->Destroy(); + // ios->Destroy(); + // return false; + // } + + // readCallback->RegisterReadFunction(EmbedReadCBFunction, (void*)&fbx); + // importer->SetEmbeddedFileReadCallback(readCallback); + + // // let fbx own readCallback + // fbx.readCallback = readCallback; + + if (!importer->Import(scene)) { + FbxString error = importer->GetStatus().GetErrorString(); + TF_WARN("Call to FbxImporter::Import() failed.\n"); + TF_WARN("Error returned: %s\n", error.Buffer()); + importer->Destroy(); + ios->Destroy(); + if (scene) { + scene->Destroy(); + } + return nullptr; + } + + importer->Destroy(); + ios->Destroy(); + return scene; +} + +FbxLoaderSingleton::FbxLoaderSingleton() +{ + manager = FbxManager::Create(); + if (!manager) { + TF_WARN("ERROR: Unable to create FBX manager\n"); + } +} + +FbxScene* +getFbxSceneFromUsd(const std::filesystem::path& usdFilepath, + const std::filesystem::path& tempDirName) +{ + std::filesystem::path fbxFilename = usdFilepath.filename(); + fbxFilename.replace_extension(".fbx"); + + // Create a temporary folder + std::filesystem::path tempDir = usdFilepath.parent_path() / tempDirName; + if (!std::filesystem::exists(tempDir)) { + std::filesystem::create_directory(tempDir); + } + std::filesystem::path fbxPath = tempDir / fbxFilename; + + // Convert USD to FBX + UsdStageRefPtr stage = UsdStage::Open(usdFilepath.string()); + if (!stage) { + TF_WARN("Failed to open USD stage"); + return nullptr; + } + stage->Export(fbxPath.string()); + + // Initialize the FBX loader + auto& fbxLoader = FbxLoaderSingleton::getInstance(); + + // Load the FBX file + FbxScene* fbxScene = fbxLoader.loadScene(fbxPath.string()); + + // Delete the fbx file now that it's been loaded + std::filesystem::remove(fbxPath); + + // Delete the temporary folder + std::filesystem::remove(tempDir); + + return fbxScene; +} + +FbxNode* +getFbxNodeByPath(FbxScene* scene, std::string nodePath) +{ + if (!scene) { + TF_WARN("Cannot find node with path '%s' because scene is null", nodePath.c_str()); + return nullptr; + } + + FbxNode* rootNode = scene->GetRootNode(); + if (!rootNode) { + TF_WARN("Cannot find node with path '%s' because root node is null", nodePath.c_str()); + return nullptr; + } + + std::vector nodePathVector = TfStringTokenize(nodePath, "/"); + if (nodePathVector.empty()) { + TF_WARN("Cannot find node with non tokenizable name %s", nodePath.c_str()); + return nullptr; + } + + // Verify that the first node in the path matches the root node's name + if (nodePathVector[0] != rootNode->GetName()) { + TF_WARN("Root node \"%s\" not found in path %s", + rootNode->GetName(), + nodePath.c_str()); + return nullptr; + } + + // Iterate over the path, looking for a child node with the next expected name. We start with + // index 1 because we already verified index 0 (the root node) above + FbxNode* currentNode = rootNode; + for (size_t pathIndex = 1; pathIndex < nodePathVector.size(); pathIndex++) { + const std::string& nodeName = nodePathVector[pathIndex]; + currentNode = currentNode->FindChild(nodeName.c_str(), false); + + if (!currentNode) { + TF_WARN("Could not find expected node with name \"%s\" in path %s", + nodeName.c_str(), + nodePath.c_str()); + return nullptr; + } + } + + // Found all nodes in the path, so we return the last one + return currentNode; +} + +/** + * Private helper function: + * + * Recursively traverse the FBX node tree and collect paths to all nodes. The paths found will be + * added to the provided vector. Paths will be constructed using forward slashes. + * + * @param node The current FbxNode to process. + * @param currentPath The path accumulated so far, which will be updated with the current node + * name. + * @param paths A vector to collect all paths found in the FBX node tree. This vector will be + * modified. + */ +void +getFbxNodePathsHelper(FbxNode* node, std::string currentPath, std::vector& paths) +{ + if (!node) { + return; + } + + // Traverse the node + std::string nodeName = node->GetName(); + + // Matches USD paths with forward slash, as opposed to the filesystem path + currentPath += "/" + nodeName; + paths.push_back(currentPath); + + // Recursively process all child nodes + for (int i = 0; i < node->GetChildCount(); ++i) { + getFbxNodePathsHelper(node->GetChild(i), currentPath, paths); + } +} + +std::vector +getFbxNodePaths(FbxScene* scene) +{ + if (!scene) { + TF_WARN("Cannot get FBX node paths because scene is null"); + return {}; + } + + FbxNode* rootNode = scene->GetRootNode(); + if (!rootNode) { + TF_WARN("Cannot get FBX node paths because root node is null"); + return {}; + } + + // Start with an empty path and collect all paths + std::vector paths; + getFbxNodePathsHelper(rootNode, "", paths); + return paths; +} \ No newline at end of file diff --git a/fbx/tests/util.h b/fbx/tests/util.h new file mode 100644 index 00000000..7e77cc3c --- /dev/null +++ b/fbx/tests/util.h @@ -0,0 +1,107 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +class FbxLoaderSingleton +{ + public: + /** + * Get the singleton instance of FbxLoaderSingleton. + */ + static FbxLoaderSingleton& getInstance(); + ~FbxLoaderSingleton(); + + /** + * Load an FBX scene from a file. + * + * NOTE: This requires scene->Destroy() to be called eventually to free resources! + * + * @param filename The path to the FBX file to load. + * + * @return A pointer to the loaded FbxScene, or nullptr if loading failed. + */ + FbxScene* loadScene(std::string filename); + + /** + * Get the FbxManager associated with a given FbxScene. + */ + inline fbxsdk::FbxManager* GetFbxManager(const fbxsdk::FbxScene* scene) + { + return scene ? scene->GetFbxManager() : nullptr; + } + + private: + FbxLoaderSingleton(); + std::recursive_mutex mFbxLoaderMutex; + + FbxManager* manager; +}; + +/** + * Export a USD file to an FBX file on disk, and load it in as an FbxScene. This can be used for + * verifying FBX export. + * + * Note that a temporary file will be created and deleted on disk with the name of the USD file, + * but with a ".fbx" extension. It will be located next to the original USD file. + * + * @param usdFilepath The path to the USD file to convert. It should have a valid USD extension + * (e.g., .usd, .usda, .usdc), and should be relative to the current working + * directory. It is recommended for this to simply be a filename + * @param tempDirName The name of the temporary directory where the FBX file will be created. This + * directory will be created next to the USD file, and the FBX file will be + * placed inside it. It will be deleted after the conversion. Defaults to "tmp" + * + * WARNING: a directory next to the given USD with the same name as tempDirName (tmp by default) + * will be deleted! Do not run this function if there is a folder with such name that should not + * be removed + * + * @return A pointer to the loaded FbxScene, or nullptr if the conversion failed. + */ +FbxScene* +getFbxSceneFromUsd(const std::filesystem::path& usdFilepath, + const std::filesystem::path& tempDirName = "tmp"); + +/** + * Get a specific FbxNode by its path within the FBX file. The path should be an absolute path + * starting with "/RootNode" or RootNode, using forward slashes. A leading slash is optional. + * + * For instance, with the following hierarchy: + * RootNode -> ChildNode -> GrandChildNode + * The expected path to find GrandChildNode would be "/RootNode/ChildNode/GrandChildNode". + * + * @param scene The FBX scene + * @param nodePath The path to the node to find + * + * @return A pointer to the found FbxNode, or nullptr if not found. + */ +FbxNode* +getFbxNodeByPath(FbxScene* scene, std::string nodePath); + +/** + * Get the paths of all FbxNodes in the given FbxNode hierarchy. + * The paths are returned as a vector of strings, where each string is a path starting with the + * root node. + * + * @param scene The FbxScene from which to retrieve the node paths. + * + * @return A vector of strings containing the paths of all nodes. + */ +std::vector +getFbxNodePaths(FbxScene* scene); diff --git a/gltf/README.md b/gltf/README.md index 4fb0cdef..13698956 100644 --- a/gltf/README.md +++ b/gltf/README.md @@ -77,6 +77,7 @@ During material import, the ASM shading model is used as an intermediate transpo | KHR_materials_unlit |❌| | KHR_materials_variants |❌| | KHR_materials_volume |✅| +| KHR_materials_volume_scatter |✅| | KHR_mesh_quantization |❌| | KHR_texture_basisu |❌| | KHR_texture_transform |✅|Written to a UsdTransform2d node| @@ -86,6 +87,7 @@ During material import, the ASM shading model is used as an intermediate transpo | EXT_texture_webp |✅| | ADOBE_materials_clearcoat_specular |✅| | ADOBE_materials_clearcoat_tint |✅| +| EXT_materials_clearcoat_color |✅| | KHR_materials_pbrSpecularGlossiness |✅| Anisotropy @@ -130,7 +132,7 @@ Export can optionally make use glTF material extensions. During material export, the ASM shading model is used as an intermediate transport layer. -If the material extensions are turned off, transmission is transcoded into opacity +If the material extensions are turned off, transmission is transcoded into opacity to preserve glass and similar translucent materials as best as possible. Export of node TRS works for animated nodes, but not for static ones at the moment. @@ -141,36 +143,67 @@ Mesh bounding box exported as min and max accessor bounds in glTF. **Import:** -* `gltfAssetsPath`: Filesystem path where image assets are saved to. - The default is that image assets are not copied, but the generated usd file will resolve them from the original file. - The following saves images to the path `myPath` during `UsdStage::Open` and then exports the stage to that same path. +* `assetsPath`: Filesystem path where image assets are saved to during import. Default is `""` + + By default image textures used by the asset are not copied during import, but are kept in memory and are available + via an associated `ArResolver` plugin. By specifying a filesystem location via `assetsPath`, the import process will + copy the image textures to that location and provide asset paths to those locations in the generated USD data. This + file format argument allows an easy way to export associated images textures to disk when converting an asset to USD. + + This snippet saves image textures to the path at `exportPath` during `Usd.Stage.Open` and then also exports the stage + to that same location, so that the USD data and the used images a co-located. ``` - UsdStageRefPtr stage = UsdStage::Open("cube.gltf:SDF_FORMAT_ARGS:gltfAssetsPath=myPath") - stage->Export("myPath/cube.usd") + from pxr import Usd + stage = Usd.Stage.Open("asset.gltf:SDF_FORMAT_ARGS:assetsPath=exportPath") + stage.Export("exportPath/asset.usd") ``` +* `gltfAssetsPath`: Deprecated in favor of `assetsPath`. + +* `writeUsdPreviewSurface`: Generate a UsdPreviewSurface based network for each material. Default is `true` + + UsdPreviewSurface and its associated nodes are a universally understood USD material description + and all application should support them. The PBR capabilities are limited. + +* `writeASM`: Generate a ASM (Adobe Standard Material) based network for each material. Default is `true` + + ASM is a standard supported by many Adobe applications with richer support for PBR capabilities. + It will be superseded by OpenPBR in the near future. + +* `writeOpenPBR`: Generate a OpenPBR based material network for each material. Default is `false` + + OpenPBR is a new industry standard that will have wide spread support, but is still in its infancy. + The material network uses `MaterialX` nodes to express individual operations and has an `OpenPBR` surface, + which has rich support for PBR oriented materials. + +* `gltfAnimationStacks`: Import multiple animation tracks. Default is `false` + + By default only the first animation track is imported. + It is only recommended to use this parameter in order to convert from GLTF to another format that supports multiple + animation tracks, such as FBX. It is not recommended to export a .usd file after importing a file with this parameter + set, as there is no standard way to encode this information. + + The following allows additional animation tracks to be imported, and adds metadata to USD to encode where each track + begins and ends. The exporter can then read this metadata to export the tracks properly. + ``` + from pxr import Usd + stage = Usd.Stage.Open("animAsset.gltf:SDF_FORMAT_ARGS:gltfAnimationStacks=true") + stage.Export("animAsset.fbx") + ``` + + **Export:** * `embedImages` Embed images, as base64 for `gltf` or as binary data for `glb`. Default is `true`. + The following exports to `glb` and does not embed images: ``` - UsdStageRefPtr stage = UsdStage::Open("cube.usd"); - SdfLayer::FileFormatArguments args = { {"embedImages", "false"} }; - stage->Export("cube.glb", false, args); + from pxr import Usd + stage = Usd.Stage.Open("cube.usd"); + stage.Export("cube.glb", args={ "embedImages": "false" }); ``` -* `useMaterialExtensions`: Use glTF material extensions. Default is `true`. -* `gltfAnimationTracks`: Import multiple animation tracks. Default is `false` - The default is that only the first animation track is imported. - It is only recommended to use this parameter in order to convert from gltf to another format, such as GLTF. - It is not recommended to export a .usd file after importing a file with this parameter set. - ``` - The following allows additional animation tracks to be imported, and adds metadata to USD to encode where - each track begins and ends. The exporter can then read this metadata to export the tracks properly. - ``` - UsdStageRefPtr stage = UsdStage::Open("cube.gltf:SDF_FORMAT_ARGS:gltfAnimationTracks=true") - stage->Export("myPath/cube.gltf") - ``` +* `useMaterialExtensions`: Use glTF material extensions. Default is `true`. ## Debug codes * `FILE_FORMAT_GLTF`: Common debug messages. diff --git a/gltf/src/fileFormat.cpp b/gltf/src/fileFormat.cpp index 27bf414e..6d637156 100644 --- a/gltf/src/fileFormat.cpp +++ b/gltf/src/fileFormat.cpp @@ -34,6 +34,7 @@ using namespace adobe::usd; const TfToken UsdGltfFileFormat::assetsPathToken("gltfAssetsPath", TfToken::Immortal); const TfToken UsdGltfFileFormat::animationTracksToken("gltfAnimationTracks", TfToken::Immortal); +const TfToken UsdGltfFileFormat::computeBitangentsToken("computeBitangents", TfToken::Immortal); TF_DEFINE_PUBLIC_TOKENS(UsdGltfFileFormatTokens, USDGLTF_FILE_FORMAT_TOKENS); @@ -61,9 +62,15 @@ UsdGltfFileFormat::InitData(const FileFormatArguments& args) const TF_DEBUG_MSG( FILE_FORMAT_GLTF, "FileFormatArg: %s = %s\n", arg.first.c_str(), arg.second.c_str()); } - argReadBool(args, AdobeTokens->writeMaterialX.GetText(), pd->writeMaterialX, DEBUG_TAG); - argReadString(args, assetsPathToken.GetText(), pd->assetsPath, DEBUG_TAG); - argReadBool(args, animationTracksToken.GetText(), pd->animationTracks, DEBUG_TAG); + pd->parseFromFileFormatArgs(args, DEBUG_TAG); + + // "gltfAssetsPath" is deprecated in favor of the universal "assetsPath" argument - 2025-3-18 + // If both are present, "gltfAssetsPath" is stronger. + argReadString(args, assetsPathToken.GetString(), pd->assetsPath, DEBUG_TAG); + argWarnDeprecatedArg(args, assetsPathToken.GetString(), DEBUG_TAG); + + argReadBool(args, animationTracksToken.GetString(), pd->animationTracks, DEBUG_TAG); + argReadBool(args, computeBitangentsToken.GetString(), pd->computeBitangents, DEBUG_TAG); return pd; } @@ -157,12 +164,11 @@ UsdGltfFileFormat::Read(PXR_NS::SdfLayer* layer, options.importGeometry = true; options.importMaterials = true; options.importImages = true; + options.computeBitangents = data->computeBitangents; GUARD(importGltf(options, gltf, usd, resolvedPath), "Error translating glTF to USD\n"); - WriteLayerOptions layerOptions; - layerOptions.writeMaterialX = data->writeMaterialX; + WriteLayerOptions layerOptions(*data); layerOptions.pruneJoints = false; - layerOptions.assetsPath = data->assetsPath; layerOptions.animationTracks = data->animationTracks; std::string ext = isAscii ? "GLTF" : "GLB"; GUARD( @@ -208,12 +214,11 @@ UsdGltfFileFormat::ReadFromString(SdfLayer* layer, const std::string& str) const options.importGeometry = true; options.importMaterials = true; options.importImages = true; + options.computeBitangents = data->computeBitangents; GUARD(importGltf(options, gltf, usd, ""), "Error translating glTF to USD\n"); - WriteLayerOptions layerOptions; - layerOptions.writeMaterialX = data->writeMaterialX; + WriteLayerOptions layerOptions(*data); layerOptions.pruneJoints = false; - layerOptions.assetsPath = data->assetsPath; layerOptions.animationTracks = data->animationTracks; std::string ext = isAscii ? "GLTF" : "GLB"; GUARD( diff --git a/gltf/src/fileFormat.h b/gltf/src/fileFormat.h index 4244d440..a86b733a 100644 --- a/gltf/src/fileFormat.h +++ b/gltf/src/fileFormat.h @@ -42,8 +42,8 @@ class ArAsset; class GltfData : public FileFormatDataBase { public: - std::string assetsPath; bool animationTracks = false; + bool computeBitangents = false; static GltfDataRefPtr InitData(const SdfFileFormat::FileFormatArguments& args); }; @@ -101,6 +101,7 @@ class USDGLTF_API UsdGltfFileFormat protected: static const TfToken assetsPathToken; static const TfToken animationTracksToken; + static const TfToken computeBitangentsToken; SDF_FILE_FORMAT_FACTORY_ACCESS; diff --git a/gltf/src/gltf.cpp b/gltf/src/gltf.cpp index c97e4348..f666aa57 100644 --- a/gltf/src/gltf.cpp +++ b/gltf/src/gltf.cpp @@ -130,6 +130,56 @@ CustomWriteImageData(const std::string* basepathString, return true; } +bool +preValidateGLB(const unsigned char* buffer, size_t bufferSize) +{ + // GLB validation: check buffer size mismatch + const uint32_t* header = reinterpret_cast(buffer); + + // Check if GLB file (magic number 'glTF') + if (header[0] != 0x46546C67) { + TF_WARN("Binary file missing GLB magic number (expected 0x46546C67)", + header[0]); + return false; // Reject invalid binary files + } + + // Get JSON chunk length and position + uint32_t jsonChunkLength = header[3]; + size_t jsonStart = 20; + size_t binChunkStart = jsonStart + jsonChunkLength; + + // If there's a binary chunk, check buffer size match + if (binChunkStart + 8 <= bufferSize) { + const uint32_t* binChunkHeader = reinterpret_cast(buffer + binChunkStart); + uint32_t actualBinSize = binChunkHeader[0]; + + // Parse JSON for declared buffer.byteLength + std::string jsonStr(reinterpret_cast(buffer + jsonStart), jsonChunkLength); + size_t buffersPos = jsonStr.find("\"buffers\""); + if (buffersPos != std::string::npos) { + size_t pos = jsonStr.find("\"byteLength\"", buffersPos); + if (pos != std::string::npos) { + pos = jsonStr.find(":", pos); + if (pos != std::string::npos) { + uint32_t declaredBufSize = std::stoul(jsonStr.substr(pos + 1)); + + // GLB chunks are padded to 4-byte boundaries + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#glb-stored-buffer + // Reject if mismatch exceeds alignment padding (potential size attack) + int sizeDiff = static_cast(actualBinSize) - static_cast(declaredBufSize); + if (sizeDiff < 0 || sizeDiff >= 4) { + TF_WARN("Buffer size mismatch beyond alignment padding: JSON declares %u bytes, binary chunk is %u bytes (diff: %d)", + declaredBufSize, actualBinSize, sizeDiff); + return false; + } + } + } + } + } + + return true; +} + bool readGltfFromMemory(tinygltf::Model& gltf, const std::string& baseDir, @@ -137,6 +187,12 @@ readGltfFromMemory(tinygltf::Model& gltf, const char* buffer, size_t bufferSize) { + // Pre-validate GLB structure before tinygltf processes it + if (!isAscii && !preValidateGLB(reinterpret_cast(buffer), bufferSize)) { + TF_WARN("GLB pre-validation failed - file may be malicious"); + return false; + } + tinygltf::TinyGLTF loader; loader.SetImageLoader(CustomLoadImageData, nullptr); @@ -158,9 +214,10 @@ readGltfFromMemory(tinygltf::Model& gltf, } if (!result) { TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Failed to read glTF\n"); + return false; } - return result; + return true; } bool @@ -467,17 +524,58 @@ getAccessorElementCount(const tinygltf::Model& model, int accessorIndex) void readAccessorData(const tinygltf::Model& model, int accessorIndex, uint8_t* dst) { - if (accessorIndex < 0 || static_cast(accessorIndex) >= model.accessors.size()) { + if (accessorIndex < 0) { + TF_CODING_ERROR("Accessor index %d is invalid (< 0). File should be rejected.", accessorIndex); + return; + } + if (static_cast(accessorIndex) >= model.accessors.size()) { + TF_CODING_ERROR("Accessor %d out of bounds (length %zu). File should be rejected.", + accessorIndex, model.accessors.size()); return; } const tinygltf::Accessor& accessor = model.accessors[accessorIndex]; + + if (accessor.bufferView < 0 || static_cast(accessor.bufferView) >= model.bufferViews.size()) { + TF_WARN("Accessor %d has invalid buffer view index %d", accessorIndex, accessor.bufferView); + return; + } const tinygltf::BufferView& bufferView = model.bufferViews[accessor.bufferView]; + + if (bufferView.buffer < 0 || static_cast(bufferView.buffer) >= model.buffers.size()) { + TF_WARN("Buffer view %d has invalid buffer index %d", accessor.bufferView, bufferView.buffer); + return; + } const tinygltf::Buffer& buffer = model.buffers[bufferView.buffer]; + size_t componentSize = tinygltf::GetComponentSizeInBytes(accessor.componentType); size_t componentCount = tinygltf::GetNumComponentsInType(accessor.type); size_t elementSize = componentSize * componentCount; size_t elementStride = accessor.ByteStride(bufferView); + // Validate buffer view bounds to prevent buffer over-read attacks + if (bufferView.byteOffset >= buffer.data.size()) { + TF_WARN("Buffer view %d has byteOffset %zu exceeding or equal to buffer size %zu", + accessor.bufferView, bufferView.byteOffset, buffer.data.size()); + return; + } + if (bufferView.byteOffset + bufferView.byteLength > buffer.data.size()) { + TF_WARN("Buffer view %d extends beyond buffer bounds (offset %zu + length %zu > buffer size %zu)", + accessor.bufferView, bufferView.byteOffset, bufferView.byteLength, buffer.data.size()); + return; + } + + // Validate accessor count to prevent buffer over-read attacks + size_t accessorStartOffset = accessor.byteOffset; + size_t accessorTotalSize = (elementStride == elementSize) ? + accessor.count * elementSize : + (accessor.count > 0 ? (accessor.count - 1) * elementStride + elementSize : 0); + + if (accessorStartOffset + accessorTotalSize > bufferView.byteLength) { + TF_WARN("Accessor %d data extends beyond buffer view bounds (accessor offset %zu + size %zu > view length %zu)", + accessorIndex, accessorStartOffset, accessorTotalSize, bufferView.byteLength); + return; + } + const uint8_t* src = buffer.data.data() + bufferView.byteOffset + accessor.byteOffset; if (elementStride == elementSize) { memcpy(dst, src, accessor.count * elementSize); @@ -514,18 +612,59 @@ normalizedFloat(float value) void readAccessorDataToFloat(const tinygltf::Model& model, int accessorIndex, float* dst) { - if (accessorIndex < 0 || static_cast(accessorIndex) >= model.accessors.size()) { + if (accessorIndex < 0) { + TF_CODING_ERROR("Accessor index %d is invalid (< 0). File should be rejected.", accessorIndex); + return; + } + if (static_cast(accessorIndex) >= model.accessors.size()) { + TF_CODING_ERROR("Accessor %d out of bounds (length %zu). File should be rejected.", + accessorIndex, model.accessors.size()); return; } const tinygltf::Accessor& accessor = model.accessors[accessorIndex]; + + if (accessor.bufferView < 0 || static_cast(accessor.bufferView) >= model.bufferViews.size()) { + TF_WARN("Accessor %d has invalid buffer view index %d", accessorIndex, accessor.bufferView); + return; + } const tinygltf::BufferView& bufferView = model.bufferViews[accessor.bufferView]; + + if (bufferView.buffer < 0 || static_cast(bufferView.buffer) >= model.buffers.size()) { + TF_WARN("Buffer view %d has invalid buffer index %d", accessor.bufferView, bufferView.buffer); + return; + } const tinygltf::Buffer& buffer = model.buffers[bufferView.buffer]; + size_t componentSize = tinygltf::GetComponentSizeInBytes(accessor.componentType); size_t componentCount = tinygltf::GetNumComponentsInType(accessor.type); size_t elementSize = componentSize * componentCount; size_t elementStride = accessor.ByteStride(bufferView); bool normalized = accessor.normalized; + // Validate buffer view bounds to prevent buffer over-read attacks + if (bufferView.byteOffset >= buffer.data.size()) { + TF_WARN("Buffer view %d has byteOffset %zu exceeding or equal to buffer size %zu", + accessor.bufferView, bufferView.byteOffset, buffer.data.size()); + return; + } + if (bufferView.byteOffset + bufferView.byteLength > buffer.data.size()) { + TF_WARN("Buffer view %d extends beyond buffer bounds (offset %zu + length %zu > buffer size %zu)", + accessor.bufferView, bufferView.byteOffset, bufferView.byteLength, buffer.data.size()); + return; + } + + // Validate accessor count to prevent buffer over-read attacks + size_t accessorStartOffset = accessor.byteOffset; + size_t accessorTotalSize = (elementStride == elementSize) ? + accessor.count * elementSize : + (accessor.count > 0 ? (accessor.count - 1) * elementStride + elementSize : 0); + + if (accessorStartOffset + accessorTotalSize > bufferView.byteLength) { + TF_WARN("Accessor %d data extends beyond buffer view bounds (accessor offset %zu + size %zu > view length %zu)", + accessorIndex, accessorStartOffset, accessorTotalSize, bufferView.byteLength); + return; + } + const uint8_t* src = buffer.data.data() + bufferView.byteOffset + accessor.byteOffset; const size_t elementCount = accessor.count; if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) { @@ -713,10 +852,35 @@ readColor(const tinygltf::Model& model, void readAccessorInts(const tinygltf::Model& model, int accessorIndex, PXR_NS::VtArray& dst) { - if (accessorIndex < 0 || static_cast(accessorIndex) >= model.accessors.size()) { + if (accessorIndex < 0) { + TF_CODING_ERROR("Accessor index %d is invalid (< 0). File should be rejected.", accessorIndex); + return; + } + if (static_cast(accessorIndex) >= model.accessors.size()) { + TF_CODING_ERROR("Accessor %d out of bounds (length %zu). File should be rejected.", + accessorIndex, model.accessors.size()); return; } const tinygltf::Accessor& accessor = model.accessors[accessorIndex]; + + // Validate accessor type for indices - must be SCALAR, not VEC2/VEC3/VEC4 + if (accessor.type != TINYGLTF_TYPE_SCALAR) { + TF_WARN("Accessor %d used as indices has invalid type %d (expected SCALAR type %d). Rejecting to prevent type confusion attack.", + accessorIndex, accessor.type, TINYGLTF_TYPE_SCALAR); + return; + } + + // Validate component type is one of the allowed unsigned integer types + // glTF 2.0 spec: indices MUST be UNSIGNED_BYTE, UNSIGNED_SHORT, or UNSIGNED_INT + // Using whitelist approach for security - reject anything not explicitly allowed + if (accessor.componentType != TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE && + accessor.componentType != TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT && + accessor.componentType != TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { + TF_WARN("Accessor %d used as indices has invalid component type %d. Only UNSIGNED_BYTE (5121), UNSIGNED_SHORT (5123), or UNSIGNED_INT (5125) are allowed for indices.", + accessorIndex, accessor.componentType); + return; + } + int componentSize = tinygltf::GetComponentSizeInBytes(accessor.componentType); if (componentSize == 1) { PXR_NS::VtArray temp(dst.size()); diff --git a/gltf/src/gltfAnisotropy.cpp b/gltf/src/gltfAnisotropy.cpp index b6a0c623..962355f4 100644 --- a/gltf/src/gltfAnisotropy.cpp +++ b/gltf/src/gltfAnisotropy.cpp @@ -291,12 +291,15 @@ importAnisotropyData(ImportGltfContext& ctx, AnisotropyData& anisotropy, Image& anisotropySrcImage) { + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "importAnisotropyData for material '%s'\n", m.name.c_str()); bool ret = false; bool haveStrength = readDoubleValue(anisoExt.Get("anisotropyStrength"), anisotropy.strength); bool haveRotation = readDoubleValue(anisoExt.Get("anisotropyRotation"), anisotropy.rotation); readTextureInfo(anisoExt.Get("anisotropyTexture"), anisotropy.texture); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, " texture.index: %d\n", anisotropy.texture.index); Input anisotropyInput; if (anisotropy.texture.index > -1) { + TF_DEBUG_MSG(FILE_FORMAT_GLTF, " calling importImage with index %d\n", anisotropy.texture.index); int imageIndex = importImage(ctx, anisotropy.texture.index, m.name, "anisotropy"); importTexture(ctx.gltf, imageIndex, @@ -339,10 +342,16 @@ importAnisotropyTexture(ImportGltfContext& ctx, const Image& anisotropySrcImage, std::unordered_map& cache) { + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "importAnisotropyTexture for material '%s'\n", m.displayName.c_str()); // Get Roughness image const tinygltf::Image* roughnessImage = nullptr; - if (gm.pbrMetallicRoughness.metallicRoughnessTexture.index < ctx.gltf->textures.size()) { - roughnessImage = getImage(ctx.gltf, gm.pbrMetallicRoughness.metallicRoughnessTexture.index); + // Validate texture index before use to prevent signed/unsigned comparison bug + int roughnessTexIdx = gm.pbrMetallicRoughness.metallicRoughnessTexture.index; + TF_DEBUG_MSG(FILE_FORMAT_GLTF, " roughness texture index: %d\n", roughnessTexIdx); + if (roughnessTexIdx >= 0 && static_cast(roughnessTexIdx) < ctx.gltf->textures.size()) { + TF_DEBUG_MSG(FILE_FORMAT_GLTF, " calling getImage for roughness\n"); + roughnessImage = getImage(ctx.gltf, roughnessTexIdx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, " getImage returned: %p\n", roughnessImage); } // Check if the anisotropy textures are already in the cache diff --git a/gltf/src/gltfExport.cpp b/gltf/src/gltfExport.cpp index c215b237..6ba1ee91 100644 --- a/gltf/src/gltfExport.cpp +++ b/gltf/src/gltfExport.cpp @@ -12,6 +12,7 @@ governing permissions and limitations under the License. #include "gltfExport.h" #include "debugCodes.h" #include "gltfAnisotropy.h" +#include #include #include #include @@ -1051,25 +1052,17 @@ exportTextureTransform(ExportGltfContext& ctx, const Input& input, ExtMap& exten bool hasRot = false; bool hasScale = false; bool hasTrans = false; - if (input.transformRotation.IsHolding()) { - rot = input.transformRotation.UncheckedGet() * deg2rad; - hasRot = rot != 0.0f; + if (input.uvRotation != kDefaultUvRotation) { + rot = input.uvRotation * deg2rad; + hasRot = true; } - if (input.transformScale.IsHolding()) { - scale = input.transformScale.UncheckedGet(); - scale[1] = -scale[1]; + if (input.uvScale != kDefaultUvScale) { + scale = input.uvScale; hasScale = scale[0] != 1.0f || scale[1] != 1.0f; - } else { - scale[1] = -1.0f; - hasScale = true; } - if (input.transformTranslation.IsHolding()) { - trans = input.transformTranslation.UncheckedGet(); - trans[1] = 1.0f - trans[1]; + if (input.uvTranslation != kDefaultUvTranslation) { + trans = input.uvTranslation; hasTrans = trans[0] != 0.0f || trans[1] != 0.0f; - } else { - trans[1] = 1.0f; - hasTrans = true; } if (hasRot || hasScale || hasTrans) { @@ -1120,17 +1113,15 @@ addTextureToExt(ExportGltfContext& ctx, if (!factorName.empty()) { if (input.channel == AdobeTokens->rgb) { - if (translatedInput.scale.IsHolding()) { - const GfVec4f& scale = translatedInput.scale.UncheckedGet(); - if (scale[0] != factorDefaultValue || scale[1] != factorDefaultValue || - scale[2] != factorDefaultValue) { - addColorValueToExt(ext, factorName, GfVec3f(scale[0], scale[1], scale[2])); - } + GfVec3f scale( + translatedInput.scale[0], translatedInput.scale[1], translatedInput.scale[2]); + if (scale != GfVec3f(factorDefaultValue)) { + addColorValueToExt(ext, factorName, scale); } } else { int channel = token2Channel(input.channel); - if (channel != -1 && translatedInput.scale.IsHolding()) { - float scale = translatedInput.scale.UncheckedGet()[channel]; + if (channel != -1) { + float scale = translatedInput.scale[channel]; if (scale != factorDefaultValue) { addFloatValueToExt(ext, factorName, scale); } @@ -1256,6 +1247,14 @@ exportSpecularExtension(ExportGltfContext& ctx, "specularColorTexture", "specularColorFactor", 1.0f)) { + // We will always add the EXT_materials_specular_edge_color sub-extension to tell + // glTF loaders that this material can be interpreted using the ASM/OpenPBR specular model. + std::map extensions; + std::map extObj; + // Empty objects seem to be serialized as null in glTF, so we need to add a dummy value for now. + extObj["specularEdgeColorEnabled"] = tinygltf::Value(true); + addExtension(ctx, extensions, "EXT_materials_specular_edge_color", extObj, false); + ext["extensions"] = tinygltf::Value(extensions); addMaterialExt(ctx, gm, "KHR_materials_specular", ext); return true; } @@ -1325,7 +1324,7 @@ exportAdobeClearcoatSpecularExtension(ExportGltfContext& ctx, } bool -exportAdobeClearcoatTintExtension(ExportGltfContext& ctx, +exportClearcoatColorExtension(ExportGltfContext& ctx, InputTranslator& inputTranslator, const Material& m, tinygltf::Material& gm) @@ -1335,9 +1334,9 @@ exportAdobeClearcoatTintExtension(ExportGltfContext& ctx, inputTranslator, ext, m.clearcoatColor, - "clearcoatTintTexture", - "clearcoatTintFactor")) { - addMaterialExt(ctx, gm, "ADOBE_materials_clearcoat_tint", ext); + "clearcoatColorTexture", + "clearcoatColorFactor")) { + addMaterialExt(ctx, gm, "EXT_materials_clearcoat_color", ext); return true; } @@ -1382,8 +1381,8 @@ exportMaterials(ExportGltfContext& ctx) // approximated as opacity if (!ctx.options.useMaterialExtensions && !m.transmission.isEmpty()) { m.opacity = m.transmission; - GfVec4f scale = m.opacity.scale.GetWithDefault(GfVec4f(1.0f)); - GfVec4f bias = m.opacity.bias.GetWithDefault(GfVec4f(0.0f)); + GfVec4f scale = m.opacity.scale; + GfVec4f bias = m.opacity.bias; // When converting from transmission to opacity, we should not convert full transmission // into zero opacity, since that completely removes the material. It also prevents any @@ -1434,9 +1433,7 @@ exportMaterials(ExportGltfContext& ctx) if (texOpacity >= 0 || ch < 0) { float opacityValue = 1.0f; if (ch >= 0) { - GfVec4f scale = m.opacity.scale.GetWithDefault(GfVec4f(1.0f)); - GfVec4f bias = m.opacity.bias.GetWithDefault(GfVec4f(0.0f)); - opacityValue = scale[ch] * texOpacity + bias[ch]; + opacityValue = m.opacity.scale[ch] * texOpacity + m.opacity.bias[ch]; } else { // the channel token is invalid (eg rgb) so we default to an opacity value // of 1.0 @@ -1446,8 +1443,8 @@ exportMaterials(ExportGltfContext& ctx) m.opacity.image = -1; m.opacity.value = opacityValue; // Clear the scale and bias since it was applied to the constant value - m.opacity.scale = VtValue(); - m.opacity.bias = VtValue(); + m.opacity.scale = kDefaultTexScale; + m.opacity.bias = kDefaultTexBias; TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write opacity for %s is a constant %f (texture omitted)\n", gm.name.c_str(), @@ -1485,13 +1482,11 @@ exportMaterials(ExportGltfContext& ctx) // 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. - GfVec4f bias = m.opacity.bias.GetWithDefault(GfVec4f(0.0f)); - if (bias != GfVec4f(0.0f)) { - GfVec4f scale = m.opacity.scale.GetWithDefault(GfVec4f(1.0f)); + if (m.opacity.bias != kDefaultTexBias) { Input opacity = m.opacity; int chIdx = m.opacity.image >= 0 ? token2Channel(m.opacity.channel) : 0; - float opacityScale = scale[chIdx]; - float opacityBias = bias[chIdx]; + float opacityScale = m.opacity.scale[chIdx]; + float opacityBias = m.opacity.bias[chIdx]; TF_DEBUG_MSG(FILE_FORMAT_GLTF, "glTF::write material %s, opacity uses bias -> affine transform " "image: %d %f %f\n", @@ -1612,9 +1607,9 @@ exportMaterials(ExportGltfContext& ctx) // Emit a warning if there are both roughness and metallic textures and their // transforms differ if ((m.roughness.image >= 0 && m.metallic.image >= 0) && - (m.roughness.transformRotation != m.metallic.transformRotation || - m.roughness.transformScale != m.metallic.transformScale || - m.roughness.transformTranslation != m.metallic.transformTranslation)) { + (m.roughness.uvRotation != m.metallic.uvRotation || + m.roughness.uvScale != m.metallic.uvScale || + m.roughness.uvTranslation != m.metallic.uvTranslation)) { TF_WARN("glTF::write material %s, roughness and metallic textures have different " "transforms but will be combined into a single texture\n", @@ -1632,12 +1627,11 @@ exportMaterials(ExportGltfContext& ctx) ctx, roughnessMetallic, gm.pbrMetallicRoughness.metallicRoughnessTexture.extensions); } - if (m.diffuseColor.image >= 0 && m.diffuseColor.scale.IsHolding()) { - GfVec4f scale = baseColor.scale.UncheckedGet(); + if (m.diffuseColor.image >= 0 && m.diffuseColor.scale != kDefaultTexScale) { gm.pbrMetallicRoughness.baseColorFactor.resize(4, 1); - gm.pbrMetallicRoughness.baseColorFactor[0] = scale[0]; - gm.pbrMetallicRoughness.baseColorFactor[1] = scale[1]; - gm.pbrMetallicRoughness.baseColorFactor[2] = scale[2]; + gm.pbrMetallicRoughness.baseColorFactor[0] = m.diffuseColor.scale[0]; + gm.pbrMetallicRoughness.baseColorFactor[1] = m.diffuseColor.scale[1]; + gm.pbrMetallicRoughness.baseColorFactor[2] = m.diffuseColor.scale[2]; } else if (m.diffuseColor.value.IsHolding()) { GfVec4f value = baseColor.value.UncheckedGet(); gm.pbrMetallicRoughness.baseColorFactor.resize(4, 1); @@ -1645,10 +1639,9 @@ exportMaterials(ExportGltfContext& ctx) gm.pbrMetallicRoughness.baseColorFactor[1] = value[1]; gm.pbrMetallicRoughness.baseColorFactor[2] = value[2]; } - if (m.opacity.image >= 0 && m.opacity.scale.IsHolding()) { - GfVec4f scale = m.opacity.scale.UncheckedGet(); + if (m.opacity.image >= 0 && m.opacity.scale != kDefaultTexScale) { gm.pbrMetallicRoughness.baseColorFactor.resize(4, 1); - gm.pbrMetallicRoughness.baseColorFactor[3] = scale[3]; + gm.pbrMetallicRoughness.baseColorFactor[3] = m.opacity.scale[3]; } else if (m.opacity.value.IsHolding()) { float value = m.opacity.value.UncheckedGet(); gm.pbrMetallicRoughness.baseColorFactor.resize(4, 1); @@ -1656,8 +1649,8 @@ exportMaterials(ExportGltfContext& ctx) } float emissiveStrength = 1.0f; if (m.emissiveColor.image >= 0) { - if (m.emissiveColor.scale.IsHolding()) { - GfVec4f scale = m.emissiveColor.scale.UncheckedGet(); + if (m.emissiveColor.scale != kDefaultTexScale) { + GfVec4f scale = m.emissiveColor.scale; // The emissiveFactor can only go up to 1.0 per component. Anything beyond that // needs to be handled by the emissiveStrength extension. float maxFactor = std::max(scale[0], std::max(scale[1], scale[2])); @@ -1694,17 +1687,15 @@ exportMaterials(ExportGltfContext& ctx) gm.emissiveFactor[1] = value[1]; gm.emissiveFactor[2] = value[2]; } - if (m.occlusion.image >= 0 && m.occlusion.scale.IsHolding()) { - GfVec4f scale = m.occlusion.scale.UncheckedGet(); - gm.occlusionTexture.strength = scale[0]; + if (m.occlusion.image >= 0 && m.occlusion.scale != kDefaultTexScale) { + gm.occlusionTexture.strength = m.occlusion.scale[0]; } else if (m.occlusion.value.IsHolding()) { float value = m.occlusion.value.UncheckedGet(); gm.occlusionTexture.strength = value; } if (m.metallic.image >= 0) { - if (m.metallic.scale.IsHolding()) { - GfVec4f scale = m.metallic.scale.UncheckedGet(); - gm.pbrMetallicRoughness.metallicFactor = scale[0]; + if (m.metallic.scale != kDefaultTexScale) { + gm.pbrMetallicRoughness.metallicFactor = m.metallic.scale[0]; } } else if (m.metallic.value.IsHolding()) { float value = m.metallic.value.UncheckedGet(); @@ -1716,9 +1707,8 @@ exportMaterials(ExportGltfContext& ctx) } if (m.roughness.image >= 0) { - if (m.roughness.scale.IsHolding()) { - GfVec4f scale = m.roughness.scale.UncheckedGet(); - gm.pbrMetallicRoughness.roughnessFactor = scale[0]; + if (m.roughness.scale != kDefaultTexScale) { + gm.pbrMetallicRoughness.roughnessFactor = m.roughness.scale[0]; } } else if (m.roughness.value.IsHolding()) { float value = m.roughness.value.UncheckedGet(); @@ -1760,7 +1750,7 @@ exportMaterials(ExportGltfContext& ctx) if (exportClearcoat) { exportClearcoatExtension(ctx, inputTranslator, m, gm); exportAdobeClearcoatSpecularExtension(ctx, inputTranslator, m, gm); - exportAdobeClearcoatTintExtension(ctx, inputTranslator, m, gm); + exportClearcoatColorExtension(ctx, inputTranslator, m, gm); } } @@ -1938,36 +1928,102 @@ exportMeshes(ExportGltfContext& ctx) mesh.normals.values.data(), true); - int tangentsAccessor = addAccessor(ctx.gltf, - "tangents", - TINYGLTF_TARGET_ARRAY_BUFFER, - TINYGLTF_TYPE_VEC4, - TINYGLTF_COMPONENT_TYPE_FLOAT, - mesh.tangents.values.size(), - mesh.tangents.values.data(), - true); + int tangentsAccessor = -1; + std::vector gltfTangents; + + if (mesh.tangents.values.size() > 0) { + // If we have both tangents and bitangents, we need to reconstruct the proper tangent format with handedness in w + if (mesh.bitangents.values.size() == mesh.tangents.values.size() && + mesh.normals.values.size() == mesh.tangents.values.size()) { + + gltfTangents.resize(mesh.tangents.values.size()); + for (size_t k = 0; k < mesh.tangents.values.size(); k++) { + const PXR_NS::GfVec4f& usdTangent = mesh.tangents.values[k]; + const PXR_NS::GfVec3f& normal = mesh.normals.values[k]; + const PXR_NS::GfVec3f& bitangent = mesh.bitangents.values[k]; + + PXR_NS::GfVec3f tangentXYZ(usdTangent[0], usdTangent[1], usdTangent[2]); + + // bitangent - cross product: normal × tangentXYZ + PXR_NS::GfVec3f expectedBitangent( + normal[1] * tangentXYZ[2] - normal[2] * tangentXYZ[1], + normal[2] * tangentXYZ[0] - normal[0] * tangentXYZ[2], + normal[0] * tangentXYZ[1] - normal[1] * tangentXYZ[0] + ); + + float dot = bitangent[0] * expectedBitangent[0] + + bitangent[1] * expectedBitangent[1] + + bitangent[2] * expectedBitangent[2]; + float handedness = dot >= 0.0f ? 1.0f : -1.0f; + + // Validate the vectors are normalized + float tangentLength = std::sqrt(tangentXYZ[0]*tangentXYZ[0] + tangentXYZ[1]*tangentXYZ[1] + tangentXYZ[2]*tangentXYZ[2]); + float normalLength = std::sqrt(normal[0]*normal[0] + normal[1]*normal[1] + normal[2]*normal[2]); + float bitangentLength = std::sqrt(bitangent[0]*bitangent[0] + bitangent[1]*bitangent[1] + bitangent[2]*bitangent[2]); + + if (tangentLength < 0.001f || normalLength < 0.001f || bitangentLength < 0.001f) { + TF_WARN("Degenerate tangent space vectors detected at vertex %zu " + "(tangent: %f, normal: %f, bitangent: %f). " + "Using default handedness +1.", + k, tangentLength, normalLength, bitangentLength); + handedness = 1.0f; + } + + gltfTangents[k] = PXR_NS::GfVec4f(tangentXYZ[0], tangentXYZ[1], tangentXYZ[2], handedness); + } + + tangentsAccessor = addAccessor(ctx.gltf, + "tangents", + TINYGLTF_TARGET_ARRAY_BUFFER, + TINYGLTF_TYPE_VEC4, + TINYGLTF_COMPONENT_TYPE_FLOAT, + gltfTangents.size(), + gltfTangents.data(), + true); + } else { + // Only tangents available, use them directly + tangentsAccessor = addAccessor(ctx.gltf, + "tangents", + TINYGLTF_TARGET_ARRAY_BUFFER, + TINYGLTF_TYPE_VEC4, + TINYGLTF_COMPONENT_TYPE_FLOAT, + mesh.tangents.values.size(), + mesh.tangents.values.data(), + true); + } + } std::vector uvsAccessors; + // Create a copy of UV coordinates with flipped V values for glTF export + PXR_NS::VtVec2fArray flippedUvs = mesh.uvs.values; + for (auto& uv : flippedUvs) { + uv[1] = 1.0f - uv[1]; + } int uvsAccessor = addAccessor(ctx.gltf, "texCoords", TINYGLTF_TARGET_ARRAY_BUFFER, TINYGLTF_TYPE_VEC2, TINYGLTF_COMPONENT_TYPE_FLOAT, - mesh.uvs.values.size(), - mesh.uvs.values.data(), + flippedUvs.size(), + flippedUvs.data(), true); if (uvsAccessor >= 0) uvsAccessors.push_back(uvsAccessor); int extraUVsCount = 0; for (auto const& uvs : mesh.extraUVSets) { + // Create a copy of extra UV coordinates with flipped V values for glTF export + PXR_NS::VtVec2fArray flippedExtraUvs = uvs.values; + for (auto& uv : flippedExtraUvs) { + uv[1] = 1.0f - uv[1]; + } uvsAccessor = addAccessor(ctx.gltf, "texCoords" + std::to_string(extraUVsCount + 1), TINYGLTF_TARGET_ARRAY_BUFFER, TINYGLTF_TYPE_VEC2, TINYGLTF_COMPONENT_TYPE_FLOAT, - uvs.values.size(), - uvs.values.data(), + flippedExtraUvs.size(), + flippedExtraUvs.data(), true); if (uvsAccessor >= 0) { uvsAccessors.push_back(uvsAccessor); diff --git a/gltf/src/gltfImport.cpp b/gltf/src/gltfImport.cpp index 32742f01..ef4042eb 100644 --- a/gltf/src/gltfImport.cpp +++ b/gltf/src/gltfImport.cpp @@ -14,6 +14,7 @@ governing permissions and limitations under the License. #include "gltfAnisotropy.h" #include "gltfSpecGloss.h" #include "importGltfContext.h" +#include #include #include #include @@ -22,6 +23,7 @@ governing permissions and limitations under the License. #include #include +#include using namespace PXR_NS; @@ -79,9 +81,22 @@ importMetadata(ImportGltfContext& ctx) ctx.usd->metadata.SetValueAtPath(extra.first, PXR_NS::VtValue(extra.second.Get())); } + // 'generator' could be on both asset.generator and asset.extras["generator"]. Regardless, - // replace with our own. - ctx.usd->metadata.SetValueAtPath("generator", PXR_NS::VtValue("Adobe usdGltf 1.0")); + // reference and incorporate into our own. Prioritize `generator` over `extras["generator"]`. + std::string generator = "Adobe usdGltf 1.0"; + std::string gltfGenerator = ""; + if (!ctx.gltf->asset.generator.empty()) { + gltfGenerator = ctx.gltf->asset.generator; + } else if (ctx.gltf->asset.extras.Has("generator")) { + gltfGenerator = ctx.gltf->asset.extras.Get("generator").Get(); + } + // If the glTF specified a generator, and it's not empty, add it to the USD generator string + if (!gltfGenerator.empty()) { + generator += "; glTF generator: " + gltfGenerator; + } + ctx.usd->metadata.SetValueAtPath("generator", PXR_NS::VtValue(generator)); + // 'copyright' could be on both asset.copyright and asset.extras["copyright"]. Give priority to // the former. if (!ctx.gltf->asset.copyright.empty()) { @@ -282,6 +297,13 @@ importImage(ImportGltfContext& ctx, const std::string& materialName, const std::string& imageName) { + // Validate texture index to prevent out-of-bounds access + if (textureIndex < 0 || static_cast(textureIndex) >= ctx.gltf->textures.size()) { + TF_WARN("Invalid texture index %d for material '%s' (valid range: 0-%zu)", + textureIndex, materialName.c_str(), ctx.gltf->textures.size() - 1); + return -1; + } + // Check the cache on the context if we've processed this texture before auto [it, inserted] = ctx.imageMap.insert({ textureIndex, -1 }); if (!inserted) { @@ -312,8 +334,11 @@ importImage(ImportGltfContext& ctx, usdImage.name = !image.name.empty() ? image.name : !uriStem.empty() ? uriStem : materialName + "_" + imageName; + + removeBrackets(usdImage.name); ctx.uniqueImageNameEnforcer.enforceUniqueness(usdImage.name); usdImage.uri = usdImage.name; + if (uriExtension == "png" || image.mimeType == "image/png") { usdImage.format = ImageFormatPng; usdImage.uri += ".png"; @@ -432,15 +457,11 @@ importTextureTransform(const tinygltf::ExtensionMap& extensions, Input& input) { auto posIt = extensions.find("KHR_texture_transform"); - // If the "KHR_texture_transform" is not supported, we ignore the - // transform values on the input. However, we still need to perform the - // (1.0 - T) flip which is applied here. Previously, we were flipping the - // V values of the UV coordinates when reading the mesh but since the - // glTF texture coordinates may have been defined using non-normalized values, - // the V inversion is applied here. + // If the "KHR_texture_transform" is not supported, we use default values. + // Note: We no longer apply V-coordinate flipping here since UV coordinates + // are now flipped during mesh import for consistency with tangent computation. if (posIt == extensions.end()) { - input.transformScale = GfVec2f(1.0f, -1.0f); - input.transformTranslation = GfVec2f(0.0f, 1.0f); + // No texture transform, use identity values return true; } @@ -453,36 +474,35 @@ importTextureTransform(const tinygltf::ExtensionMap& extensions, Input& input) if (rotation.IsNumber()) { float rotationValue = rotation.GetNumberAsDouble() * rad2deg; if (rotationValue != 0.0f) { - input.transformRotation = rotationValue; + input.uvRotation = rotationValue; } } - // As mentioned above, the T flip needs to be applied here. This is done - // by multiplying the y-scale value by -1 and using (1.0 - ty) as the new - // ty translation. + // Process scale values - no longer need to flip Y since UV coordinates + // are flipped during mesh import float sx = 1.0f; - float sy = -1.0f; + float sy = 1.0f; if (scale.IsArray() && scale.ArrayLen() == 2) { const tinygltf::Value& v0 = scale.Get(0); const tinygltf::Value& v1 = scale.Get(1); sx = v0.GetNumberAsDouble(); - sy = -v1.GetNumberAsDouble(); + sy = v1.GetNumberAsDouble(); } if (sx != 1.0f || sy != 1.0f) { - input.transformScale = GfVec2f(sx, sy); + input.uvScale = GfVec2f(sx, sy); } float tx = 0.0f; - float ty = 1.0f; + float ty = 0.0f; if (offset.IsArray() && offset.ArrayLen() == 2) { const tinygltf::Value& v0 = offset.Get(0); const tinygltf::Value& v1 = offset.Get(1); tx = v0.GetNumberAsDouble(); - ty = 1.0f - v1.GetNumberAsDouble(); + ty = v1.GetNumberAsDouble(); } if (tx != 0.0f || ty != 0.0f) { - input.transformTranslation = GfVec2f(tx, ty); + input.uvTranslation = GfVec2f(tx, ty); } return true; } @@ -579,8 +599,9 @@ void applyInputMultiplier(Input& input, const GfVec3f& mult) { if (input.image >= 0) { - GfVec4f scale = input.scale.GetWithDefault(GfVec4f(1.0f)); - input.scale = GfVec4f(mult[0] * scale[0], mult[1] * scale[1], mult[2] * scale[2], scale[3]); + input.scale[0] *= mult[0]; + input.scale[1] *= mult[1]; + input.scale[2] *= mult[2]; } else if (input.value.IsHolding()) { const GfVec3f& value = input.value.UncheckedGet(); input.value = GfVec3f(mult[0] * value[0], mult[1] * value[1], mult[2] * value[2]); @@ -753,22 +774,32 @@ importAdobeClearcoatSpecular(const tinygltf::ExtensionMap& extensions, return false; } -// Adobe extension for supporting colored tinting of clearcoat -struct AdobeClearcoatTint +// Multi-vendor extension for supporting colored tinting of clearcoat +struct ClearcoatColor { double factor[3] = { 1.0, 1.0, 1.0 }; tinygltf::TextureInfo texture; // rgb channels }; bool -importAdobeClearcoatTint(const tinygltf::ExtensionMap& extensions, - AdobeClearcoatTint* clearcoatTint) +importClearcoatColor(const tinygltf::ExtensionMap& extensions, + ClearcoatColor* clearcoatColor) { - auto extIt = extensions.find("ADOBE_materials_clearcoat_tint"); + // The multi-vendor version of coat tinting takes priority over the + // old, Adobe-specific, version. + auto extIt = extensions.find("EXT_materials_clearcoat_color"); + if (extIt != extensions.end()) { + const tinygltf::Value& coatExt = extIt->second; + readDoubleArray(coatExt.Get("clearcoatColorFactor"), clearcoatColor->factor, 3); + readTextureInfo(coatExt.Get("clearcoatColorTexture"), clearcoatColor->texture); + return true; + } + + extIt = extensions.find("ADOBE_materials_clearcoat_tint"); if (extIt != extensions.end()) { const tinygltf::Value& coatExt = extIt->second; - readDoubleArray(coatExt.Get("clearcoatTintFactor"), clearcoatTint->factor, 3); - readTextureInfo(coatExt.Get("clearcoatTintTexture"), clearcoatTint->texture); + readDoubleArray(coatExt.Get("clearcoatTintFactor"), clearcoatColor->factor, 3); + readTextureInfo(coatExt.Get("clearcoatTintTexture"), clearcoatColor->texture); return true; } @@ -832,6 +863,100 @@ importSubsurface(const tinygltf::ExtensionMap& extensions, Subsurface* subsurfac return false; } +// This is not a ratified extension yet! +// KHR_materials_volume_scatter +struct VolumeScatter +{ + double scatterAnisotropy = 0.0; // ASM does not support scatter anisotropy but OpenPBR does + double multiscatterColor[3] = { 0.0, 0.0, 0.0 }; + double scatteringDistanceScale[3] = { 0.0, 0.0, 0.0 }; + double scatteringDistance = 1.0; +}; + +bool +importVolumeScatter(const tinygltf::ExtensionMap& extensions, VolumeScatter* volumeScatter) +{ + auto extIt = extensions.find("KHR_materials_volume_scatter"); + if (extIt != extensions.end()) { + const tinygltf::Value& sssExt = extIt->second; + readDoubleArray(sssExt.Get("multiscatterColor"), volumeScatter->multiscatterColor, 3); + + // Look up the previously-read volume extension to get the attenuation distance and color + double attenuationDistance = 0.0f; + GfVec3d attenuationColor(1.0, 1.0, 1.0); + auto volumeExtIt = extensions.find("KHR_materials_volume"); + if (extIt != extensions.end()) { + const tinygltf::Value& volumeExt = volumeExtIt->second; + readDoubleValue(volumeExt.Get("attenuationDistance"), attenuationDistance); + readDoubleArray(volumeExt.Get("attenuationColor"), attenuationColor.data(), 3); + } + + // Calculate the single-scattering albedo + // This formulation is taken directly from the ASM implementation in Eclair (in + // asm_volume_utils.h) + GfVec3f multiscatterColor(volumeScatter->multiscatterColor[0], + volumeScatter->multiscatterColor[1], + volumeScatter->multiscatterColor[2]); + GfVec3f s = GfVec3f(4.09712f) + GfCompMult(GfVec3f(4.20863f), multiscatterColor); + GfVec3f p = GfVec3f(9.59217f) + GfCompMult(GfVec3f(41.6808f), multiscatterColor) + + GfCompMult(GfVec3f(17.7126f), GfCompMult(multiscatterColor, multiscatterColor)); + s = s - GfVec3f(GfSqrt(p[0]), GfSqrt(p[1]), GfSqrt(p[2])); + GfVec3f singleScatteringAlbedo = GfVec3f(1.0f) - GfCompMult(s, s); + + // Calculate the extinction coefficient from the attenuation color already in the volume + // Now that we have the scattering extension, we know that this coefficient represents both + // absorption and scattering. We will convert it to ASM using only ASM's scattering + // properties. + GfVec3f extinctionCoefficient(-std::log(attenuationColor[0]) / attenuationDistance, + -std::log(attenuationColor[1]) / attenuationDistance, + -std::log(attenuationColor[2]) / attenuationDistance); + + // Calculate the extinction coefficient that would be considered to be from the scattering + // part of ASM. This code is partly taken from the ASM implementation in Eclair (in + // asm_volume_utils.h) It puts limits on the extinction coefficient to keep it in a + // reasonable range and determines an appropriate extinction coefficient using the single + // scattering albedo and scattering distance. + float scatterDistance = std::fmaxf(1e-3f, attenuationDistance); + const float minExtinction = 1.0f / scatterDistance; + GfVec3f extinctionFromScattering(minExtinction); + const float maxAlbedo = + std::fmaxf(singleScatteringAlbedo[0], + std::fmaxf(singleScatteringAlbedo[1], singleScatteringAlbedo[2])); + if (maxAlbedo > 0.0f) { + // The max extinction can only be this many times bigger than the min extinction. + constexpr float maxMultiplier = 1e3f; + constexpr float inverseMaxMultiplier = 1.0f / maxMultiplier; + GfVec3f multiplier = GfVec3f(maxAlbedo); + GfVec3f multiplier2 = GfVec3f(maxAlbedo * inverseMaxMultiplier); + multiplier2 = GfVec3f(std::fmaxf(singleScatteringAlbedo[0], multiplier2[0]), + std::fmaxf(singleScatteringAlbedo[1], multiplier2[1]), + std::fmaxf(singleScatteringAlbedo[2], multiplier2[2])); + multiplier = GfCompDiv(multiplier, multiplier2); + extinctionFromScattering = GfCompMult(extinctionFromScattering, multiplier); + } + // Once we have an extinction coeff from scattering, we can compare it to the real + // extinction coeff and determine the scatter_distance_scale that we need to apply to + // acheive the same amount of scattering and absorption. + GfVec3f scatterDistanceScale = GfCompDiv(extinctionFromScattering, extinctionCoefficient); + + // If the scatter distance scale ended up being greater than 1, we need to scale the scatter + // distance to compensate. + float maxScatterDistance = std::fmaxf( + scatterDistanceScale[0], std::fmaxf(scatterDistanceScale[1], scatterDistanceScale[2])); + if (maxScatterDistance > 1.0f) { + scatterDistance *= maxScatterDistance; + scatterDistanceScale = GfCompDiv(scatterDistanceScale, GfVec3f(maxScatterDistance)); + } + volumeScatter->scatteringDistance = scatterDistance; + volumeScatter->scatteringDistanceScale[0] = scatterDistanceScale[0]; + volumeScatter->scatteringDistanceScale[1] = scatterDistanceScale[1]; + volumeScatter->scatteringDistanceScale[2] = scatterDistanceScale[2]; + return true; + } + + return false; +} + bool importUnlit(const tinygltf::ExtensionMap& extensions) { @@ -969,9 +1094,9 @@ importMaterials(ImportGltfContext& ctx) AdobeTokens->a, AdobeTokens->raw); importScale1(m.opacity, diffuse[3]); - m.opacity.transformRotation = m.diffuseColor.transformRotation; - m.opacity.transformScale = m.diffuseColor.transformScale; - m.opacity.transformTranslation = m.diffuseColor.transformTranslation; + m.opacity.uvRotation = m.diffuseColor.uvRotation; + m.opacity.uvScale = m.diffuseColor.uvScale; + m.opacity.uvTranslation = m.diffuseColor.uvTranslation; } } else if (diffuse.size()) { importValue3(m.diffuseColor, diffuse.data()); @@ -999,9 +1124,9 @@ importMaterials(ImportGltfContext& ctx) importScale1(m.roughness, gm.pbrMetallicRoughness.roughnessFactor); importTextureTransform(gm.pbrMetallicRoughness.metallicRoughnessTexture.extensions, m.roughness); - m.metallic.transformRotation = m.roughness.transformRotation; - m.metallic.transformScale = m.roughness.transformScale; - m.metallic.transformTranslation = m.roughness.transformTranslation; + m.metallic.uvRotation = m.roughness.uvRotation; + m.metallic.uvScale = m.roughness.uvScale; + m.metallic.uvTranslation = m.roughness.uvTranslation; } else { importValue1(m.metallic, gm.pbrMetallicRoughness.metallicFactor); importValue1(m.roughness, gm.pbrMetallicRoughness.roughnessFactor); @@ -1092,14 +1217,14 @@ importMaterials(ImportGltfContext& ctx) 1.0); } - AdobeClearcoatTint clearcoatTint; - if (importAdobeClearcoatTint(gm.extensions, &clearcoatTint)) { + ClearcoatColor clearcoatColor; + if (importClearcoatColor(gm.extensions, &clearcoatColor)) { importColorInput(ctx, m.displayName, "clearcoatColor", m.clearcoatColor, - clearcoatTint.texture, - clearcoatTint.factor, + clearcoatColor.texture, + clearcoatColor.factor, 1.0); } @@ -1212,10 +1337,23 @@ importMaterials(ImportGltfContext& ctx) applyInputMultiplier(m.absorptionColor, mult); } - Subsurface subsurface; - if (importSubsurface(gm.extensions, &subsurface)) { - importValue1(m.scatteringDistance, subsurface.scatterDistance); - importValue3(m.scatteringColor, subsurface.scatterColor); + VolumeScatter volumeScatter; + if (importVolumeScatter(gm.extensions, &volumeScatter)) { + importValue3(m.scatteringColor, volumeScatter.multiscatterColor); + importValue3(m.scatteringDistanceScale, volumeScatter.scatteringDistanceScale); + importValue1(m.scatteringDistance, volumeScatter.scatteringDistance); + // If we've imported the volume scatter extension, the attenuation color has been reinterpreted + // to include scattering and we need to erase the previously calculated absorption color. + double absorptionColor[3] = { 1.0, 1.0, 1.0 }; + importValue3(m.absorptionColor, absorptionColor); + importValue1(m.absorptionDistance, 0.0); + + } else { + Subsurface subsurface; + if (importSubsurface(gm.extensions, &subsurface)) { + importValue1(m.scatteringDistance, subsurface.scatterDistance); + importValue3(m.scatteringColor, subsurface.scatterColor); + } } } bool unlit = importUnlit(gm.extensions); @@ -1342,6 +1480,37 @@ importMeshJointWeights(const tinygltf::Model& model, if (jointCounts[0] == 0) return; + // Validate accessor types for joints and weights to prevent buffer overflow attacks + for (int i = 0; i < numJointSets; ++i) { + if (jointsIndices[i] >= 0) { + if (jointsIndices[i] >= static_cast(model.accessors.size())) { + TF_WARN("Joint accessor index %d out of bounds (length %zu) for mesh '%s'", + jointsIndices[i], model.accessors.size(), mesh.displayName.c_str()); + return; + } + const tinygltf::Accessor& jointAccessor = model.accessors[jointsIndices[i]]; + if (jointAccessor.type != TINYGLTF_TYPE_VEC4) { + TF_WARN("Joint accessor %d has invalid type %d (expected VEC4) for mesh '%s'", + jointsIndices[i], jointAccessor.type, mesh.displayName.c_str()); + return; + } + } + + if (weightsIndices[i] >= 0) { + if (weightsIndices[i] >= static_cast(model.accessors.size())) { + TF_WARN("Weight accessor index %d out of bounds (length %zu) for mesh '%s'", + weightsIndices[i], model.accessors.size(), mesh.displayName.c_str()); + return; + } + const tinygltf::Accessor& weightAccessor = model.accessors[weightsIndices[i]]; + if (weightAccessor.type != TINYGLTF_TYPE_VEC4) { + TF_WARN("Weight accessor %d has invalid type %d (expected VEC4) for mesh '%s'", + weightsIndices[i], weightAccessor.type, mesh.displayName.c_str()); + return; + } + } + } + // validate the joint indices and weights counts match for (int i = 0; i < numJointSets; ++i) { if (jointCounts[i] != weightCounts[i] || (i > 0 && jointCounts[i] != jointCounts[0])) { @@ -1434,43 +1603,119 @@ importMeshes(ImportGltfContext& ctx) // Be aware of properly combining UV subsets const tinygltf::Primitive& primitive = gmesh.primitives[j]; + + // Get accessor indices before adding mesh (for early validation) + int positionsIndex = getPrimitiveAttribute(primitive, "POSITION"); + int normalsIndex = getPrimitiveAttribute(primitive, "NORMAL"); + int tangentsIndex = getPrimitiveAttribute(primitive, "TANGENT"); + int uvsIndex = getPrimitiveAttribute(primitive, "TEXCOORD_0"); + int indicesIndex = primitive.indices; + + // Get vertex count for validation + size_t vertexCount = getAccessorElementCount(*ctx.gltf, positionsIndex); + + // Pre-validate indices before loading mesh data + bool skipLoadingData = false; + if (indicesIndex >= 0) { + PXR_NS::VtArray tempIndices; + getIndices(*ctx.gltf, indicesIndex, vertexCount, tempIndices); + + if (!tempIndices.empty() && vertexCount > 0) { + int maxIndex = *std::max_element(tempIndices.begin(), tempIndices.end()); + if (maxIndex >= static_cast(vertexCount)) { + TF_WARN("Mesh '%s' primitive %zu has indices (max %d) exceeding vertex count (%zu). Creating empty mesh to prevent crash.", + gmesh.name.c_str(), j, maxIndex, vertexCount); + skipLoadingData = true; + } + } + } + + // Always add mesh (even if invalid) to maintain index consistency + // If invalid, we'll leave it empty auto [meshIndex, mesh] = ctx.usd->addMesh(); ctx.meshes[i][j] = meshIndex; + + // Skip loading data if validation failed - leave mesh empty + if (skipLoadingData) { + continue; + } 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"); - int uvsIndex = getPrimitiveAttribute(primitive, "TEXCOORD_0"); - - int indicesIndex = primitive.indices; + // POSITION is required in GLTF mesh.points = PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, positionsIndex)); readAccessorDataToFloat( *ctx.gltf, positionsIndex, reinterpret_cast(mesh.points.data())); - mesh.normals.values = - PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, normalsIndex)); - readAccessorDataToFloat( - *ctx.gltf, normalsIndex, reinterpret_cast(mesh.normals.values.data())); - mesh.normals.interpolation = UsdGeomTokens->vertex; + // NORMAL is optional - only read if present + if (normalsIndex >= 0) { + mesh.normals.values = + PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, normalsIndex)); + readAccessorDataToFloat( + *ctx.gltf, normalsIndex, reinterpret_cast(mesh.normals.values.data())); + mesh.normals.interpolation = UsdGeomTokens->vertex; + } - mesh.tangents.values = - PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, tangentsIndex)); - readAccessorDataToFloat( - *ctx.gltf, tangentsIndex, reinterpret_cast(mesh.tangents.values.data())); - mesh.tangents.interpolation = UsdGeomTokens->vertex; + // TANGENT is optional - only read if present + if (tangentsIndex >= 0) { + mesh.tangents.values = + PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, tangentsIndex)); + readAccessorDataToFloat( + *ctx.gltf, tangentsIndex, reinterpret_cast(mesh.tangents.values.data())); + mesh.tangents.interpolation = UsdGeomTokens->vertex; + + // GLTF tangent format: (x, y, z, w) where w is handedness (+1 or -1) + // Binormal = cross(normal, tangent.xyz) * tangent.w + // Only compute bitangents if explicitly requested + if (ctx.options->computeBitangents && mesh.normals.values.size() == mesh.tangents.values.size()) { + mesh.bitangents.values.resize(mesh.tangents.values.size()); + for (size_t k = 0; k < mesh.tangents.values.size(); k++) { + const PXR_NS::GfVec3f& normal = mesh.normals.values[k]; + const PXR_NS::GfVec4f& tangent = mesh.tangents.values[k]; + PXR_NS::GfVec3f tangentXYZ(tangent[0], tangent[1], tangent[2]); + float handedness = tangent[3]; + + if (std::abs(handedness) < 0.5f) { + TF_WARN("Invalid handedness value %f in tangent data, assuming +1", handedness); + handedness = 1.0f; + } else { + handedness = handedness >= 0.0f ? 1.0f : -1.0f; + } + + // Compute bitangent using cross product: normal × tangentXYZ + PXR_NS::GfVec3f crossProduct( + normal[1] * tangentXYZ[2] - normal[2] * tangentXYZ[1], // x = ny*tz - nz*ty + normal[2] * tangentXYZ[0] - normal[0] * tangentXYZ[2], // y = nz*tx - nx*tz + normal[0] * tangentXYZ[1] - normal[1] * tangentXYZ[0] // z = nx*ty - ny*tx + ); + mesh.bitangents.values[k] = crossProduct * handedness; + } + mesh.bitangents.interpolation = UsdGeomTokens->vertex; + } else if (ctx.options->computeBitangents && mesh.normals.values.size() > 0) { + TF_WARN("Tangent and normal vertex counts don't match (%zu tangents, %zu normals). " + "Skipping bitangent computation.", + mesh.tangents.values.size(), + mesh.normals.values.size()); + } + } - mesh.uvs.values = - PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, uvsIndex)); - readAccessorDataToFloat( - *ctx.gltf, uvsIndex, reinterpret_cast(mesh.uvs.values.data())); - mesh.uvs.interpolation = UsdGeomTokens->vertex; + // TEXCOORD_0 is optional - only read if present + if (uvsIndex >= 0) { + mesh.uvs.values = + PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, uvsIndex)); + readAccessorDataToFloat( + *ctx.gltf, uvsIndex, reinterpret_cast(mesh.uvs.values.data())); + // Flip V coordinates for glTF files to match USD convention + for (auto& uv : mesh.uvs.values) { + uv[1] = 1.0f - uv[1]; + } + mesh.uvs.interpolation = UsdGeomTokens->vertex; + } // if there is one uv set, check for more if (uvsIndex >= 0 && mesh.uvs.values.size()) { @@ -1488,6 +1733,10 @@ importMeshes(ImportGltfContext& ctx) getAccessorElementCount(*ctx.gltf, uvsIndex)); readAccessorDataToFloat( *ctx.gltf, uvsIndex, reinterpret_cast(uvs.values.data())); + // Flip V coordinates for additional UV sets as well + for (auto& uv : uvs.values) { + uv[1] = 1.0f - uv[1]; + } uvs.interpolation = UsdGeomTokens->vertex; } } @@ -1503,7 +1752,7 @@ importMeshes(ImportGltfContext& ctx) TF_WARN("GLTF TRIANGLE primitive has a number of indices not divisible " "by 3\n"); } - + break; case TINYGLTF_MODE_TRIANGLE_STRIP: { PXR_NS::VtArray stripIndices; @@ -1569,58 +1818,127 @@ importMeshes(ImportGltfContext& ctx) opacityPV.interpolation = UsdGeomTokens->vertex; } if (primitive.material >= 0) { - mesh.material = primitive.material; - mesh.doubleSided = ctx.gltf->materials[primitive.material].doubleSided; + if (ctx.gltf->materials.size() > primitive.material) { + mesh.material = primitive.material; + mesh.doubleSided = ctx.gltf->materials[primitive.material].doubleSided; + } else { + TF_WARN("Encountered GLTF primitive with an out of bounds material index %d\n", + primitive.material); + } } } } } +// Traverses the glTF nodes to construct names appropriate for UsdSkel API consumption +// (for the Skeleton::joints attribute), of the form: n0/n1/n2... +bool _buildSkeletonNodeNames(ImportGltfContext& ctx, int parentIndex, int nodeIndex, std::unordered_set& traversedNodes) { + if (traversedNodes.count(nodeIndex) > 0) { + TF_WARN("Node index %d is already traversed, skipping", nodeIndex); + return false; + } + traversedNodes.insert(nodeIndex); + + // First, we'll build the name for the node + std::string name = "n" + std::to_string(nodeIndex); + if (parentIndex >= 0) { + auto parentIt = ctx.skeletonNodeNames.find(parentIndex); + if (parentIt != ctx.skeletonNodeNames.end()) { + name = parentIt->second + "/" + name; + } + } + ctx.skeletonNodeNames[nodeIndex] = name; + + // Then we'll check if the node index is valid + if (nodeIndex < 0 || nodeIndex >= ctx.gltf->nodes.size()) { + TF_WARN("Node index %d out of bounds (length %zu)", nodeIndex, ctx.gltf->nodes.size()); + + // This is a bad node index, so we won't look for children. + return false; + } + + const tinygltf::Node& node = ctx.gltf->nodes[nodeIndex]; + for (size_t i = 0; i < node.children.size(); i++) { + _buildSkeletonNodeNames(ctx, nodeIndex, node.children[i], traversedNodes); + } + + return true; +}; + // Import skeletons from gltf. -// First traverses all glTF nodes in the scene, to construct names appropriate for UsdSkel API -// consumption (for the Skeleton::joints attribute), of the form: -// n0/n1/n2... -// Then traverses all glTF skins and assembles skeleton data in the Usdata cache. +// Generate UsdSkel API node names. +// Then traverse all glTF skins and assembles skeleton data in the Usdata cache. // This doesn't specify instantiation of any skeletons, which is done by importNodes. // It's ok that importNodes runs before this one, because the skins and skeletons counts are // equal. void importSkeletons(ImportGltfContext& ctx) { - ctx.skeletonNodeNames.resize(ctx.gltf->nodes.size(), ""); - std::function buildSkeletonNodeNames; - buildSkeletonNodeNames = [&](int parentIndex, int nodeIndex) { - const tinygltf::Node& node = ctx.gltf->nodes[nodeIndex]; - std::string name = "n" + std::to_string(nodeIndex); - ctx.skeletonNodeNames[nodeIndex] = - parentIndex >= 0 ? ctx.skeletonNodeNames[parentIndex] + "/" + name : name; - for (size_t i = 0; i < node.children.size(); i++) { - buildSkeletonNodeNames(nodeIndex, node.children[i]); - } - return true; - }; + std::unordered_set traversedNodes; for (const tinygltf::Scene& scene : ctx.gltf->scenes) { for (int rootNodeIndex : scene.nodes) { - buildSkeletonNodeNames(-1, rootNodeIndex); + _buildSkeletonNodeNames(ctx, -1, rootNodeIndex, traversedNodes); } } + // ctx.usd->skeletons was resized at the very start to match the size of ctx.gltf->skins, + // but let's make sure it's still the same size. + if (ctx.usd->skeletons.size() != ctx.gltf->skins.size()) { + TF_CODING_ERROR("usd->skeletons size (%zu) does not match gltf->skins size (%zu)", + ctx.usd->skeletons.size(), ctx.gltf->skins.size()); + } + // Then build the skeletons - 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]; + for (size_t skinIndex = 0; skinIndex < ctx.gltf->skins.size(); skinIndex++) { + const tinygltf::Skin& skin = ctx.gltf->skins[skinIndex]; + + Skeleton& skeleton = ctx.usd->skeletons[skinIndex]; + + // Populate the skeleton with the data from the skin 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()); skeleton.bindTransforms = PXR_NS::VtMatrix4dArray(skin.joints.size()); - for (size_t j = 0; j < skin.joints.size(); j++) { - int nodeIndex = skin.joints[j]; + + // Populate the skeleton with the data from the skin's joints + for (size_t jointIdx = 0; jointIdx < skin.joints.size(); jointIdx++) { + int nodeIndex = skin.joints[jointIdx]; + + // Validate node index BEFORE using it to prevent out-of-bounds access + if (nodeIndex < 0 || nodeIndex >= static_cast(ctx.gltf->nodes.size())) { + TF_WARN("Skin joint index %d out of bounds (must be 0-%zu) for skin '%s'", + nodeIndex, ctx.gltf->nodes.size() - 1, skin.name.c_str()); + + // Create placeholder for bad joint index + skeleton.joints[jointIdx] = PXR_NS::TfToken("bad_index_node_" + + std::to_string(nodeIndex)); + skeleton.jointNames[jointIdx] = PXR_NS::TfToken("Bad Index Node " + + std::to_string(nodeIndex)); + skeleton.restTransforms[jointIdx] = PXR_NS::GfMatrix4d(1); + skeleton.bindTransforms[jointIdx] = PXR_NS::GfMatrix4d(1); + continue; + } + + auto nodeIt = ctx.nodeMap.find(nodeIndex); + if (nodeIt == ctx.nodeMap.end()) { + TF_WARN("Could not find USD node index for glTF node %d", nodeIndex); + continue; + } + + if (nodeIt->second < 0 || nodeIt->second >= static_cast(ctx.usd->nodes.size())) { + TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + ctx.usd->nodes.size()); + continue; + } + Node& usdNode = ctx.usd->nodes[nodeIt->second]; + usdNode.isJoint = true; + const tinygltf::Node& node = ctx.gltf->nodes[nodeIndex]; - Node& usdNode = ctx.usd->nodes[ctx.nodeMap[nodeIndex]]; + // Recall all glTF nodes are going to be imported as USD nodes // but we still mark this node as a skeleton joint in the cache. - usdNode.isJoint = true; + PXR_NS::GfVec3d t = node.translation.size() ? PXR_NS::GfVec3d(node.translation[0], node.translation[1], node.translation[2]) @@ -1631,19 +1949,47 @@ importSkeletons(ImportGltfContext& ctx) node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]) : PXR_NS::GfQuatd(0); PXR_NS::GfMatrix4d m = PXR_NS::GfMatrix4d(r, t); - const std::string& name = ctx.skeletonNodeNames[nodeIndex]; - skeleton.joints[j] = PXR_NS::TfToken(name); - skeleton.jointNames[j] = PXR_NS::TfToken(node.name); - skeleton.restTransforms[j] = m; + + // We already checked above that the node index is valid + auto nameIt = ctx.skeletonNodeNames.find(nodeIndex); + if (nameIt == ctx.skeletonNodeNames.end()) { + TF_WARN("Could not find skeleton node name for glTF node %d", nodeIndex); + continue; + } + const std::string& name = nameIt->second; + skeleton.joints[jointIdx] = PXR_NS::TfToken(name); + skeleton.jointNames[jointIdx] = PXR_NS::TfToken(node.name); + skeleton.restTransforms[jointIdx] = m; + } + + // Validate inverse bind matrices accessor to prevent type confusion attacks + if (skin.inverseBindMatrices >= 0) { + if (skin.inverseBindMatrices >= static_cast(ctx.gltf->accessors.size())) { + TF_WARN("Inverse bind matrices accessor index %d out of bounds (length %zu) for skin '%s'", + skin.inverseBindMatrices, ctx.gltf->accessors.size(), skeleton.displayName.c_str()); + continue; + } + const tinygltf::Accessor& ibmAccessor = ctx.gltf->accessors[skin.inverseBindMatrices]; + if (ibmAccessor.type != TINYGLTF_TYPE_MAT4) { + TF_WARN("Inverse bind matrices accessor %d has invalid type %d (expected MAT4) for skin '%s'", + skin.inverseBindMatrices, ibmAccessor.type, skeleton.displayName.c_str()); + continue; + } + if (ibmAccessor.count != skin.joints.size()) { + TF_WARN("Inverse bind matrices accessor %d count %zu does not match joints count %zu for skin '%s'", + skin.inverseBindMatrices, ibmAccessor.count, skin.joints.size(), skeleton.displayName.c_str()); + continue; + } } + PXR_NS::VtArray inverseBindMatricesFloat( getAccessorElementCount(*ctx.gltf, skin.inverseBindMatrices)); readAccessorData(*ctx.gltf, skin.inverseBindMatrices, reinterpret_cast(inverseBindMatricesFloat.data())); - for (size_t i = 0; i < skin.joints.size(); i++) { - skeleton.bindTransforms[i] = - PXR_NS::GfMatrix4d(inverseBindMatricesFloat[i]).GetInverse(); + for (size_t jointIdx = 0; jointIdx < skin.joints.size(); jointIdx++) { + skeleton.bindTransforms[jointIdx] = + PXR_NS::GfMatrix4d(inverseBindMatricesFloat[jointIdx]).GetInverse(); } } } @@ -1659,16 +2005,45 @@ importChannel(const tinygltf::Model& gltf, float& maxTime) { if (channel.target_path == name) { + // Validate animation sampler accessors to prevent buffer overflow attacks + if (sampler.input < 0 || sampler.input >= static_cast(gltf.accessors.size())) { + TF_WARN("Animation sampler input accessor index %d out of bounds (length %zu) for channel '%s'", + sampler.input, gltf.accessors.size(), name.c_str()); + return false; + } + + if (sampler.output < 0 || sampler.output >= static_cast(gltf.accessors.size())) { + TF_WARN("Animation sampler output accessor index %d out of bounds (length %zu) for channel '%s'", + sampler.output, gltf.accessors.size(), name.c_str()); + return false; + } + int offset = values.times.size(); int count = getAccessorElementCount(gltf, sampler.input); int count2 = getAccessorElementCount(gltf, sampler.output); + + // Validate accessor element counts to prevent buffer access violations + if (count <= 0) { + TF_WARN("Animation sampler input accessor %d has invalid count %d for channel '%s'", + sampler.input, count, name.c_str()); + return false; + } + if (count2 <= 0) { + TF_WARN("Animation sampler output accessor %d has invalid count %d for channel '%s'", + sampler.output, count2, name.c_str()); + return false; + } + values.times.resize(offset + count); values.values.resize(offset + count2); - readAccessorDataToFloat(gltf, sampler.input, reinterpret_cast(values.times.data())); + readAccessorDataToFloat(gltf, sampler.input, + reinterpret_cast(values.times.data() + offset)); readAccessorDataToFloat( - gltf, sampler.output, reinterpret_cast(values.values.data())); - minTime = std::min(minTime, values.times[0]); - maxTime = std::max(maxTime, values.times[values.times.size() - 1]); + gltf, sampler.output, reinterpret_cast(values.values.data() + offset)); + + // Safe to access array elements since we validated count > 0 + minTime = std::min(minTime, values.times[offset]); + maxTime = std::max(maxTime, values.times[offset + count - 1]); return true; } return false; @@ -1697,8 +2072,23 @@ importNodeAnimations(ImportGltfContext& ctx) AnimationTrack& track = ctx.usd->animationTracks[animationTrackIndex]; for (const tinygltf::AnimationChannel& channel : animation.channels) { + if (channel.sampler < 0 || channel.sampler >= animation.samplers.size()) { + TF_WARN("Animation sampler index %d is out of bounds (max: %zu)", + channel.sampler, animation.samplers.size()); + continue; + } const tinygltf::AnimationSampler& sampler = animation.samplers[channel.sampler]; - Node& node = ctx.usd->nodes[ctx.nodeMap[channel.target_node]]; + auto nodeIt = ctx.nodeMap.find(channel.target_node); + if (nodeIt == ctx.nodeMap.end()) { + TF_WARN("Could not find USD node index for glTF node %d", channel.target_node); + continue; + } + if (nodeIt->second < 0 || nodeIt->second >= ctx.usd->nodes.size()) { + TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + ctx.usd->nodes.size()); + continue; + } + Node& node = ctx.usd->nodes[nodeIt->second]; // Modify the existing nodeAnimation if we had one, or use a new one if not bool hadNodeAnimation = !node.animations.empty(); @@ -1760,12 +2150,27 @@ importSkeletonAnimations(ImportGltfContext& ctx) // Select those animated nodes that correspond to skeleton nodes for (const tinygltf::AnimationChannel& channel : animation.channels) { - if (!ctx.usd->nodes[ctx.nodeMap[channel.target_node]].isJoint) { - const tinygltf::Node& node = ctx.gltf->nodes[channel.target_node]; - TF_DEBUG_MSG(FILE_FORMAT_GLTF, - "Found non skeleton node %d %s\n", - channel.target_node, - node.name.c_str()); + auto nodeIt = ctx.nodeMap.find(channel.target_node); + if (nodeIt == ctx.nodeMap.end()) { + TF_WARN("Could not find USD node index for glTF node %d", channel.target_node); + continue; + } + if (nodeIt->second < 0 || nodeIt->second >= ctx.usd->nodes.size()) { + TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + ctx.usd->nodes.size()); + continue; + } + if (!ctx.usd->nodes[nodeIt->second].isJoint) { + if (channel.target_node < 0 || channel.target_node >= ctx.gltf->nodes.size()) { + TF_WARN("Node index %d out of bounds (length %zu)", channel.target_node, + ctx.gltf->nodes.size()); + } else { + const tinygltf::Node& node = ctx.gltf->nodes[channel.target_node]; + TF_DEBUG_MSG(FILE_FORMAT_GLTF, + "Found non skeleton node %d %s\n", + channel.target_node, + node.name.c_str()); + } continue; } animatedNodeSet.insert(channel.target_node); @@ -1777,9 +2182,17 @@ importSkeletonAnimations(ImportGltfContext& ctx) return; } - for (size_t j = 0; j < ctx.gltf->skins.size(); j++) { - const tinygltf::Skin& skin = ctx.gltf->skins[j]; - Skeleton& skeleton = ctx.usd->skeletons[j]; + // ctx.usd->skeletons was resized at the very start to match the size of ctx.gltf->skins, + // but let's make sure it's still the same size. + if (ctx.usd->skeletons.size() != ctx.gltf->skins.size()) { + TF_CODING_ERROR("usd->skeletons size (%zu) does not match gltf->skins size (%zu)", + ctx.usd->skeletons.size(), ctx.gltf->skins.size()); + } + + for (size_t skinIdx = 0; skinIdx < ctx.gltf->skins.size(); skinIdx++) { + const tinygltf::Skin& skin = ctx.gltf->skins[skinIdx]; + + Skeleton& skeleton = ctx.usd->skeletons[skinIdx]; // Determine the set of animated nodes affecting this skeleton std::vector skelAnimNodes; @@ -1798,23 +2211,40 @@ importSkeletonAnimations(ImportGltfContext& ctx) // all tracks and poplulate them with the relevant animation data. skeleton.skeletonAnimations.resize(ctx.usd->animationTracks.size()); skeleton.animatedJoints.resize(skelAnimNodes.size()); - for (size_t j = 0; j < skelAnimNodes.size(); j++) { - std::string name = ctx.skeletonNodeNames[skelAnimNodes[j]]; - skeleton.animatedJoints[j] = PXR_NS::TfToken(name); + for (size_t skelAnimIdx = 0; skelAnimIdx < skelAnimNodes.size(); skelAnimIdx++) { + auto nameIt = ctx.skeletonNodeNames.find(skelAnimNodes[skelAnimIdx]); + if (nameIt == ctx.skeletonNodeNames.end()) { + TF_WARN("Could not find skeleton node name for glTF node %d", + skelAnimNodes[skelAnimIdx]); + continue; + } + std::string name = nameIt->second; + skeleton.animatedJoints[skelAnimIdx] = PXR_NS::TfToken(name); } for (size_t animationTrackIndex = 0; animationTrackIndex < ctx.usd->animationTracks.size(); animationTrackIndex++) { const tinygltf::Animation& animation = ctx.gltf->animations[animationTrackIndex]; AnimationTrack& track = ctx.usd->animationTracks[animationTrackIndex]; - SkeletonAnimation& skeletonAnimation = skeleton.skeletonAnimations[animationTrackIndex]; + SkeletonAnimation& skeletonAnimation = + skeleton.skeletonAnimations[animationTrackIndex]; // Build a definitive time scale by inserting time points from every times array. // TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Assembling animation time"); std::vector definitiveTimes; for (int animNode : skelAnimNodes) { - const Node& node = ctx.usd->nodes[ctx.nodeMap[animNode]]; - if (node.animations.size() > animationTrackIndex) { + auto nodeIt = ctx.nodeMap.find(animNode); + if (nodeIt == ctx.nodeMap.end()) { + TF_WARN("Could not find USD node index for glTF node %d", animNode); + continue; + } + if (nodeIt->second < 0 || nodeIt->second >= ctx.usd->nodes.size()) { + TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + ctx.usd->nodes.size()); + continue; + } + const Node& node = ctx.usd->nodes[nodeIt->second]; + if (animationTrackIndex < node.animations.size()) { const NodeAnimation& nodeAnimation = node.animations[animationTrackIndex]; addToTimeMap(definitiveTimes, nodeAnimation.rotations.times); addToTimeMap(definitiveTimes, nodeAnimation.translations.times); @@ -1824,7 +2254,7 @@ importSkeletonAnimations(ImportGltfContext& ctx) // TODO: when implementing weights animation, might be able to remove this guard if (definitiveTimes.size() <= 0) { TF_DEBUG_MSG(FILE_FORMAT_GLTF, - "Animation %lu %s has no times\n", + "Animation %lu %s has no times", animationTrackIndex, animation.name.c_str()); continue; @@ -1845,9 +2275,27 @@ importSkeletonAnimations(ImportGltfContext& ctx) std::vector> definitiveScales( skelAnimNodes.size(), PXR_NS::VtArray(definitiveTimes.size(), PXR_NS::GfVec3f(1))); - for (size_t j = 0; j < skelAnimNodes.size(); j++) { - const Node& n = ctx.usd->nodes[ctx.nodeMap[skelAnimNodes[j]]]; - const tinygltf::Node& node = ctx.gltf->nodes[skelAnimNodes[j]]; + for (size_t skelAnimIdx = 0; skelAnimIdx < skelAnimNodes.size(); skelAnimIdx++) { + int nodeIndex = skelAnimNodes[skelAnimIdx]; + auto nodeIt = ctx.nodeMap.find(nodeIndex); + + if (nodeIt == ctx.nodeMap.end()) { + TF_WARN("Could not find USD node index for glTF node %d", nodeIndex); + continue; + } + if (nodeIt->second < 0 || nodeIt->second >= ctx.usd->nodes.size()) { + TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + ctx.usd->nodes.size()); + continue; + } + const Node& n = ctx.usd->nodes[nodeIt->second]; + + if (nodeIndex < 0 || nodeIndex >= ctx.gltf->nodes.size()) { + TF_WARN("Node index %d out of bounds (length %zu)", nodeIndex, + ctx.gltf->nodes.size()); + continue; + } + const tinygltf::Node& node = ctx.gltf->nodes[nodeIndex]; const NodeAnimation emptyNodeAnimation; const NodeAnimation& na = n.animations.size() > animationTrackIndex ? n.animations[animationTrackIndex] @@ -1857,37 +2305,41 @@ importSkeletonAnimations(ImportGltfContext& ctx) interpolateData(definitiveTimes, na.rotations.times, na.rotations.values, - definitiveRotations[j]); + definitiveRotations[skelAnimIdx]); } else { PXR_NS::GfQuatf restRotation = node.rotation.size() ? PXR_NS::GfQuatf( node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]) : PXR_NS::GfQuatf(0); - definitiveRotations[j].assign(definitiveTimes.size(), restRotation); + definitiveRotations[skelAnimIdx].assign(definitiveTimes.size(), restRotation); } if (na.translations.values.size() > 1) { interpolateData(definitiveTimes, na.translations.times, na.translations.values, - definitiveTranslations[j]); + definitiveTranslations[skelAnimIdx]); } else { PXR_NS::GfVec3f restTranslation = node.translation.size() ? PXR_NS::GfVec3f(node.translation[0], node.translation[1], node.translation[2]) : PXR_NS::GfVec3f(0); - definitiveTranslations[j].assign(definitiveTimes.size(), restTranslation); + definitiveTranslations[skelAnimIdx].assign(definitiveTimes.size(), + restTranslation); } if (na.scales.values.size() > 1) { interpolateData( - definitiveTimes, na.scales.times, na.scales.values, definitiveScales[j]); + definitiveTimes, + na.scales.times, + na.scales.values, + definitiveScales[skelAnimIdx]); } else { PXR_NS::GfVec3f restScale = node.scale.size() ? PXR_NS::GfVec3f(node.scale[0], node.scale[1], node.scale[2]) : PXR_NS::GfVec3f(1); - definitiveScales[j].assign(definitiveTimes.size(), restScale); + definitiveScales[skelAnimIdx].assign(definitiveTimes.size(), restScale); } } @@ -1898,14 +2350,19 @@ importSkeletonAnimations(ImportGltfContext& ctx) definitiveTimes.size(), PXR_NS::VtArray(skelAnimNodes.size())); skeletonAnimation.scales.resize(definitiveTimes.size(), PXR_NS::VtArray(skelAnimNodes.size())); - for (size_t j = 0; j < definitiveTimes.size(); j++) { - // TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Time[" << k << "] = " << definitiveTimes[j]); - skeletonAnimation.times[j] = definitiveTimes[j]; - for (size_t k = 0; k < skelAnimNodes.size(); k++) { - skeletonAnimation.rotations[j][k] = definitiveRotations[k][j]; - skeletonAnimation.translations[j][k] = - PXR_NS::GfVec3f(definitiveTranslations[k][j]); - skeletonAnimation.scales[j][k] = PXR_NS::GfVec3h(definitiveScales[k][j]); + for (size_t defTimeIdx = 0; defTimeIdx < definitiveTimes.size(); defTimeIdx++) { + + skeletonAnimation.times[defTimeIdx] = definitiveTimes[defTimeIdx]; + for (size_t skelAnimIdx = 0; skelAnimIdx < skelAnimNodes.size(); skelAnimIdx++) { + + skeletonAnimation.rotations[defTimeIdx][skelAnimIdx] = + definitiveRotations[skelAnimIdx][defTimeIdx]; + + skeletonAnimation.translations[defTimeIdx][skelAnimIdx] = + PXR_NS::GfVec3f(definitiveTranslations[skelAnimIdx][defTimeIdx]); + + skeletonAnimation.scales[defTimeIdx][skelAnimIdx] = + PXR_NS::GfVec3h(definitiveScales[skelAnimIdx][defTimeIdx]); } } } @@ -2069,55 +2526,133 @@ importNgpExtension(const tinygltf::Value& ngp, NgpData& ngpData) GfMatrix4d(GfRotation(GfVec3d(1.0, 0.0, 0.0), -90.0), GfVec3d(0.0, 0.0, 0.0)); } -// Import nodes from tinygltf Model to UsdData. // We traverse the glTF nodes recursively from root to children and assign each node a usd index -// k. We maintain a mapping from the gltf node index to the usd node index in `nodeMap` for -// reference. -// For nodes with mesh and skin, we add the mesh to the root node of the skeleton held by the -// skin. -bool -importNodes(ImportGltfContext& ctx) -{ - int k = 0; - int nodeCount = ctx.gltf->nodes.size(); - ctx.nodeMap.resize(nodeCount); // maps glTF node index to USD node index - ctx.usd->nodes.resize(nodeCount); // stores USD nodes in order of traversal - ctx.parentMap.resize(nodeCount); // maps glTF node index to parent glTF node index +// We maintain a mapping from the gltf node index to the usd node index in `nodeMap` for reference. +int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& curUsdIndex, + int parentIndex, int nodeIndex, std::unordered_set& traversedNodes) { + + if (traversedNodes.count(nodeIndex) > 0) { + TF_WARN("Node index %d is already traversed, skipping", nodeIndex); + auto it = ctx.nodeMap.find(nodeIndex); + if (it != ctx.nodeMap.end()) { + return it->second; + } + TF_RUNTIME_ERROR("Could not find node index in nodeMap for node we should have processed."); + return -1; + } + traversedNodes.insert(nodeIndex); + + // Get the next slot in the ctx.usd->nodes vector + int usdNodeIndex = curUsdIndex; + curUsdIndex++; - // Stores gltf nodeIndex - std::vector skinnedNodes; + if (usdNodeIndex < 0 || usdNodeIndex >= ctx.usd->nodes.size()) { + // You're trying to process a node that we haven't processed, but + // we don't have any more space in the usd nodes vector? That shouldn't happen. + // This must be a malformed gltf file. The number of usd nodes is set + // in importNodes: "ctx.usd->nodes.resize(ctx.gltf->nodes.size());" + TF_WARN("usdNodeIndex %d is out of bounds (max: %zu)", usdNodeIndex, ctx.usd->nodes.size()); - std::function traverse; - traverse = [&](int parentIndex, int nodeIndex) -> int { - const tinygltf::Node& node = ctx.gltf->nodes[nodeIndex]; - int usdNodeIndex = k++; + // We haven't processed this node, so we'll remove it from the traversedNodes set + traversedNodes.erase(nodeIndex); + + // But we can't return a valid usdNodeIndex, so we return -1 + return -1; + } + + // Validate the parentIndex + int usdParentIndex = -1; + if (parentIndex != -1) { + auto it = ctx.nodeMap.find(parentIndex); + if (it != ctx.nodeMap.end()) { + usdParentIndex = it->second; + } + } + + if (nodeIndex < 0 || nodeIndex >= ctx.gltf->nodes.size()) { + TF_WARN("Node index %d is out of bounds (max: %zu)", nodeIndex, ctx.gltf->nodes.size()); + + // There's a bad node index, but to preserve the mapping, we'll create a placeholder node Node& n = ctx.usd->nodes[usdNodeIndex]; ctx.nodeMap[nodeIndex] = usdNodeIndex; ctx.parentMap[nodeIndex] = parentIndex; - n.displayName = node.name; - n.translation = - !node.translation.empty() - ? PXR_NS::GfVec3d(node.translation[0], node.translation[1], node.translation[2]) - : PXR_NS::GfVec3d(0); - n.rotation = !node.rotation.empty() - ? PXR_NS::GfQuatf( - node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]) - : PXR_NS::GfQuatf(0); - n.scale = !node.scale.empty() ? PXR_NS::GfVec3f(node.scale[0], node.scale[1], node.scale[2]) - : PXR_NS::GfVec3f(1); - if (!node.matrix.empty()) { - n.hasTransform = true; - copyMatrix(node.matrix, n.transform); - } - if (node.camera >= 0) { + n.name = "bad_index_node_" + std::to_string(nodeIndex); + n.displayName = "Bad Index Node " + std::to_string(nodeIndex); + n.parent = usdParentIndex; + return usdNodeIndex; + } + + const tinygltf::Node& node = ctx.gltf->nodes[nodeIndex]; + + Node& n = ctx.usd->nodes[usdNodeIndex]; + ctx.nodeMap[nodeIndex] = usdNodeIndex; + ctx.parentMap[nodeIndex] = parentIndex; + n.displayName = node.name; + // Validate translation vector size before accessing elements + if (node.translation.size() >= 3) { + n.translation = PXR_NS::GfVec3d(node.translation[0], node.translation[1], node.translation[2]); + } else if (!node.translation.empty()) { + TF_WARN("Node '%s' has invalid translation size %zu (expected 3)", + node.name.c_str(), node.translation.size()); + n.translation = PXR_NS::GfVec3d(0); + } else { + n.translation = PXR_NS::GfVec3d(0); + } + // Validate rotation vector size before accessing elements + if (node.rotation.size() >= 4) { + n.rotation = PXR_NS::GfQuatf(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]); + } else if (!node.rotation.empty()) { + TF_WARN("Node '%s' has invalid rotation size %zu (expected 4)", + node.name.c_str(), node.rotation.size()); + n.rotation = PXR_NS::GfQuatf(0); + } else { + n.rotation = PXR_NS::GfQuatf(0); + } + // Validate scale vector size before accessing elements + if (node.scale.size() >= 3) { + n.scale = PXR_NS::GfVec3f(node.scale[0], node.scale[1], node.scale[2]); + } else if (!node.scale.empty()) { + TF_WARN("Node '%s' has invalid scale size %zu (expected 3)", + node.name.c_str(), node.scale.size()); + n.scale = PXR_NS::GfVec3f(1); + } else { + n.scale = PXR_NS::GfVec3f(1); + } + // Validate matrix vector size before copying + if (node.matrix.size() >= 16) { + n.hasTransform = true; + copyMatrix(node.matrix, n.transform); + } else if (!node.matrix.empty()) { + TF_WARN("Node '%s' has invalid matrix size %zu (expected 16)", + node.name.c_str(), node.matrix.size()); + } + // Validate camera index before use + if (node.camera >= 0) { + if (static_cast(node.camera) >= ctx.gltf->cameras.size()) { + TF_WARN("Node '%s' references invalid camera index %d (max: %zu)", + node.name.c_str(), node.camera, ctx.gltf->cameras.size() - 1); + } else { n.camera = node.camera; } - if (node.light >= 0) { + } + // Validate light index before use + if (node.light >= 0) { + if (static_cast(node.light) >= ctx.gltf->lights.size()) { + TF_WARN("Node '%s' references invalid light index %d (max: %zu)", + node.name.c_str(), node.light, ctx.gltf->lights.size() - 1); + } else { n.light = node.light; } - int usdParentIndex = (parentIndex != -1) ? ctx.nodeMap[parentIndex] : -1; - n.parent = usdParentIndex; - if (node.mesh >= 0) { + } + + n.parent = usdParentIndex; + + // Validate mesh index before accessing meshUseCount/meshes vectors + if (node.mesh >= 0) { + if (static_cast(node.mesh) >= ctx.gltf->meshes.size()) { + TF_WARN("Node '%s' references invalid mesh index %d (max: %zu)", + node.name.c_str(), node.mesh, ctx.gltf->meshes.size() - 1); + } else { ctx.meshUseCount[node.mesh]++; // If the node has a skin, add the mesh to the root node of the skeleton held by the // skin. @@ -2129,39 +2664,99 @@ importNodes(ImportGltfContext& ctx) n.staticMeshes = ctx.meshes[node.mesh]; } } - const auto ngp_iter = node.extensions.find(getNerfExtString()); - if (ngp_iter != node.extensions.end()) { - const tinygltf::Value& ngp = ngp_iter->second; - n.ngp = ctx.usd->ngps.size(); - ctx.usd->ngps.push_back(NgpData()); - importNgpExtension(ngp, ctx.usd->ngps[n.ngp]); + } + const auto ngp_iter = node.extensions.find(getNerfExtString()); + if (ngp_iter != node.extensions.end()) { + const tinygltf::Value& ngp = ngp_iter->second; + n.ngp = ctx.usd->ngps.size(); + ctx.usd->ngps.push_back(NgpData()); + importNgpExtension(ngp, ctx.usd->ngps[n.ngp]); + } + + // Make sure we only traverse children that are valid + std::vector validChildren; + for (int childIndex : node.children) { + if (traversedNodes.count(childIndex) > 0) { + continue; // No loops + } + if (childIndex < 0 || childIndex >= ctx.gltf->nodes.size()) { + continue; // No bad indices } - n.children.resize(node.children.size()); - for (size_t i = 0; i < node.children.size(); i++) { - n.children[i] = traverse(nodeIndex, node.children[i]); + validChildren.push_back(childIndex); + } + + int rtnIndex; + int validCount = 0; + n.children.resize(validChildren.size()); + for (auto childIndex : validChildren) { + rtnIndex = _traverseNodes(ctx, skinnedNodes, curUsdIndex, nodeIndex, childIndex, traversedNodes); + if (rtnIndex >= 0) { + n.children[validCount] = rtnIndex; + validCount++; } - return usdNodeIndex; - }; + } + n.children.resize(validCount); + return usdNodeIndex; +} + +// Import nodes from tinygltf Model to UsdData. We traverse the glTF nodes recursively +// For nodes with mesh and skin, we add the mesh to the root node of the skeleton held by the skin. +bool +importNodes(ImportGltfContext& ctx) +{ + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "importNodes: %zu nodes to process\n", ctx.gltf->nodes.size()); + if (ctx.gltf->nodes.size() <= 0) { + TF_WARN("No nodes in gltf"); + return false; + } + + int curUsdIndex = 0; + int numNodes = ctx.gltf->nodes.size(); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Resizing USD nodes array to %d\n", numNodes); + ctx.usd->nodes.resize(numNodes); // stores USD nodes in order of traversal + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting node traversal...\n"); + + // Stores gltf nodeIndex + std::vector skinnedNodes; // 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 + int rtnIndex; + std::unordered_set traversedNodes; for (const tinygltf::Scene& scene : ctx.gltf->scenes) { for (int rootNodeIndex : scene.nodes) { - int usdNodeIndex = traverse(-1, rootNodeIndex); - ctx.usd->rootNodes.push_back(usdNodeIndex); + rtnIndex = _traverseNodes(ctx, skinnedNodes, curUsdIndex, -1, rootNodeIndex, traversedNodes); + if (rtnIndex >= 0) { + ctx.usd->rootNodes.push_back(rtnIndex); + } } } // Set up relationships for skinned nodes, now that the traversal is done for (int nodeIndex : skinnedNodes) { + // These nodeIndices are valid, we only pushed back ones we could find in the gltf->nodes const tinygltf::Node& node = ctx.gltf->nodes[nodeIndex]; int gltfSkinRootNodexIndex = nodeIndex; + + if (node.skin < 0 || node.skin >= ctx.gltf->skins.size()) { + TF_WARN("Skin index %d is out of bounds (max: %zu)", node.skin, + ctx.gltf->skins.size()); + continue; + } + if (node.mesh < 0 || node.mesh >= ctx.meshes.size()) { + TF_WARN("Mesh index %d is out of bounds (max: %zu)", node.mesh, + ctx.meshes.size()); + continue; + } + int gltfSkeletonNodeIndex = ctx.gltf->skins[node.skin].skeleton; // If the skin has a skeleton, find the parent node of the skeleton if (gltfSkeletonNodeIndex >= 0) { - int gltfSkeletonNodeParentIndex = ctx.parentMap[gltfSkeletonNodeIndex]; + auto parentIt = ctx.parentMap.find(gltfSkeletonNodeIndex); + int gltfSkeletonNodeParentIndex = (parentIt != ctx.parentMap.end()) ? + parentIt->second : -1; // Check if the parent of the skeleton exists if (gltfSkeletonNodeParentIndex != -1) { @@ -2169,14 +2764,22 @@ importNodes(ImportGltfContext& ctx) } } else { // If the skin has no skeleton, find the parent node of the skin - int parentIndex = ctx.parentMap[nodeIndex]; + auto parentIt = ctx.parentMap.find(nodeIndex); + int parentIndex = (parentIt != ctx.parentMap.end()) ? parentIt->second : -1; if (parentIndex != -1) { gltfSkinRootNodexIndex = parentIndex; } } - int usdSkinRootNodeIndex = ctx.nodeMap[gltfSkinRootNodexIndex]; + auto nodeIt = ctx.nodeMap.find(gltfSkinRootNodexIndex); + if (nodeIt == ctx.nodeMap.end()) { + TF_WARN("Could not find USD node index for glTF node %d", gltfSkinRootNodexIndex); + continue; + } + int usdSkinRootNodeIndex = nodeIt->second; + // ctx.usd->skeletons was resized at the very start to match the size of ctx.gltf->skins + // and we've validated the skin index above, so we can safely access it here. Skeleton& skeleton = ctx.usd->skeletons[node.skin]; skeleton.parent = usdSkinRootNodeIndex; @@ -2202,11 +2805,17 @@ checkMeshInstancing(ImportGltfContext& ctx) if (useCount > 1) { const std::vector& meshPrimitiveIndices = ctx.meshes[meshIdx]; for (int primitiveIdx : meshPrimitiveIndices) { + if (primitiveIdx < 0 || primitiveIdx >= ctx.usd->meshes.size()) { + TF_WARN("Primitive index %d is out of bounds (max: %zu)", + primitiveIdx, ctx.usd->meshes.size()); + continue; + } ctx.usd->meshes[primitiveIdx].instanceable = true; } } if (useCount == 0) { + // ctx.meshUseCount is resized to match the size of ctx.gltf->meshes const tinygltf::Mesh& gmesh = ctx.gltf->meshes[meshIdx]; TF_WARN("Mesh %zu (%s) appears to be unused", meshIdx, gmesh.name.c_str()); } @@ -2239,6 +2848,8 @@ static const std::set supportedExtension = { // Vendor extensions "ADOBE_materials_clearcoat_specular", "ADOBE_materials_clearcoat_tint", + "EXT_materials_clearcoat_color", // Multi-vendor version of ADOBE_materials_clearcoat_tint + "EXT_materials_specular_edge_color", getNerfExtString(), // Archived extensions @@ -2246,8 +2857,9 @@ static const std::set supportedExtension = { // In-development extensions "KHR_materials_diffuse_transmission", - "KHR_materials_subsurface", - "KHR_materials_sss" // previous name of KHR_materials_subsurface + "KHR_materials_volume_scatter", + "KHR_materials_subsurface", // previous incarnation of KHR_materials_volume_scatter + "KHR_materials_sss" // previous name of KHR_materials_subsurface }; void @@ -2320,18 +2932,28 @@ importGltf(const ImportGltfOptions& options, if (options.importMaterials) { importMaterials(ctx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Materials import completed successfully\n"); } if (options.importGeometry) { + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting lights import...\n"); importLights(ctx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting meshes import...\n"); importMeshes(ctx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Meshes import completed\n"); // Resize the skeletons array before importing nodes, to allow skinning targets to be // added during importNodes ctx.usd->skeletons.resize(ctx.gltf->skins.size()); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting nodes import...\n"); importNodes(ctx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting skeletons import...\n"); importSkeletons(ctx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting animation tracks import...\n"); importAnimationTracks(ctx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting node animations import...\n"); importNodeAnimations(ctx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting skeleton animations import...\n"); importSkeletonAnimations(ctx); + TF_DEBUG_MSG(FILE_FORMAT_GLTF, "Starting mesh instancing check...\n"); checkMeshInstancing(ctx); } diff --git a/gltf/src/gltfImport.h b/gltf/src/gltfImport.h index 2e95fe5a..f9c566bc 100644 --- a/gltf/src/gltfImport.h +++ b/gltf/src/gltfImport.h @@ -22,6 +22,7 @@ struct ImportGltfOptions bool importGeometry = true; bool importMaterials = true; bool importImages = true; + bool computeBitangents = false; }; /// \ingroup usdgltf diff --git a/gltf/src/importGltfContext.h b/gltf/src/importGltfContext.h index 4770ec74..b1f1d527 100644 --- a/gltf/src/importGltfContext.h +++ b/gltf/src/importGltfContext.h @@ -25,9 +25,9 @@ struct ImportGltfContext const tinygltf::Model* gltf = nullptr; UsdData* usd = nullptr; std::string path; - std::vector nodeMap; - std::vector parentMap; - std::vector skeletonNodeNames; + std::unordered_map nodeMap; // maps glTF node index to USD node index + std::unordered_map parentMap; // maps glTF node index to parent glTF node index + std::unordered_map skeletonNodeNames; // maps glTF node index to skeleton node name std::vector> meshes; std::vector meshUseCount; diff --git a/gltf/tests/sanityTests.cpp b/gltf/tests/sanityTests.cpp index 854998f2..2d67c3b2 100644 --- a/gltf/tests/sanityTests.cpp +++ b/gltf/tests/sanityTests.cpp @@ -17,10 +17,10 @@ governing permissions and limitations under the License. #include #include +PXR_NAMESPACE_USING_DIRECTIVE + TEST(GlTFSanityTests, LoadCube) { - PXR_NAMESPACE_USING_DIRECTIVE - // Load an FBX UsdStageRefPtr stage = UsdStage::Open("SanityCube.gltf"); ASSERT_TRUE(stage); diff --git a/obj/README.md b/obj/README.md index 82e78535..ea8acd6b 100644 --- a/obj/README.md +++ b/obj/README.md @@ -59,7 +59,7 @@ Allows importing obj from ZBrush with vertex color (#MRGB tag) **Export:** -Meshes distributed in the node hierarchy in USD will be transformed by their global transform +Meshes distributed in the node hierarchy in USD will be transformed by their global transform during the export, since obj does not support nodes. Also, the resulting meshes are unitless (obj does not support units). No adjustments will be applied to the scale based on the input usd units. This is because obj readers in the industry make different assumptions on the units. @@ -87,27 +87,52 @@ Also, the resulting meshes are unitless (obj does not support units). No adjustm **Import:** -Example of how to pass a dynamic file format option to export images to a certain location. -This makes the asset paths be pointing to newly generated images in the filesystem. -Then the stage is exported to that same location. -``` -from pxr import Usd -stage = Usd.Stage.Open("assets/obj/car/Pony_Cartoon.obj:SDF_FORMAT_ARGS:objAssetsPath=assets-build") -stage.Export("assets-build/car.usda") -``` - -By default, the plugin imports the diffuse component only, without specularities, but you can force to import the full phong model like this: -``` -from pxr import Usd -stage = Usd.Stage.Open("assets/obj/car.obj:SDF_FORMAT_ARGS:objAssetsPath=assets-build&objPhong=true") -stage.Export("assets-build/car.usda") -``` -The phong to PBR conversion follows https://docs.microsoft.com/en-us/azure/remote-rendering/reference/material-mapping. Keep in mind it is a lossy conversion. -> Note: currently this only works when also providing objAssetsPath (TODO fix). +* `assetsPath`: Filesystem path where image assets are saved to during import. Default is `""` + + By default image textures used by the asset are not copied during import, but are kept in memory and are available + via an associated `ArResolver` plugin. By specifying a filesystem location via `assetsPath`, the import process will + copy the image textures to that location and provide asset paths to those locations in the generated USD data. This + file format argument allows an easy way to export associated images textures to disk when converting an asset to USD. + + This snippet saves image textures to the path at `exportPath` during `Usd.Stage.Open` and then also exports the stage + to that same location, so that the USD data and the used images a co-located. + ``` + from pxr import Usd + stage = Usd.Stage.Open("asset.obj:SDF_FORMAT_ARGS:assetsPath=exportPath") + stage.Export("exportPath/asset.usd") + ``` + +* `objAssetsPath`: Deprecated in favor of `assetsPath`. + +* `writeUsdPreviewSurface`: Generate a UsdPreviewSurface based network for each material. Default is `true` + + UsdPreviewSurface and its associated nodes are a universally understood USD material description + and all application should support them. The PBR capabilities are limited. + +* `writeASM`: Generate a ASM (Adobe Standard Material) based network for each material. Default is `true` + + ASM is a standard supported by many Adobe applications with richer support for PBR capabilities. + It will be superseded by OpenPBR in the near future. + +* `writeOpenPBR`: Generate a OpenPBR based material network for each material. Default is `false` + + OpenPBR is a new industry standard that will have wide spread support, but is still in its infancy. + The material network uses `MaterialX` nodes to express individual operations and has an `OpenPBR` surface, + which has rich support for PBR oriented materials. + +* `objPhong`: Turn on the full import of the Phong shading model. Default is `false` + + By default, the plugin imports the diffuse component only, without specularities, but you can force the import of the full phong model like this: + ``` + from pxr import Usd + stage = Usd.Stage.Open("asset.obj:SDF_FORMAT_ARGS:objPhong=true&assetsPath=exportPath") + stage.Export("exportPath/asset.usd") + ``` + The phong to PBR conversion follows https://docs.microsoft.com/en-us/azure/remote-rendering/reference/material-mapping. + Keep in mind it is a lossy conversion. + > Note: currently this only works when also providing assetsPath (TODO fix). ## Debug codes * `FILE_FORMAT_OBJ`: Common debug messages. * OBJ_PACKAGE_RESOLVER - - diff --git a/obj/src/fileFormat.cpp b/obj/src/fileFormat.cpp index b6f3a623..6b852db5 100644 --- a/obj/src/fileFormat.cpp +++ b/obj/src/fileFormat.cpp @@ -59,10 +59,15 @@ UsdObjFileFormat::InitData(const FileFormatArguments& args) const TF_DEBUG_MSG( FILE_FORMAT_OBJ, "FileFormatArg: %s = %s\n", arg.first.c_str(), arg.second.c_str()); } - argReadBool(args, AdobeTokens->writeMaterialX.GetText(), pd->writeMaterialX, DEBUG_TAG); - argReadString(args, assetsPathToken.GetText(), pd->assetsPath, DEBUG_TAG); - argReadBool(args, phongToken.GetText(), pd->phong, DEBUG_TAG); - argReadString(args, originalColorSpaceToken.GetText(), pd->originalColorSpace, DEBUG_TAG); + pd->parseFromFileFormatArgs(args, DEBUG_TAG); + + // "objAssetsPath" is deprecated in favor of the universal "assetsPath" argument - 2025-3-18 + // If both are present, "objAssetsPath" is stronger. + argReadString(args, assetsPathToken.GetString(), pd->assetsPath, DEBUG_TAG); + argWarnDeprecatedArg(args, assetsPathToken.GetString(), DEBUG_TAG); + + argReadBool(args, phongToken.GetString(), pd->phong, DEBUG_TAG); + argReadString(args, originalColorSpaceToken.GetString(), pd->originalColorSpace, DEBUG_TAG); return pd; } void @@ -110,14 +115,13 @@ UsdObjFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool me options.importMaterials = true; options.importImages = readImages; options.importPhong = data->phong; - WriteLayerOptions layerOptions; - layerOptions.writeMaterialX = data->writeMaterialX; - layerOptions.assetsPath = data->assetsPath; + WriteLayerOptions layerOptions(*data); obj.originalColorSpace = data->originalColorSpace; GUARD( readObj(obj, resolvedPath, readImages), "Error reading OBJ from %s\n", resolvedPath.c_str()); GUARD(importObj(options, obj, usd), "Error translating OBJ to USD\n"); - GUARD(writeLayer(layerOptions, usd, layer, layerData, fileType, DEBUG_TAG, SdfFileFormat::_SetLayerData), + GUARD(writeLayer( + layerOptions, usd, layer, layerData, fileType, DEBUG_TAG, SdfFileFormat::_SetLayerData), "Error writing to the USD layer\n"); w.Stop(); TF_DEBUG_MSG(FILE_FORMAT_OBJ, "Total time: %ld\n", static_cast(w.GetMilliseconds())); @@ -137,13 +141,20 @@ UsdObjFileFormat::ReadFromString(SdfLayer* layer, const std::string& input) cons TfStopwatch w; w.Start(); SdfAbstractDataRefPtr layerData = InitData(layer->GetFileFormatArguments()); + ObjDataConstPtr data = TfDynamic_cast(layerData); UsdData usd; Obj obj; ImportObjOptions options; - WriteLayerOptions layerOptions; + bool readImages = !data->assetsPath.empty(); + WriteLayerOptions layerOptions(*data); + options.importGeometry = true; + options.importMaterials = true; + options.importImages = readImages; + options.importPhong = data->phong; GUARD(readObj(obj, input.c_str(), input.size()), "Error reading OBJ from string\n"); GUARD(importObj(options, obj, usd), "Error translating OBJ to USD\n"); - GUARD(writeLayer(layerOptions, usd, layer, layerData, "obj", DEBUG_TAG, SdfFileFormat::_SetLayerData), + GUARD(writeLayer( + layerOptions, usd, layer, layerData, "obj", DEBUG_TAG, SdfFileFormat::_SetLayerData), "Error writing to the USD stage\n"); w.Stop(); TF_DEBUG_MSG(FILE_FORMAT_OBJ, "Total time: %ld\n", static_cast(w.GetMilliseconds())); diff --git a/obj/src/fileFormat.h b/obj/src/fileFormat.h index 4e47a545..c2dd9d70 100644 --- a/obj/src/fileFormat.h +++ b/obj/src/fileFormat.h @@ -10,12 +10,15 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once + #include "api.h" -#include +#include + #include #include #include -#include + +#include #include #include @@ -31,7 +34,6 @@ TF_DECLARE_WEAK_AND_REF_PTRS(UsdObjFileFormat); class ObjData : public FileFormatDataBase { public: - std::string assetsPath; bool phong = false; TfToken originalColorSpace; static ObjDataRefPtr InitData(const SdfFileFormat::FileFormatArguments& args); diff --git a/obj/src/obj.cpp b/obj/src/obj.cpp index 58abc5ae..0802668c 100644 --- a/obj/src/obj.cpp +++ b/obj/src/obj.cpp @@ -60,6 +60,14 @@ governing permissions and limitations under the License. using namespace PXR_NS; +#if defined(_WIN32) && !defined(__CYGWIN__) + #define ftell64 _ftelli64 + #define fseek64 _fseeki64 +#else + #define ftell64 ftello + #define fseek64 fseeko +#endif + namespace adobe::usd { /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -201,16 +209,32 @@ readFileContents(const std::string& filename, std::vector& buffer) if (!file) { return false; } - fseek(file, 0, SEEK_END); - int length = ftell(file); + fseek64(file, 0, SEEK_END); + long long length = ftell64(file); if (length < 0) { TF_WARN("Unable to read file %s"); return false; } else { - fseek(file, 0, SEEK_SET); + fseek64(file, 0, SEEK_SET); buffer.resize(length + 1); - fread(buffer.data(), length, 1, file); + + // fread does not guarantee that all data will be read in one call. Iterate over read + // calls until either an error has occured or the entire file has been read. + long long total_read = 0; + char* data = buffer.data(); + while (total_read < length) { + size_t current_read = fread(data + total_read, 1, length - total_read, file); + if (feof(file)) { + break; + } + if (ferror(file)) { + TF_WARN("failed to read file"); + break; + } + total_read += current_read; + } buffer[length] = '\0'; + fclose(file); return true; } @@ -280,6 +304,23 @@ nextFloat3(const char*& p, const char* end, GfVec3f& x) return nextFloat(p, end, x[0]) && nextFloat(p, end, x[1]) && nextFloat(p, end, x[2]); } +/// Helper parsing function. `p` is the moving pointer into the data. allows for arguments to have 1 or three values +bool +nextFloat1or3(const char*& p, const char* end, GfVec3f& x) +{ + if (nextFloat(p, end, x[0])) { + if (nextFloat(p, end, x[1])) { + return nextFloat(p, end, x[2]); + } + else { + x[2] = x[1] = x[0]; + return true; + } + } + return false; +} + + /// Helper parsing function. `p` is the moving pointer into the data. bool nextInteger(const char*& p, const char* end, int& x) @@ -1309,6 +1350,7 @@ readObjMtl(Obj& obj, } } else { nextSpacedText(p, end, map.filename); + std::replace(map.filename.begin(), map.filename.end(), '\\', '/'); map.image = addImage(obj, map.filename, imageMap, parentPath, readImages); map.defined = map.image != -1; break; @@ -1345,8 +1387,8 @@ readObjMtl(Obj& obj, TF_WARN("MTL parsing error on line %d, after Ks: expected 3 floats", line); } } else if (checkWord(p, end, ke)) { - if (!nextFloat3(p, end, m->ke)) { - TF_WARN("MTL parsing error on line %d, after Ke: expected 3 floats", line); + if (!nextFloat1or3(p, end, m->ke)) { + TF_WARN("MTL parsing error on line %d, after Ke: expected 1 or 3 floats", line); } } else if (checkWord(p, end, tf)) { if (!nextFloat3(p, end, m->tf)) { diff --git a/obj/src/objExport.cpp b/obj/src/objExport.cpp index 24dc39a2..e1afa1e9 100644 --- a/obj/src/objExport.cpp +++ b/obj/src/objExport.cpp @@ -75,13 +75,11 @@ writeObjMap(const UsdData& usd, ObjMap& map, const Input& input) map.image = input.image; // XXX note that mtl doesn't support uv rotation so we only handle translation and scale - if (input.transformScale.IsHolding()) { - GfVec2f scale = input.transformScale.UncheckedGet(); - map.scale = GfVec3f(scale[0], scale[1], 1.0f); + if (input.uvScale != kDefaultUvScale) { + map.scale = GfVec3f(input.uvScale[0], input.uvScale[1], 1.0f); } - if (input.transformTranslation.IsHolding()) { - GfVec2f trans = input.transformTranslation.UncheckedGet(); - map.origin = GfVec3f(trans[0], trans[1], 0.0f); + if (input.uvTranslation != kDefaultUvTranslation) { + map.origin = GfVec3f(input.uvTranslation[0], input.uvTranslation[1], 0.0f); } } } diff --git a/obj/src/objImport.cpp b/obj/src/objImport.cpp index 3e3ac72a..e5b93af7 100644 --- a/obj/src/objImport.cpp +++ b/obj/src/objImport.cpp @@ -109,10 +109,10 @@ importMaterialProperty(const ObjMap& map, input.scale = makeVec4f(value); } if (map.origin != GfVec3f(0.0f)) { - input.transformTranslation = GfVec2f(map.origin[0], map.origin[1]); + input.uvTranslation = GfVec2f(map.origin[0], map.origin[1]); } if (map.scale != GfVec3f(1.0f)) { - input.transformScale = GfVec2f(map.scale[0], map.scale[1]); + input.uvScale = GfVec2f(map.scale[0], map.scale[1]); } return true; } else if (value != T(-1)) { // different than the default value diff --git a/ply/src/fileFormat.cpp b/ply/src/fileFormat.cpp index e3d58c6f..566833bb 100644 --- a/ply/src/fileFormat.cpp +++ b/ply/src/fileFormat.cpp @@ -60,7 +60,7 @@ UsdPlyFileFormat::InitData(const FileFormatArguments& args) const TF_DEBUG_MSG( FILE_FORMAT_PLY, "FileFormatArg: %s = %s\n", arg.first.c_str(), arg.second.c_str()); } - argReadBool(args, AdobeTokens->writeMaterialX.GetText(), pd->writeMaterialX, DEBUG_TAG); + pd->parseFromFileFormatArgs(args, DEBUG_TAG); argReadBool(args, UsdPlyFileFormatTokens->points.GetText(), pd->points, DEBUG_TAG); argReadFloat(args, UsdPlyFileFormatTokens->pointWidth.GetText(), pd->pointWidth, DEBUG_TAG); argReadBool(args, @@ -119,8 +119,7 @@ UsdPlyFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool me options.pointWidth = data->pointWidth; options.importWithUpAxisCorrection = data->withUpAxisCorrection; options.importGsplatClippingBox = data->gsplatsClippingBox; - WriteLayerOptions layerOptions; - layerOptions.writeMaterialX = data->writeMaterialX; + WriteLayerOptions layerOptions(*data); // 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 diff --git a/ply/src/plyExport.cpp b/ply/src/plyExport.cpp index 3ca1b8dd..d5f4ae11 100644 --- a/ply/src/plyExport.cpp +++ b/ply/src/plyExport.cpp @@ -94,33 +94,35 @@ aggregateMeshInstance(PlyTotalMesh& totalMesh, const Mesh& mesh, const GfMatrix4d& modelMatrix, const GfMatrix4d& normalMatrix, - bool shouldExpand, + bool shouldExpand, + bool subMeshHasNormals, + bool subMeshHasUvs, bool subMeshHasColor, bool subMeshHasOpacity) { size_t currentMeshPointsSize = mesh.points.size(); + size_t indicesOffset = totalMesh.indices.size(); size_t pointsOffset = totalMesh.points.size(); size_t normalsOffset = totalMesh.normals.size(); size_t uvsOffset = totalMesh.uvs.size(); size_t colorOffset = totalMesh.color.size(); size_t opacityOffset = totalMesh.opacity.size(); + totalMesh.indices.resize(indicesOffset + mesh.faces.size()); totalMesh.points.resize(pointsOffset + currentMeshPointsSize); - totalMesh.uvs.resize(uvsOffset + mesh.uvs.values.size()); - totalMesh.normals.resize(normalsOffset + mesh.normals.values.size()); if (subMeshHasOpacity) { - totalMesh.opacity.resize(opacityOffset + mesh.points.size()); + totalMesh.opacity.resize(opacityOffset + currentMeshPointsSize); if (mesh.opacities.size()) { // need to check if the information is per vertex or per face - if (mesh.opacities[0].values.size() == mesh.points.size()) { - for (size_t i = 0; i < mesh.points.size(); i++) { + if (mesh.opacities[0].values.size() == currentMeshPointsSize) { + for (size_t i = 0; i < currentMeshPointsSize; i++) { totalMesh.opacity[opacityOffset + i] = mesh.opacities[0].values[i]; } } else if (mesh.opacities[0].values.size() == mesh.faces.size()) { - // in a case which we have colors or opacity per face, we need to add per vertex values - // since ply format needs per vertex color and opacity + // in a case which we have colors or opacity per face, we need to add per vertex + // values since ply format needs per vertex color and opacity for (size_t i = 0, k = 0; i < mesh.faces.size(); i++) { const float opacityValue = mesh.opacities[0].values[i]; for (int j = 0; j < mesh.faces[i]; j++) { @@ -137,16 +139,16 @@ aggregateMeshInstance(PlyTotalMesh& totalMesh, } if (subMeshHasColor) { - totalMesh.color.resize(colorOffset + mesh.points.size()); + totalMesh.color.resize(colorOffset + currentMeshPointsSize); if (mesh.colors.size()) { // need to check if the information is per vertex or per face - if (mesh.colors[0].values.size() == mesh.points.size()) { - for (size_t i = 0; i < mesh.points.size(); i++) { + if (mesh.colors[0].values.size() == currentMeshPointsSize) { + for (size_t i = 0; i < currentMeshPointsSize; i++) { totalMesh.color[colorOffset + i] = mesh.colors[0].values[i]; } } else if (mesh.colors[0].values.size() == mesh.faces.size()) { - // in a case which we have colors or opacity per face, we need to add per vertex values - // since ply format needs per vertex color and opacity + // in a case which we have colors or opacity per face, we need to add per vertex + // values since ply format needs per vertex color and opacity for (size_t i = 0, k = 0; i < mesh.faces.size(); i++) { GfVec3f colorValue = mesh.colors[0].values[i]; for (int j = 0; j < mesh.faces[i]; j++) { @@ -158,7 +160,8 @@ aggregateMeshInstance(PlyTotalMesh& totalMesh, TF_WARN("Mesh has color property which is not per vertex nor per face."); } } else { - std::fill(totalMesh.color.begin() + colorOffset, totalMesh.color.end() , GfVec3f(1.0, 1.0, 1.0)); + std::fill( + totalMesh.color.begin() + colorOffset, totalMesh.color.end(), GfVec3f(1.0, 1.0, 1.0)); } } @@ -181,12 +184,77 @@ aggregateMeshInstance(PlyTotalMesh& totalMesh, for (size_t i = 0; i < currentMeshPointsSize; i++) { totalMesh.points[pointsOffset + i] = GfVec3f(modelMatrix.Transform(mesh.points[i])); } - for (size_t i = 0; i < mesh.normals.values.size(); i++) { - totalMesh.normals[normalsOffset + i] = GfVec3f(normalMatrix.TransformDir(mesh.normals.values[i])); - totalMesh.normals[normalsOffset + i].Normalize(); + + if (subMeshHasNormals) { + totalMesh.normals.resize(normalsOffset + currentMeshPointsSize); + + bool generateNormalsFromFaces = false; + if (mesh.normals.values.size()) { + // Need to check that currentMeshPointsSize is the same as the number of normals + if (currentMeshPointsSize == mesh.normals.values.size()) { + for (size_t i = 0; i < currentMeshPointsSize; i++) { + GfVec3f normal(normalMatrix.TransformDir(mesh.normals.values[i])); + normal.Normalize(); + totalMesh.normals[normalsOffset + i] = normal; + } + } else { + // This is an unexpected situation. The number of normals should be made the same + // when we expand them using the indices in exportPly(). We will ignore the + // provided normals and generate normals from the faces (below). + generateNormalsFromFaces = true; + TF_WARN("Number of normals in mesh does not match the number of vertices."); + } + } else { + // There are no normals but they are needed so set the flag to trigger their generation + generateNormalsFromFaces = true; + } + if (generateNormalsFromFaces) { + // we need to compute vertex normals and so we'll just use face normals + for (size_t i = 0, k = 0; i < mesh.faces.size(); i++) { + int nverts = mesh.faces[i]; + if (nverts >= 3) { + GfVec3f v0 = mesh.points[k]; + GfVec3f v1 = mesh.points[k + 1]; + GfVec3f v2 = mesh.points[k + 2]; + GfVec3f normal = GfCross(v1 - v0, v2 - v0); + GfVec3f xfNormal(normalMatrix.TransformDir(normal)); + xfNormal.Normalize(); + for (size_t j = 0; j < nverts; j++) { + totalMesh.normals[normalsOffset + k + j] = xfNormal; + } + } else { + // The faces is degenerate, so we just assign a default value + for (size_t j = 0; j < nverts; j++) { + totalMesh.normals[normalsOffset + k + j] = GfVec3f(0, 0, 1); + } + } + k += nverts; + } + } } - for (size_t i = 0; i < mesh.uvs.values.size(); i++) { - totalMesh.uvs[uvsOffset + i] = mesh.uvs.values[i]; + + if (subMeshHasUvs) { + totalMesh.uvs.resize(uvsOffset + currentMeshPointsSize); + + bool filled = false; + if (mesh.uvs.values.size()) { + // Need to check that currentMeshPointsSize is the same as the number of UVs + if (currentMeshPointsSize == mesh.uvs.values.size()) { + for (size_t i = 0; i < currentMeshPointsSize; i++) { + totalMesh.uvs[uvsOffset + i] = mesh.uvs.values[i]; + } + filled = true; + } else { + TF_WARN("Number of uvs in mesh does not match the number of vertices."); + // As mentioned above for the similar situation for normals, this case is + // unexpected and so we just fill the array with a default value for uvs (below) + } + } + if (!filled) { + for (size_t i = 0; i < currentMeshPointsSize; i++) { + totalMesh.uvs[uvsOffset + i] = GfVec2f(0, 0); + } + } } if (totalMesh.asGsplats) { @@ -237,47 +305,90 @@ traverseNodesAndFindGsplats(UsdData& usd, PlyTotalMesh& totalMesh, int nodeIndex } } +std::size_t +traverseNodesAndFindMaxNumSHCoeffs(UsdData& usd, int nodeIndex) +{ + const Node& node = usd.nodes[nodeIndex]; + std::size_t maxNumSHCoeffs = 0; + for (int meshIndex : node.staticMeshes) { + Mesh& mesh = usd.meshes[meshIndex]; + if (mesh.asGsplats) + maxNumSHCoeffs = std::max(maxNumSHCoeffs, mesh.pointSHCoeffs.size()); + } + + for (size_t i = 0; i < node.children.size(); i++) { + maxNumSHCoeffs = std::max(maxNumSHCoeffs, traverseNodesAndFindMaxNumSHCoeffs(usd, node.children[i])); + } + return maxNumSHCoeffs; +} + +void +aggregateMeshDataRequirements(std::vector& meshes, + bool& subMeshHasNormals, + bool& subMeshHasUvs, + bool& subMeshHasOpacity, + bool& subMeshHasColor) +{ + for (const Mesh& m : meshes) { + subMeshHasNormals |= !m.normals.values.empty(); + subMeshHasUvs |= !m.uvs.values.empty(); + subMeshHasColor |= !m.colors.empty(); + subMeshHasOpacity |= !m.opacities.empty(); + } +} + void traverseNodesAndAggregateMeshes(UsdData& usd, PlyTotalMesh& totalMesh, const GfMatrix4d& correctionTransform, bool shouldExpand, - int nodeIndex) + int nodeIndex, + bool subMeshHasNormals, + bool subMeshHasUvs, + bool subMeshHasOpacity, + bool subMeshHasColor) { const Node& node = usd.nodes[nodeIndex]; GfMatrix4d modelMatrix = node.worldTransform * correctionTransform; GfMatrix4d normalMatrix = modelMatrix.GetInverse().GetTranspose(); - bool subMeshHasOpacity = false; - bool subMeshHasColor = false; - - // This loops covers the case when the first sub mesh has no opacity or color but other - // sub meshes have opacity or color - for (int meshIndex : node.staticMeshes) { - Mesh& mesh = usd.meshes[meshIndex]; - subMeshHasColor |= !mesh.colors.empty(); - subMeshHasOpacity |= !mesh.opacities.empty(); - } - for (const auto& [skeletonIndex, meshIndices] : node.skinnedMeshes) { - for (int meshIndex : meshIndices) { - Mesh& mesh = usd.meshes[meshIndex]; - subMeshHasColor |= !mesh.colors.empty(); - subMeshHasOpacity |= !mesh.opacities.empty(); - } - } + for (int meshIndex : node.staticMeshes) { Mesh& mesh = usd.meshes[meshIndex]; - aggregateMeshInstance(totalMesh, mesh, modelMatrix, normalMatrix, shouldExpand, subMeshHasColor, subMeshHasOpacity); + aggregateMeshInstance(totalMesh, + mesh, + modelMatrix, + normalMatrix, + shouldExpand, + subMeshHasNormals, + subMeshHasUvs, + subMeshHasColor, + subMeshHasOpacity); } - + for (const auto& [skeletonIndex, meshIndices] : node.skinnedMeshes) { for (int meshIndex : meshIndices) { Mesh& mesh = usd.meshes[meshIndex]; - aggregateMeshInstance(totalMesh, mesh, modelMatrix, normalMatrix, shouldExpand, subMeshHasColor, subMeshHasOpacity); + aggregateMeshInstance(totalMesh, + mesh, + modelMatrix, + normalMatrix, + shouldExpand, + subMeshHasNormals, + subMeshHasUvs, + subMeshHasColor, + subMeshHasOpacity); } } for (size_t i = 0; i < node.children.size(); i++) { - traverseNodesAndAggregateMeshes( - usd, totalMesh, correctionTransform, shouldExpand, node.children[i]); + traverseNodesAndAggregateMeshes(usd, + totalMesh, + correctionTransform, + shouldExpand, + node.children[i], + subMeshHasNormals, + subMeshHasUvs, + subMeshHasColor, + subMeshHasOpacity); } } @@ -320,19 +431,62 @@ exportPly(UsdData& usd, happly::PLYData& ply) if (m.asPoints) continue; + TF_DEBUG_MSG(FILE_FORMAT_PLY, + "mesh: faces:%d indices:%d pts:%d norInd:%d normals:%d uvInd:%d uvs:%d\n", + m.faces.size(), + m.indices.size(), + m.points.size(), + m.normals.indices.size(), + m.normals.values.size(), + m.uvs.indices.size(), + m.uvs.values.size()); + expandIndexedValues(m.indices, m.points); - expandIndexedValues(m.uvs.indices.size() ? m.uvs.indices : m.indices, m.uvs.values); - expandIndexedValues(m.normals.indices.size() ? m.normals.indices : m.indices, - m.normals.values); + if (m.uvs.indices.size()) { + if (m.uvs.indices.size() == m.indices.size()) { + expandIndexedValues(m.uvs.indices, m.uvs.values); + } else { + expandIndexedValuesIndirect(m.indices, m.uvs.indices, m.uvs.values); + } + } else if (m.indices.size() != m.uvs.values.size()) { + expandIndexedValues(m.indices, m.uvs.values); + } + if (m.normals.indices.size()) { + if (m.normals.indices.size() == m.indices.size()) { + expandIndexedValues(m.normals.indices, m.normals.values); + } else { + expandIndexedValuesIndirect(m.indices, m.normals.indices, m.normals.values); + } + } else if (m.indices.size() != m.normals.values.size()) { + expandIndexedValues(m.indices, m.normals.values); + } + if (m.colors.size()) { // translate only first set of colors Primvar& colorSet = m.colors[0]; - expandIndexedValues(colorSet.indices.size() ? colorSet.indices : m.indices, - colorSet.values); + + if (colorSet.indices.size()) { + if (colorSet.indices.size() == m.indices.size()) { + expandIndexedValues(colorSet.indices, colorSet.values); + } else { + expandIndexedValuesIndirect(m.indices, colorSet.indices, colorSet.values); + } + } else { + expandIndexedValues(m.indices, colorSet.values); + } } if (m.opacities.size()) { // translate only first set of opacities - Primvar& opaciySet = m.opacities[0]; - expandIndexedValues(opaciySet.indices.size() ? opaciySet.indices : m.indices, - opaciySet.values); + Primvar& opacitySet = m.opacities[0]; + + if (opacitySet.indices.size()) { + if (opacitySet.indices.size() == m.indices.size()) { + expandIndexedValues(opacitySet.indices, opacitySet.values); + } else { + expandIndexedValuesIndirect( + m.indices, opacitySet.indices, opacitySet.values); + } + } else { + expandIndexedValues(m.indices, opacitySet.values); + } } } } @@ -350,18 +504,43 @@ exportPly(UsdData& usd, happly::PLYData& ply) break; } - constexpr std::size_t numGsplatsSHCoeffs = 45; + std::size_t numGsplatsSHCoeffs = 0; + for (size_t i = 0; i < usd.rootNodes.size(); i++) { + numGsplatsSHCoeffs = + std::max(numGsplatsSHCoeffs, traverseNodesAndFindMaxNumSHCoeffs(usd, usd.rootNodes[i])); + } if (totalMesh.asGsplats) { totalMesh.shCoeffs.resize(numGsplatsSHCoeffs); ply.comments.push_back("Gaussian Splats with Y-axis up"); } correctionTransform = getTransformToMetersPositiveY(usd.metersPerUnit, usd.upAxis); + bool subMeshHasNormals = false; + bool subMeshHasUvs = false; + bool subMeshHasOpacity = false; + bool subMeshHasColor = false; + aggregateMeshDataRequirements( + usd.meshes, subMeshHasNormals, subMeshHasUvs, subMeshHasOpacity, subMeshHasColor); + for (size_t i = 0; i < usd.rootNodes.size(); i++) { - traverseNodesAndAggregateMeshes( - usd, totalMesh, correctionTransform, shouldExpand, usd.rootNodes[i]); + traverseNodesAndAggregateMeshes(usd, + totalMesh, + correctionTransform, + shouldExpand, + usd.rootNodes[i], + subMeshHasNormals, + subMeshHasUvs, + subMeshHasOpacity, + subMeshHasColor); } + TF_DEBUG_MSG(FILE_FORMAT_PLY, + "totalMesh: points=%d indices=%d normals=%d uvs=%d\n", + totalMesh.points.size(), + totalMesh.indices.size(), + totalMesh.normals.size(), + totalMesh.uvs.size()); + if (totalMesh.points.size()) { std::string faceName = "face"; std::string vertexName = "vertex"; @@ -483,7 +662,7 @@ exportPly(UsdData& usd, happly::PLYData& ply) } happly::Element& vertexElement = ply.getElement(vertexName); const std::string propName = std::string("f_rest_") + std::to_string(shIndex); - vertexElement.addProperty(propName, shCoeff); + vertexElement.addProperty(propName, shCoeff); } } else { // Mesh or regular point cloud. @@ -492,9 +671,12 @@ exportPly(UsdData& usd, happly::PLYData& ply) std::vector g(totalMesh.color.size()); std::vector b(totalMesh.color.size()); for (size_t i = 0; i < totalMesh.color.size(); i++) { - r[i] = static_cast(std::clamp(totalMesh.color[i][0] * 255.0f, 0.0f, 255.0f)); - g[i] = static_cast(std::clamp(totalMesh.color[i][1] * 255.0f, 0.0f, 255.0f)); - b[i] = static_cast(std::clamp(totalMesh.color[i][2] * 255.0f, 0.0f, 255.0f)); + r[i] = static_cast( + std::clamp(totalMesh.color[i][0] * 255.0f, 0.0f, 255.0f)); + g[i] = static_cast( + std::clamp(totalMesh.color[i][1] * 255.0f, 0.0f, 255.0f)); + b[i] = static_cast( + std::clamp(totalMesh.color[i][2] * 255.0f, 0.0f, 255.0f)); } happly::Element& vertexElement = ply.getElement(vertexName); vertexElement.addProperty("red", r); @@ -504,13 +686,13 @@ exportPly(UsdData& usd, happly::PLYData& ply) if (totalMesh.opacity.size()) { std::vector a(totalMesh.opacity.size()); for (size_t i = 0; i < totalMesh.opacity.size(); i++) { - a[i] = static_cast(std::clamp(totalMesh.opacity[i] * 255.0f, 0.0f, 255.0f)); + a[i] = + static_cast(std::clamp(totalMesh.opacity[i] * 255.0f, 0.0f, 255.0f)); } happly::Element& vertexElement = ply.getElement(vertexName); vertexElement.addProperty("alpha", a); } } - } return true; } diff --git a/ply/src/plyImport.cpp b/ply/src/plyImport.cpp index 739fbb5b..55c3ab10 100644 --- a/ply/src/plyImport.cpp +++ b/ply/src/plyImport.cpp @@ -56,14 +56,13 @@ getPropertyDataPtr(happly::Element& element, const std::string& target) struct FloatOrHalfLoader { - std::vector scratchData; - std::vector* getPropertyDataPtr(happly::Element& element, const std::string& target) { happly::TypedProperty* property = dynamic_cast*>(element.getPropertyPtr(target).get()); if (property) { - return &(property->data); + dataPtr = &(property->data); + return dataPtr; } happly::TypedProperty* halfProperty = @@ -78,6 +77,25 @@ struct FloatOrHalfLoader throw std::runtime_error("PLY import: element " + element.name + " does not have property " + target + " with the specific type."); } + + std::vector* getPropertyDataPtr() + { + if (dataPtr) + return dataPtr; + else if (scratchData.size()) + return &scratchData; + throw std::runtime_error("PLY import: no property data pointer set."); + } + + FloatOrHalfLoader() = default; + FloatOrHalfLoader(happly::Element& element, const std::string& target) + { + this->getPropertyDataPtr(element, target); + } + + private: + std::vector scratchData; + std::vector* dataPtr = nullptr; }; } // namespace @@ -123,7 +141,6 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) std::vector* gsRotation1 = nullptr; std::vector* gsRotation2 = nullptr; std::vector* gsRotation3 = nullptr; - std::array*, 45> gsSHCoeffs = {}; FloatOrHalfLoader gsColorCoeff0Loader; FloatOrHalfLoader gsColorCoeff1Loader; @@ -136,14 +153,14 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) FloatOrHalfLoader gsRotation1Loader; FloatOrHalfLoader gsRotation2Loader; FloatOrHalfLoader gsRotation3Loader; - std::array gsSHCoeffsLoaders; + std::vector gsSHCoeffsLoaders; auto [meshIndex, mesh] = usd.addMesh(); mesh.asPoints = options.importAsPoints || !ply.hasElement("face"); // Will check later. An asset is a Gsplat only if it contains points and has all the Gsplat-related fields. mesh.asGsplats = mesh.asPoints; - bool hasHighOrderSH = false; + int numHighOrderSHCoeffs = 0; try { Element& element = ply.getElement("vertex"); // happly provides plyIn.getVertexPositions(), but it uses double, so avoid it to avoid @@ -237,24 +254,43 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) } if (mesh.asGsplats) { - hasHighOrderSH = mesh.asGsplats; - // Higher order SH coefficients are optional. - for (int i = 0; mesh.asGsplats && i < 45; ++i) - { - std::string propName = std::string("f_rest_") + std::to_string(i); + // Higher order SH coefficients are optional. We first detect how many coefficients + // are present, and then load them. + int numSHBands = 0; + while (true) { + const int numSHBandsNext = numSHBands + 1; + // The total required number of coefficients for a single channel of current band is + // band * (band + 2), then multiplied by 3 for RGB channels. + const int nextBandMaxNumSH = numSHBandsNext * (numSHBandsNext + 2) * 3; + const std::string propName = + std::string("f_rest_") + std::to_string(nextBandMaxNumSH - 1); if (!element.hasProperty(propName)) { - hasHighOrderSH = false; break; } - - try { - gsSHCoeffs[i] = gsSHCoeffsLoaders[i].getPropertyDataPtr(element, propName); - } catch (std::exception& e) { - hasHighOrderSH = false; - TF_DEBUG_MSG( - FILE_FORMAT_PLY, "Invalid Gaussian splatting SH data: %s\n", e.what()); - break; + numSHBands = numSHBandsNext; + numHighOrderSHCoeffs = nextBandMaxNumSH; + } + + try { + gsSHCoeffsLoaders.reserve(numHighOrderSHCoeffs); + for (int i = 0; i < numHighOrderSHCoeffs; ++i) { + const std::string propName = std::string("f_rest_") + std::to_string(i); + if (!element.hasProperty(propName)) { + TF_DEBUG_MSG(FILE_FORMAT_PLY, + "Missing Gaussian splatting SH coefficient property %s\n", + propName.c_str()); + numHighOrderSHCoeffs = 0; + gsSHCoeffsLoaders.clear(); + break; + } + gsSHCoeffsLoaders.emplace_back(element, propName); } + } catch (std::exception& e) { + TF_DEBUG_MSG(FILE_FORMAT_PLY, + "Invalid Gaussian splatting SH coefficients data: %s\n", + e.what()); + numHighOrderSHCoeffs = 0; + gsSHCoeffsLoaders.clear(); } } } catch (std::exception& e) { @@ -305,9 +341,9 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) // is 1/sqrt(4*pi). constexpr float shC0 = 0.28209479177387814f; for (size_t i = 0; i < colors.values.size(); i++) { - colors.values[i][0] = std::clamp((*gsColorCoeff0)[i] * shC0 + 0.5f, 0.0f, 1.0f); - colors.values[i][1] = std::clamp((*gsColorCoeff1)[i] * shC0 + 0.5f, 0.0f, 1.0f); - colors.values[i][2] = std::clamp((*gsColorCoeff2)[i] * shC0 + 0.5f, 0.0f, 1.0f); + colors.values[i][0] = (*gsColorCoeff0)[i] * shC0 + 0.5f; + colors.values[i][1] = (*gsColorCoeff1)[i] * shC0 + 0.5f; + colors.values[i][2] = (*gsColorCoeff2)[i] * shC0 + 0.5f; } } else if (r && r->size()) { auto [colorIndex, colors] = usd.addColorSet(meshIndex); @@ -399,13 +435,14 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) mesh.pointRotations.values[i] = mesh.pointRotations.values[i].GetNormalized(); } - if (hasHighOrderSH) { - for (std::size_t shIndex = 0; shIndex < gsSHCoeffs.size(); ++shIndex) { + if (numHighOrderSHCoeffs > 0) { + for (std::size_t shIndex = 0; shIndex < numHighOrderSHCoeffs; ++shIndex) { auto [shCoeffIndex, shCoeffs] = usd.addPointSHCoeffSet(meshIndex); shCoeffs.interpolation = UsdGeomTokens->vertex; - shCoeffs.values.resize((*gsSHCoeffs[shIndex]).size()); + const auto& gsSHCoeffs = *(gsSHCoeffsLoaders[shIndex].getPropertyDataPtr()); + shCoeffs.values.resize(gsSHCoeffs.size()); for (size_t i = 0; i < shCoeffs.values.size(); i++) { - shCoeffs.values[i] = (*gsSHCoeffs[shIndex])[i]; + shCoeffs.values[i] = gsSHCoeffs[i]; } } } diff --git a/sbsar/CMakeLists.txt b/sbsar/CMakeLists.txt index 3251c416..fb117931 100644 --- a/sbsar/CMakeLists.txt +++ b/sbsar/CMakeLists.txt @@ -10,6 +10,10 @@ option(USDSBSAR_ENABLE_TEXTURE_TRANSFORM "Enables properties for tiling/scaling/ option(USDSBSAR_ENABLE_FIX_STORM_16BIT "Enables fix storm 16bit textures issues" ON) # Tests options (avaible only on windows) option(USDSBSAR_TEST_UNDEFINED_LIBS "Raise error if at the end of compilation libs are not correctly linked" ON) +# Cache settings +set(USDSBSAR_CACHE_SIZE 1000000000 CACHE "" STRING) +set(USDSBSAR_IMAGE_CACHE_SIZE 1000000000 CACHE "" STRING) +set(USDSBSAR_PACKAGE_LIMIT 10 CACHE "" STRING) # Check if we're on an Apple silicon platform set(USDSBSAR_BUILD_APPLE_SILICON OFF) diff --git a/sbsar/src/CMakeLists.txt b/sbsar/src/CMakeLists.txt index 6f01e8ca..ff3b1d49 100644 --- a/sbsar/src/CMakeLists.txt +++ b/sbsar/src/CMakeLists.txt @@ -28,7 +28,7 @@ target_sources(${PLUGIN_NAME} usdGeneration/sbsarAsm.cpp usdGeneration/sbsarLuxDomeLight.cpp usdGeneration/sbsarMaterial.cpp - usdGeneration/sbsarMtlx.cpp + usdGeneration/sbsarOpenPBR.cpp usdGeneration/sbsarSymbolMapper.cpp usdGeneration/sbsarUsdPreviewSurface.cpp usdGeneration/usdGenerationHelpers.cpp diff --git a/sbsar/src/config/sbsarConfig.cpp b/sbsar/src/config/sbsarConfig.cpp index 8aa3934c..dc1fd2cd 100644 --- a/sbsar/src/config/sbsarConfig.cpp +++ b/sbsar/src/config/sbsarConfig.cpp @@ -73,8 +73,7 @@ void SbsarConfig::setAssetCacheSize(std::size_t size) { if (size == 0) { - TF_WARN("SbsarConfig: Asset cache size cannot be 0"); - return; + TF_STATUS("SbsarConfig: Asset cache size is 0, which means the cache is unlimited and never cleared!"); } m_assetCacheSize = size; } diff --git a/sbsar/src/plugInfo.json.in b/sbsar/src/plugInfo.json.in index e74e8fc8..401e05cd 100644 --- a/sbsar/src/plugInfo.json.in +++ b/sbsar/src/plugInfo.json.in @@ -21,21 +21,21 @@ "type": "dictionary" }, "writeASM": { - "appliesTo": [ "prims" ], - "displayGroup": "Core", - "documentation:": "Whether to write ASM shader", + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Whether to write ASM shader", "type": "bool" }, "writeUsdPreviewSurface": { - "appliesTo": [ "prims" ], - "displayGroup": "Core", - "documentation:": "Whether to write USD Preview Surface shader", + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Whether to write USD Preview Surface shader", "type": "bool" }, - "writeMaterialX": { - "appliesTo": [ "prims" ], - "displayGroup": "Core", - "documentation:": "Whether to write MaterialX shader", + "writeOpenPBR": { + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Whether to write MaterialX shader", "type": "bool" } }, @@ -75,9 +75,9 @@ "imageTypes": ["sbsarimage"] }, "SbsarConfig": { - "assetCacheSize": 1000000000, - "inputImageCacheSize": 1000000000, - "packageCacheSize": 10 + "assetCacheSize": ${USDSBSAR_CACHE_SIZE}, + "inputImageCacheSize": ${USDSBSAR_IMAGE_CACHE_SIZE}, + "packageCacheSize": ${USDSBSAR_PACKAGE_LIMIT} } } }, diff --git a/sbsar/src/sbsarEngine/sbsarAssetCache.cpp b/sbsar/src/sbsarEngine/sbsarAssetCache.cpp index 354da5f4..66ff33b1 100644 --- a/sbsar/src/sbsarEngine/sbsarAssetCache.cpp +++ b/sbsar/src/sbsarEngine/sbsarAssetCache.cpp @@ -137,7 +137,7 @@ AssetCache::addRenderResult(const adobe::usd::sbsar::ParsePathResult& pathResult renderResult.computeSize(); // Before adding a new entry, check the cache size and clean the cache if necessary to ensure // there is enough space - if (m_size + renderResult.getSize() > getSbsarConfig()->getAssetCacheSize()) + if (getSbsarConfig()->getAssetCacheSize() > 0 && (m_size + renderResult.getSize() > getSbsarConfig()->getAssetCacheSize())) cleanCache(); renderResult.updateLastAccessTime(); std::size_t assetCount = renderResult.getAssetCount(); diff --git a/sbsar/src/sbsarEngine/sbsarEngine.cpp b/sbsar/src/sbsarEngine/sbsarEngine.cpp index c73cf8ff..ac88edb5 100644 --- a/sbsar/src/sbsarEngine/sbsarEngine.cpp +++ b/sbsar/src/sbsarEngine/sbsarEngine.cpp @@ -49,9 +49,7 @@ PXR_NAMESPACE_USING_DIRECTIVE #define TOSTRING(x) STRINGIFY(x) namespace { -// Borrowed from -// https://git.corp.adobe.com/substance-integrations/integrations-common - +// Common Substance integrations information const char* const engineCreateContextSymbol = "substanceContextInitImpl"; const char* const engineReleaseContextSymbol = "substanceContextRelease"; typedef void (*engineVersionFunction)(SubstanceVersion* version, unsigned int apiVersion); diff --git a/sbsar/src/sbsarfileformat.cpp b/sbsar/src/sbsarfileformat.cpp index 81faa028..6d167dd2 100644 --- a/sbsar/src/sbsarfileformat.cpp +++ b/sbsar/src/sbsarfileformat.cpp @@ -184,9 +184,9 @@ parseFileFormatArguments(const SBSARFileFormat::FileFormatArguments& args) data.depth = 0; } - argReadBool(args, "writeMaterialX", data.writeMaterialX, "SBSAR"); - argReadBool(args, "writeASM", data.writeASM, "SBSAR"); argReadBool(args, "writeUsdPreviewSurface", data.writeUsdPreviewSurface, "SBSAR"); + argReadBool(args, "writeASM", data.writeASM, "SBSAR"); + argReadBool(args, "writeOpenPBR", data.writeOpenPBR, "SBSAR"); return data; } diff --git a/sbsar/src/sbsarfileformat.h b/sbsar/src/sbsarfileformat.h index e6a1fdfb..188e8401 100644 --- a/sbsar/src/sbsarfileformat.h +++ b/sbsar/src/sbsarfileformat.h @@ -23,9 +23,9 @@ struct SBSAROptions { PXR_NS::VtDictionary sbsarParameters; std::uint32_t depth = 0; - bool writeMaterialX = false; - bool writeASM = true; bool writeUsdPreviewSurface = true; + bool writeASM = true; + bool writeOpenPBR = false; }; } diff --git a/sbsar/src/usdGeneration/dictEncoder.h b/sbsar/src/usdGeneration/dictEncoder.h index 57311627..81b3d78c 100644 --- a/sbsar/src/usdGeneration/dictEncoder.h +++ b/sbsar/src/usdGeneration/dictEncoder.h @@ -16,9 +16,18 @@ governing permissions and limitations under the License. namespace adobe::usd::sbsar { namespace DictEncoder { + +/// \brief Serializes a VtDictionary to the given output stream. +/// \param dict The VtDictionary to serialize. +/// \param output The output stream to write the serialized dictionary to. void writeDict(const PXR_NS::VtDictionary& dict, std::ostream& output); + +/// \brief Deserializes a VtDictionary from the given input stream. +/// \param input The input stream to read the serialized dictionary from. +/// \return The deserialized VtDictionary. PXR_NS::VtDictionary readDict(std::istream& input); + } } diff --git a/sbsar/src/usdGeneration/sbsarAsm.cpp b/sbsar/src/usdGeneration/sbsarAsm.cpp index fdfccfd9..c222d40f 100644 --- a/sbsar/src/usdGeneration/sbsarAsm.cpp +++ b/sbsar/src/usdGeneration/sbsarAsm.cpp @@ -81,9 +81,10 @@ bindTexture(SdfAbstractData* sdfData, const BindInfo& bindInfo, const SdfPath& uvOutputAttrPath, const SdfPath& textureAssetAttrPath, - const SdfPath& fallbackAttrPath, const SdfPath& scaleAttrPath, - const SdfPath& biasAttrPath) + const SdfPath& biasAttrPath, + const SdfPath& uvWrapSPath, + const SdfPath& uvWrapTPath) { TF_DEBUG(FILE_FORMAT_SBSAR) .Msg("bindTexture: Binding texture channel %s\n", bindInfo.name.c_str()); @@ -93,14 +94,13 @@ bindTexture(SdfAbstractData* sdfData, TfToken("file" + bindInfo.name), AdobeTokens->UsdUVTexture, bindInfo.outputName, - { { "sourceColorSpace", bindInfo.colorSpace }, - { "wrapS", AdobeTokens->repeat }, - { "wrapT", AdobeTokens->repeat } }, + { { "sourceColorSpace", bindInfo.colorSpace } }, { { "st", uvOutputAttrPath }, { "file", textureAssetAttrPath }, - { "fallback", fallbackAttrPath }, { "scale", scaleAttrPath }, - { "bias", biasAttrPath } }); + { "bias", biasAttrPath }, + { "wrapS", uvWrapSPath }, + { "wrapT", uvWrapTPath } }); return resultPath; } @@ -118,6 +118,8 @@ addUsdAsmShaderImpl(SdfAbstractData* sdfData, createPrimSpec(sdfData, materialPath, AdobeTokens->ASM, UsdShadeTokens->NodeGraph); SdfPath uvChannelNamePath = inputPath(materialPath, uv_channel_name); + SdfPath uvWrapSPath = inputPath(materialPath, uv_wrap_s_name); + SdfPath uvWrapTPath = inputPath(materialPath, uv_wrap_t_name); // Create Texcoord Reader SdfPath txOutputPath = createShader(sdfData, @@ -164,14 +166,6 @@ addUsdAsmShaderImpl(SdfAbstractData* sdfData, std::string texAssetName = getTextureAssetName(usage); SdfPath textureAssetAttrPath = inputPath(materialPath, texAssetName); - // Add default value if present - SdfPath fallbackAttrPath; - auto defaultIt = default_channels.find(usage); - if (defaultIt != default_channels.end()) { - auto defaultName = getDefaultValueNames(usage); - fallbackAttrPath = inputPath(materialPath, defaultName.first); - } - SdfPath scaleAttrPath, biasAttrPath; if (isNormal(usage)) { const auto [scaleName, biasName] = getNormalMapScaleAndBiasNames(usage); @@ -185,9 +179,10 @@ addUsdAsmShaderImpl(SdfAbstractData* sdfData, bindInfo, uvOutputPath, textureAssetAttrPath, - fallbackAttrPath, scaleAttrPath, - biasAttrPath); + biasAttrPath, + uvWrapSPath, + uvWrapTPath); if (usage == "emissive") { inputValues.emplace_back("emissiveIntensity", 1.0f); @@ -200,8 +195,8 @@ addUsdAsmShaderImpl(SdfAbstractData* sdfData, // Connect to uniform values for (auto& usage : uniform_usages) { if (hasUsage(usage, graphDesc)) { - SdfPath textureAssetAttrPath = inputPath(materialPath, usage); - inputConnections.emplace_back(usage, textureAssetAttrPath); + SdfPath uniformAttrPath = inputPath(materialPath, usage); + inputConnections.emplace_back(usage, uniformAttrPath); } } diff --git a/sbsar/src/usdGeneration/sbsarAsm.h b/sbsar/src/usdGeneration/sbsarAsm.h index 02ad00bc..c9145c77 100644 --- a/sbsar/src/usdGeneration/sbsarAsm.h +++ b/sbsar/src/usdGeneration/sbsarAsm.h @@ -17,6 +17,15 @@ governing permissions and limitations under the License. namespace adobe::usd::sbsar { +/// @brief Adds an ASM (Adobe Standard Material) material network to the material +/// +/// The network is created from the provided Substance graph description and connected to the +/// material as the 'adobe' surface. +/// +/// @param sdfData SDF data container to store the material in +/// @param materialPath Path of the parent material +/// @param graphDesc Description of the current SBSAR graph +/// @return true if the material was successfully added, false otherwise bool addAsmShader(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& materialPath, diff --git a/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp b/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp index 66318b4d..b8fbfb7a 100644 --- a/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp +++ b/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp @@ -27,6 +27,20 @@ using namespace SubstanceAir; namespace adobe::usd::sbsar { +// Helper function to determine which usage string to use for light textures +std::string +getDomeLightUsage(const SubstanceAir::GraphDesc& graphDesc) +{ + if (hasUsage("environment", graphDesc)) { + return "environment"; + } + if (hasUsage("panorama", graphDesc)) { + return "panorama"; + } + // fallback to environment + return "environment"; +} + SdfPath addLuxDomeLight(SdfAbstractData* sdfData, const MappedSymbol& graphName, @@ -86,8 +100,9 @@ addLuxDomeLight(SdfAbstractData* sdfData, SdfPath texAttrPath = createShaderInput(sdfData, lightPath, "texture:file", SdfValueTypeNames->Asset); JsValue params = convertSbsarParameters(sbsarData.sbsarParameters); + std::string usageString = getDomeLightUsage(graphDesc); SdfAssetPath path = - SdfAssetPath(generateSbsarInfoPath("environment", graphName, sbsarHash, params)); + SdfAssetPath(generateSbsarInfoPath(usageString, graphName, sbsarHash, params)); setAttributeMetadata(sdfData, texAttrPath, SdfFieldKeys->Hidden, VtValue(true)); setAttributeDefaultValue(sdfData, texAttrPath, path); } diff --git a/sbsar/src/usdGeneration/sbsarLuxDomeLight.h b/sbsar/src/usdGeneration/sbsarLuxDomeLight.h index 07a0b975..92adfdc9 100644 --- a/sbsar/src/usdGeneration/sbsarLuxDomeLight.h +++ b/sbsar/src/usdGeneration/sbsarLuxDomeLight.h @@ -23,6 +23,18 @@ governing permissions and limitations under the License. namespace adobe::usd::sbsar { +/// @brief Add a LuxDomeLight prim to the given Sdf layer. +/// +/// The LuxDomeLight prim represents a dome light with an IBL texture. +/// +/// @param sdfData Sdf data to store the layer in. +/// @param graphName Name of the current sbsar graph. +/// @param graphDesc Description of the current sbsar graph. +/// @param packagePath Path of the sbsar file. +/// @param sbsarHash Hash of the sbsar. +/// @param symbolMapper Symbol mapper to avoid conflict between parameters. +/// @param sbsarData Options for the sbsar. See SBSAROptions. +/// @return The path of the created dome light prim. PXR_NS::SdfPath addLuxDomeLight(PXR_NS::SdfAbstractData* sdfData, const MappedSymbol& graphName, diff --git a/sbsar/src/usdGeneration/sbsarMaterial.cpp b/sbsar/src/usdGeneration/sbsarMaterial.cpp index 87d8e18a..370c493a 100644 --- a/sbsar/src/usdGeneration/sbsarMaterial.cpp +++ b/sbsar/src/usdGeneration/sbsarMaterial.cpp @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ #include "sbsarMaterial.h" #include "sbsarAsm.h" -#include "sbsarMtlx.h" +#include "sbsarOpenPBR.h" #include "sbsarUsdPreviewSurface.h" #include "usdGenerationHelpers.h" #include @@ -84,22 +84,7 @@ initDefaultMaterialInputs(SdfAbstractData* sdfData, // Not setting a default value here, so that it has to be overwritten in the payload // reference } - auto defaultIt = default_channels.find(usage); - if (defaultIt != default_channels.end()) { - auto names = getDefaultValueNames(usage); - - SdfPath inputPath = - createShaderInput(sdfData, materialPath, names.first, defaultIt->second.type); - setAttributeDefaultValue(sdfData, inputPath, defaultIt->second.value); - setRangeMetadata(sdfData, inputPath, defaultIt->second.range); - setAttributeMetadata(sdfData, inputPath, SdfFieldKeys->Hidden, VtValue(true)); - - SdfPath textureBlendPath = - createShaderInput(sdfData, materialPath, names.second, SdfValueTypeNames->Float); - setAttributeDefaultValue(sdfData, textureBlendPath, 1.0f); - 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 = @@ -165,7 +150,8 @@ setMaterialValues(SdfAbstractData* sdfData, SdfPath textureAssetPath = createShaderInput( sdfData, materialPath, textureAssetName, defaultIt->second.type); std::string infoPath = generateSbsarInfoPath(usage, graphName, sbsarHash, jsParams); - TF_DEBUG(FILE_FORMAT_SBSAR).Msg("Using engine to get value for %s", usage.c_str()); + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("Using engine to get value for %s\n", usage.c_str()); setAttributeDefaultValue( sdfData, textureAssetPath, renderSbsarValue(packagePath, infoPath)); } @@ -238,9 +224,6 @@ addStandardMaterial(SdfAbstractData* sdfData, const SubstanceAir::GraphDesc& graphDesc, const SBSAROptions& options) { - - bool isRefractive = hasUsage("refraction", graphDesc); - #ifdef USDSBSAR_ENABLE_TEXTURE_TRANSFORM addMaterialTransform(sdfData, materialPath); #endif // USDSBSAR_ENABLE_TEXTURE_TRANSFORM @@ -251,31 +234,35 @@ addStandardMaterial(SdfAbstractData* sdfData, setAttributeDefaultValue(sdfData, uvChannelNamePath, std::string("st")); setAttributeMetadata(sdfData, uvChannelNamePath, SdfFieldKeys->Hidden, VtValue(true)); + // Expose the texture wrap modes for texture reading nodes. This is shared by ASM and + // UsdPreviewSurface + if (options.writeASM || options.writeUsdPreviewSurface) { + SdfPath uvWrapSPath = + createShaderInput(sdfData, materialPath, uv_wrap_s_name, SdfValueTypeNames->Token); + SdfPath uvWrapTPath = + createShaderInput(sdfData, materialPath, uv_wrap_t_name, SdfValueTypeNames->Token); + setAttributeDefaultValue(sdfData, uvWrapSPath, AdobeTokens->repeat); + setAttributeDefaultValue(sdfData, uvWrapTPath, AdobeTokens->repeat); + VtTokenArray wrapModes = { + AdobeTokens->repeat, AdobeTokens->mirror, AdobeTokens->clamp, AdobeTokens->black + }; + setAttributeMetadata(sdfData, uvWrapSPath, SdfFieldKeys->AllowedTokens, VtValue(wrapModes)); + setAttributeMetadata(sdfData, uvWrapTPath, SdfFieldKeys->AllowedTokens, VtValue(wrapModes)); + } + // Add ASM Implementation if (options.writeASM) { addAsmShader(sdfData, materialPath, graphDesc); } - if (isRefractive) { - // Add Refractive UsdPreviewSurface Implementation - if (options.writeUsdPreviewSurface) { - addUsdPreviewSurfaceRefractive(sdfData, materialPath, graphDesc); - } - // Add Refractive MaterialX Implementation - if (options.writeMaterialX) { - addMtlxShaderRefractive(sdfData, materialPath, graphDesc); - } + // Add UsdPreviewSurface Implementation + if (options.writeUsdPreviewSurface) { + addUsdPreviewSurface(sdfData, materialPath, graphDesc); } - else { - // Add UsdPreviewSurface Implementation - if (options.writeUsdPreviewSurface) { - addUsdPreviewSurface(sdfData, materialPath, graphDesc); - } - // Add Refractive MaterialX Implementation - if (options.writeMaterialX) { - addMtlxShader(sdfData, materialPath, graphDesc); - } + // Add Refractive MaterialX Implementation + if (options.writeOpenPBR) { + addOpenPbrShader(sdfData, materialPath, graphDesc); } } @@ -356,6 +343,9 @@ addMaterialPrim(SdfAbstractData* sdfData, createMaterialPrimSpec(sdfData, rootPath, TfToken(graphName.usdName)); // process usd sbsarParameters into a js dict JsValue jsParams = convertSbsarParameters(sbsarData.sbsarParameters); + // We assume opengl in the initial state, but the substance engine assumes directx, this + // will tell the engine to use opengl formatting + jsParams = applyDefaultNormalFormatInput(graphDesc, jsParams); // Set the procedural texture paths based on the sbsarParameters setMaterialTexturePaths(sdfData, materialPath, graphDesc, graphName, sbsarHash, jsParams); // Set procedural values for uniform usage diff --git a/sbsar/src/usdGeneration/sbsarMaterial.h b/sbsar/src/usdGeneration/sbsarMaterial.h index 2468da71..71d59697 100644 --- a/sbsar/src/usdGeneration/sbsarMaterial.h +++ b/sbsar/src/usdGeneration/sbsarMaterial.h @@ -23,26 +23,26 @@ governing permissions and limitations under the License. namespace adobe::usd::sbsar { -//! \brief Add usd material primitive to the given Sdf layer. -//! Depending on the sbsarData.depth, the content of the prim is different. -//! Depth = 0 -> Only variant: Preset and resolution -//! Depth = 1 -> All default parameters of the current graph -//! Depth = 2 -> Asset path and standard material (Depending to the compilation -//! parameter) -//! This system will generate several layers (by depth). This is useful for two reasons: -//! 1 - Control priority of parameters, in the order of: User -> Variant -> default parameters -//! 2 - The Layer 1 and 2 are split because the plugin needs to compose all default -//! parameters. (in SBSARFileFormat::ComposeFieldsForFileFormatArguments) to catch all -//! updates and regenerate asset path. -//! \param sdfData Sdf data to store the layer in. -//! \param graphName Name of the current sbsar graph. -//! \param graphDesc Description of the current sbsar graph. -//! \param packagePath Path of the sbsar file. -//! \param classPath Path of the class material. -//! \param sbsarHash Hash of the sbsar. -//! \param symbolMapper Symbol mapper to avoid conflict between parameters. -//! \param sbsarData Options for the sbsar. See SBSAROptions. -//! \return The path of the created prim. +/// \brief Add usd material primitive to the given Sdf layer. +/// Depending on the sbsarData.depth, the content of the prim is different. +/// Depth = 0 -> Only variant: Preset and resolution +/// Depth = 1 -> All default parameters of the current graph +/// Depth = 2 -> Asset path and standard material (Depending to the compilation +/// parameter) +/// This system will generate several layers (by depth). This is useful for two reasons: +/// 1 - Control priority of parameters, in the order of: User -> Variant -> default parameters +/// 2 - The Layer 1 and 2 are split because the plugin needs to compose all default +/// parameters. (in SBSARFileFormat::ComposeFieldsForFileFormatArguments) to catch all +/// updates and regenerate asset path. +/// \param sdfData Sdf data to store the layer in. +/// \param graphName Name of the current sbsar graph. +/// \param graphDesc Description of the current sbsar graph. +/// \param packagePath Path of the sbsar file. +/// \param classPath Path of the class material. +/// \param sbsarHash Hash of the sbsar. +/// \param symbolMapper Symbol mapper to avoid conflict between parameters. +/// \param sbsarData Options for the sbsar. See SBSAROptions. +/// \return The path of the created prim. PXR_NS::SdfPath addMaterialPrim(PXR_NS::SdfAbstractData* sdfData, const MappedSymbol& graphName, @@ -53,12 +53,12 @@ addMaterialPrim(PXR_NS::SdfAbstractData* sdfData, SymbolMapper& symbolMapper, const SBSAROptions& sbsarData); -//! \brief Add a class prim to the given Sdf layer. -//! The class prim is a global prim with a "class" specifier. It contains -//! attributes that are set once and inherited by all material prims. -//! \param sdfData Sdf data to store the layer in. -//! \param className Name of the class. -//! \param classType Type of the class. +/// \brief Add a class prim to the given Sdf layer. +/// The class prim is a global prim with a "class" specifier. It contains +/// attributes that are set once and inherited by all material prims. +/// \param sdfData Sdf data to store the layer in. +/// \param className Name of the class. +/// \param classType Type of the class. PXR_NS::SdfPath addClassPrim(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::TfToken& className, diff --git a/sbsar/src/usdGeneration/sbsarMtlx.cpp b/sbsar/src/usdGeneration/sbsarMtlx.cpp deleted file mode 100644 index 352a29d7..00000000 --- a/sbsar/src/usdGeneration/sbsarMtlx.cpp +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2024 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 "sbsarMtlx.h" -#include "usdGenerationHelpers.h" -#include - -// File format utils -#include -#include -#include - -#include - -using namespace SubstanceAir; -PXR_NAMESPACE_USING_DIRECTIVE - -namespace { - -using namespace adobe::usd; -using namespace adobe::usd::sbsar; - -// clang-format off -TF_DEFINE_PRIVATE_TOKENS(_tokens, - (TexCoordReader) - (Mtlx) - (UvRotate) - (UvScale) - (UvTranslate) - (WsNormal) -); -// clang-format on - -struct BindInfo -{ - std::string name; - SdfValueTypeName sdfType; - std::string outputName; - std::string color_space; -}; - -static std::map _opaqueMapBindings = { - { "baseColor", { "base_color", SdfValueTypeNames->Color3f, "out", "sRGB" } }, - { "ambientOcclusion", { "ambient_occlusion", SdfValueTypeNames->Float, "out", "raw" } }, - { "roughness", { "roughness", SdfValueTypeNames->Float, "out", "raw" } }, - { "metallic", { "metallic", SdfValueTypeNames->Float, "out", "raw" } }, - { "normal", { "normal", SdfValueTypeNames->Float3, "out", "raw" } }, - { "opacity", { "opacity", SdfValueTypeNames->Float, "out", "raw" } }, - { "emissive", { "emission_color", SdfValueTypeNames->Color3f, "out", "sRGB" } } -}; - -static std::map _refractiveMapBindings = { - { "baseColor", { "base_color", SdfValueTypeNames->Color3f, "out", "sRGB" } }, - { "ambientOcclusion", { "ambient_occlusion", SdfValueTypeNames->Float, "out", "raw" } }, - { "roughness", { "roughness", SdfValueTypeNames->Float, "out", "raw" } }, - { "metallic", { "metallic", SdfValueTypeNames->Float, "out", "raw" } }, - { "normal", { "normal", SdfValueTypeNames->Float3, "out", "raw" } }, - { "refraction", { "opacity", SdfValueTypeNames->Float, "out", "raw" } }, - { "emissive", { "emission_color", SdfValueTypeNames->Color3f, "out", "sRGB" } } -}; - -SdfPath -bindTexture(SdfAbstractData* sdfData, - const SdfPath& parentPath, - const BindInfo& bindInfo, - const SdfPath& uvOutputAttrPath, - const SdfPath& textureAssetAttrPath) -{ - TF_DEBUG(FILE_FORMAT_SBSAR) - .Msg("bindTexture: Binding texture channel %s\n", bindInfo.name.c_str()); - - TfToken shaderType; - if (bindInfo.sdfType == SdfValueTypeNames->Color3f) { - shaderType = MtlXTokens->ND_image_color3; - } else if (bindInfo.sdfType == SdfValueTypeNames->Float3) { - shaderType = MtlXTokens->ND_image_vector3; - } else if (bindInfo.sdfType == SdfValueTypeNames->Float) { - shaderType = MtlXTokens->ND_image_float; - } else { - TF_CODING_ERROR("Unsupported texture type %s", bindInfo.sdfType.GetAsToken().GetText()); - return {}; - } - - // Note, there is currently no support for the color space choice. Also no support for a - // fallback value. Bias and scale are also not supported. - SdfPath resultPath = createShader( - sdfData, - parentPath, - TfToken("file" + bindInfo.name), - shaderType, - "out", - { { "uaddressmode", std::string("periodic") }, { "vaddressmode", std::string("periodic") } }, - { { "texcoord", uvOutputAttrPath }, { "file", textureAssetAttrPath } }); - - return resultPath; -} - -bool -addUsdMtlxShaderImpl(SdfAbstractData* sdfData, - const SdfPath& materialPath, - const GraphDesc& graphDesc, - const std::map& mapBindings) -{ - TF_DEBUG(FILE_FORMAT_SBSAR).Msg("addUsdMtlxShaderImpl: Adding MaterialX Implementation\n"); - - // Create a scope for the UsdPreviewSurface implementation - SdfPath scopePath = - createPrimSpec(sdfData, materialPath, _tokens->Mtlx, UsdShadeTokens->NodeGraph); - - // Create Texcoord Reader - SdfPath txOutputPath = createShader( - sdfData, scopePath, _tokens->TexCoordReader, MtlXTokens->ND_texcoord_vector2, "out"); - -#ifdef USDSBSAR_ENABLE_TEXTURE_TRANSFORM - SdfPath uvScaleInputPath = inputPath(materialPath, uv_scale_input); - SdfPath uvRotationInputPath = inputPath(materialPath, uv_rotation_input); - SdfPath uvTranslationInputPath = inputPath(materialPath, uv_translation_input); - - // Create UV transform by applying rotation, scale and transform, in that order - SdfPath rotOutputPath = - createShader(sdfData, - scopePath, - _tokens->UvRotate, - MtlXTokens->ND_rotate2d_vector2, - "out", - {}, - { { "amount", uvRotationInputPath }, { "in", txOutputPath } }); - - SdfPath scaleOutputPath = - createShader(sdfData, - scopePath, - _tokens->UvScale, - MtlXTokens->ND_multiply_vector2, - "out", - {}, - { { "in1", uvScaleInputPath }, { "in2", rotOutputPath } }); - - SdfPath uvOutputPath = - createShader(sdfData, - scopePath, - _tokens->UvTranslate, - MtlXTokens->ND_add_vector2, - "out", - {}, - { { "in1", uvTranslationInputPath }, { "in2", scaleOutputPath } }); - -#else // NOT USDSBSAR_ENABLE_TEXTURE_TRANSFORM - SdfPath uvOutputPath = txOutputPath; -#endif // USDSBSAR_ENABLE_TEXTURE_TRANSFORM - - // Create texture sampling nodes - InputConnections inputConnections; - for (auto& usage : mapped_usages) { - if (hasUsage(usage, graphDesc)) { - auto it = mapBindings.find(usage); - if (it != mapBindings.end()) { - const BindInfo& bindInfo = it->second; - - // Get the path of the texture attribute on the Material prim - std::string texAssetName = getTextureAssetName(usage); - SdfPath textureAssetAttrPath = inputPath(materialPath, texAssetName); - - // Create the texture reader - SdfPath texResultPath = - bindTexture(sdfData, scopePath, bindInfo, uvOutputPath, textureAssetAttrPath); - - 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, - // MtlXTokens->ND_normalmap, - // "out", - // {}, - // { { "in", texResultPath } }); - - // inputConnections.emplace_back(bindInfo.name, wsNormalPath); - } else { - inputConnections.emplace_back(bindInfo.name, texResultPath); - } - } - } - } - - // Create MaterialX shader for Adobe Standard Material - SdfPath surfaceOutputPath = createShader(sdfData, - scopePath, - MtlXTokens->ND_adobe_standard_material, - MtlXTokens->ND_adobe_standard_material, - "surface", - {}, - inputConnections); - createShaderOutput( - sdfData, materialPath, "mtlx:surface", SdfValueTypeNames->Token, surfaceOutputPath); - - return true; -} - -} - -namespace adobe::usd::sbsar { - -bool -addMtlxShader(SdfAbstractData* sdfData, - const SdfPath& materialPath, - const SubstanceAir::GraphDesc& graphDesc) -{ - return addUsdMtlxShaderImpl(sdfData, materialPath, graphDesc, _opaqueMapBindings); -} - -bool -addMtlxShaderRefractive(SdfAbstractData* sdfData, - const SdfPath& materialPath, - const SubstanceAir::GraphDesc& graphDesc) -{ - return addUsdMtlxShaderImpl(sdfData, materialPath, graphDesc, _refractiveMapBindings); -} - -} diff --git a/sbsar/src/usdGeneration/sbsarOpenPBR.cpp b/sbsar/src/usdGeneration/sbsarOpenPBR.cpp new file mode 100644 index 00000000..6459c8ac --- /dev/null +++ b/sbsar/src/usdGeneration/sbsarOpenPBR.cpp @@ -0,0 +1,375 @@ +/* +Copyright 2024 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 "sbsarOpenPBR.h" +#include "usdGenerationHelpers.h" +#include + +// File format utils +#include +#include +#include + +#include + +using namespace SubstanceAir; +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { + +using namespace adobe::usd; +using namespace adobe::usd::sbsar; + +// clang-format off +TF_DEFINE_PRIVATE_TOKENS(_tokens, + (TexCoordReader) + (OpenPBR) + (UvTransform) + (WsNormal) + (Surface) + (Displacement) +); +// clang-format on + +struct BindInfo +{ + TfToken name; + SdfValueTypeName sdfType; + std::string outputName; + TfToken colorSpace; +}; + +// This is a mapping from SBSAR usage to OpenPBR inputs +// Notes: +// * OpenPBR does not directly support ambient occlusion +// * IOR is not a texturable output and we don't have a mapping for uniform values yet +// * "anisotropyAngle" would be expressed via geometry_tangent +// * Not clear how "coatSpecularLevel" factors in +// * "height" for displacement is not handled here +// * ND_displacement_float +// * displacement - float +// * scale - float +// * out - displacementshader +// * "refraction" is not supported +// * The colors, at least for base color seem to be off in OpenPBR/MaterialX in Eclair +// * Maybe we need an explicit color conversion. The colorSpace is currently not considered +static std::map _materialMapBindings = { + // * Base + // base_weight (no source info) + { "baseColor", + { OpenPbrTokens->base_color, SdfValueTypeNames->Color3f, "out", AdobeTokens->sRGB } }, + // base_diffuse_roughness (no source info) see above + { "metallic", + { OpenPbrTokens->base_metalness, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + + // * Specular + { "specularLevel", + { OpenPbrTokens->specular_weight, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + { "specularEdgeColor", + { OpenPbrTokens->specular_color, SdfValueTypeNames->Color3f, "out", AdobeTokens->sRGB } }, + { "roughness", + { OpenPbrTokens->specular_roughness, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + // specular_ior (no source info) + // XXX does this work? + //{ "IOR", { OpenPbrTokens->specular_ior, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + { "anisotropyLevel", + { OpenPbrTokens->specular_roughness_anisotropy, + SdfValueTypeNames->Float, + "out", + AdobeTokens->raw } }, + + // * Transmission + { "translucency", + { OpenPbrTokens->transmission_weight, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + { "absorptionColor", + { OpenPbrTokens->transmission_color, SdfValueTypeNames->Color3f, "out", AdobeTokens->sRGB } }, + // transmission_depth (no source info) (absorption distance?) + // transmission_scatter (no source info) + // transmission_scatter_anisotropy (no source info) + // transmission_dispersion_scale (no source info) + // transmission_dispersion_abbe_number (no source info) + + // * Subsurface + // subsurface_weight (no source info) (is set to 1 if we have scatterng color or distance scale) + { "scatteringColor", + { OpenPbrTokens->transmission_scatter, + SdfValueTypeNames->Color3f, + "out", + AdobeTokens->sRGB } }, + { "scatteringDistanceScale", + { OpenPbrTokens->subsurface_radius_scale, + SdfValueTypeNames->Color3f, + "out", + AdobeTokens->sRGB } }, + // subsurface_radius_scale (no source info) (maps to ASM scatteringDistanceScale) + // subsurface_anisotropy (no source info) + // subsurface_scatter_anisotropy (no source info) + + // * Fuzz + { "sheenOpacity", + { OpenPbrTokens->fuzz_weight, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + { "sheenColor", + { OpenPbrTokens->fuzz_color, SdfValueTypeNames->Color3f, "out", AdobeTokens->sRGB } }, + { "sheenRoughness", + { OpenPbrTokens->fuzz_roughness, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + + // * Coat + { "coatOpacity", + { OpenPbrTokens->coat_weight, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + { "coatColor", + { OpenPbrTokens->coat_color, SdfValueTypeNames->Color3f, "out", AdobeTokens->sRGB } }, + { "coatRoughness", + { OpenPbrTokens->coat_roughness, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + // coat_roughness_anisotropy (no source info) + // coat_ior (no source info) + // coat_darkening (no source info) + + // * Thin film + // thin_film_weight (no source info) + // thin_film_thickness (no source info) + // thin_film_ior (no source info) + + // * Emission + // emission_luminance (no source info) (is set to 1 if we have "emissive" input) + { "emissive", + { OpenPbrTokens->emission_color, SdfValueTypeNames->Color3f, "out", AdobeTokens->sRGB } }, + + // * Geometry + { "opacity", + { OpenPbrTokens->geometry_opacity, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + { "normal", + { OpenPbrTokens->geometry_normal, SdfValueTypeNames->Float3, "out", AdobeTokens->raw } }, + { "coatNormal", + { OpenPbrTokens->geometry_coat_normal, SdfValueTypeNames->Float3, "out", AdobeTokens->raw } }, + // geometry_tangent (no source info) (derive from anisotropyAngle?) + // geometry_coat_tangent (no source info) +}; + +SdfPath +bindTexture(SdfAbstractData* sdfData, + const SdfPath& parentPath, + const BindInfo& bindInfo, + const SdfPath& uvOutputAttrPath, + const SdfPath& textureAssetAttrPath, + const SdfPath& uAddressModeAttrPath, + const SdfPath& vAddressModeAttrPath) +{ + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("bindTexture: Binding texture channel %s\n", bindInfo.name.GetText()); + + TfToken shaderType; + if (bindInfo.sdfType == SdfValueTypeNames->Color3f) { + shaderType = MtlXTokens->ND_image_color3; + } else if (bindInfo.sdfType == SdfValueTypeNames->Float3) { + shaderType = MtlXTokens->ND_image_vector3; + } else if (bindInfo.sdfType == SdfValueTypeNames->Float) { + shaderType = MtlXTokens->ND_image_float; + } else { + TF_CODING_ERROR("Unsupported texture type %s", bindInfo.sdfType.GetAsToken().GetText()); + return {}; + } + + // Note, there is currently no support for the color space choice. Also no support for a + // fallback value. Bias and scale are also not supported. + SdfPath resultPath = createShader(sdfData, + parentPath, + TfToken("file" + bindInfo.name.GetString()), + shaderType, + "out", + {}, + { { "texcoord", uvOutputAttrPath }, + { "file", textureAssetAttrPath }, + { "uaddressmode", uAddressModeAttrPath }, + { "vaddressmode", vAddressModeAttrPath } }); + + return resultPath; +} + +bool +addUsdOpenPbrShaderImpl(SdfAbstractData* sdfData, + const SdfPath& materialPath, + const GraphDesc& graphDesc, + const std::map& mapBindings) +{ + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("addUsdOpenPbrShaderImpl: Adding OpenPBR/MaterialX Implementation\n"); + + // Create top level inputs to control the UV coordinate channel and the UV address modes. + // Note, this is an unfortunate duplication of the similar setup for ASM and UsdPreviewSurface + // based networks. For those two scenarios we need three tokens for the named UV primvar and + // wrap modes, where here we need an int for the UV index and two strings for the address modes. + SdfPath uvChannelIndexPath = + createShaderInput(sdfData, materialPath, "uvChannelIndex", SdfValueTypeNames->Int); + setAttributeDefaultValue(sdfData, uvChannelIndexPath, 0); + + VtTokenArray addressModes = { TfToken("periodic"), TfToken("clamp") }; + SdfPath uAddressModePath = + createShaderInput(sdfData, materialPath, "uaddressmode", SdfValueTypeNames->String); + setAttributeDefaultValue(sdfData, uAddressModePath, "periodic"); + setAttributeMetadata( + sdfData, uAddressModePath, SdfFieldKeys->AllowedTokens, VtValue(addressModes)); + + SdfPath vAddressModePath = + createShaderInput(sdfData, materialPath, "vaddressmode", SdfValueTypeNames->String); + setAttributeDefaultValue(sdfData, vAddressModePath, "periodic"); + setAttributeMetadata( + sdfData, vAddressModePath, SdfFieldKeys->AllowedTokens, VtValue(addressModes)); + + // Create a scope for the OpenPBR implementation + SdfPath scopePath = + createPrimSpec(sdfData, materialPath, _tokens->OpenPBR, UsdShadeTokens->NodeGraph); + + // Create Texcoord Reader + SdfPath txOutputPath = createShader(sdfData, + scopePath, + _tokens->TexCoordReader, + MtlXTokens->ND_texcoord_vector2, + "out", + {}, + { { "index", uvChannelIndexPath } }); + +#ifdef USDSBSAR_ENABLE_TEXTURE_TRANSFORM + SdfPath uvScaleInputPath = inputPath(materialPath, uv_scale_input); + SdfPath uvRotationInputPath = inputPath(materialPath, uv_rotation_input); + SdfPath uvTranslationInputPath = inputPath(materialPath, uv_translation_input); + + // Create UV transform by applying scale, rotation and translation, in that order + // This matches what the UsdTransform2d node does + SdfPath uvOutputPath = createShader(sdfData, + scopePath, + _tokens->UvTransform, + MtlXTokens->ND_place2d_vector2, + "out", + {}, + { { "texcoord", txOutputPath }, + { "scale", uvScaleInputPath }, + { "rotate", uvRotationInputPath }, + { "offset", uvTranslationInputPath } }); +#else // NOT USDSBSAR_ENABLE_TEXTURE_TRANSFORM + SdfPath uvOutputPath = txOutputPath; +#endif // USDSBSAR_ENABLE_TEXTURE_TRANSFORM + + // Create texture sampling nodes + InputValues inputValues; + InputConnections inputConnections; + bool enableSubsurface = false; + for (const auto& usage : mapped_usages) { + if (hasUsage(usage, graphDesc)) { + auto it = mapBindings.find(usage); + if (it != mapBindings.end()) { + const BindInfo& bindInfo = it->second; + + // Get the path of the texture attribute on the Material prim + std::string texAssetName = getTextureAssetName(usage); + SdfPath textureAssetAttrPath = inputPath(materialPath, texAssetName); + + // Create the texture reader + SdfPath texResultPath = bindTexture(sdfData, + scopePath, + bindInfo, + uvOutputPath, + textureAssetAttrPath, + uAddressModePath, + vAddressModePath); + + if (isNormal(usage)) { + // Route normal map through a normal map node + // TODO: 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, + MtlXTokens->ND_normalmap, + "out", + {}, + { { "in", texResultPath } }); + + inputConnections.emplace_back(bindInfo.name.GetString(), wsNormalPath); + } else { + inputConnections.emplace_back(bindInfo.name.GetString(), texResultPath); + } + + if (usage == "scatteringColor" || usage == "scatteringDistanceScale") { + enableSubsurface = true; + } + + if (usage == "emissive") { + // The luminance should be part of of the `scale` or `value` of the + // emission_color input texture reader, but that is missing. + // Still we need to turn emission on by setting the luminance to 1.0, + // otherwise emission is turned off. + inputValues.emplace_back(OpenPbrTokens->emission_luminance, 1.0f); + } + } + } + } + + if (enableSubsurface) { + inputValues.emplace_back(OpenPbrTokens->subsurface_weight, 1.0f); + } + +// TODO: build mapping table for uniform values from the SBSAR usages to the corresponding +// inputs for OpenPBR. E.g. IOR -> specular_ior, etc. +#if 0 + // Connect to uniform values + for (auto& usage : uniform_usages) { + if (hasUsage(usage, graphDesc)) { + SdfPath uniformAttrPath = inputPath(materialPath, usage); + inputConnections.emplace_back(usage, uniformAttrPath); + } + } +#endif + + // Create MaterialX shader for Adobe Standard Material + SdfPath surfaceOutputPath = createShader(sdfData, + scopePath, + _tokens->Surface, + MtlXTokens->ND_open_pbr_surface_surfaceshader, + "out", + inputValues, + inputConnections); + createShaderOutput( + sdfData, materialPath, "mtlx:surface", SdfValueTypeNames->Token, surfaceOutputPath); + +// TODO: add support to map the "height" usage to the displacement +// We should check for "height" usage and then create the corresponding `texResultPath` and connect +// it here. We might want to look for uniform heightLevel and heightScale to remap the height into +// the right range. +#if 0 + SdfPath displacementOutputPath = createShader(sdfData, + scopePath, + _tokens->Displacement, + MtlXTokens->ND_displacement_float, + "out", + { { "scale", 1.0f } }, + { { "displacement", heightResultPath } }); + + createShaderOutput( + sdfData, materialPath, "mtlx:displacement", SdfValueTypeNames->Token, displacementOutputPath); +#endif + + return true; +} +} + +namespace adobe::usd::sbsar { + +bool +addOpenPbrShader(SdfAbstractData* sdfData, + const SdfPath& materialPath, + const SubstanceAir::GraphDesc& graphDesc) +{ + return addUsdOpenPbrShaderImpl(sdfData, materialPath, graphDesc, _materialMapBindings); +} + +} diff --git a/sbsar/src/usdGeneration/sbsarMtlx.h b/sbsar/src/usdGeneration/sbsarOpenPBR.h similarity index 54% rename from sbsar/src/usdGeneration/sbsarMtlx.h rename to sbsar/src/usdGeneration/sbsarOpenPBR.h index 132cae34..af4a17e8 100644 --- a/sbsar/src/usdGeneration/sbsarMtlx.h +++ b/sbsar/src/usdGeneration/sbsarOpenPBR.h @@ -18,14 +18,18 @@ governing permissions and limitations under the License. namespace adobe::usd::sbsar { +/// @brief Adds an OpenPBR/MaterialX material network to the material +/// +/// The network is created from the provided Substance graph description and connected to the +/// material as the 'mtlx' surface. +/// +/// @param sdfData SDF data container to store the material in +/// @param materialPath Path of the parent material +/// @param graphDesc Description of the current SBSAR graph +/// @return true if the material was successfully added, false otherwise bool -addMtlxShader(PXR_NS::SdfAbstractData* sdfData, - const PXR_NS::SdfPath& materialPath, - const SubstanceAir::GraphDesc& graphDesc); - -bool -addMtlxShaderRefractive(PXR_NS::SdfAbstractData* sdfData, - const PXR_NS::SdfPath& materialPath, - const SubstanceAir::GraphDesc& graphDesc); +addOpenPbrShader(PXR_NS::SdfAbstractData* sdfData, + const PXR_NS::SdfPath& materialPath, + const SubstanceAir::GraphDesc& graphDesc); } diff --git a/sbsar/src/usdGeneration/sbsarSymbolMapper.h b/sbsar/src/usdGeneration/sbsarSymbolMapper.h index 750c3270..259f0b72 100644 --- a/sbsar/src/usdGeneration/sbsarSymbolMapper.h +++ b/sbsar/src/usdGeneration/sbsarSymbolMapper.h @@ -16,8 +16,9 @@ governing permissions and limitations under the License. #include namespace adobe::usd::sbsar { -// Represents a mapped symbol, holds both the original -// substance symbol and the corresponding Usd symbol + +/// @brief Represents a mapped symbol, holds both the original +/// substance symbol and the corresponding Usd symbol struct MappedSymbol { std::string substanceName; @@ -25,20 +26,21 @@ struct MappedSymbol bool invalid(); }; -// Keeps track of mapping of names between substance and USD in a reversible -// way -// Guarantees the same Usd Symbol doesn't occur multiple times in the same -// mapper +/// @brief Keeps track of mapping of names between substance and USD in a reversible way. +/// @details Guarantees the same Usd Symbol doesn't occur multiple times in the same mapper. class SymbolMapper { public: SymbolMapper(); virtual ~SymbolMapper(); - // Ask for a Usd symbol for the argument substance symbol - // if the symbol is known, give back the existing mapping - // if it's unknown, generate a new mapping with a Usd - // compatible name that is not a duplicate of a previously - // known usd name in the mapper + + /// @brief Ask for a Usd symbol for the argument substance symbol. + /// @details If the symbol is known, gives back the existing mapping. + /// If it's unknown, generates a new mapping with a Usd + /// compatible name that is not a duplicate of a previously + /// known usd name in the mapper. + /// @param substanceSymbol The substance symbol to map. + /// @return The mapped symbol. MappedSymbol GetSymbol(const std::string& substanceSymbol); private: @@ -48,4 +50,5 @@ class SymbolMapper // Existing usd symbols std::set usd_symbols; }; + } diff --git a/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.cpp b/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.cpp index ea15682c..eb9e1b97 100644 --- a/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.cpp +++ b/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.cpp @@ -70,9 +70,10 @@ bindTexture(SdfAbstractData* sdfData, const BindInfo& bindInfo, const SdfPath& uvOutputAttrPath, const SdfPath& textureAssetAttrPath, - const SdfPath& fallbackAttrPath, const SdfPath& scaleAttrPath, - const SdfPath& biasAttrPath) + const SdfPath& biasAttrPath, + const SdfPath& uvWrapSPath, + const SdfPath& uvWrapTPath) { TF_DEBUG(FILE_FORMAT_SBSAR) @@ -82,14 +83,13 @@ bindTexture(SdfAbstractData* sdfData, TfToken("file" + bindInfo.name), AdobeTokens->UsdUVTexture, bindInfo.outputName, - { { "sourceColorSpace", bindInfo.color_space }, - { "wrapS", AdobeTokens->repeat }, - { "wrapT", AdobeTokens->repeat } }, + { { "sourceColorSpace", bindInfo.color_space } }, { { "st", uvOutputAttrPath }, { "file", textureAssetAttrPath }, - { "fallback", fallbackAttrPath }, { "scale", scaleAttrPath }, - { "bias", biasAttrPath } }); + { "bias", biasAttrPath }, + { "wrapS", uvWrapSPath }, + { "wrapT", uvWrapTPath } }); return resultPath; } @@ -108,6 +108,8 @@ addUsdPreviewSurfaceImpl(SdfAbstractData* sdfData, sdfData, materialPath, AdobeTokens->UsdPreviewSurface, UsdShadeTokens->NodeGraph); SdfPath uvChannelNamePath = inputPath(materialPath, uv_channel_name); + SdfPath uvWrapSPath = inputPath(materialPath, uv_wrap_s_name); + SdfPath uvWrapTPath = inputPath(materialPath, uv_wrap_t_name); // Create Texcoord Reader SdfPath txOutputPath = createShader(sdfData, @@ -151,14 +153,6 @@ addUsdPreviewSurfaceImpl(SdfAbstractData* sdfData, std::string texAssetName = getTextureAssetName(usage); SdfPath textureAssetAttrPath = inputPath(materialPath, texAssetName); - // Add default value if present - SdfPath fallbackAttrPath; - auto defaultIt = default_channels.find(usage); - if (defaultIt != default_channels.end()) { - auto defaultName = getDefaultValueNames(usage); - fallbackAttrPath = inputPath(materialPath, defaultName.first); - } - SdfPath scaleAttrPath, biasAttrPath; if (isNormal(usage)) { const auto [scaleName, biasName] = getNormalMapScaleAndBiasNames(usage); @@ -172,9 +166,10 @@ addUsdPreviewSurfaceImpl(SdfAbstractData* sdfData, bindInfo, uvOutputPath, textureAssetAttrPath, - fallbackAttrPath, scaleAttrPath, - biasAttrPath); + biasAttrPath, + uvWrapSPath, + uvWrapTPath); inputConnections.emplace_back(bindInfo.name, texResultPath); } @@ -204,15 +199,11 @@ addUsdPreviewSurface(SdfAbstractData* sdfData, const SdfPath& materialPath, const SubstanceAir::GraphDesc& graphDesc) { - return addUsdPreviewSurfaceImpl(sdfData, materialPath, graphDesc, _opaqueMapBindings); -} - -bool -addUsdPreviewSurfaceRefractive(SdfAbstractData* sdfData, - const SdfPath& materialPath, - const SubstanceAir::GraphDesc& graphDesc) -{ - return addUsdPreviewSurfaceImpl(sdfData, materialPath, graphDesc, _refractiveMapBindings); + if (hasUsage("refraction", graphDesc)) { + return addUsdPreviewSurfaceImpl(sdfData, materialPath, graphDesc, _refractiveMapBindings); + } else { + return addUsdPreviewSurfaceImpl(sdfData, materialPath, graphDesc, _opaqueMapBindings); + } } } diff --git a/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.h b/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.h index a5631e77..ab5d5429 100644 --- a/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.h +++ b/sbsar/src/usdGeneration/sbsarUsdPreviewSurface.h @@ -18,14 +18,18 @@ governing permissions and limitations under the License. namespace adobe::usd::sbsar { +/// @brief Adds a UsdPreviewSurface material network to the material +/// +/// The network is created from the provided Substance graph description and connected to the +/// material as the universal surface. +/// +/// @param sdfData SDF data container to store the material in +/// @param materialPath Path of the parent material +/// @param graphDesc Description of the current SBSAR graph +/// @return true if the material was successfully added, false otherwise bool addUsdPreviewSurface(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& materialPath, const SubstanceAir::GraphDesc& graphDesc); -bool -addUsdPreviewSurfaceRefractive(PXR_NS::SdfAbstractData* sdfData, - const PXR_NS::SdfPath& materialPath, - const SubstanceAir::GraphDesc& graphDesc); - } diff --git a/sbsar/src/usdGeneration/usdGenerationHelpers.cpp b/sbsar/src/usdGeneration/usdGenerationHelpers.cpp index 3fc9c912..3456864f 100644 --- a/sbsar/src/usdGeneration/usdGenerationHelpers.cpp +++ b/sbsar/src/usdGeneration/usdGenerationHelpers.cpp @@ -219,6 +219,8 @@ const std::string uv_rotation_input("uvrotation"); const std::string uv_translation_input("uvtranslation"); const std::string uv_channel_name("uvChannelName"); +const std::string uv_wrap_s_name("uvWrapS"); +const std::string uv_wrap_t_name("uvWrapT"); const std::string proceduralParameterPrefix("procedural_sbsar:"); @@ -253,7 +255,7 @@ guessGraphType(const SubstanceAir::GraphDesc& graphDesc) } } // Check for light - if (hasUsage("environment", graphDesc)) { + if (hasUsage("environment", graphDesc) || hasUsage("panorama", graphDesc)) { return GraphType::Light; } // Didn't find anything relevant @@ -263,7 +265,7 @@ guessGraphType(const SubstanceAir::GraphDesc& graphDesc) std::pair getDefaultValueNames(const std::string& channelName) { - return { channelName + "_default", channelName + "_textureInfluence" }; + return { channelName, channelName + "TextureInfluence" }; } std::string @@ -382,6 +384,22 @@ adjustNormalFormatInput(const SubstanceAir::string& identifier, return false; } +JsValue +applyDefaultNormalFormatInput(const SubstanceAir::GraphDesc& graphDesc, const JsValue& jsParams) +{ + bool hasNormalFormatInput = hasInput(normalFormatParamName, graphDesc); + + if (hasNormalFormatInput) { + auto normalFormat = determineNormalFormat(jsParams); + if (normalFormat == NormalFormat::Unknown) { + auto jsObject = jsParams.GetJsObject(); + jsObject[normalFormatParamName] = JsValue(1); // 1 is OpenGL + return JsValue(jsObject); + } + } + return jsParams; +} + NormalFormat getDefaultNormalFormat(const SubstanceAir::GraphDesc& graphDesc) { @@ -469,7 +487,7 @@ generateSbsarInfoPath(const std::string& usage, std::string getTextureAssetName(const std::string& usage) { - return usage + "_texture"; + return usage + "Texture"; } MappedSymbol diff --git a/sbsar/src/usdGeneration/usdGenerationHelpers.h b/sbsar/src/usdGeneration/usdGenerationHelpers.h index e009dc68..47f2f535 100644 --- a/sbsar/src/usdGeneration/usdGenerationHelpers.h +++ b/sbsar/src/usdGeneration/usdGenerationHelpers.h @@ -23,133 +23,256 @@ governing permissions and limitations under the License. namespace adobe::usd::sbsar { +/// Default resolution level for SBSAR textures (log2 scale, where 9 = 512x512) static constexpr int SBSAR_DEFAULT_RESOLUTION = 9; + +/// @brief Represents default properties for a texture channel. +/// +/// This structure encapsulates the default type, value, and valid range +/// for a specific texture channel in SBSAR materials. struct DefaultChannel { - PXR_NS::SdfValueTypeName type; - PXR_NS::VtValue value; - std::pair range; // (min,max) pair + PXR_NS::SdfValueTypeName type; ///< USD value type for this channel + PXR_NS::VtValue value; ///< Default value for the channel + std::pair range; ///< Valid range (min,max) for the channel }; + +/// List of SBSAR channel usages that have a known mapping extern const std::vector mapped_usages; + +/// List of SBSAR channel usages that should use uniform values extern const std::vector uniform_usages; + +/// List of SBSAR channel usages that represent normal maps extern const std::vector normal_usages; + +/// Default resolution values for different quality levels extern const std::vector default_resolutions; + +/// Mapping of channel names to their default properties extern const std::map default_channels; +/// Input parameter name for UV scale transformation extern const std::string uv_scale_input; + +/// Input parameter name for UV rotation transformation extern const std::string uv_rotation_input; + +/// Input parameter name for UV translation transformation extern const std::string uv_translation_input; +/// Name used for UV channel attributes extern const std::string uv_channel_name; +/// Name used for UV wrap mode in S direction +extern const std::string uv_wrap_s_name; + +/// Name used for UV wrap mode in T direction +extern const std::string uv_wrap_t_name; + +/// Prefix used for procedural parameter attribute names extern const std::string proceduralParameterPrefix; +/// @brief Generate a variant name for a specific resolution. +/// @param xResLog2 X-axis resolution as log2 value (e.g., 9 for 512) +/// @param yResLog2 Y-axis resolution as log2 value (e.g., 9 for 512) +/// @return String representation of the resolution variant name USDSBSAR_API std::string getResolutionVariantName(size_t xResLog2, size_t yResLog2); +/// @brief Enumeration of different graph types in SBSAR files. +/// +/// SBSAR files can contain different types of graphs that produce +/// different kinds of outputs (materials, lights, etc.). enum class GraphType { - Material = 0, - Light = 1, - Unknown = 2 + Material = 0, ///< Graph represents a material with multiple channels + Light = 1, ///< Graph represents an environment light with an IBL texture + Unknown = 2 ///< Graph type could not be determined }; +/// @brief Determine the type of graph based on its description. +/// @param graphDesc Description of the SBSAR graph to analyze +/// @return GraphType indicating the detected graph type GraphType guessGraphType(const SubstanceAir::GraphDesc& graphDesc); +/// @brief Get the default value attribute names for a given channel. +/// @param channelName Name of the texture channel +/// @return Pair of strings representing the default value attribute name and the texture influence +/// attribute name std::pair getDefaultValueNames(const std::string& channelName); + +/// @brief Extract the graph name from a graph description. +/// @param desc Graph description to extract name from +/// @return String containing the graph name std::string getGraphName(const SubstanceAir::GraphDesc& desc); + +/// @brief Check if a graph has an output channel with the specified usage name. +/// @param usage Output usage to check for +/// @param graphDesc Graph description to search in +/// @return True if the graph has the specified usage output bool hasUsage(const std::string& usage, const SubstanceAir::GraphDesc& graphDesc); + +/// @brief Check if a graph has an input parameter with the specified identifier. +/// @param identifier Input parameter identifier to check for +/// @param graphDesc Graph description to search in +/// @return True if the graph has the specified input parameter bool hasInput(const std::string& identifier, const SubstanceAir::GraphDesc& graphDesc); + +/// @brief Determine if a usage string represents a normal map. +/// @param usage Usage string to check +/// @return True if the usage represents a normal map bool isNormal(const std::string& usage); +/// @brief Convert SBSAR parameters from VtDictionary to JsValue format. +/// @param sbsarParmeters Dictionary of SBSAR parameters to convert +/// @return JsValue containing the converted parameters USDSBSAR_API PXR_NS::JsValue convertSbsarParameters(const PXR_NS::VtDictionary& sbsarParmeters); +/// @brief Convert color values from linear to sRGB color space. +/// @param value Color value as GfVec3f to convert (modified in place) void convertColorLinearToSRGB(PXR_NS::VtValue& value); + +/// @brief Convert color values from sRGB to linear color space. +/// @param value Color value as GfVec3fto convert (modified in place) void convertColorSRGBToLinear(PXR_NS::VtValue& value); -//! Returns the name of the scale and bias interface attributes for a given normal channel. +/// Returns the name of the scale and bias interface attributes for a given normal channel. std::pair getNormalMapScaleAndBiasNames(const std::string& channelName); +/// @brief Enumeration of normal map formats. +/// +/// Different rendering engines use different conventions for normal maps, +/// particularly regarding the Y-axis direction. enum class NormalFormat { - Unknown, - DirectX, - OpenGL + Unknown, ///< Normal format could not be determined + DirectX, ///< DirectX-style normal maps (Y-axis down) + OpenGL ///< OpenGL-style normal maps (Y-axis up) }; -//! If a graph has a "normal_format" input, this is the default we're using for USD +/// Default normal format used for USD output (OpenGL convention) 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. +/// @brief Apply the default normal format to parameters. +/// +/// This function applies the default normal format (OpenGL) to the jsParams to ensure the Substance +/// engine is using OpenGL, if the SBSAR has a standard input parameter for that. Adding this to the +/// JsParams ensures texture paths generate the right normal maps. +/// +/// @param graphDesc Description of the SBSAR graph +/// @param jsParams Current parameters to modify +/// @return Modified JsValue with default normal format applied +PXR_NS::JsValue +applyDefaultNormalFormatInput(const SubstanceAir::GraphDesc& graphDesc, + const PXR_NS::JsValue& jsParams); + +/// @brief Determine the default normal format for a graph. +/// +/// This function checks if the graph supports the "normal_format" input parameter. +/// If it does, it returns the defaultNormalFormat. If the graph doesn't support +/// that input, it assumes DirectX-style normal maps. +/// +/// @param graphDesc Description of the SBSAR graph to check +/// @return NormalFormat indicating the default format for this graph 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. +/// @brief Determine normal format from current parameters. +/// +/// This function looks for the "normal_format" parameter in the current parameters. +/// Not all SBSAR files have this parameter, but all Substance Source materials do. +/// When available, it can be used to determine the normal format being generated. +/// +/// @param jsParams Current parameters to examine +/// @return NormalFormat determined from the parameters 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. +/// @brief Get scale and bias values for normal map texture readers. +/// +/// Returns the appropriate scale and bias values for a texture reader +/// based on the normal map format being used. +/// +/// @param normalFormat The normal map format to get values for +/// @return Pair of GfVec4f values (scale, bias) for texture reading 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"}] -//! - Path : Path to the .sbsar file (Not set in this function) -//! - Graph: Graph Name -//! - Usage: The output texture -//! - Hash: Hash of the .sbsar -//! - Parameters: Parameters to send to the sbsar to generate the texture. -//! And this function setup the part between the []. -//! \param usage Output texture -//! \param graphName Graph name in the sbsar -//! \param sbsarHash Hash of the sbsar. -//! \param params Sbsar parameters used to generate texture asset -//! path. -//! \return +/// @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"}] +/// - Path : Path to the .sbsar file (Not set in this function) +/// - Graph: Graph Name +/// - Usage: The output texture +/// - Hash: Hash of the .sbsar +/// - Parameters: Parameters to send to the sbsar to generate the texture. +/// And this function setup the part between the []. +/// @param usage Output texture +/// @param graphName Graph name in the sbsar +/// @param sbsarHash Hash of the sbsar. +/// @param params Sbsar parameters used to generate texture asset +/// path. +/// @return String containing the generated SBSAR info path USDSBSAR_API std::string generateSbsarInfoPath(const std::string& usage, const MappedSymbol& graphName, std::size_t sbsarHash, const PXR_NS::JsValue& params); +/// @brief Generate an asset name for a texture based on its usage. +/// @param usage The usage string for the texture +/// @return String representing the asset name std::string getTextureAssetName(const std::string& usage); +/// @brief Get the category of a graph using symbol mapping. +/// @param graphDesc Description of the SBSAR graph +/// @param symbolMapper Symbol mapper for name resolution +/// @return MappedSymbol representing the graph category MappedSymbol getGraphCategory(const SubstanceAir::GraphDesc& graphDesc, SymbolMapper& symbolMapper); +/// @brief Set graph metadata on a USD primitive. +/// +/// This function adds metadata from the SBSAR graph description +/// to the specified USD primitive. +/// +/// @param sdfData SDF data container to modify +/// @param primPath Path to the primitive to add metadata to +/// @param graphDesc Graph description containing the metadata void setGraphMetadataOnPrim(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& primPath, const SubstanceAir::GraphDesc& graphDesc); +/// @brief Generate a USD token for a Substance input parameter. +/// @param symbolMapper Symbol mapper for name resolution +/// @param substanceInputName Name of the Substance input parameter +/// @return TfToken representing the USD parameter name PXR_NS::TfToken getInputParamToken(SymbolMapper& symbolMapper, const std::string& substanceInputName); -//! \brief Setup procedural paramters as default attribut of the prim. -//! Each parameters is set with the default value in the graph -//! and meta data are added : Identified, label, min/max threshold, etc... -//! \param sdfData Sdf data to store the layer in. -//! \param primPath Path of the prim to setup. -//! \param inputs All public input of the current sbsar graph. -//! \param symbolMapper Symbol mapper to avoid conflict between parameters. -//! \param isEnvironmentTexture Bool that indicates the graph produces an environment texture +/// @brief Set up procedural parameters as default attributes of the primitive. +/// +/// Each parameter is set with the default value from the graph, and metadata +/// is added including identifier, label, min/max thresholds, etc. +/// +/// @param sdfData SDF data container to store the layer in +/// @param primPath Path of the primitive to set up +/// @param inputs All public inputs of the current SBSAR graph +/// @param symbolMapper Symbol mapper to avoid conflicts between parameters +/// @param isEnvironmentTexture Bool indicating if the graph produces an environment texture void setupProceduralParameters(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& primPath, @@ -157,13 +280,16 @@ setupProceduralParameters(PXR_NS::SdfAbstractData* sdfData, SymbolMapper& symbolMapper, bool isEnvironmentTexture = false); -//! \brief Add the preset variant to control preset parameters. Create one variant value per preset. -//! \param sdfData Sdf data to store the layer in. -//! \param symbolMapper Symbol mapper to avoid conflict between parameters. -//! \param graphDesc Description of the current sbsar graph. -//! \param packagePath Path of the sbsar file. -//! \param primPath Path to add the variant to. -//! \param targetPrimPath Path in the payload that should be pulled in. +/// @brief Add preset variant to control preset parameters. +/// +/// Creates one variant value per preset defined in the SBSAR graph. +/// +/// @param sdfData SDF data container to store the layer in +/// @param symbolMapper Symbol mapper to avoid conflicts between parameters +/// @param graphDesc Description of the current SBSAR graph +/// @param packagePath Path of the SBSAR file +/// @param primPath Path to add the variant to +/// @param targetPrimPath Path in the payload that should be pulled in void addPresetVariant(PXR_NS::SdfAbstractData* sdfData, SymbolMapper& symbolMapper, @@ -172,13 +298,17 @@ addPresetVariant(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& primPath, const PXR_NS::SdfPath& targetPrimPath); -//! \brief Add resolution variant to control the outputsize parameters with explicit value. -//! \param sdfData Sdf data to store the layer in. -//! \param symbolMapper Symbol mapper to avoid conflict between parameters. -//! \param packagePath Path of the sbsar file. -//! \param primPath Path to add the variant to. -//! \param targetPrimPath Path in the payload that should be pulled in. -//! \param isEnvironmentTexture Bool that indicates the graph produces an environment texture +/// @brief Add resolution variant set to control output size parameters. +/// +/// Creates variant set with explicit resolution values to control texture output sizes. +/// +/// @param sdfData SDF data container to store the layer in +/// @param symbolMapper Symbol mapper to avoid conflicts between parameters +/// @param graphDesc Description of the current SBSAR graph +/// @param packagePath Path of the SBSAR file +/// @param primPath Path to add the variant to +/// @param targetPrimPath Path in the payload that should be pulled in +/// @param isEnvironmentTexture Bool indicating if the graph produces an environment texture void addResolutionVariantSet(PXR_NS::SdfAbstractData* sdfData, SymbolMapper& symbolMapper, @@ -188,23 +318,30 @@ addResolutionVariantSet(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& targetPrimPath, bool isEnvironmentTexture = false); -//! \brief Add resolution variant choice to control the outputsize parameters with explicit value. -//! \param sdfData Sdf data to store the layer in. -//! \param primPath Path to add the variant choice to. -//! \param isEnvironmentTexture Bool that indicates whether the graph produces an environment -//! texture +/// @brief Add resolution variant selection to control output size parameters. +/// +/// Sets the default resolution variant choice for the primitive. +/// +/// @param sdfData SDF data container to store the layer in +/// @param primPath Path to add the variant choice to +/// @param isEnvironmentTexture Bool indicating whether the graph produces an environment texture +/// @param resolution Default resolution level to select void addResolutionVariantSelection(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& primPath, bool isEnvironmentTexture = false, int resolution = SBSAR_DEFAULT_RESOLUTION); -//! \brief Add payload arc to a prim to reference this package again with different parameters -//! \param sdfData Sdf data to store the layer in. -//! \param packagePath Path of the sbsar file. -//! \param primPath Path to add the payload to. -//! \param targetPrimPath Path in the payload that should be pulled in. -//! \param depth Recursion depth for the file format plugin +/// @brief Add payload arc to a primitive to reference the package with different parameters. +/// +/// Creates a payload reference that allows the same SBSAR package to be loaded +/// with different parameter configurations. +/// +/// @param sdfData SDF data container to store the layer in +/// @param packagePath Path of the SBSAR file +/// @param primPath Path to add the payload to +/// @param targetPrimPath Path in the payload that should be pulled in +/// @param depth Recursion depth for the file format plugin void addPayload(PXR_NS::SdfAbstractData* sdfData, const std::string& packagePath, diff --git a/sbsar/thirdparties.md b/sbsar/thirdparties.md index ee2f7501..0311295d 100644 --- a/sbsar/thirdparties.md +++ b/sbsar/thirdparties.md @@ -1,7 +1,7 @@ # Dependencies on thirdparties -## Usd -The plugin depends on the USD ([https://github.com/PixarAnimationStudios/USD]) +## Usd +The plugin depends on the USD ([https://github.com/PixarAnimationStudios/USD]) The bulk of USD is provided with the application it's compiled for but parts of the USD code base will be in the compiled plugin when distributing it. USD itself depends on a number of external code bases, the only ones required to build the plugin is: @@ -23,7 +23,7 @@ in case it's desirable using the **BUILD_PNG** flag to cmake when compiling. Libpng and zlib are referenced as git submodules ## Gtest -The plugin implements its testing using gtest ([https://git.corp.adobe.com:substance-integrations/thirdparty-gtest.git]) +The plugin implements its testing using gtest ([https://github.com/google/googletest]) The tests are external to the plugin and are not needed in order to run the binary. The binary itself doesn't use gtest. gtest is referenced as git submodules diff --git a/spz/README.md b/spz/README.md index b28c9a5e..513015e2 100644 --- a/spz/README.md +++ b/spz/README.md @@ -1,13 +1,5 @@ # USDSPZ -[![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/windows-2022-2411-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/windows-2022-2308-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) - -[![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/macOS-14-2411-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/macOS-14-2408-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) - -[![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/macOS-13-2411-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/macOS-13-2308-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) - -[![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/ubuntu-22.04-2411-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) [![](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kwblackstone/264643f3d2acacc5369a0ba70854dfb6/raw/ubuntu-22.04-2308-SPZ.json)](https://github.com/adobe/USD-Fileformat-plugins/actions/workflows/ci.yml) - ## Supported features |Feature|Import|Export| diff --git a/spz/src/fileFormat.cpp b/spz/src/fileFormat.cpp index 698f8624..daa61259 100644 --- a/spz/src/fileFormat.cpp +++ b/spz/src/fileFormat.cpp @@ -56,10 +56,16 @@ UsdSpzFileFormat::InitData(const FileFormatArguments& args) const TF_DEBUG_MSG( FILE_FORMAT_SPZ, "FileFormatArg: %s = %s\n", arg.first.c_str(), arg.second.c_str()); } + // Note, currently the SPZ format plugin does not support any of the default file format args, + // but we want to have this general infrastructure in place to parse and forward generic + // arguments. + pd->parseFromFileFormatArgs(args, DEBUG_TAG); argReadBool( args, UsdSpzFileFormatTokens->gsplatsWithZup.GetText(), pd->gsplatsWithZup, DEBUG_TAG); - argReadFloatArray( - args, UsdSpzFileFormatTokens->gsplatsClippingBox.GetText(), pd->gsplatsClippingBox, DEBUG_TAG); + argReadFloatArray(args, + UsdSpzFileFormatTokens->gsplatsClippingBox.GetText(), + pd->gsplatsClippingBox, + DEBUG_TAG); return pd; } @@ -101,14 +107,16 @@ UsdSpzFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool me SpzDataConstPtr data = TfDynamic_cast(layerData); UsdData usd; try { - WriteLayerOptions layerOptions; - ImportSpzOptions options; - options.importGsplatWithZup = data->gsplatsWithZup; - options.importGsplatClippingBox = data->gsplatsClippingBox; - GaussianCloud gaussianCloud = loadSpz(resolvedPath); - GUARD(importSpz(options, gaussianCloud, usd), "Error translating SPZ to USD\n"); + WriteLayerOptions layerOptions(*data); + ImportSpzOptions importSpzOptions; + importSpzOptions.importGsplatWithZup = data->gsplatsWithZup; + importSpzOptions.importGsplatClippingBox = data->gsplatsClippingBox; + spz::UnpackOptions unpackOptions; + GaussianCloud gaussianCloud = loadSpz(resolvedPath, unpackOptions); + GUARD(importSpz(importSpzOptions, gaussianCloud, usd), "Error translating SPZ 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_SPZ, "Failed to open %s: %s\n", resolvedPath.c_str(), e.what()); @@ -145,7 +153,8 @@ UsdSpzFileFormat::WriteToFile(const SdfLayer& layer, try { const std::string parentPath = TfGetPathName(filename); TfMakeDirs(parentPath, -1, true); - saveSpz(gaussianCloud, filename); + spz::PackOptions packOptions; + saveSpz(gaussianCloud, packOptions, filename); } catch (std::exception& e) { TF_DEBUG_MSG(FILE_FORMAT_SPZ, "Error writing SPZ to %s: %s\n", filename.c_str(), e.what()); } diff --git a/spz/src/spzExport.cpp b/spz/src/spzExport.cpp index d438dea8..59eb5fbb 100644 --- a/spz/src/spzExport.cpp +++ b/spz/src/spzExport.cpp @@ -240,6 +240,7 @@ exportSpz(const UsdData& usd, spz::GaussianCloud& gaussianCloud) gaussianCloud.sh.resize(numGsplatsSHCoeffs * totalMesh.points.size()); // SPZ stores SH coefficients in row-major order, different than USD's column-major order. + // Also, SPZ stores SH coefficients in an Array-of-Structs (AoS) format. for (size_t shRowIndex = 0; shRowIndex < numNonZeroSHBands; ++shRowIndex) { for (size_t shColIndex = 0; shColIndex < 3; ++shColIndex) { const std::size_t spzSHIndex = shRowIndex * 3 + shColIndex; @@ -247,7 +248,8 @@ exportSpz(const UsdData& usd, spz::GaussianCloud& gaussianCloud) const std::size_t spzShCoeffOffset = spzSHIndex * totalMesh.points.size(); for (size_t i = 0; i < totalMesh.points.size(); ++i) { - gaussianCloud.sh[spzShCoeffOffset + i] = totalMesh.shCoeffs[usdSHIndex][i]; + gaussianCloud.sh[i * numGsplatsSHCoeffs + spzSHIndex] = + totalMesh.shCoeffs[usdSHIndex][i]; } } } diff --git a/spz/src/spzImport.cpp b/spz/src/spzImport.cpp index 35f86272..cac3a641 100644 --- a/spz/src/spzImport.cpp +++ b/spz/src/spzImport.cpp @@ -67,12 +67,9 @@ importSpz(const ImportSpzOptions& options, const spz::GaussianCloud& gaussianClo if (gaussianCloud.colors.size() < static_cast(gaussianCloud.numPoints * 3)) throw std::runtime_error("Invalid color data size"); for (size_t i = 0; i < colors.values.size(); i++) { - colors.values[i][0] = - std::clamp(gaussianCloud.colors[i * 3 + 0] * shC0 + 0.5f, 0.0f, 1.0f); - colors.values[i][1] = - std::clamp(gaussianCloud.colors[i * 3 + 1] * shC0 + 0.5f, 0.0f, 1.0f); - colors.values[i][2] = - std::clamp(gaussianCloud.colors[i * 3 + 2] * shC0 + 0.5f, 0.0f, 1.0f); + colors.values[i][0] = gaussianCloud.colors[i * 3 + 0] * shC0 + 0.5f; + colors.values[i][1] = gaussianCloud.colors[i * 3 + 1] * shC0 + 0.5f; + colors.values[i][2] = gaussianCloud.colors[i * 3 + 2] * shC0 + 0.5f; } auto [opacityIndex, opacity] = usd.addOpacitySet(meshIndex); @@ -115,6 +112,7 @@ importSpz(const ImportSpzOptions& options, const spz::GaussianCloud& gaussianClo } const size_t shDim = gaussianCloud.shDegree * (gaussianCloud.shDegree + 2); + const size_t numGsplatsSHCoeffs = shDim * 3; if (gaussianCloud.sh.size() < static_cast(gaussianCloud.numPoints * shDim * 3)) throw std::runtime_error("Invalid SH coefficient data size"); for (std::size_t shColIndex = 0; shColIndex < 3; ++shColIndex) { @@ -126,10 +124,12 @@ importSpz(const ImportSpzOptions& options, const spz::GaussianCloud& gaussianClo // SPZ stores SH coefficients in a row-major order, where // we need to convert it to a column-major order that we // use for USD. + // Also, SPZ stores SH coefficients in an Array-of-Structs (AoS) format, + // packing the SH coefficients for each point together, while USD expects + // a Struct-of-Arrays (SoA) format, packing one coefficient for all points together. const size_t spzShIndex = shRowIndex * 3 + shColIndex; - const size_t spzShBase = spzShIndex * gaussianCloud.numPoints; for (size_t i = 0; i < shCoeffs.values.size(); i++) { - shCoeffs.values[i] = gaussianCloud.sh[spzShBase + i]; + shCoeffs.values[i] = gaussianCloud.sh[i * numGsplatsSHCoeffs + spzShIndex]; } } } diff --git a/stl/README.md b/stl/README.md index d1d4c279..238cdf02 100644 --- a/stl/README.md +++ b/stl/README.md @@ -40,6 +40,8 @@ ## Translation Notes +Geometric normals will calculated and used on both import and export. Custom per-face normals will be ignored and are not supported at this point. + **Import:** The generated USD will be set with up axis = +z as that's the most common for stl files. @@ -47,7 +49,8 @@ The generated USD will be set with up axis = +z as that's the most common for st ## File Format Arguments **Export:** -* `exportAscii`: If true, the stl file will be in ascii format, otherwise in binary format. +* `exportAscii`: If true, the stl file will be in ascii format, otherwise in binary format. (Note: there is currently a known bug, where the file may still export in binary regardless of this setting) + Example: ``` #usda 1.0 diff --git a/stl/src/stlExport.cpp b/stl/src/stlExport.cpp index 50d3f635..fa917603 100644 --- a/stl/src/stlExport.cpp +++ b/stl/src/stlExport.cpp @@ -74,25 +74,8 @@ exportStl(const ExportStlOptions& options, const UsdData& usd, StlModel& stl) facet.vertices[j] = vertex; } - StlNormal normal; - // compute facet normals from topology - StlVec3f faceEdge1; - faceEdge1.x = facet.vertices[2].x - facet.vertices[0].x; - faceEdge1.y = facet.vertices[2].y - facet.vertices[0].y; - faceEdge1.z = facet.vertices[2].z - facet.vertices[0].z; - - StlVec3f faceEdge2; - faceEdge2.x = facet.vertices[1].x - facet.vertices[0].x; - faceEdge2.y = facet.vertices[1].y - facet.vertices[0].y; - faceEdge2.z = facet.vertices[1].z - facet.vertices[0].z; - - normal = crossProduct(faceEdge1, faceEdge2); - // handle degenerate normals - if (normal.x == 0.f && normal.y == 0.f && normal.z == 0.f) - normal.y = 1.f; // Synthesize a valid normal. Actual value is irrelevant because - // the triangle won't be visible - - normal.normalize(); + // TODO: preserve original normals on export (and do the same on import) + StlNormal normal = calculateNormalOfFacet(facet); facet.normal = normal; stl.AddFacet(facet); diff --git a/stl/src/stlImport.cpp b/stl/src/stlImport.cpp index 6ea52d71..33c7db9e 100644 --- a/stl/src/stlImport.cpp +++ b/stl/src/stlImport.cpp @@ -75,13 +75,22 @@ importStl(UsdData& usd, const StlModel& stl) mesh.points[3 * i] = GfVec3f(v0.x, v0.y, v0.z); mesh.points[3 * i + 1] = GfVec3f(v1.x, v1.y, v1.z); mesh.points[3 * i + 2] = GfVec3f(v2.x, v2.y, v2.z); - GfVec3f usdNormal = GfVec3f(facet.normal.x, facet.normal.y, facet.normal.z); - usdNormal.Normalize(); - // Handle degenerate normals - if (usdNormal.GetLengthSq() < 1e-3f) { - usdNormal = GfVec3f(0.0f, 1.0f, 0.0f); // Synthesize a valid normal + /* + // TODO: preserve original normals on import, once the same is done on export + + StlNormal normal = facet.normal; + + // Handle degenerate or missing normals + if (normal.lengthSq() < 1e-6f) { + normal = calculateNormalOfFacet(facet); } + */ + + StlNormal normal = calculateNormalOfFacet(facet); + + GfVec3f usdNormal = GfVec3f(normal.x, normal.y, normal.z); + usdNormal.Normalize(); mesh.normals.values[i] = usdNormal; } diff --git a/stl/src/stlModel.cpp b/stl/src/stlModel.cpp index 504f3356..497ff46d 100644 --- a/stl/src/stlModel.cpp +++ b/stl/src/stlModel.cpp @@ -121,6 +121,34 @@ crossProduct(StlVec3f a, StlVec3f b) return product; } +StlNormal +calculateNormalOfFacet(StlFacet facet) +{ + StlNormal normal; + + // compute facet normals from topology + StlVec3f faceEdge1; + faceEdge1.x = facet.vertices[1].x - facet.vertices[0].x; + faceEdge1.y = facet.vertices[1].y - facet.vertices[0].y; + faceEdge1.z = facet.vertices[1].z - facet.vertices[0].z; + + StlVec3f faceEdge2; + faceEdge2.x = facet.vertices[2].x - facet.vertices[0].x; + faceEdge2.y = facet.vertices[2].y - facet.vertices[0].y; + faceEdge2.z = facet.vertices[2].z - facet.vertices[0].z; + + normal = crossProduct(faceEdge1, faceEdge2); + + // handle degenerate normals + if (normal.x == 0.f && normal.y == 0.f && normal.z == 0.f) + normal.y = 1.f; // Synthesize a valid normal. Actual value is irrelevant because + // the triangle won't be visible + + normal.normalize(); + + return normal; +} + bool isAsciiStl(std::ifstream& infile) { diff --git a/stl/src/stlModel.h b/stl/src/stlModel.h index 6802ee76..9c9dd713 100644 --- a/stl/src/stlModel.h +++ b/stl/src/stlModel.h @@ -32,9 +32,11 @@ struct StlNormal z = 0.0; } + float lengthSq() { return (x * x) + (y * y) + (z * z); } + void normalize() { - float length = sqrt((x * x) + (y * y) + (z * z)); + float length = sqrt(lengthSq()); x = x / length; y = y / length; z = z / length; @@ -82,4 +84,15 @@ class StlModel StlNormal crossProduct(StlVec3f a, StlVec3f b); + +/** + * Calculate the unit normal perpendicular to a given StlFacet. + * + * If the calculated normal would be (0,0,0), when the facet is a degenerate triangle, then a + * normal of (0, 1, 0) is returned instead. The value will be irrelevant because the triangle + * won't be visible. + */ +StlNormal +calculateNormalOfFacet(StlFacet facet); + } \ No newline at end of file diff --git a/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly.jpg b/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly.jpg index 68296d95..f53b46a8 100644 Binary files a/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly.jpg and b/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly.jpg differ diff --git a/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly_roundtrip.jpg b/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly_roundtrip.jpg index 7a0a29c3..f53b46a8 100644 Binary files a/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly_roundtrip.jpg and b/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly_roundtrip.jpg differ diff --git a/test/baseline/Darwin/fbx/SimpleRobotwithUdims.jpg b/test/baseline/Darwin/fbx/SimpleRobotwithUdims.jpg index 4541c2f5..44784720 100644 Binary files a/test/baseline/Darwin/fbx/SimpleRobotwithUdims.jpg and b/test/baseline/Darwin/fbx/SimpleRobotwithUdims.jpg differ diff --git a/test/baseline/Darwin/fbx/SimpleRobotwithUdims_roundtrip.jpg b/test/baseline/Darwin/fbx/SimpleRobotwithUdims_roundtrip.jpg index dd12b00d..44784720 100644 Binary files a/test/baseline/Darwin/fbx/SimpleRobotwithUdims_roundtrip.jpg and b/test/baseline/Darwin/fbx/SimpleRobotwithUdims_roundtrip.jpg differ diff --git a/test/baseline/Darwin/fbx/cube-colors.jpg b/test/baseline/Darwin/fbx/cube-colors.jpg index c497f19a..846780b1 100644 Binary files a/test/baseline/Darwin/fbx/cube-colors.jpg and b/test/baseline/Darwin/fbx/cube-colors.jpg differ diff --git a/test/baseline/Darwin/fbx/cube-colors_roundtrip.jpg b/test/baseline/Darwin/fbx/cube-colors_roundtrip.jpg index 05c373df..846780b1 100644 Binary files a/test/baseline/Darwin/fbx/cube-colors_roundtrip.jpg and b/test/baseline/Darwin/fbx/cube-colors_roundtrip.jpg differ diff --git a/test/baseline/Darwin/fbx/cube.jpg b/test/baseline/Darwin/fbx/cube.jpg index d4206061..33dd6353 100644 Binary files a/test/baseline/Darwin/fbx/cube.jpg and b/test/baseline/Darwin/fbx/cube.jpg differ diff --git a/test/baseline/Darwin/fbx/cube_roundtrip.jpg b/test/baseline/Darwin/fbx/cube_roundtrip.jpg index 0883ddb7..33dd6353 100644 Binary files a/test/baseline/Darwin/fbx/cube_roundtrip.jpg and b/test/baseline/Darwin/fbx/cube_roundtrip.jpg differ diff --git a/test/baseline/Darwin/gltf/Emoticon_40.jpg b/test/baseline/Darwin/gltf/Emoticon_40.jpg index 18868fe6..612f3d19 100644 Binary files a/test/baseline/Darwin/gltf/Emoticon_40.jpg and b/test/baseline/Darwin/gltf/Emoticon_40.jpg differ diff --git a/test/baseline/Darwin/gltf/Emoticon_40_roundtrip.jpg b/test/baseline/Darwin/gltf/Emoticon_40_roundtrip.jpg index 18868fe6..612f3d19 100644 Binary files a/test/baseline/Darwin/gltf/Emoticon_40_roundtrip.jpg and b/test/baseline/Darwin/gltf/Emoticon_40_roundtrip.jpg differ diff --git a/test/baseline/Darwin/gltf/VET.jpg b/test/baseline/Darwin/gltf/VET.jpg index d9f2e3f1..a319ceab 100644 Binary files a/test/baseline/Darwin/gltf/VET.jpg and b/test/baseline/Darwin/gltf/VET.jpg differ diff --git a/test/baseline/Darwin/gltf/VET_roundtrip.jpg b/test/baseline/Darwin/gltf/VET_roundtrip.jpg index d9f2e3f1..a319ceab 100644 Binary files a/test/baseline/Darwin/gltf/VET_roundtrip.jpg and b/test/baseline/Darwin/gltf/VET_roundtrip.jpg differ diff --git a/test/baseline/Darwin/gltf/book f.jpg b/test/baseline/Darwin/gltf/book f.jpg index f367ee92..c0868440 100644 Binary files a/test/baseline/Darwin/gltf/book f.jpg and b/test/baseline/Darwin/gltf/book f.jpg differ diff --git a/test/baseline/Darwin/gltf/book f_roundtrip.jpg b/test/baseline/Darwin/gltf/book f_roundtrip.jpg index f367ee92..c0868440 100644 Binary files a/test/baseline/Darwin/gltf/book f_roundtrip.jpg and b/test/baseline/Darwin/gltf/book f_roundtrip.jpg differ diff --git a/test/baseline/Darwin/gltf/cube-colors.jpg b/test/baseline/Darwin/gltf/cube-colors.jpg index 1b94b76f..4e16b442 100644 Binary files a/test/baseline/Darwin/gltf/cube-colors.jpg and b/test/baseline/Darwin/gltf/cube-colors.jpg differ diff --git a/test/baseline/Darwin/gltf/cube-colors_roundtrip.jpg b/test/baseline/Darwin/gltf/cube-colors_roundtrip.jpg index 1b94b76f..4e16b442 100644 Binary files a/test/baseline/Darwin/gltf/cube-colors_roundtrip.jpg and b/test/baseline/Darwin/gltf/cube-colors_roundtrip.jpg differ diff --git a/test/baseline/Darwin/obj/cube/cube.jpg b/test/baseline/Darwin/obj/cube/cube.jpg index b14acfe7..b24cf289 100644 Binary files a/test/baseline/Darwin/obj/cube/cube.jpg and b/test/baseline/Darwin/obj/cube/cube.jpg differ diff --git a/test/baseline/Darwin/obj/cube/cube_roundtrip.jpg b/test/baseline/Darwin/obj/cube/cube_roundtrip.jpg index b14acfe7..b24cf289 100644 Binary files a/test/baseline/Darwin/obj/cube/cube_roundtrip.jpg and b/test/baseline/Darwin/obj/cube/cube_roundtrip.jpg differ diff --git a/test/baseline/Darwin/obj/nut_obj/nut.jpg b/test/baseline/Darwin/obj/nut_obj/nut.jpg index 90f1bf23..0d3677d5 100644 Binary files a/test/baseline/Darwin/obj/nut_obj/nut.jpg and b/test/baseline/Darwin/obj/nut_obj/nut.jpg differ diff --git a/test/baseline/Darwin/obj/nut_obj/nut_roundtrip.jpg b/test/baseline/Darwin/obj/nut_obj/nut_roundtrip.jpg index 90f1bf23..cbbb4ce7 100644 Binary files a/test/baseline/Darwin/obj/nut_obj/nut_roundtrip.jpg and b/test/baseline/Darwin/obj/nut_obj/nut_roundtrip.jpg differ diff --git a/test/baseline/Darwin/obj/stone/stone.jpg b/test/baseline/Darwin/obj/stone/stone.jpg index 47878aa2..3e2f7d84 100644 Binary files a/test/baseline/Darwin/obj/stone/stone.jpg and b/test/baseline/Darwin/obj/stone/stone.jpg differ diff --git a/test/baseline/Darwin/obj/stone/stone_roundtrip.jpg b/test/baseline/Darwin/obj/stone/stone_roundtrip.jpg index 47878aa2..35ca8714 100644 Binary files a/test/baseline/Darwin/obj/stone/stone_roundtrip.jpg and b/test/baseline/Darwin/obj/stone/stone_roundtrip.jpg differ diff --git a/test/baseline/Darwin/ply/colors.jpg b/test/baseline/Darwin/ply/colors.jpg index 6afca37d..5d6f1894 100644 Binary files a/test/baseline/Darwin/ply/colors.jpg and b/test/baseline/Darwin/ply/colors.jpg differ diff --git a/test/baseline/Darwin/ply/colors_roundtrip.jpg b/test/baseline/Darwin/ply/colors_roundtrip.jpg index 6afca37d..5d6f1894 100644 Binary files a/test/baseline/Darwin/ply/colors_roundtrip.jpg and b/test/baseline/Darwin/ply/colors_roundtrip.jpg differ diff --git a/test/baseline/Darwin/ply/cube.jpg b/test/baseline/Darwin/ply/cube.jpg index b14acfe7..b24cf289 100644 Binary files a/test/baseline/Darwin/ply/cube.jpg and b/test/baseline/Darwin/ply/cube.jpg differ diff --git a/test/baseline/Darwin/ply/cube_roundtrip.jpg b/test/baseline/Darwin/ply/cube_roundtrip.jpg index b14acfe7..b24cf289 100644 Binary files a/test/baseline/Darwin/ply/cube_roundtrip.jpg and b/test/baseline/Darwin/ply/cube_roundtrip.jpg differ diff --git a/test/baseline/Darwin/ply/gargo.jpg b/test/baseline/Darwin/ply/gargo.jpg index 0c2b4048..fb50223a 100644 Binary files a/test/baseline/Darwin/ply/gargo.jpg and b/test/baseline/Darwin/ply/gargo.jpg differ diff --git a/test/baseline/Darwin/ply/gargo_roundtrip.jpg b/test/baseline/Darwin/ply/gargo_roundtrip.jpg index 0c2b4048..20f7888b 100644 Binary files a/test/baseline/Darwin/ply/gargo_roundtrip.jpg and b/test/baseline/Darwin/ply/gargo_roundtrip.jpg differ diff --git a/test/baseline/Darwin/sbsar/cube.jpg b/test/baseline/Darwin/sbsar/cube.jpg index b645e8af..e20ace30 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 33c69297..8905770d 100644 Binary files a/test/baseline/Darwin/sbsar/sphere.jpg and b/test/baseline/Darwin/sbsar/sphere.jpg differ diff --git a/test/baseline/Darwin/stl/ascii.jpg b/test/baseline/Darwin/stl/ascii.jpg index b14acfe7..b24cf289 100644 Binary files a/test/baseline/Darwin/stl/ascii.jpg and b/test/baseline/Darwin/stl/ascii.jpg differ diff --git a/test/baseline/Darwin/stl/ascii_roundtrip.jpg b/test/baseline/Darwin/stl/ascii_roundtrip.jpg index b14acfe7..b24cf289 100644 Binary files a/test/baseline/Darwin/stl/ascii_roundtrip.jpg and b/test/baseline/Darwin/stl/ascii_roundtrip.jpg differ diff --git a/test/baseline/Darwin/stl/car_roundtrip.jpg b/test/baseline/Darwin/stl/car_roundtrip.jpg index 61799f24..dd1d7f1e 100644 Binary files a/test/baseline/Darwin/stl/car_roundtrip.jpg and b/test/baseline/Darwin/stl/car_roundtrip.jpg differ diff --git a/test/baseline/Darwin/stl/cube.jpg b/test/baseline/Darwin/stl/cube.jpg index b14acfe7..b24cf289 100644 Binary files a/test/baseline/Darwin/stl/cube.jpg and b/test/baseline/Darwin/stl/cube.jpg differ diff --git a/test/baseline/Darwin/stl/cube_roundtrip.jpg b/test/baseline/Darwin/stl/cube_roundtrip.jpg index b14acfe7..b24cf289 100644 Binary files a/test/baseline/Darwin/stl/cube_roundtrip.jpg and b/test/baseline/Darwin/stl/cube_roundtrip.jpg differ diff --git a/test/baseline/Darwin/stl/simple_ascii.jpg b/test/baseline/Darwin/stl/simple_ascii.jpg index 1bcee808..cd7b1de5 100644 Binary files a/test/baseline/Darwin/stl/simple_ascii.jpg and b/test/baseline/Darwin/stl/simple_ascii.jpg differ diff --git a/test/baseline/Darwin/stl/simple_ascii_roundtrip.jpg b/test/baseline/Darwin/stl/simple_ascii_roundtrip.jpg index 1bcee808..cd7b1de5 100644 Binary files a/test/baseline/Darwin/stl/simple_ascii_roundtrip.jpg and b/test/baseline/Darwin/stl/simple_ascii_roundtrip.jpg differ diff --git a/test/baseline/Darwin/stl/suzanne.jpg b/test/baseline/Darwin/stl/suzanne.jpg index 0b661498..67112583 100644 Binary files a/test/baseline/Darwin/stl/suzanne.jpg and b/test/baseline/Darwin/stl/suzanne.jpg differ diff --git a/test/baseline/Darwin/stl/suzanne_roundtrip.jpg b/test/baseline/Darwin/stl/suzanne_roundtrip.jpg index 0b661498..f014bb30 100644 Binary files a/test/baseline/Darwin/stl/suzanne_roundtrip.jpg and b/test/baseline/Darwin/stl/suzanne_roundtrip.jpg differ diff --git a/test/baseline/Linux/fbx/Megaphone_01_Lowpoly.jpg b/test/baseline/Linux/fbx/Megaphone_01_Lowpoly.jpg index 3941582e..8755f63e 100644 Binary files a/test/baseline/Linux/fbx/Megaphone_01_Lowpoly.jpg and b/test/baseline/Linux/fbx/Megaphone_01_Lowpoly.jpg differ diff --git a/test/baseline/Linux/fbx/SimpleRobotwithUdims.jpg b/test/baseline/Linux/fbx/SimpleRobotwithUdims.jpg index 2be31f69..d9bd6f72 100644 Binary files a/test/baseline/Linux/fbx/SimpleRobotwithUdims.jpg and b/test/baseline/Linux/fbx/SimpleRobotwithUdims.jpg differ diff --git a/test/baseline/Linux/fbx/cube-colors.jpg b/test/baseline/Linux/fbx/cube-colors.jpg index 3a5efbfa..6a35ad6c 100644 Binary files a/test/baseline/Linux/fbx/cube-colors.jpg and b/test/baseline/Linux/fbx/cube-colors.jpg differ diff --git a/test/baseline/Linux/fbx/cube.jpg b/test/baseline/Linux/fbx/cube.jpg index 0ee2b978..76ae3340 100644 Binary files a/test/baseline/Linux/fbx/cube.jpg and b/test/baseline/Linux/fbx/cube.jpg differ diff --git a/test/baseline/Linux/gltf/Emoticon_40.jpg b/test/baseline/Linux/gltf/Emoticon_40.jpg index fe584a2b..3fd0325b 100644 Binary files a/test/baseline/Linux/gltf/Emoticon_40.jpg and b/test/baseline/Linux/gltf/Emoticon_40.jpg differ diff --git a/test/baseline/Linux/gltf/VET.jpg b/test/baseline/Linux/gltf/VET.jpg index 12b31e89..fadf4557 100644 Binary files a/test/baseline/Linux/gltf/VET.jpg and b/test/baseline/Linux/gltf/VET.jpg differ diff --git a/test/baseline/Linux/gltf/book f.jpg b/test/baseline/Linux/gltf/book f.jpg index 098b1fa9..6d1c5c51 100644 Binary files a/test/baseline/Linux/gltf/book f.jpg and b/test/baseline/Linux/gltf/book f.jpg differ diff --git a/test/baseline/Linux/gltf/cube-colors.jpg b/test/baseline/Linux/gltf/cube-colors.jpg index c6a480bc..81664193 100644 Binary files a/test/baseline/Linux/gltf/cube-colors.jpg and b/test/baseline/Linux/gltf/cube-colors.jpg differ diff --git a/test/baseline/Linux/gltf/cube-colors_roundtrip.jpg b/test/baseline/Linux/gltf/cube-colors_roundtrip.jpg index c6a480bc..81664193 100644 Binary files a/test/baseline/Linux/gltf/cube-colors_roundtrip.jpg and b/test/baseline/Linux/gltf/cube-colors_roundtrip.jpg differ diff --git a/test/baseline/Linux/obj/cube/cube.jpg b/test/baseline/Linux/obj/cube/cube.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Linux/obj/cube/cube.jpg and b/test/baseline/Linux/obj/cube/cube.jpg differ diff --git a/test/baseline/Linux/obj/cube/cube_roundtrip.jpg b/test/baseline/Linux/obj/cube/cube_roundtrip.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Linux/obj/cube/cube_roundtrip.jpg and b/test/baseline/Linux/obj/cube/cube_roundtrip.jpg differ diff --git a/test/baseline/Linux/obj/nut_obj/nut.jpg b/test/baseline/Linux/obj/nut_obj/nut.jpg index 936d8204..abaf44af 100644 Binary files a/test/baseline/Linux/obj/nut_obj/nut.jpg and b/test/baseline/Linux/obj/nut_obj/nut.jpg differ diff --git a/test/baseline/Linux/obj/stone/stone.jpg b/test/baseline/Linux/obj/stone/stone.jpg index 47878aa2..9b3860ba 100644 Binary files a/test/baseline/Linux/obj/stone/stone.jpg and b/test/baseline/Linux/obj/stone/stone.jpg differ diff --git a/test/baseline/Linux/ply/colors.jpg b/test/baseline/Linux/ply/colors.jpg index fba07107..404f8d6c 100644 Binary files a/test/baseline/Linux/ply/colors.jpg and b/test/baseline/Linux/ply/colors.jpg differ diff --git a/test/baseline/Linux/ply/colors_roundtrip.jpg b/test/baseline/Linux/ply/colors_roundtrip.jpg index fba07107..404f8d6c 100644 Binary files a/test/baseline/Linux/ply/colors_roundtrip.jpg and b/test/baseline/Linux/ply/colors_roundtrip.jpg differ diff --git a/test/baseline/Linux/ply/cube.jpg b/test/baseline/Linux/ply/cube.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Linux/ply/cube.jpg and b/test/baseline/Linux/ply/cube.jpg differ diff --git a/test/baseline/Linux/ply/cube_roundtrip.jpg b/test/baseline/Linux/ply/cube_roundtrip.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Linux/ply/cube_roundtrip.jpg and b/test/baseline/Linux/ply/cube_roundtrip.jpg differ diff --git a/test/baseline/Linux/ply/gargo.jpg b/test/baseline/Linux/ply/gargo.jpg index 3f6797ba..08d68586 100644 Binary files a/test/baseline/Linux/ply/gargo.jpg and b/test/baseline/Linux/ply/gargo.jpg differ diff --git a/test/baseline/Linux/ply/gargo_roundtrip.jpg b/test/baseline/Linux/ply/gargo_roundtrip.jpg index 3f6797ba..0f6b7eb1 100644 Binary files a/test/baseline/Linux/ply/gargo_roundtrip.jpg and b/test/baseline/Linux/ply/gargo_roundtrip.jpg differ diff --git a/test/baseline/Linux/sbsar/cube.jpg b/test/baseline/Linux/sbsar/cube.jpg index df47e44a..d632ef52 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 fa137ff5..9acc606c 100644 Binary files a/test/baseline/Linux/sbsar/sphere.jpg and b/test/baseline/Linux/sbsar/sphere.jpg differ diff --git a/test/baseline/Linux/stl/ascii.jpg b/test/baseline/Linux/stl/ascii.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Linux/stl/ascii.jpg and b/test/baseline/Linux/stl/ascii.jpg differ diff --git a/test/baseline/Linux/stl/ascii_roundtrip.jpg b/test/baseline/Linux/stl/ascii_roundtrip.jpg index f0abc046..38a9f28d 100644 Binary files a/test/baseline/Linux/stl/ascii_roundtrip.jpg and b/test/baseline/Linux/stl/ascii_roundtrip.jpg differ diff --git a/test/baseline/Linux/stl/car_roundtrip.jpg b/test/baseline/Linux/stl/car_roundtrip.jpg index b59f9a54..da7e822b 100644 Binary files a/test/baseline/Linux/stl/car_roundtrip.jpg and b/test/baseline/Linux/stl/car_roundtrip.jpg differ diff --git a/test/baseline/Linux/stl/cube.jpg b/test/baseline/Linux/stl/cube.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Linux/stl/cube.jpg and b/test/baseline/Linux/stl/cube.jpg differ diff --git a/test/baseline/Linux/stl/cube_roundtrip.jpg b/test/baseline/Linux/stl/cube_roundtrip.jpg index f0abc046..38a9f28d 100644 Binary files a/test/baseline/Linux/stl/cube_roundtrip.jpg and b/test/baseline/Linux/stl/cube_roundtrip.jpg differ diff --git a/test/baseline/Linux/stl/simple_ascii.jpg b/test/baseline/Linux/stl/simple_ascii.jpg index 1bcee808..3392d07a 100644 Binary files a/test/baseline/Linux/stl/simple_ascii.jpg and b/test/baseline/Linux/stl/simple_ascii.jpg differ diff --git a/test/baseline/Linux/stl/simple_ascii_roundtrip.jpg b/test/baseline/Linux/stl/simple_ascii_roundtrip.jpg index 1bcee808..74d15da9 100644 Binary files a/test/baseline/Linux/stl/simple_ascii_roundtrip.jpg and b/test/baseline/Linux/stl/simple_ascii_roundtrip.jpg differ diff --git a/test/baseline/Linux/stl/suzanne.jpg b/test/baseline/Linux/stl/suzanne.jpg index 94f266ed..e6935d86 100644 Binary files a/test/baseline/Linux/stl/suzanne.jpg and b/test/baseline/Linux/stl/suzanne.jpg differ diff --git a/test/baseline/Linux/stl/suzanne_roundtrip.jpg b/test/baseline/Linux/stl/suzanne_roundtrip.jpg index 94f266ed..e8aa7234 100644 Binary files a/test/baseline/Linux/stl/suzanne_roundtrip.jpg and b/test/baseline/Linux/stl/suzanne_roundtrip.jpg differ diff --git a/test/baseline/Windows/fbx/Megaphone_01_Lowpoly.jpg b/test/baseline/Windows/fbx/Megaphone_01_Lowpoly.jpg index 3941582e..8755f63e 100644 Binary files a/test/baseline/Windows/fbx/Megaphone_01_Lowpoly.jpg and b/test/baseline/Windows/fbx/Megaphone_01_Lowpoly.jpg differ diff --git a/test/baseline/Windows/fbx/SimpleRobotwithUdims.jpg b/test/baseline/Windows/fbx/SimpleRobotwithUdims.jpg index 2be31f69..d9bd6f72 100644 Binary files a/test/baseline/Windows/fbx/SimpleRobotwithUdims.jpg and b/test/baseline/Windows/fbx/SimpleRobotwithUdims.jpg differ diff --git a/test/baseline/Windows/fbx/cube-colors.jpg b/test/baseline/Windows/fbx/cube-colors.jpg index 3a5efbfa..6a35ad6c 100644 Binary files a/test/baseline/Windows/fbx/cube-colors.jpg and b/test/baseline/Windows/fbx/cube-colors.jpg differ diff --git a/test/baseline/Windows/fbx/cube.jpg b/test/baseline/Windows/fbx/cube.jpg index 0ee2b978..76ae3340 100644 Binary files a/test/baseline/Windows/fbx/cube.jpg and b/test/baseline/Windows/fbx/cube.jpg differ diff --git a/test/baseline/Windows/gltf/Emoticon_40.jpg b/test/baseline/Windows/gltf/Emoticon_40.jpg index fe584a2b..3fd0325b 100644 Binary files a/test/baseline/Windows/gltf/Emoticon_40.jpg and b/test/baseline/Windows/gltf/Emoticon_40.jpg differ diff --git a/test/baseline/Windows/gltf/VET.jpg b/test/baseline/Windows/gltf/VET.jpg index 12b31e89..fadf4557 100644 Binary files a/test/baseline/Windows/gltf/VET.jpg and b/test/baseline/Windows/gltf/VET.jpg differ diff --git a/test/baseline/Windows/gltf/book f.jpg b/test/baseline/Windows/gltf/book f.jpg index 098b1fa9..6d1c5c51 100644 Binary files a/test/baseline/Windows/gltf/book f.jpg and b/test/baseline/Windows/gltf/book f.jpg differ diff --git a/test/baseline/Windows/gltf/cube-colors.jpg b/test/baseline/Windows/gltf/cube-colors.jpg index c6a480bc..81664193 100644 Binary files a/test/baseline/Windows/gltf/cube-colors.jpg and b/test/baseline/Windows/gltf/cube-colors.jpg differ diff --git a/test/baseline/Windows/gltf/cube-colors_roundtrip.jpg b/test/baseline/Windows/gltf/cube-colors_roundtrip.jpg index c6a480bc..81664193 100644 Binary files a/test/baseline/Windows/gltf/cube-colors_roundtrip.jpg and b/test/baseline/Windows/gltf/cube-colors_roundtrip.jpg differ diff --git a/test/baseline/Windows/obj/cube/cube.jpg b/test/baseline/Windows/obj/cube/cube.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Windows/obj/cube/cube.jpg and b/test/baseline/Windows/obj/cube/cube.jpg differ diff --git a/test/baseline/Windows/obj/cube/cube_roundtrip.jpg b/test/baseline/Windows/obj/cube/cube_roundtrip.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Windows/obj/cube/cube_roundtrip.jpg and b/test/baseline/Windows/obj/cube/cube_roundtrip.jpg differ diff --git a/test/baseline/Windows/obj/nut_obj/nut.jpg b/test/baseline/Windows/obj/nut_obj/nut.jpg index 936d8204..abaf44af 100644 Binary files a/test/baseline/Windows/obj/nut_obj/nut.jpg and b/test/baseline/Windows/obj/nut_obj/nut.jpg differ diff --git a/test/baseline/Windows/obj/stone/stone.jpg b/test/baseline/Windows/obj/stone/stone.jpg index 47878aa2..9b3860ba 100644 Binary files a/test/baseline/Windows/obj/stone/stone.jpg and b/test/baseline/Windows/obj/stone/stone.jpg differ diff --git a/test/baseline/Windows/ply/colors.jpg b/test/baseline/Windows/ply/colors.jpg index fba07107..404f8d6c 100644 Binary files a/test/baseline/Windows/ply/colors.jpg and b/test/baseline/Windows/ply/colors.jpg differ diff --git a/test/baseline/Windows/ply/colors_roundtrip.jpg b/test/baseline/Windows/ply/colors_roundtrip.jpg index fba07107..404f8d6c 100644 Binary files a/test/baseline/Windows/ply/colors_roundtrip.jpg and b/test/baseline/Windows/ply/colors_roundtrip.jpg differ diff --git a/test/baseline/Windows/ply/cube.jpg b/test/baseline/Windows/ply/cube.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Windows/ply/cube.jpg and b/test/baseline/Windows/ply/cube.jpg differ diff --git a/test/baseline/Windows/ply/cube_roundtrip.jpg b/test/baseline/Windows/ply/cube_roundtrip.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Windows/ply/cube_roundtrip.jpg and b/test/baseline/Windows/ply/cube_roundtrip.jpg differ diff --git a/test/baseline/Windows/ply/gargo.jpg b/test/baseline/Windows/ply/gargo.jpg index 3f6797ba..08d68586 100644 Binary files a/test/baseline/Windows/ply/gargo.jpg and b/test/baseline/Windows/ply/gargo.jpg differ diff --git a/test/baseline/Windows/ply/gargo_roundtrip.jpg b/test/baseline/Windows/ply/gargo_roundtrip.jpg index 3f6797ba..0f6b7eb1 100644 Binary files a/test/baseline/Windows/ply/gargo_roundtrip.jpg and b/test/baseline/Windows/ply/gargo_roundtrip.jpg differ diff --git a/test/baseline/Windows/sbsar/cube.jpg b/test/baseline/Windows/sbsar/cube.jpg index df47e44a..404c3a17 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 fa137ff5..9acc606c 100644 Binary files a/test/baseline/Windows/sbsar/sphere.jpg and b/test/baseline/Windows/sbsar/sphere.jpg differ diff --git a/test/baseline/Windows/stl/ascii.jpg b/test/baseline/Windows/stl/ascii.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Windows/stl/ascii.jpg and b/test/baseline/Windows/stl/ascii.jpg differ diff --git a/test/baseline/Windows/stl/ascii_roundtrip.jpg b/test/baseline/Windows/stl/ascii_roundtrip.jpg index f0abc046..38a9f28d 100644 Binary files a/test/baseline/Windows/stl/ascii_roundtrip.jpg and b/test/baseline/Windows/stl/ascii_roundtrip.jpg differ diff --git a/test/baseline/Windows/stl/car_roundtrip.jpg b/test/baseline/Windows/stl/car_roundtrip.jpg index b59f9a54..da7e822b 100644 Binary files a/test/baseline/Windows/stl/car_roundtrip.jpg and b/test/baseline/Windows/stl/car_roundtrip.jpg differ diff --git a/test/baseline/Windows/stl/cube.jpg b/test/baseline/Windows/stl/cube.jpg index f0abc046..9a49b3af 100644 Binary files a/test/baseline/Windows/stl/cube.jpg and b/test/baseline/Windows/stl/cube.jpg differ diff --git a/test/baseline/Windows/stl/cube_roundtrip.jpg b/test/baseline/Windows/stl/cube_roundtrip.jpg index f0abc046..38a9f28d 100644 Binary files a/test/baseline/Windows/stl/cube_roundtrip.jpg and b/test/baseline/Windows/stl/cube_roundtrip.jpg differ diff --git a/test/baseline/Windows/stl/simple_ascii.jpg b/test/baseline/Windows/stl/simple_ascii.jpg index ece74326..acbba4a0 100644 Binary files a/test/baseline/Windows/stl/simple_ascii.jpg and b/test/baseline/Windows/stl/simple_ascii.jpg differ diff --git a/test/baseline/Windows/stl/simple_ascii_roundtrip.jpg b/test/baseline/Windows/stl/simple_ascii_roundtrip.jpg index ece74326..74d15da9 100644 Binary files a/test/baseline/Windows/stl/simple_ascii_roundtrip.jpg and b/test/baseline/Windows/stl/simple_ascii_roundtrip.jpg differ diff --git a/test/baseline/Windows/stl/suzanne.jpg b/test/baseline/Windows/stl/suzanne.jpg index 94f266ed..e6935d86 100644 Binary files a/test/baseline/Windows/stl/suzanne.jpg and b/test/baseline/Windows/stl/suzanne.jpg differ diff --git a/test/baseline/Windows/stl/suzanne_roundtrip.jpg b/test/baseline/Windows/stl/suzanne_roundtrip.jpg index 94f266ed..e8aa7234 100644 Binary files a/test/baseline/Windows/stl/suzanne_roundtrip.jpg and b/test/baseline/Windows/stl/suzanne_roundtrip.jpg differ diff --git a/test/test.py b/test/test.py index 5b074bea..d3ffde98 100644 --- a/test/test.py +++ b/test/test.py @@ -26,39 +26,43 @@ OUTPUT_FOLDERNAME = "output" CONVERTED_SUFFIX = "_roundtrip" -def compare_images_with_similarity_threshold(img1_path, img2_path, similarity_threshold=0.9): +def compare_images_with_similarity_threshold(baseline_img_path, generated_img_path, similarity_threshold=0.9): """ Compare two images and check if they are similar based on a similarity threshold. Parameters: - img1_path (str): File path to the first image. - img2_path (str): File path to the second image. + baseline_img_path (str): File path to the baseline image used to compare against. + generated_img_path (str): File path to the generated image to be compared to the baseline. similarity_threshold (float): The threshold for similarity ratio (0 to 1). Returns: bool: True if images are similar above the given threshold, False otherwise. """ - img1 = cv2.imread(img1_path) - img2 = cv2.imread(img2_path) + baseline_img = cv2.imread(baseline_img_path) + generated_img = cv2.imread(generated_img_path) # Check if images are loaded - if img1 is None or img2 is None: - raise ValueError("One or both images could not be loaded.") + if baseline_img is None and generated_img is None: + raise ValueError(f"Both images could not be loaded.\nBaseline image path: {baseline_img_path}\nGenerated image path: {generated_img_path}") + elif baseline_img is None: + raise ValueError(f"Could not load baseline image: {baseline_img_path}") + elif generated_img is None: + raise ValueError(f"Could not load generated image: {generated_img_path}") # Check if images have the same dimensions - if img1.shape != img2.shape: + if baseline_img.shape != generated_img.shape: return False # Calculate the absolute difference and then the binary difference - difference = cv2.absdiff(img1, img2) + difference = cv2.absdiff(baseline_img, generated_img) _, binary_difference = cv2.threshold(difference, 0, 255, cv2.THRESH_BINARY) # Calculate the percentage of similar pixels similar_pixels = np.count_nonzero(binary_difference == 0) - total_pixels = img1.shape[0] * img1.shape[1] * img1.shape[2] + total_pixels = baseline_img.shape[0] * baseline_img.shape[1] * baseline_img.shape[2] similarity_ratio = similar_pixels / total_pixels if similarity_ratio < similarity_threshold: - logging.error(f"Images are not similar enough: {similarity_ratio}") + logging.error(f"Images are not similar enough, similar_pixels={similar_pixels}, total_pixels={total_pixels}, similarity_ratio={similarity_ratio}\nBaseline image path: {baseline_img_path}\nGenerated image path: {generated_img_path}") return similarity_ratio >= similarity_threshold diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index a2e81bc1..9c209245 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -24,9 +24,10 @@ set(HEADERS "transforms.h" "images.h" "layerRead.h" + "layerReadMaterial.h" "layerWriteShared.h" "layerWriteMaterial.h" - "layerWriteMaterialX.h" + "layerWriteOpenPBR.h" "layerWriteSdfData.h" "materials.h" "neuralAssetsHelper.h" @@ -44,9 +45,10 @@ set(SOURCES "transforms.cpp" "images.cpp" "layerRead.cpp" + "layerReadMaterial.cpp" "layerWriteShared.cpp" "layerWriteMaterial.cpp" - "layerWriteMaterialX.cpp" + "layerWriteOpenPBR.cpp" "layerWriteSdfData.cpp" "materials.cpp" "neuralAssetsHelper.cpp" @@ -87,6 +89,7 @@ target_include_directories(fileformatUtils target_link_libraries(fileformatUtils PUBLIC OpenImageIO::OpenImageIO + OpenImageIO::OpenImageIO_Util tf sdf usd @@ -122,3 +125,7 @@ if(USD_FILEFORMATS_BUILD_TESTS) endif() install(TARGETS fileformatUtils) + +if(USD_FILEFORMATS_BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/utils/include/fileformatutils/common.h b/utils/include/fileformatutils/common.h index b947ada5..b7cf04f9 100644 --- a/utils/include/fileformatutils/common.h +++ b/utils/include/fileformatutils/common.h @@ -61,7 +61,6 @@ governing permissions and limitations under the License. (rotation) \ (translation) \ (normals) \ - (normalScale) \ (tangents) \ (varname) \ (UsdUVTexture) \ @@ -71,62 +70,13 @@ governing permissions and limitations under the License. (stPrimvarName) \ (surface) \ (UsdPreviewSurface) \ - (useSpecularWorkflow) \ - (diffuseColor) \ - (emissiveColor) \ - (specularColor) \ - (normal) \ - (metallic) \ - (roughness) \ - (clearcoat) \ - (clearcoatColor) \ - (clearcoatIor) \ - (clearcoatNormal) \ - (clearcoatRoughness) \ - (clearcoatSpecular) \ - (sheenOpacity) \ - (sheenColor) \ - (sheenRoughness) \ - (anisotropyLevel) \ (anisotropyLevelTexture) \ - (anisotropyAngle) \ (anisotropyAngleTexture) \ - (opacity) \ - (opacityThreshold) \ - (displacement) \ - (occlusion) \ - (ior) \ (ASM) \ ((adobeStandardMaterial, "AdobeStandardMaterial_4_0")) \ - (baseColor) \ - (specularEdgeColor) \ - (specularLevel) \ - (height) \ - (heightLevel) \ - (heightScale) \ - (emissiveIntensity) \ - (emissive) \ - (translucency) \ - (IOR) \ - (dispersion) \ - (absorptionColor) \ - (absorptionDistance) \ - (scatter) \ - (scatteringColor) \ - (scatteringDistance) \ - (coatOpacity) \ - (coatColor) \ - (coatRoughness) \ - (coatIOR) \ - (coatSpecularLevel) \ - (coatNormal) \ - (ambientOcclusion) \ - (volumeThickness) \ (clearcoatModelsTransmissionTint) \ (unlit) \ - (writeMaterialX) \ (transmission) \ - (subsurfaceWeight) \ (min) \ (max) \ (originalColorSpace) @@ -135,7 +85,7 @@ governing permissions and limitations under the License. /// Tokens for MaterialX nodes // clang-format off #define MATERIAL_X_TOKENS \ - (MaterialX) \ + (OpenPBR) \ (srgb_texture) \ (ND_image_vector4) \ (ND_image_color3) \ @@ -155,45 +105,114 @@ governing permissions and limitations under the License. (ND_separate4_vector4) \ (ND_convert_float_color3) \ (ND_normalmap) \ - (ND_adobe_standard_material) \ (ND_open_pbr_surface_surfaceshader) // clang-format on +/// Tokens for the inputs of the UsdPreviewSurface shader +/// The order of tokens listed below is based on the order defined in +/// https://github.com/PixarAnimationStudios/OpenUSD/blob/b9282cb274d111878707baff97d4223a81ef23d8/pxr/usd/plugin/usdShaders/shaders/shaderDefs.usda +// clang-format off +#define USD_PREVIEW_SURFACE_TOKENS \ + (diffuseColor) \ + (emissiveColor) \ + (useSpecularWorkflow) \ + (specularColor) \ + (metallic) \ + (roughness) \ + (clearcoat) \ + (clearcoatRoughness) \ + (opacity) \ + (opacityMode) \ + (opacityThreshold) \ + (ior) \ + (normal) \ + (displacement) \ + (occlusion) +// clang-format on + +/// Tokens for the inputs of the AdobeStandardMaterial 4.0 shader +/// The order of tokens listed below is based on the order defined in the ASM spec found at +/// https://helpx.adobe.com/substance-3d-general/adobe-standard-material.html +// clang-format off +#define ASM_TOKENS \ + (baseColor) \ + (roughness) \ + (metallic) \ + (opacity) \ + (specularLevel) \ + (specularEdgeColor) \ + (normal) \ + (normalScale) \ + (combineNormalAndHeight) \ + (height) \ + (heightScale) \ + (heightLevel) \ + (anisotropyLevel) \ + (anisotropyAngle) \ + (emissiveIntensity) \ + (emissive) \ + (sheenOpacity) \ + (sheenColor) \ + (sheenRoughness) \ + (translucency) \ + (IOR) \ + (dispersion) \ + (absorptionColor) \ + (absorptionDistance) \ + (scatter) \ + (scatteringColor) \ + (scatteringDistance) \ + (scatteringDistanceScale) \ + (scatteringRedShift) \ + (scatteringRayleigh) \ + (coatOpacity) \ + (coatColor) \ + (coatRoughness) \ + (coatIOR) \ + (coatSpecularLevel) \ + (coatNormal) \ + (coatNormalScale) \ + (ambientOcclusion) \ + (volumeThickness) \ + (volumeThicknessScale) +// clang-format on + /// Tokens for the inputs of the OpenPBR surface shader +/// The order of tokens listed below is based on the order defined in +/// https://github.com/AcademySoftwareFoundation/OpenPBR/blob/main/reference/open_pbr_surface.mtlx // clang-format off #define OPEN_PBR_TOKENS \ (base_weight) \ (base_color) \ - (base_roughness) \ + (base_diffuse_roughness) \ (base_metalness) \ (specular_weight) \ (specular_color) \ (specular_roughness) \ (specular_ior) \ - (specular_ior_level) \ - (specular_anisotropy) \ - (specular_rotation) \ + (specular_roughness_anisotropy) \ (transmission_weight) \ (transmission_color) \ (transmission_depth) \ (transmission_scatter) \ (transmission_scatter_anisotropy) \ - (transmission_dispersion) \ + (transmission_dispersion_scale) \ + (transmission_dispersion_abbe_number) \ (subsurface_weight) \ (subsurface_color) \ (subsurface_radius) \ (subsurface_radius_scale) \ - (subsurface_anisotropy) \ + (subsurface_scatter_anisotropy) \ (fuzz_weight) \ (fuzz_color) \ (fuzz_roughness) \ (coat_weight) \ (coat_color) \ (coat_roughness) \ - (coat_anisotropy) \ - (coat_rotation) \ + (coat_roughness_anisotropy) \ (coat_ior) \ - (coat_ior_level) \ + (coat_darkening) \ + (thin_film_weight) \ (thin_film_thickness) \ (thin_film_ior) \ (emission_luminance) \ @@ -202,7 +221,33 @@ governing permissions and limitations under the License. (geometry_thin_walled) \ (geometry_normal) \ (geometry_coat_normal) \ - (geometry_tangent) + (geometry_tangent) \ + (geometry_coat_tangent) +// clang-format on + +/// Tokens for the naming of OpenPBR inputs on the material that don't have ASM equivalents +// clang-format off +#define OPEN_PBR_MATERIAL_INPUT_TOKENS \ + (baseDiffuseRoughness) \ + (baseWeight) \ + (coatDarkening) \ + (coatRoughnessAnisotropy) \ + (coatTangent) \ + (emissionLuminance) \ + (fuzzWeight) \ + (specularWeight) \ + (subsurfaceRadiusScale) \ + (subsurfaceScatterAnisotropy) \ + (subsurfaceWeight) \ + (tangent) \ + (thinFilmIOR) \ + (thinFilmThickness) \ + (thinFilmWeight) \ + (thinWalled) \ + (transmissionDispersionAbbeNumber) \ + (transmissionDispersionScale) \ + (transmissionScatter) \ + (transmissionScatterAnisotropy) // clang-format on /// Tokens for the inputs of the neural graphics primitives (NGPs) @@ -244,61 +289,19 @@ governing permissions and limitations under the License. (widths1) \ (widths2) -#define ADOBE_GSPLAT_SH_TOKENS \ - (fRest0) \ - (fRest1) \ - (fRest2) \ - (fRest3) \ - (fRest4) \ - (fRest5) \ - (fRest6) \ - (fRest7) \ - (fRest8) \ - (fRest9) \ - (fRest10) \ - (fRest11) \ - (fRest12) \ - (fRest13) \ - (fRest14) \ - (fRest15) \ - (fRest16) \ - (fRest17) \ - (fRest18) \ - (fRest19) \ - (fRest20) \ - (fRest21) \ - (fRest22) \ - (fRest23) \ - (fRest24) \ - (fRest25) \ - (fRest26) \ - (fRest27) \ - (fRest28) \ - (fRest29) \ - (fRest30) \ - (fRest31) \ - (fRest32) \ - (fRest33) \ - (fRest34) \ - (fRest35) \ - (fRest36) \ - (fRest37) \ - (fRest38) \ - (fRest39) \ - (fRest40) \ - (fRest41) \ - (fRest42) \ - (fRest43) \ - (fRest44) // clang-format on PXR_NAMESPACE_OPEN_SCOPE TF_DECLARE_PUBLIC_TOKENS(AdobeTokens, USDFFUTILS_API, ADOBE_TOKENS); TF_DECLARE_PUBLIC_TOKENS(MtlXTokens, USDFFUTILS_API, MATERIAL_X_TOKENS); +TF_DECLARE_PUBLIC_TOKENS(UsdPreviewSurfaceTokens, USDFFUTILS_API, USD_PREVIEW_SURFACE_TOKENS); +TF_DECLARE_PUBLIC_TOKENS(AsmTokens, USDFFUTILS_API, ASM_TOKENS); TF_DECLARE_PUBLIC_TOKENS(OpenPbrTokens, USDFFUTILS_API, OPEN_PBR_TOKENS); +TF_DECLARE_PUBLIC_TOKENS(OpenPbrMaterialInputTokens, + USDFFUTILS_API, + OPEN_PBR_MATERIAL_INPUT_TOKENS); TF_DECLARE_PUBLIC_TOKENS(AdobeNgpTokens, USDFFUTILS_API, ADOBE_NGP_TOKENS); TF_DECLARE_PUBLIC_TOKENS(AdobeGsplatBaseTokens, USDFFUTILS_API, ADOBE_GSPLAT_BASE_TOKENS); -TF_DECLARE_PUBLIC_TOKENS(AdobeGsplatSHTokens, USDFFUTILS_API, ADOBE_GSPLAT_SH_TOKENS); PXR_NAMESPACE_CLOSE_SCOPE #define VOID_GUARD(x, ...) \ @@ -376,6 +379,12 @@ argReadFloatArray(const PXR_NS::SdfFileFormat::FileFormatArguments& args, PXR_NS::VtFloatArray& target, const std::string& debugTag); +/// Issues a warning if the specified arg is present that it has been deprecated +void USDFFUTILS_API +argWarnDeprecatedArg(const PXR_NS::SdfFileFormat::FileFormatArguments& args, + const std::string& arg, + const std::string& debugTag); + std::string USDFFUTILS_API getFileExtension(const std::string& filePath, const std::string& defaultValue); diff --git a/utils/include/fileformatutils/geometry.h b/utils/include/fileformatutils/geometry.h index e09be220..164dcbbc 100644 --- a/utils/include/fileformatutils/geometry.h +++ b/utils/include/fileformatutils/geometry.h @@ -118,11 +118,12 @@ expandIndexedValues(const PXR_NS::VtIntArray& indices, PXR_NS::VtArray& value values.assign(indices.size(), values.front()); } else { PXR_NS::VtArray temp = std::move(values); - unsigned int size = indices.size(); + const size_t size = indices.size(); + const int tempSize = temp.size(); values.resize(size); - for (unsigned int i = 0; i < size; i++) { + for (size_t i = 0; i < size; i++) { int index = indices[i]; - if (index < 0 && index >= temp.size()) { + if (index < 0 || index >= tempSize) { // set invalid indices to 0 index = 0; } @@ -131,6 +132,45 @@ expandIndexedValues(const PXR_NS::VtIntArray& indices, PXR_NS::VtArray& value } } +/// \ingroup utils_geometry +/// \brief Remove the indexing out of a set of values using the vertexIndices to find an +// index into the valuesIndices array which is then used to index the values array. +template +void +expandIndexedValuesIndirect(const PXR_NS::VtIntArray& vertexIndices, + const PXR_NS::VtIntArray& valuesIndices, + PXR_NS::VtArray& values) +{ + if (values.empty()) { + return; + } else if (values.size() == 1) { + values.assign(vertexIndices.size(), values.front()); + } else { + PXR_NS::VtArray temp = std::move(values); + const size_t indicesSize = vertexIndices.size(); + // Use int instead of size_t for these size variables because we will + // be comparing against an int value obtained from an indices array. + const int valuesIndicesSize = valuesIndices.size(); + const int valuesSize = temp.size(); + values.resize(indicesSize); + for (size_t i = 0; i < indicesSize; i++) { + int index = vertexIndices[i]; + if (index >= 0 && index < valuesIndicesSize) { + index = valuesIndices[index]; + if (index < 0 && index >= valuesSize) { + // set invalid indices to 0 + index = 0; + } + } else { + // set invalid indices to 0 + index = 0; + } + + values[i] = temp[index]; + } + } +} + /// \ingroup utils_geometry /// \brief Transform a mesh with the given transform USDFFUTILS_API void diff --git a/utils/include/fileformatutils/layerRead.h b/utils/include/fileformatutils/layerRead.h index fc98913d..9eb115b8 100644 --- a/utils/include/fileformatutils/layerRead.h +++ b/utils/include/fileformatutils/layerRead.h @@ -12,9 +12,16 @@ governing permissions and limitations under the License. #pragma once #include "api.h" #include "usdData.h" + #include #include #include +#include +#include + +#include +#include +#include namespace adobe::usd { @@ -30,6 +37,22 @@ struct USDFFUTILS_API ReadLayerOptions int maxMeshInfluenceCount = 4; }; +struct USDFFUTILS_API ReadLayerContext +{ + PXR_NS::UsdStageRefPtr stage; + UsdData* usd; + const ReadLayerOptions* options; + std::unordered_map prototypes; + std::unordered_map images; + std::unordered_map imageNames; + std::unordered_map materials; + std::unordered_map ngps; + std::vector materialBindings; + std::vector> subsetMaterialBindings; + PXR_NS::UsdGeomXformCache xformCache; + std::string debugTag; +}; + /// \ingroup utils_layer /// \brief Takes a SBSAR texture parameterization. USDFFUTILS_API std::string diff --git a/utils/include/fileformatutils/layerReadMaterial.h b/utils/include/fileformatutils/layerReadMaterial.h new file mode 100644 index 00000000..e29e868f --- /dev/null +++ b/utils/include/fileformatutils/layerReadMaterial.h @@ -0,0 +1,24 @@ +/* +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 "layerRead.h" + +#include + +namespace adobe::usd { + +/// Read Material at UsdMaterial prim +USDFFUTILS_API bool +readMaterial(ReadLayerContext& ctx, const PXR_NS::UsdPrim& prim, int parent); + +} \ No newline at end of file diff --git a/utils/include/fileformatutils/layerWriteMaterial.h b/utils/include/fileformatutils/layerWriteMaterial.h index df3096da..f04095d6 100644 --- a/utils/include/fileformatutils/layerWriteMaterial.h +++ b/utils/include/fileformatutils/layerWriteMaterial.h @@ -20,13 +20,13 @@ namespace adobe::usd { USDFFUTILS_API void writeUsdPreviewSurface(WriteSdfContext& ctx, const PXR_NS::SdfPath& materialPath, - const Material& material, + const OpenPbrMaterial& material, MaterialInputs& materialInputs); USDFFUTILS_API void writeAsmMaterial(WriteSdfContext& ctx, const PXR_NS::SdfPath& materialPath, - const Material& material, + const OpenPbrMaterial& material, MaterialInputs& materialInputs); } diff --git a/utils/include/fileformatutils/layerWriteMaterialX.h b/utils/include/fileformatutils/layerWriteOpenPBR.h similarity index 79% rename from utils/include/fileformatutils/layerWriteMaterialX.h rename to utils/include/fileformatutils/layerWriteOpenPBR.h index 195f1ab2..3bd86586 100644 --- a/utils/include/fileformatutils/layerWriteMaterialX.h +++ b/utils/include/fileformatutils/layerWriteOpenPBR.h @@ -13,14 +13,13 @@ governing permissions and limitations under the License. #include "api.h" #include "layerWriteShared.h" #include "sdfMaterialUtils.h" -#include namespace adobe::usd { USDFFUTILS_API void -writeMaterialX(WriteSdfContext& ctx, - const PXR_NS::SdfPath& materialPath, - const Material& material, - MaterialInputs& materialInputs); +writeOpenPBR(WriteSdfContext& ctx, + const PXR_NS::SdfPath& materialPath, + const OpenPbrMaterial& material, + MaterialInputs& materialInputs); } diff --git a/utils/include/fileformatutils/layerWriteShared.h b/utils/include/fileformatutils/layerWriteShared.h index 00cad721..c26e4fad 100644 --- a/utils/include/fileformatutils/layerWriteShared.h +++ b/utils/include/fileformatutils/layerWriteShared.h @@ -11,6 +11,7 @@ governing permissions and limitations under the License. */ #pragma once #include "api.h" +#include "sdfUtils.h" #include "usdData.h" #include @@ -20,11 +21,22 @@ namespace adobe::usd { struct WriteLayerOptions { + WriteLayerOptions() {} + WriteLayerOptions(const PXR_NS::FileFormatDataBase& fileFormatData) + : writeUsdPreviewSurface(fileFormatData.writeUsdPreviewSurface) + , writeASM(fileFormatData.writeASM) + , writeOpenPBR(fileFormatData.writeOpenPBR) + , assetsPath(fileFormatData.assetsPath) + { + } + + bool writeUsdPreviewSurface = true; + bool writeASM = true; + bool writeOpenPBR = false; bool pruneJoints = false; - bool writeMaterialX = false; - std::string assetsPath; - bool createRenderSettingsPrim = false; bool animationTracks = false; + bool createRenderSettingsPrim = false; + std::string assetsPath; }; struct WriteSdfContext @@ -64,4 +76,119 @@ getTextureZeroVtValue(const PXR_NS::TfToken& channel); USDFFUTILS_API std::string createTexturePath(const std::string& srcAssetFilename, const std::string& imageUri); +/// @brief OpenPBR material struct +/// This is based on OpenPBR 1.0 +/// https://github.com/AcademySoftwareFoundation/OpenPBR/blob/44fe76650880914980402221672446ad44df15bd/reference/open_pbr_surface.mtlx +/// +/// The latest version can be found here (currently at 1.1) +/// https://github.com/AcademySoftwareFoundation/OpenPBR/blob/main/reference/open_pbr_surface.mtlx +/// +/// Note that there are additions at the bottom that are not from the OpenPBR spec, but that are +/// useful extensions to carry additional information that is important for the transcoding of +/// materials, especially for the backwards compatibility with ASM. +struct USDFFUTILS_API OpenPbrMaterial +{ + std::string name; + std::string displayName; + + // Note, the naming convention here follows the OpenPBR input names + Input base_weight; + Input base_color; + Input base_diffuse_roughness; + Input base_metalness; + Input specular_weight; + Input specular_color; + Input specular_roughness; + Input specular_ior; + Input specular_roughness_anisotropy; + Input transmission_weight; + Input transmission_color; + Input transmission_depth; + Input transmission_scatter; + Input transmission_scatter_anisotropy; + Input transmission_dispersion_scale; + Input transmission_dispersion_abbe_number; + Input subsurface_weight; + Input subsurface_color; + Input subsurface_radius; + Input subsurface_radius_scale; + Input subsurface_scatter_anisotropy; + Input fuzz_weight; + Input fuzz_color; + Input fuzz_roughness; + Input coat_weight; + Input coat_color; + Input coat_roughness; + Input coat_roughness_anisotropy; + Input coat_ior; + Input coat_darkening; + Input thin_film_weight; + Input thin_film_thickness; + Input thin_film_ior; + Input emission_luminance; + Input emission_color; + Input geometry_opacity; + Input geometry_thin_walled; + Input geometry_normal; + Input geometry_coat_normal; + Input geometry_tangent; + Input geometry_coat_tangent; + + /// The OpenPBR spec is only concerned with BXDF properties and hence does not have a + /// displacement input. But his can be expressed in MaterialX via displacement shader and + /// directly in other material models. + Input displacement; + + /// An occlusion signal is sometimes available for renderers that do implement their own global + /// illumination + Input occlusion; + + /// This is an ASM concept, which is hard to express in OpenPBR as the anisotropy direction is + /// derived from the tangent and not a texturable input of the angle. + /// We're keeping this for now until we have an actual transfer mechanism. + Input anisotropyAngle; + + /// This is an ASM concept, to control the strength of the specular reflection of the coat. + /// In OpenPBR some of this control is available via the coat_ior, but the equation is not + /// trivial and coat_ior or coatSpecularLevel could be a constant or textured + Input coatSpecularLevel; + + /// This is an ASM concept, with no correspondence in OpenPBR. It is designed for real-time + /// rasterizers to have an approximate notion of the depth of a absorbing/scattering object. + Input volumeThickness; + + /// This is an ASM concept, which can also be expressed via the scale of the normal Input. + /// We have it here for backwards compatibility, but should consider removing it. + float normalScale = 1.0f; + + /// This is a flag used by UsdPreviewSurface to switch between a metallic workflow, where the + /// specular color is derived from the base_color and a workflow that has an explicit + /// specular_color. + bool useSpecularWorkflow = false; + + /// This float value is used by UsdPreviewSurface to express alpha masking based on an opacity + /// texture that is thresholded by this value. If this is zero, normal opacity is used. If this + /// larger than 0.0 the masking will be used. This maps to the alphaCutoff value in GLTF. + float opacityThreshold = 0.0f; + + // 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 + // not want to export clearcoat to GLTF again. + bool clearcoatModelsTransmissionTint = false; + + // Since USD doesn't support glTF unlit materials, we convert them on import to emissive. We + // keep this information, and store it as metadata in the file, so we can convert it back on + // export + bool isUnlit = false; +}; + +/// @brief Converts a Material struct into an OpenPbrMaterial struct +/// +/// It implements a channel-by-channel mapping where there is a correspondence between the +/// UsdPreviewSurface and ASM channels in the Material struct and the OpenPBR inputs. It also +/// transfer many channels that do not exist in OpenPBR, but that are required to implement previous +/// behaviors. The documentation for these is on the OpenPbrMaterial struct. +OpenPbrMaterial +mapMaterialStructToOpenPbrMaterialStruct(const Material& material); + } diff --git a/utils/include/fileformatutils/sdfMaterialUtils.h b/utils/include/fileformatutils/sdfMaterialUtils.h index 0aa9d6b8..49ac6ee6 100644 --- a/utils/include/fileformatutils/sdfMaterialUtils.h +++ b/utils/include/fileformatutils/sdfMaterialUtils.h @@ -92,6 +92,15 @@ struct KeyVtValuePair , second(value) { } + + // Convenient constructor to create the key from a TfToken and the VtValue from an arbitrarily + // typed value + template + KeyVtValuePair(const PXR_NS::TfToken& key, const T& value) + : first(key.GetString()) + , second(value) + { + } }; struct InputTypePair @@ -174,18 +183,17 @@ createShader(PXR_NS::SdfAbstractData* data, const InputConnections& inputConnections = {}, const InputColorSpaces& inputColorSpaces = {}); -using TokenToSdfValueTypeMap = std::unordered_map; +using TokenToSdfValueTypeMap = + std::unordered_map; struct ShaderInfo { TokenToSdfValueTypeMap inputTypes; TokenToSdfValueTypeMap outputTypes; - PXR_NS::SdfValueTypeName - getInputType(const PXR_NS::TfToken& inputName) const; + PXR_NS::SdfValueTypeName getInputType(const PXR_NS::TfToken& inputName) const; - PXR_NS::SdfValueTypeName - getOutputType(const PXR_NS::TfToken& outputName) const; + PXR_NS::SdfValueTypeName getOutputType(const PXR_NS::TfToken& outputName) const; }; // Table of shaders with input and outputs and their respective types @@ -193,46 +201,41 @@ struct ShaderInfo // The data here is essentially a mini form of the shader schemas. If we're concerned about this // staying up-to-date we could investigate gathering this information at run-time via the // shader definition registry (Sdr) module. Unfortunate, the ASM terminal nodes are not found there. -class ShaderRegistry { -public: - static ShaderRegistry& - getInstance() { +class ShaderRegistry +{ + public: + static ShaderRegistry& getInstance() + { static ShaderRegistry m_instance; return m_instance; } /// Return the shader info tokens - const std::map& - getShaderInfos() const { - return m_shaderInfos; - } + const std::map& getShaderInfos() const { return m_shaderInfos; } /// Given a token for a material input, return a pointer (possibly null) to the range - const MinMaxVtValuePair* - getMaterialInputRange(const PXR_NS::TfToken& input) const { + const MinMaxVtValuePair* getMaterialInputRange(const PXR_NS::TfToken& input) const + { auto it = m_inputRanges.find(input); return (it == m_inputRanges.cend()) ? nullptr : &(it->second); } /// Return UsdPreviewSurface shader inputs to material inputs map - const InputToMaterialInputTypeMap& - getUsdPreviewSurfaceInputRemapping() const { + const InputToMaterialInputTypeMap& getUsdPreviewSurfaceInputRemapping() const + { return m_usdPreviewSurfaceInputRemapping; } /// Return ASM shader inputs to material inputs map - const InputToMaterialInputTypeMap& - getAsmInputRemapping() const { - return m_asmInputRemapping; - } + const InputToMaterialInputTypeMap& getAsmInputRemapping() const { return m_asmInputRemapping; } /// Return MaterialX shader inputs to material inputs map - const InputToMaterialInputTypeMap& - getMaterialXInputRemapping() const { - return m_materialXInputRemapping; + const InputToMaterialInputTypeMap& getOpenPbrInputRemapping() const + { + return m_openPbrInputRemapping; } -private: + private: ShaderRegistry(); ~ShaderRegistry() = default; @@ -241,10 +244,11 @@ class ShaderRegistry { ShaderRegistry& operator=(const ShaderRegistry&) = delete; std::map m_shaderInfos; - std::unordered_map m_inputRanges; + std::unordered_map + m_inputRanges; InputToMaterialInputTypeMap m_usdPreviewSurfaceInputRemapping; InputToMaterialInputTypeMap m_asmInputRemapping; - InputToMaterialInputTypeMap m_materialXInputRemapping; + InputToMaterialInputTypeMap m_openPbrInputRemapping; }; } diff --git a/utils/include/fileformatutils/sdfUtils.h b/utils/include/fileformatutils/sdfUtils.h index 27af83b1..fff6c129 100644 --- a/utils/include/fileformatutils/sdfUtils.h +++ b/utils/include/fileformatutils/sdfUtils.h @@ -17,6 +17,7 @@ governing permissions and limitations under the License. #include #include #include +#include namespace adobe::usd { @@ -309,10 +310,28 @@ PXR_NAMESPACE_OPEN_SCOPE /// \ingroup utils_layer /// \brief SdfData specialization. -class FileFormatDataBase : public SdfData +class USDFFUTILS_API FileFormatDataBase : public SdfData { public: - bool writeMaterialX = false; + FileFormatDataBase() + { + // It's very important to create the pseudo root spec right away as there are codepaths that + // don't involve file reading (where we currently create this pseudo root spec). Without + // this calling CreateAnonymous or CreateNew on a SdfLayer for the fileformats will produce + // a totally empty layer that triggers TF_CODING_ERRORS when this layer get changes + // notifications. Creating this here mimics how regular USDA layers are created and ensures + // the pseudo root is always created and available in all code paths. + adobe::usd::createPseudoRootSpec(this); + }; + + bool writeUsdPreviewSurface = true; + bool writeASM = true; + bool writeOpenPBR = false; + std::string assetsPath; + + /// Parse common settings from the file format arguments + void parseFromFileFormatArgs(const SdfLayer::FileFormatArguments& args, + const std::string& debugTag); }; PXR_NAMESPACE_CLOSE_SCOPE diff --git a/utils/include/fileformatutils/test.h b/utils/include/fileformatutils/test.h index 1e6089cc..74fad2c8 100644 --- a/utils/include/fileformatutils/test.h +++ b/utils/include/fileformatutils/test.h @@ -18,59 +18,19 @@ governing permissions and limitations under the License. /// #include "api.h" -#include + #include #include -#include +#include #include #include +#include - - -#define TEST_TOKENS \ - (invalid) \ - (r) \ - (g) \ - (b) \ - (a) \ - (rgb) \ - (rgba) \ - (repeat) \ - (clamp) \ - (wrapS) \ - (wrapT) \ - (mirror) \ - (sourceColorSpace) \ - (result) \ - (raw) \ - (sRGB) \ - (st) \ - (file) \ - (scale) \ - (bias) \ - (normals) \ - (tangents) \ - (varname) \ - (UsdUVTexture) \ - (UsdPrimvarReader_float2) \ - (UsdTransform2d) \ - ((frame_stPrimvarName, "frame:stPrimvarName")) \ - (surface) \ - (UsdPreviewSurface) \ - (useSpecularWorkflow) \ - (diffuseColor) \ - (emissiveColor) \ - (specularColor) \ - (normal) \ - (metallic) \ - (roughness) \ - (clearcoat) \ - (clearcoatRoughness) \ - (opacity) \ - (opacityThreshold) \ - (displacement) \ - (occlusion) \ - (ior) \ +#define TEST_TOKENS \ + (invalid)(r)( \ + g)(b)(a)(rgb)(rgba)(repeat)(clamp)(wrapS)(wrapT)(mirror)(sourceColorSpace)(result)(raw)(sRGB)(st)(file)(scale)(bias)(normals)(tangents)(varname)(UsdUVTexture)(UsdPrimvarReader_float2)(UsdTransform2d)(( \ + frame_stPrimvarName, "frame:stPrimvarName"))( \ + surface)(UsdPreviewSurface)(useSpecularWorkflow)(diffuseColor)(emissiveColor)(specularColor)(normal)(metallic)(roughness)(clearcoat)(clearcoatRoughness)(opacity)(opacityThreshold)(displacement)(occlusion)(ior) PXR_NAMESPACE_OPEN_SCOPE TF_DECLARE_PUBLIC_TOKENS(TestTokens, USDFFUTILS_API, TEST_TOKENS); @@ -87,39 +47,49 @@ PXR_NAMESPACE_CLOSE_SCOPE #define ASSERT_DISPLAY_NAME(...) assertDisplayName(__VA_ARGS__) #define ASSERT_VISIBILITY(...) assertVisibility(__VA_ARGS__) #ifdef DO_RENDER - #define ASSERT_RENDER(...) assertRender(__VA_ARGS__) +# define ASSERT_RENDER(...) assertRender(__VA_ARGS__) #else - #define ASSERT_RENDER(...) {} +# define ASSERT_RENDER(...) \ + { \ + } #endif +// XXX This duplication of structs is highly suspicious template -struct USDFFUTILS_API ArrayData { +struct USDFFUTILS_API ArrayData +{ size_t size; PXR_NS::VtArray values; // a subset of the expected array data }; template -struct USDFFUTILS_API PrimvarData { +struct USDFFUTILS_API PrimvarData +{ PXR_NS::TfToken interpolation; ArrayData values; ArrayData indices; }; -struct USDFFUTILS_API MeshData { +struct USDFFUTILS_API MeshData +{ ArrayData faceVertexCounts; ArrayData faceVertexIndices; ArrayData points; PrimvarData normals; + PrimvarData tangents; + PrimvarData bitangents; PrimvarData uvs; PrimvarData displayColor; PrimvarData displayOpacity; }; -struct USDFFUTILS_API PointsData { +struct USDFFUTILS_API PointsData +{ size_t pointsCount; }; -struct USDFFUTILS_API InputData { +struct USDFFUTILS_API InputData +{ PXR_NS::VtValue value; int uvIndex; PXR_NS::TfToken channel; @@ -128,13 +98,14 @@ struct USDFFUTILS_API InputData { PXR_NS::TfToken colorspace; PXR_NS::VtValue scale; PXR_NS::VtValue bias; - PXR_NS::VtValue transformRotation; - PXR_NS::VtValue transformScale; - PXR_NS::VtValue transformTranslation; + PXR_NS::VtValue uvRotation; + PXR_NS::VtValue uvScale; + PXR_NS::VtValue uvTranslation; std::string file; // a relative path to the current binary dir }; -struct USDFFUTILS_API MaterialData { +struct USDFFUTILS_API MaterialData +{ InputData useSpecularWorkflow; InputData diffuseColor; InputData emissiveColor; @@ -157,14 +128,14 @@ struct USDFFUTILS_API AnimationData { std::map orient; std::map scale; - std::map translate; + std::map translate; }; struct USDFFUTILS_API CameraData { PXR_NS::GfQuatf orient; PXR_NS::GfVec3f scale; - PXR_NS::GfVec3f translate; + PXR_NS::GfVec3d translate; PXR_NS::GfVec2f clippingRange; float focalLength; @@ -194,11 +165,16 @@ struct USDFFUTILS_API LightData // ImageAsset texture }; -USDFFUTILS_API void assertPrim(PXR_NS::UsdStageRefPtr stage, const std::string& path); -USDFFUTILS_API void assertNode(PXR_NS::UsdStageRefPtr stage, const std::string& path); -USDFFUTILS_API void assertMesh(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MeshData& data); -USDFFUTILS_API void assertPoints(PXR_NS::UsdStageRefPtr stage, const std::string& path, const PointsData& data); -USDFFUTILS_API void assertMaterial(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MaterialData& data); +USDFFUTILS_API void +assertPrim(PXR_NS::UsdStageRefPtr stage, const std::string& path); +USDFFUTILS_API void +assertNode(PXR_NS::UsdStageRefPtr stage, const std::string& path); +USDFFUTILS_API void +assertMesh(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MeshData& data); +USDFFUTILS_API void +assertPoints(PXR_NS::UsdStageRefPtr stage, const std::string& path, const PointsData& data); +USDFFUTILS_API void +assertMaterial(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MaterialData& data); USDFFUTILS_API void assertAnimation(PXR_NS::UsdStageRefPtr stage, const std::string& path, const AnimationData& data); USDFFUTILS_API void @@ -242,49 +218,45 @@ extractUsdAttribute(PXR_NS::UsdPrim prim, } // Class to catch messages from the USD library -class UsdDiagnosticDelegate : public PXR_NS::TfDiagnosticMgr::Delegate { -public: - UsdDiagnosticDelegate() { - PXR_NS::TfDiagnosticMgr::GetInstance().AddDelegate(this); - } +class UsdDiagnosticDelegate : public PXR_NS::TfDiagnosticMgr::Delegate +{ + public: + UsdDiagnosticDelegate() { PXR_NS::TfDiagnosticMgr::GetInstance().AddDelegate(this); } - ~UsdDiagnosticDelegate() override { + ~UsdDiagnosticDelegate() override + { PXR_NS::TfDiagnosticMgr::GetInstance().RemoveDelegate(this); } - void IssueError(const PXR_NS::TfError &err) override { + void IssueError(const PXR_NS::TfError& err) override + { m_errors.push_back(err.GetCommentary()); } - void IssueFatalError(PXR_NS::TfCallContext const &context, std::string const &msg) override { + void IssueFatalError(PXR_NS::TfCallContext const& context, std::string const& msg) override + { m_fatalErrors.push_back(msg); } - void IssueStatus(const PXR_NS::TfStatus &status) override { + void IssueStatus(const PXR_NS::TfStatus& status) override + { m_statuses.push_back(status.GetCommentary()); } - void IssueWarning(const PXR_NS::TfWarning &warning) override { + void IssueWarning(const PXR_NS::TfWarning& warning) override + { m_warnings.push_back(warning.GetCommentary()); } - const std::vector& GetErrors() const { - return m_errors; - } + const std::vector& GetErrors() const { return m_errors; } - const std::vector& GetFatalErrors() const { - return m_fatalErrors; - } + const std::vector& GetFatalErrors() const { return m_fatalErrors; } - const std::vector& GetStatuses() const { - return m_statuses; - } + const std::vector& GetStatuses() const { return m_statuses; } - const std::vector& GetWarnings() const { - return m_warnings; - } + const std::vector& GetWarnings() const { return m_warnings; } -private: + private: std::vector m_errors; std::vector m_fatalErrors; std::vector m_statuses; diff --git a/utils/include/fileformatutils/usdData.h b/utils/include/fileformatutils/usdData.h index b6b2c924..8beaa4df 100644 --- a/utils/include/fileformatutils/usdData.h +++ b/utils/include/fileformatutils/usdData.h @@ -151,6 +151,7 @@ struct USDFFUTILS_API Mesh // XXX tangents in USD are usually GfVec3f and are only supported by Hermite curves. Something // is not quite right Primvar tangents; + Primvar bitangents; Primvar uvs; std::vector> extraUVSets; std::vector> colors; @@ -333,6 +334,12 @@ struct USDFFUTILS_API Light ImageAsset texture; // IBL texture. }; +constexpr PXR_NS::GfVec4f kDefaultTexScale = PXR_NS::GfVec4f(1.0f); +constexpr PXR_NS::GfVec4f kDefaultTexBias = PXR_NS::GfVec4f(0.0f); +constexpr float kDefaultUvRotation = 0.0f; +constexpr PXR_NS::GfVec2f kDefaultUvScale = PXR_NS::GfVec2f(1.0f); +constexpr PXR_NS::GfVec2f kDefaultUvTranslation = PXR_NS::GfVec2f(0.0f); + /// \ingroup utils_materials /// \brief Material Input data struct USDFFUTILS_API Input @@ -346,17 +353,26 @@ struct USDFFUTILS_API Input PXR_NS::TfToken minFilter; PXR_NS::TfToken magFilter; PXR_NS::TfToken colorspace; - PXR_NS::VtValue scale; - PXR_NS::VtValue bias; - PXR_NS::VtValue transformRotation; - PXR_NS::VtValue transformScale; - PXR_NS::VtValue transformTranslation; + PXR_NS::GfVec4f scale = kDefaultTexScale; + PXR_NS::GfVec4f bias = kDefaultTexBias; + float uvRotation = kDefaultUvRotation; + PXR_NS::GfVec2f uvScale = kDefaultUvScale; + PXR_NS::GfVec2f uvTranslation = kDefaultUvTranslation; bool isEmpty() const; int numChannels() const; bool isZeroInput() const; bool isZeroTexture() const; bool isZeroValue() const; + bool hasDefaultScaleAndBias() const + { + return scale == kDefaultTexScale && bias == kDefaultTexBias; + } + bool hasDefaultTransform() const + { + return uvRotation == kDefaultUvRotation && uvScale == kDefaultUvScale && + uvTranslation == kDefaultUvTranslation; + } }; /// \ingroup utils_materials @@ -406,6 +422,7 @@ struct USDFFUTILS_API Material Input absorptionColor; Input scatteringDistance; Input scatteringColor; + Input scatteringDistanceScale; }; /// \ingroup utils_layer @@ -471,18 +488,16 @@ getInputValue(const Input& input, T* value) T v = input.value.UncheckedGet(); - PXR_NS::GfVec4f scale = input.scale.GetWithDefault(PXR_NS::GfVec4f(1.0f)); - PXR_NS::GfVec4f bias = input.bias.GetWithDefault(PXR_NS::GfVec4f(0.0f)); - if constexpr (std::is_same_v) { - *value = scale[0] * v + bias[0]; + *value = input.scale[0] * v + input.bias[0]; } else if constexpr (std::is_same_v) { - *value = PXR_NS::GfVec2f(scale[0], scale[1]) * v + PXR_NS::GfVec2f(bias[0], bias[1]); + *value = PXR_NS::GfVec2f(input.scale[0], input.scale[1]) * v + + PXR_NS::GfVec2f(input.bias[0], input.bias[1]); } else if constexpr (std::is_same_v) { - *value = PXR_NS::GfVec3f(scale[0], scale[1], scale[2]) * v + - PXR_NS::GfVec3f(bias[0], bias[1], bias[2]); + *value = PXR_NS::GfVec3f(input.scale[0], input.scale[1], input.scale[2]) * v + + PXR_NS::GfVec3f(input.bias[0], input.bias[1], input.bias[2]); } else if constexpr (std::is_same_v) { - *value = scale * v + bias; + *value = input.scale * v + input.bias; } else { return false; } @@ -541,6 +556,11 @@ class USDFFUTILS_API UniqueNameEnforcer void enforceUniqueness(std::string& name); }; +// Remove any brackets from the file name as they are used as sentinels in the asset resolver +// Currently only used by the GLTF plugin to adjust the file name of the image asset. +USDFFUTILS_API void +removeBrackets(std::string& name); + // Currently used by the FBX and OBJ plugins whose color space data may either be linear // or sRGB. This checks if the outputColorSpace if specifically set, if not, it will // check the USD metatadata for the original color space. diff --git a/utils/src/common.cpp b/utils/src/common.cpp index 474719a2..1f15faa4 100644 --- a/utils/src/common.cpp +++ b/utils/src/common.cpp @@ -13,7 +13,6 @@ governing permissions and limitations under the License. #include -//#include #include #include #include @@ -32,10 +31,12 @@ governing permissions and limitations under the License. PXR_NAMESPACE_OPEN_SCOPE TF_DEFINE_PUBLIC_TOKENS(AdobeTokens, ADOBE_TOKENS); TF_DEFINE_PUBLIC_TOKENS(MtlXTokens, MATERIAL_X_TOKENS); +TF_DEFINE_PUBLIC_TOKENS(UsdPreviewSurfaceTokens, USD_PREVIEW_SURFACE_TOKENS); +TF_DEFINE_PUBLIC_TOKENS(AsmTokens, ASM_TOKENS); TF_DEFINE_PUBLIC_TOKENS(OpenPbrTokens, OPEN_PBR_TOKENS); +TF_DEFINE_PUBLIC_TOKENS(OpenPbrMaterialInputTokens, OPEN_PBR_MATERIAL_INPUT_TOKENS); TF_DEFINE_PUBLIC_TOKENS(AdobeNgpTokens, ADOBE_NGP_TOKENS); TF_DEFINE_PUBLIC_TOKENS(AdobeGsplatBaseTokens, ADOBE_GSPLAT_BASE_TOKENS); -TF_DEFINE_PUBLIC_TOKENS(AdobeGsplatSHTokens, ADOBE_GSPLAT_SH_TOKENS); PXR_NAMESPACE_CLOSE_SCOPE using namespace PXR_NS; @@ -211,6 +212,19 @@ argReadFloatArray(const SdfFileFormat::FileFormatArguments& args, } } +void +argWarnDeprecatedArg(const SdfFileFormat::FileFormatArguments& args, + const std::string& arg, + const std::string& debugTag) +{ + if (const auto& it = args.find(arg); it != args.end()) { + TF_WARN( + "%s: file format argument \"%s\" is deprecated and will be removed in a future version", + debugTag.c_str(), + arg.c_str()); + } +} + std::string getFileExtension(const std::string& filePath, const std::string& defaultValue = "") { @@ -257,7 +271,7 @@ 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()); + TF_CODING_ERROR("Error creating directory:\n \"%s\"", e.what()); return false; } diff --git a/utils/src/geometry.cpp b/utils/src/geometry.cpp index 9473b867..683b4011 100644 --- a/utils/src/geometry.cpp +++ b/utils/src/geometry.cpp @@ -10,6 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #include + #include using namespace PXR_NS; @@ -729,7 +730,7 @@ computeSmoothNormals(Mesh& mesh) { size_t vertexCount = mesh.points.size(); size_t numFaces = mesh.faces.size(); - size_t numFaceVertices = mesh.indices.size(); + size_t totalNumFaceVertices = mesh.indices.size(); // Construct a normals array the same size as the vertex array mesh.normals.values.resize(vertexCount); @@ -743,11 +744,25 @@ computeSmoothNormals(Mesh& mesh) mesh.name.c_str(), vertexCount, numFaces, - numFaceVertices); + totalNumFaceVertices); int faceVertexIndex = 0; + int numBadFaces = 0; int numBadNormals = 0; for (size_t faceIdx = 0; faceIdx < numFaces; ++faceIdx) { int numFaceVertices = mesh.faces[faceIdx]; + if (numFaceVertices < 3) { + ++numBadFaces; + continue; + } + if ((size_t)(faceVertexIndex + numFaceVertices) >= totalNumFaceVertices) { + TF_WARN("Invalid mesh topology: offset {} into indices for face {} is larger than " + "total indices {}", + faceVertexIndex + numFaceVertices, + faceIdx, + totalNumFaceVertices); + break; + } + // Starting index for the vertices of this face int faceVertexIndexBase = faceVertexIndex; faceVertexIndex += numFaceVertices; @@ -757,13 +772,22 @@ computeSmoothNormals(Mesh& mesh) // we precompute the prev and current values and then move them forward by one in each // iteration int prevIndex = mesh.indices[faceVertexIndexBase + (numFaceVertices - 1)]; + if (prevIndex >= vertexCount) { + continue; + } GfVec3f prevP = mesh.points[prevIndex]; int currentIndex = mesh.indices[faceVertexIndexBase]; + if (currentIndex >= vertexCount) { + continue; + } GfVec3f currentP = mesh.points[currentIndex]; for (int i = 0; i < numFaceVertices; ++i) { // Compute the next index and position int nextIndex = mesh.indices[faceVertexIndexBase + (i + 1) % numFaceVertices]; + if (nextIndex >= vertexCount) { + continue; + } GfVec3f nextP = mesh.points[nextIndex]; // Get vectors between the points, outwards from the current point @@ -803,6 +827,11 @@ computeSmoothNormals(Mesh& mesh) vertexNormals[i].Normalize(); } + if (numBadFaces > 0) { + TF_DEBUG_MSG( + FILE_FORMAT_UTIL, "Warning: normal computation on %d faces skipped.\n", numBadFaces); + } + if (numBadNormals > 0) { TF_DEBUG_MSG(FILE_FORMAT_UTIL, "Warning: computation of %d normals had numerical issues.\n", diff --git a/utils/src/layerRead.cpp b/utils/src/layerRead.cpp index 12a4543c..76a9ad11 100644 --- a/utils/src/layerRead.cpp +++ b/utils/src/layerRead.cpp @@ -10,6 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #include +#include #include #include @@ -18,34 +19,8 @@ governing permissions and limitations under the License. #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include #include -#include -#include #include #include #include @@ -53,8 +28,6 @@ governing permissions and limitations under the License. #include #include #include -#include -#include #include #include #include @@ -62,10 +35,7 @@ governing permissions and limitations under the License. #include #include #include -#include #include -#include -#include #include #include #include @@ -73,7 +43,6 @@ governing permissions and limitations under the License. #include #include #include -#include #include #include @@ -90,22 +59,6 @@ using namespace PXR_NS; namespace adobe::usd { -struct ReadLayerContext -{ - UsdStageRefPtr stage; - UsdData* usd; - const ReadLayerOptions* options; - std::unordered_map prototypes; - std::unordered_map images; - std::unordered_map imageNames; - std::unordered_map materials; - std::unordered_map ngps; - std::vector materialBindings; - std::vector> subsetMaterialBindings; - UsdGeomXformCache xformCache; - std::string debugTag; -}; - /** * Check whether the given prim is explicitly marked invisible. For this to be the case, it must: * 1. Be a UsdGeomImageable @@ -261,7 +214,7 @@ readXformInternal(ReadLayerContext& ctx, Node& node, const UsdPrim& prim, int pa bool reset; auto ops = xformable.GetOrderedXformOps(&reset); std::vector opTypes(ops.size()); - for (unsigned int i = 0; i < ops.size(); i++) { + for (size_t i = 0; i < ops.size(); i++) { opTypes[i] = ops[i].GetOpType(); } bool hasTranslation = false; @@ -280,9 +233,9 @@ readXformInternal(ReadLayerContext& ctx, Node& node, const UsdPrim& prim, int pa { UsdGeomXformOp::TypeOrient }, { UsdGeomXformOp::TypeScale }, }; - for (unsigned int i = 0; i < opTests.size(); i++) { + for (size_t i = 0; i < opTests.size(); i++) { if (opTypes == opTests[i]) { - for (unsigned int j = 0; j < opTypes.size(); j++) { + for (size_t j = 0; j < opTypes.size(); j++) { if (opTypes[j] == UsdGeomXformOp::TypeTranslate) { hasTranslation = true; translationOp = ops[j]; @@ -311,7 +264,7 @@ readXformInternal(ReadLayerContext& ctx, Node& node, const UsdPrim& prim, int pa NodeAnimation& nodeAnimation = ensureNodeAnimation(node); nodeAnimation.translations.times.resize(times.size()); nodeAnimation.translations.values.resize(times.size()); - for (unsigned int i = 0; i < times.size(); i++) { + for (size_t i = 0; i < times.size(); i++) { nodeAnimation.translations.times[i] = times[i]; // Translation is stored as a vector of doubles. To extract it properly, we must @@ -329,7 +282,7 @@ readXformInternal(ReadLayerContext& ctx, Node& node, const UsdPrim& prim, int pa NodeAnimation& nodeAnimation = ensureNodeAnimation(node); nodeAnimation.rotations.times.resize(times.size()); nodeAnimation.rotations.values.resize(times.size()); - for (unsigned int i = 0; i < times.size(); i++) { + for (size_t i = 0; i < times.size(); i++) { nodeAnimation.rotations.times[i] = times[i]; rotationOp.Get(&nodeAnimation.rotations.values[i], nodeAnimation.rotations.times[i]); @@ -343,7 +296,7 @@ readXformInternal(ReadLayerContext& ctx, Node& node, const UsdPrim& prim, int pa NodeAnimation& nodeAnimation = ensureNodeAnimation(node); nodeAnimation.scales.times.resize(times.size()); nodeAnimation.scales.values.resize(times.size()); - for (unsigned int i = 0; i < times.size(); i++) { + for (size_t i = 0; i < times.size(); i++) { nodeAnimation.scales.times[i] = times[i]; scaleOp.Get(&nodeAnimation.scales.values[i], nodeAnimation.scales.times[i]); } @@ -482,6 +435,50 @@ readMeshOrPointsData(ReadLayerContext& ctx, Mesh& mesh, int meshIndex, const Usd normalsAttr.Get(&mesh.normals.values, 0); mesh.normals.interpolation = usdMesh.GetNormalsInterpolation(); } + + // Read tangents and binormals + // First try as primvars, then fall back to authored attributes + if (!readPrimvar(primvarsAPI, TfToken("tangents"), mesh.tangents)) { + UsdAttribute tangentsAttr = prim.GetAttribute(TfToken("tangents")); + if (tangentsAttr.IsAuthored()) { + tangentsAttr.Get(&mesh.tangents.values, 0); + // For manually authored attributes, we need to determine interpolation + // Default to faceVarying if not specified, but check for primvar-style metadata + TfToken interpolation; + if (tangentsAttr.GetMetadata(TfToken("interpolation"), &interpolation)) { + mesh.tangents.interpolation = interpolation; + } else { + mesh.tangents.interpolation = UsdGeomTokens->faceVarying; + } + } + } + + // Try reading bitangents first (new format), then fallback to binormals (old format) + if (!readPrimvar(primvarsAPI, TfToken("bitangents"), mesh.bitangents)) { + if (!readPrimvar(primvarsAPI, TfToken("binormals"), mesh.bitangents)) { + // Try as authored attributes + UsdAttribute bitangentsAttr = prim.GetAttribute(TfToken("bitangents")); + UsdAttribute binormalsAttr = prim.GetAttribute(TfToken("binormals")); + + if (bitangentsAttr.IsAuthored()) { + bitangentsAttr.Get(&mesh.bitangents.values, 0); + TfToken interpolation; + if (bitangentsAttr.GetMetadata(TfToken("interpolation"), &interpolation)) { + mesh.bitangents.interpolation = interpolation; + } else { + mesh.bitangents.interpolation = UsdGeomTokens->faceVarying; + } + } else if (binormalsAttr.IsAuthored()) { + binormalsAttr.Get(&mesh.bitangents.values, 0); + TfToken interpolation; + if (binormalsAttr.GetMetadata(TfToken("interpolation"), &interpolation)) { + mesh.bitangents.interpolation = interpolation; + } else { + mesh.bitangents.interpolation = UsdGeomTokens->faceVarying; + } + } + } + } } else if (prim.IsA()) { UsdGeomPoints usdPoints(prim); usdPoints.GetPointsAttr().Get(&mesh.points, 0); @@ -623,17 +620,18 @@ readMeshOrPointsData(ReadLayerContext& ctx, Mesh& mesh, int meshIndex, const Usd } } } - for (const TfToken& gsToken : AdobeGsplatSHTokens->allTokens) { - // SH-related tokens: fRest0 -- fRest44. + int shIndex = 0; + while (true) { Primvar shCoeffs; - readPrimvar(primvarsAPI, gsToken, shCoeffs); - if (shCoeffs.values.size()) { - auto [pointSHCoeffSetIndex, pointSHCoeffSet] = - ctx.usd->addPointSHCoeffSet(meshIndex); - pointSHCoeffSet.indices = shCoeffs.indices; - pointSHCoeffSet.values = shCoeffs.values; - pointSHCoeffSet.interpolation = shCoeffs.interpolation; - } + if (!readPrimvar( + primvarsAPI, TfToken(std::string("fRest") + std::to_string(shIndex)), shCoeffs) || !shCoeffs.values.size()) + break; + auto [pointSHCoeffSetIndex, pointSHCoeffSet] = + ctx.usd->addPointSHCoeffSet(meshIndex); + pointSHCoeffSet.indices = shCoeffs.indices; + pointSHCoeffSet.values = shCoeffs.values; + pointSHCoeffSet.interpolation = shCoeffs.interpolation; + ++shIndex; } } } @@ -757,7 +755,7 @@ readSkelRoot(ReadLayerContext& ctx, const UsdPrim& prim, int parent) skelSkeleton.GetBindTransformsAttr().Get(&skeleton.bindTransforms, 0); skeleton.jointParents.resize(skeleton.joints.size()); skeleton.inverseBindTransforms.resize(skeleton.joints.size()); - for (unsigned int i = 0; i < skeleton.joints.size(); i++) { + for (size_t i = 0; i < skeleton.joints.size(); i++) { TF_DEBUG_MSG(FILE_FORMAT_UTIL, "%s: layer::read %-10s %s\n", ctx.debugTag.c_str(), @@ -778,7 +776,7 @@ readSkelRoot(ReadLayerContext& ctx, const UsdPrim& prim, int parent) // Process skinning targets const VtArray& targets = binding.GetSkinningTargets(); skeleton.meshSkinningTargets.resize(targets.size()); - for (unsigned int i = 0; i < targets.size(); i++) { + for (size_t i = 0; i < targets.size(); i++) { const UsdSkelSkinningQuery& skinningQuery = targets[i]; const UsdPrim& meshPrim = skinningQuery.GetPrim(); if (meshPrim.IsA()) { @@ -836,12 +834,12 @@ readSkelRoot(ReadLayerContext& ctx, const UsdPrim& prim, int parent) skeleton.skeletonAnimations.resize(1); SkeletonAnimation& animation = skeleton.skeletonAnimations.front(); - unsigned int timesCount = times.size(); + size_t timesCount = times.size(); animation.times.resize(timesCount); animation.translations.resize(timesCount); animation.rotations.resize(timesCount); animation.scales.resize(timesCount); - for (unsigned int i = 0; i < timesCount; i++) { + for (size_t i = 0; i < timesCount; i++) { VtMatrix4dArray transforms; if (!skelAnimQuery.ComputeJointLocalTransforms(&transforms, times[i])) { continue; @@ -854,7 +852,7 @@ readSkelRoot(ReadLayerContext& ctx, const UsdPrim& prim, int parent) // Add all transforms to the SkeletonAnimation as long as the transforms are // for a joint referred to by skeleton.animatedJoints - for (int j = 0; j < transforms.size(); j++) { + for (size_t j = 0; j < transforms.size(); j++) { if (animatedJointPresent[j]) { GfVec3f translation; GfQuatf rotation; @@ -913,7 +911,7 @@ readPointInstancer(ReadLayerContext& ctx, const UsdPrim& prim, int parent) readPrim(ctx, p, nodeIndex); } - for (unsigned int i = 0; i < protoIndices.size(); i++) { + for (size_t i = 0; i < protoIndices.size(); i++) { const int protoIndex = meshesBeforePrototypesAdded + protoIndices[i]; const GfMatrix4d transform = xforms[i]; if (transform != GfMatrix4d(0.0f) && transform != GfMatrix4d(1.0f)) { @@ -1016,454 +1014,6 @@ 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 -populatePathPartsFromAssetPath(const SdfAssetPath& path, - std::string& resolvedAssetPath, - std::string& name, - std::string& extension) -{ - // Make sure we have a resolved path, either coming from SdfAssetPath value or by running it - // throught the resolver. - resolvedAssetPath = path.GetResolvedPath().empty() - ? ArGetResolver().Resolve(path.GetAssetPath()) - : path.GetResolvedPath(); - // This will extract the inner most path to the asset: - // path/to/package.usdz[path/to/image.png] -> path/to/image.png - std::string innerAssetPath = getLayerFilePath(resolvedAssetPath); - // This helper function will detect "funky" paths, like those to SBSAR images and convert them - // to good usable file paths - std::string filePath = extractFilePathFromAssetPath(innerAssetPath); - // Strip the path part since we only want the filename and the extension - std::string baseName = TfGetBaseName(filePath); - name = TfStringGetBeforeSuffix(baseName); - extension = TfGetExtension(baseName); -} - -bool -readImage(ReadLayerContext& ctx, const SdfAssetPath& assetPath, int& index) -{ - std::string resolvedAssetPath, name, extension; - populatePathPartsFromAssetPath(assetPath, resolvedAssetPath, name, extension); - - // Check in the cache if we've processed this image before - if (const auto& it = ctx.images.find(resolvedAssetPath); it != ctx.images.end()) { - index = it->second; - TF_DEBUG_MSG(FILE_FORMAT_UTIL, - "%s: Image (cached): %s\n", - ctx.debugTag.c_str(), - resolvedAssetPath.c_str()); - return true; - } - - // The image is new. Make sure we don't get name collisions in the short name - if (const auto& itName = ctx.imageNames.find(name); itName != ctx.imageNames.end()) { - itName->second++; - name += "_" + std::to_string(itName->second); - TF_DEBUG_MSG(FILE_FORMAT_UTIL, - "%s: Deduplicated image name: %s\n", - ctx.debugTag.c_str(), - name.c_str()); - } else { - ctx.imageNames[name] = 1; - } - - auto [imageIndex, image] = ctx.usd->addImage(); - if (extension == "sbsarimage") { - // SBSAR images are a special cases where the data is stored raw and must be transcoded to a - // different image in memory - extension = getSbsarImageExtension(resolvedAssetPath); - image.uri = name + "." + extension; - transcodeImageAssetToMemory(resolvedAssetPath, image.uri, image.image); - } else { - auto asset = ArGetResolver().OpenAsset(ArResolvedPath(resolvedAssetPath)); - if (!asset) { - TF_WARN( - "%s: Unable to open asset: %s\n", ctx.debugTag.c_str(), resolvedAssetPath.c_str()); - return false; - } - image.uri = name + "." + extension; - image.image.resize(asset->GetSize()); - memcpy(image.image.data(), asset->GetBuffer().get(), asset->GetSize()); - } - - image.name = name; - image.format = getFormat(extension); - ctx.images[resolvedAssetPath] = imageIndex; - index = imageIndex; - - TF_DEBUG_MSG(FILE_FORMAT_UTIL, - "%s: Image (new): index: %d uri: %s\n", - ctx.debugTag.c_str(), - imageIndex, - resolvedAssetPath.c_str()); - - return true; -} - -void -applyInputMult(Input& input, float mult) -{ - if (mult == 1.0f) { - return; - } - - if (input.image != -1) { - GfVec4f s = - input.scale.IsHolding() ? input.scale.UncheckedGet() : GfVec4f(1.0f); - input.scale = s * mult; - } else if (input.value.IsHolding()) { - GfVec3f v = input.value.UncheckedGet(); - v *= mult; - input.value = v; - } else if (input.value.IsHolding()) { - float v = input.value.UncheckedGet(); - v *= mult; - input.value = v; - } -} - -template -bool -getShaderInputValue(const UsdShadeShader& shader, const TfToken& name, T& value) -{ - UsdShadeInput input = shader.GetInput(name); - if (input) { - UsdShadeAttributeVector valueAttrs = input.GetValueProducingAttributes(); - if (!valueAttrs.empty()) { - const UsdAttribute& attr = valueAttrs.front(); - if (UsdShadeUtils::GetType(attr.GetName()) == UsdShadeAttributeType::Input) { - valueAttrs.front().Get(&value); - return true; - } - } - } - return false; -} - -// Fetches the first value-producing attribute connected to a given shader input. -// If 'expectShader' is true, verify that the connected source is a shader and that the connection -// exists. Returns true and sets outAttribute if a suitable attribute is found. -bool -fetchPrimaryConnectedAttribute(const UsdShadeInput& shadeInput, - UsdAttribute& outAttribute, - bool expectShader) -{ - if (expectShader) { - if (!shadeInput.HasConnectedSource()) { - TF_WARN("Input %s has no connected source.", shadeInput.GetFullName().GetText()); - return false; - } - } - UsdShadeAttributeVector attrs = shadeInput.GetValueProducingAttributes(); - if (attrs.empty()) { - return false; - } - if (attrs.size() > 1) { - TF_WARN("Input %s is connected to multiple producing attributes, only the first will be " - "processed.", - shadeInput.GetFullName().GetText()); - } - outAttribute = attrs[0]; - if (expectShader) { - UsdShadeAttributeType attrType = UsdShadeUtils::GetType(outAttribute.GetName()); - if (attrType == UsdShadeAttributeType::Input) { - TF_WARN("Input %s is connected to an attribute that is not a shader.", - shadeInput.GetFullName().GetText()); - return false; - } - } - return true; -} - -// Handle texture-related shader inputs such as file paths and wrapping modes. -void -handleTextureShader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) -{ - SdfAssetPath assetPath; - if (getShaderInputValue(shader, AdobeTokens->file, assetPath)) { - readImage(ctx, assetPath, input.image); - } - getShaderInputValue(shader, AdobeTokens->wrapS, input.wrapS); - getShaderInputValue(shader, AdobeTokens->wrapT, input.wrapT); - getShaderInputValue(shader, AdobeTokens->minFilter, input.minFilter); - getShaderInputValue(shader, AdobeTokens->magFilter, input.magFilter); - getShaderInputValue(shader, AdobeTokens->scale, input.scale); - getShaderInputValue(shader, AdobeTokens->bias, input.bias); - getShaderInputValue(shader, AdobeTokens->sourceColorSpace, input.colorspace); - - // Default to 0th UVs unless overridden in handlePrimvarReader - input.uvIndex = 0; -} - -UsdShadeShader -handleTransformShader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) -{ - - UsdShadeShader nextShader; - getShaderInputValue(shader, AdobeTokens->rotation, input.transformRotation); - getShaderInputValue(shader, AdobeTokens->scale, input.transformScale); - getShaderInputValue(shader, AdobeTokens->translation, input.transformTranslation); - - UsdShadeInput stInputCoordReader = shader.GetInput(AdobeTokens->in); - UsdAttribute stSourcesInner; - if (fetchPrimaryConnectedAttribute(stInputCoordReader, stSourcesInner, true)) { - nextShader = UsdShadeShader(stSourcesInner.GetPrim()); - } - return nextShader; -} - -void -handlePrimvarReader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) -{ - TfToken texCoordPrimvar; - std::string texCoordPrimvarStr; - getShaderInputValue(shader, AdobeTokens->varname, texCoordPrimvarStr); - - // Supports both string and token type values for the varname - // string is the correct type, but token was added to support slightly - // incorrect assets. - if (!texCoordPrimvarStr.empty()) { - texCoordPrimvar = TfToken(texCoordPrimvarStr); - } else { - getShaderInputValue(shader, AdobeTokens->varname, texCoordPrimvar); - } - int uvIndex = getSTPrimvarTokenIndex(texCoordPrimvar); - if (uvIndex >= 0) { - input.uvIndex = uvIndex; - } else { - TF_WARN("Texture reader %s is reading primvar %s. Only 'st' or 'st1'..'stN' is supported", - shader.GetPrim().GetPath().GetText(), - texCoordPrimvar.GetText()); - } -} - -void -readInput(ReadLayerContext& ctx, const UsdShadeShader& surface, const TfToken& name, Input& input) -{ - UsdShadeInput shadeInput = surface.GetInput(name); - if (!shadeInput) { - return; - } - - UsdAttribute attr; - if (fetchPrimaryConnectedAttribute(shadeInput, attr, false)) { - UsdShadeSourceInfoVector sources = shadeInput.GetConnectedSources(); - - // Attempt to retrieve the constant value from the attribute. - auto [shadingAttrName, attrType] = UsdShadeUtils::GetBaseNameAndType(attr.GetName()); - if (attrType == UsdShadeAttributeType::Input) { - if (!attr.Get(&input.value)) { - TF_WARN("Failed to get constant value for input %s", name.GetText()); - return; - } - } else { - // Process the shader connected to this attribute - UsdShadeShader connectedShader(attr.GetPrim()); - TfToken shaderId; - connectedShader.GetShaderId(&shaderId); - - if (shaderId == AdobeTokens->UsdUVTexture) { - handleTextureShader(ctx, connectedShader, input); - - UsdShadeInput stInput = connectedShader.GetInput(AdobeTokens->st); - - // The name of the output on the texture reader determines which channel(s) of the - // texture we read. - input.channel = shadingAttrName; - - // Process the connected source of the 'st' input. - if (fetchPrimaryConnectedAttribute(stInput, attr, true)) { - VtValue srcValue; - if (attr.Get(&srcValue)) { - TF_WARN( - "Texture read shader does not support a fixed UV value for input %s", - name.GetText()); - } else { - // Handle the shader connected to the UV coordinate. - UsdShadeShader stShader(attr.GetPrim()); - stShader.GetShaderId(&shaderId); - - if (shaderId == AdobeTokens->UsdTransform2d) { - UsdShadeShader nextShader = handleTransformShader(ctx, stShader, input); - if (nextShader) { - stShader = nextShader; - stShader.GetShaderId(&shaderId); - } - } - - // This is not an "else if", since we can move the stShader - // if we encounter a UV transform. - if (shaderId == AdobeTokens->UsdPrimvarReader_float2) { - handlePrimvarReader(ctx, stShader, input); - } else { - TF_WARN("Unsupported shader type %s for UV input %s", - shaderId.GetText(), - name.GetText()); - } - } - } else { - TF_WARN("Failed to fetch connected attribute for UV input %s", name.GetText()); - } - } else { - TF_WARN( - "Unsupported shader type %s for input %s", shaderId.GetText(), name.GetText()); - } - } - } else { - // If no connections were found, get the shader's input value directly - if (!getShaderInputValue(surface, name, input.value)) { - TF_WARN("Failed to get input value for %s", name.GetText()); - } - } -} - -bool -readUsdPreviewSurfaceMaterial(ReadLayerContext& ctx, - Material& material, - const UsdShadeShader& surface) -{ - TfToken infoIdToken; - surface.GetShaderId(&infoIdToken); - if (infoIdToken != AdobeTokens->UsdPreviewSurface) { - return false; - } - - readInput(ctx, surface, AdobeTokens->useSpecularWorkflow, material.useSpecularWorkflow); - readInput(ctx, surface, AdobeTokens->diffuseColor, material.diffuseColor); - readInput(ctx, surface, AdobeTokens->emissiveColor, material.emissiveColor); - readInput(ctx, surface, AdobeTokens->specularColor, material.specularColor); - readInput(ctx, surface, AdobeTokens->normal, material.normal); - readInput(ctx, surface, AdobeTokens->metallic, material.metallic); - readInput(ctx, surface, AdobeTokens->roughness, material.roughness); - readInput(ctx, surface, AdobeTokens->clearcoat, material.clearcoat); - readInput(ctx, surface, AdobeTokens->clearcoatRoughness, material.clearcoatRoughness); - readInput(ctx, surface, AdobeTokens->opacity, material.opacity); - readInput(ctx, surface, AdobeTokens->opacityThreshold, material.opacityThreshold); - readInput(ctx, surface, AdobeTokens->displacement, material.displacement); - readInput(ctx, surface, AdobeTokens->occlusion, material.occlusion); - readInput(ctx, surface, AdobeTokens->ior, material.ior); - - return true; -} - -bool -_readClearcoatModelsTransmissionTint(const UsdShadeShader& surface) -{ - bool value = false; - // Check for a custom attribute that carries an indicator where the clearcoat came from - surface.GetPrim().GetAttribute(AdobeTokens->clearcoatModelsTransmissionTint).Get(&value); - return value; -} - -bool -_readUnlit(const UsdShadeShader& surface) -{ - bool value = false; - // Check for a custom attribute that carries an indicator where the clearcoat came from - surface.GetPrim().GetAttribute(AdobeTokens->unlit).Get(&value); - return value; -} - -bool -readASMMaterial(ReadLayerContext& ctx, Material& material, const UsdShadeShader& surface) -{ - TfToken infoIdToken; - surface.GetShaderId(&infoIdToken); - if (infoIdToken != AdobeTokens->adobeStandardMaterial) { - return false; - } - - material.clearcoatModelsTransmissionTint = _readClearcoatModelsTransmissionTint(surface); - material.isUnlit = _readUnlit(surface); - - // Note, we currently only support fixed values for emissiveIntensity and sheenOpacity - // No texture support yet. - float emissiveIntensity = 0.0f; - float sheenOpacity = 0.0f; - bool scatter = false; - - auto getConstShaderInput = [&](const TfToken& inputName, auto& var) { - VtValue val; - if (getShaderInputValue(surface, inputName, val)) { - if (val.IsHolding>()) { - var = val.UncheckedGet>(); - } - } - }; - - getConstShaderInput(AdobeTokens->emissiveIntensity, emissiveIntensity); - getConstShaderInput(AdobeTokens->sheenOpacity, sheenOpacity); - getConstShaderInput(AdobeTokens->scatter, scatter); - - readInput(ctx, surface, AdobeTokens->baseColor, material.diffuseColor); - readInput(ctx, surface, AdobeTokens->roughness, material.roughness); - readInput(ctx, surface, AdobeTokens->metallic, material.metallic); - readInput(ctx, surface, AdobeTokens->opacity, material.opacity); - readInput(ctx, surface, AdobeTokens->opacityThreshold, material.opacityThreshold); - readInput(ctx, surface, AdobeTokens->specularLevel, material.specularLevel); - readInput(ctx, surface, AdobeTokens->specularEdgeColor, material.specularColor); - readInput(ctx, surface, AdobeTokens->normal, material.normal); - readInput(ctx, surface, AdobeTokens->normalScale, material.normalScale); - readInput(ctx, surface, AdobeTokens->height, material.displacement); - readInput(ctx, surface, AdobeTokens->anisotropyLevel, material.anisotropyLevel); - readInput(ctx, surface, AdobeTokens->anisotropyAngle, material.anisotropyAngle); - if (emissiveIntensity > 0.0f) { - readInput(ctx, surface, AdobeTokens->emissive, material.emissiveColor); - applyInputMult(material.emissiveColor, emissiveIntensity); - } - if (sheenOpacity > 0.0f) { - readInput(ctx, surface, AdobeTokens->sheenColor, material.sheenColor); - // XXX sheenOpacity can't really be multiplied into the color. We currently drop this value - } - readInput(ctx, surface, AdobeTokens->sheenRoughness, material.sheenRoughness); - readInput(ctx, surface, AdobeTokens->translucency, material.transmission); - readInput(ctx, surface, AdobeTokens->IOR, material.ior); - readInput(ctx, surface, AdobeTokens->absorptionColor, material.absorptionColor); - readInput(ctx, surface, AdobeTokens->absorptionDistance, material.absorptionDistance); - if (scatter) { - readInput(ctx, surface, AdobeTokens->scatteringColor, material.scatteringColor); - readInput(ctx, surface, AdobeTokens->scatteringDistance, material.scatteringDistance); - } - readInput(ctx, surface, AdobeTokens->coatOpacity, material.clearcoat); - readInput(ctx, surface, AdobeTokens->coatColor, material.clearcoatColor); - readInput(ctx, surface, AdobeTokens->coatRoughness, material.clearcoatRoughness); - readInput(ctx, surface, AdobeTokens->coatIOR, material.clearcoatIor); - readInput(ctx, surface, AdobeTokens->coatSpecularLevel, material.clearcoatSpecular); - readInput(ctx, surface, AdobeTokens->coatNormal, material.clearcoatNormal); - readInput(ctx, surface, AdobeTokens->ambientOcclusion, material.occlusion); - readInput(ctx, surface, AdobeTokens->volumeThickness, material.volumeThickness); - - return true; -} - -bool -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 - // UsdPreviewSurface - UsdShadeShader surface = usdMaterial.ComputeSurfaceSource({ AdobeTokens->adobe }); - bool success = false; - if (surface) { - success = readASMMaterial(ctx, material, surface); - if (!success) { - success = readUsdPreviewSurfaceMaterial(ctx, material, surface); - } - } else { - TF_WARN("No surface shader for material %s", prim.GetPath().GetText()); - } - - printMaterial("layer::read", prim.GetPath(), material, ctx.debugTag); - return success; -} - bool readCamera(ReadLayerContext& ctx, const UsdPrim& prim, int parent) { @@ -1714,7 +1264,7 @@ readPrim(ReadLayerContext& ctx, const UsdPrim& prim, int parent) void resolveMaterialBindings(ReadLayerContext& ctx) { - for (unsigned int i = 0; i < ctx.usd->meshes.size(); i++) { + for (size_t i = 0; i < ctx.usd->meshes.size(); i++) { std::string name = ctx.materialBindings[i]; if (!name.empty()) { if (ctx.materials.find(name) == ctx.materials.end()) { @@ -1742,7 +1292,7 @@ resolveMaterialBindings(ReadLayerContext& ctx) ctx.usd->meshes[i].material = -1; } } - for (unsigned int j = 0; j < ctx.subsetMaterialBindings[i].size(); j++) { + for (size_t j = 0; j < ctx.subsetMaterialBindings[i].size(); j++) { std::string name = ctx.subsetMaterialBindings[i][j]; if (!name.empty()) { if (ctx.materials.find(name) == ctx.materials.end()) { diff --git a/utils/src/layerReadMaterial.cpp b/utils/src/layerReadMaterial.cpp new file mode 100644 index 00000000..065755ab --- /dev/null +++ b/utils/src/layerReadMaterial.cpp @@ -0,0 +1,481 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace PXR_NS; + +namespace adobe::usd { + +// Populates the absolute path, base name, and sanitized extension for an SBSAR asset by resolving +// the absolute path from the provided URI. +void +populatePathPartsFromAssetPath(const SdfAssetPath& path, + std::string& resolvedAssetPath, + std::string& name, + std::string& extension) +{ + // Make sure we have a resolved path, either coming from SdfAssetPath value or by running it + // throught the resolver. + resolvedAssetPath = path.GetResolvedPath().empty() + ? ArGetResolver().Resolve(path.GetAssetPath()) + : path.GetResolvedPath(); + // This will extract the inner most path to the asset: + // path/to/package.usdz[path/to/image.png] -> path/to/image.png + std::string innerAssetPath = getLayerFilePath(resolvedAssetPath); + // This helper function will detect "funky" paths, like those to SBSAR images and convert them + // to good usable file paths + std::string filePath = extractFilePathFromAssetPath(innerAssetPath); + // Strip the path part since we only want the filename and the extension + std::string baseName = TfGetBaseName(filePath); + name = TfStringGetBeforeSuffix(baseName); + extension = TfGetExtension(baseName); +} + +bool +readImage(ReadLayerContext& ctx, const SdfAssetPath& assetPath, int& index) +{ + std::string resolvedAssetPath, name, extension; + populatePathPartsFromAssetPath(assetPath, resolvedAssetPath, name, extension); + + // Check in the cache if we've processed this image before + if (const auto& it = ctx.images.find(resolvedAssetPath); it != ctx.images.end()) { + index = it->second; + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Image (cached): %s\n", + ctx.debugTag.c_str(), + resolvedAssetPath.c_str()); + return true; + } + + // The image is new. Make sure we don't get name collisions in the short name + if (const auto& itName = ctx.imageNames.find(name); itName != ctx.imageNames.end()) { + itName->second++; + name += "_" + std::to_string(itName->second); + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Deduplicated image name: %s\n", + ctx.debugTag.c_str(), + name.c_str()); + } else { + ctx.imageNames[name] = 1; + } + + auto [imageIndex, image] = ctx.usd->addImage(); + if (extension == "sbsarimage") { + // SBSAR images are a special cases where the data is stored raw and must be transcoded to a + // different image in memory + extension = getSbsarImageExtension(resolvedAssetPath); + image.uri = name + "." + extension; + transcodeImageAssetToMemory(resolvedAssetPath, image.uri, image.image); + } else { + auto asset = ArGetResolver().OpenAsset(ArResolvedPath(resolvedAssetPath)); + if (!asset) { + TF_WARN( + "%s: Unable to open asset: %s\n", ctx.debugTag.c_str(), resolvedAssetPath.c_str()); + return false; + } + image.uri = name + "." + extension; + image.image.resize(asset->GetSize()); + memcpy(image.image.data(), asset->GetBuffer().get(), asset->GetSize()); + } + + image.name = name; + image.format = getFormat(extension); + ctx.images[resolvedAssetPath] = imageIndex; + index = imageIndex; + + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Image (new): index: %d uri: %s\n", + ctx.debugTag.c_str(), + imageIndex, + resolvedAssetPath.c_str()); + + return true; +} + +void +applyInputMult(Input& input, float mult) +{ + if (mult == 1.0f) { + return; + } + + if (input.image != -1) { + input.scale *= mult; + } else if (input.value.IsHolding()) { + GfVec3f v = input.value.UncheckedGet(); + v *= mult; + input.value = v; + } else if (input.value.IsHolding()) { + float v = input.value.UncheckedGet(); + v *= mult; + input.value = v; + } +} + +template +bool +getShaderInputValue(const UsdShadeShader& shader, const TfToken& name, T& value) +{ + UsdShadeInput input = shader.GetInput(name); + if (input) { + UsdShadeAttributeVector valueAttrs = input.GetValueProducingAttributes(); + if (!valueAttrs.empty()) { + const UsdAttribute& attr = valueAttrs.front(); + if (UsdShadeUtils::GetType(attr.GetName()) == UsdShadeAttributeType::Input) { + valueAttrs.front().Get(&value); + return true; + } + } + } + return false; +} + +// Fetches the first value-producing attribute connected to a given shader input. +// If 'expectShader' is true, verify that the connected source is a shader and that the connection +// exists. Returns true and sets outAttribute if a suitable attribute is found. +bool +fetchPrimaryConnectedAttribute(const UsdShadeInput& shadeInput, + UsdAttribute& outAttribute, + bool expectShader) +{ + if (expectShader) { + if (!shadeInput.HasConnectedSource()) { + TF_WARN("Input %s has no connected source.", shadeInput.GetFullName().GetText()); + return false; + } + } + UsdShadeAttributeVector attrs = shadeInput.GetValueProducingAttributes(); + if (attrs.empty()) { + return false; + } + if (attrs.size() > 1) { + TF_WARN("Input %s is connected to multiple producing attributes, only the first will be " + "processed.", + shadeInput.GetFullName().GetText()); + } + outAttribute = attrs[0]; + if (expectShader) { + UsdShadeAttributeType attrType = UsdShadeUtils::GetType(outAttribute.GetName()); + if (attrType == UsdShadeAttributeType::Input) { + TF_WARN("Input %s is connected to an attribute that is not a shader.", + shadeInput.GetFullName().GetText()); + return false; + } + } + return true; +} + +// Handle texture-related shader inputs such as file paths and wrapping modes. +void +handleTextureShader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) +{ + SdfAssetPath assetPath; + if (getShaderInputValue(shader, AdobeTokens->file, assetPath)) { + readImage(ctx, assetPath, input.image); + } + getShaderInputValue(shader, AdobeTokens->wrapS, input.wrapS); + getShaderInputValue(shader, AdobeTokens->wrapT, input.wrapT); + getShaderInputValue(shader, AdobeTokens->minFilter, input.minFilter); + getShaderInputValue(shader, AdobeTokens->magFilter, input.magFilter); + getShaderInputValue(shader, AdobeTokens->scale, input.scale); + getShaderInputValue(shader, AdobeTokens->bias, input.bias); + getShaderInputValue(shader, AdobeTokens->sourceColorSpace, input.colorspace); + + // Default to 0th UVs unless overridden in handlePrimvarReader + input.uvIndex = 0; +} + +UsdShadeShader +handleTransformShader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) +{ + + UsdShadeShader nextShader; + getShaderInputValue(shader, AdobeTokens->rotation, input.uvRotation); + getShaderInputValue(shader, AdobeTokens->scale, input.uvScale); + getShaderInputValue(shader, AdobeTokens->translation, input.uvTranslation); + + UsdShadeInput stInputCoordReader = shader.GetInput(AdobeTokens->in); + UsdAttribute stSourcesInner; + if (fetchPrimaryConnectedAttribute(stInputCoordReader, stSourcesInner, true)) { + nextShader = UsdShadeShader(stSourcesInner.GetPrim()); + } + return nextShader; +} + +void +handlePrimvarReader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) +{ + TfToken texCoordPrimvar; + std::string texCoordPrimvarStr; + getShaderInputValue(shader, AdobeTokens->varname, texCoordPrimvarStr); + + // Supports both string and token type values for the varname + // string is the correct type, but token was added to support slightly + // incorrect assets. + if (!texCoordPrimvarStr.empty()) { + texCoordPrimvar = TfToken(texCoordPrimvarStr); + } else { + getShaderInputValue(shader, AdobeTokens->varname, texCoordPrimvar); + } + int uvIndex = getSTPrimvarTokenIndex(texCoordPrimvar); + if (uvIndex >= 0) { + input.uvIndex = uvIndex; + } else { + TF_WARN("Texture reader %s is reading primvar %s. Only 'st' or 'st1'..'stN' is supported", + shader.GetPrim().GetPath().GetText(), + texCoordPrimvar.GetText()); + } +} + +void +readInput(ReadLayerContext& ctx, const UsdShadeShader& surface, const TfToken& name, Input& input) +{ + UsdShadeInput shadeInput = surface.GetInput(name); + if (!shadeInput) { + return; + } + + UsdAttribute attr; + if (fetchPrimaryConnectedAttribute(shadeInput, attr, false)) { + UsdShadeSourceInfoVector sources = shadeInput.GetConnectedSources(); + + // Attempt to retrieve the constant value from the attribute. + auto [shadingAttrName, attrType] = UsdShadeUtils::GetBaseNameAndType(attr.GetName()); + if (attrType == UsdShadeAttributeType::Input) { + if (!attr.Get(&input.value)) { + TF_WARN("Failed to get constant value for input %s", name.GetText()); + return; + } + } else { + // Process the shader connected to this attribute + UsdShadeShader connectedShader(attr.GetPrim()); + TfToken shaderId; + connectedShader.GetShaderId(&shaderId); + + if (shaderId == AdobeTokens->UsdUVTexture) { + handleTextureShader(ctx, connectedShader, input); + + UsdShadeInput stInput = connectedShader.GetInput(AdobeTokens->st); + + // The name of the output on the texture reader determines which channel(s) of the + // texture we read. + input.channel = shadingAttrName; + + // Process the connected source of the 'st' input. + if (fetchPrimaryConnectedAttribute(stInput, attr, true)) { + VtValue srcValue; + if (attr.Get(&srcValue)) { + TF_WARN( + "Texture read shader does not support a fixed UV value for input %s", + name.GetText()); + } else { + // Handle the shader connected to the UV coordinate. + UsdShadeShader stShader(attr.GetPrim()); + stShader.GetShaderId(&shaderId); + + if (shaderId == AdobeTokens->UsdTransform2d) { + UsdShadeShader nextShader = handleTransformShader(ctx, stShader, input); + if (nextShader) { + stShader = nextShader; + stShader.GetShaderId(&shaderId); + } + } + + // This is not an "else if", since we can move the stShader + // if we encounter a UV transform. + if (shaderId == AdobeTokens->UsdPrimvarReader_float2) { + handlePrimvarReader(ctx, stShader, input); + } else { + TF_WARN("Unsupported shader type %s for UV input %s", + shaderId.GetText(), + name.GetText()); + } + } + } else { + TF_WARN("Failed to fetch connected attribute for UV input %s", name.GetText()); + } + } else { + TF_WARN( + "Unsupported shader type %s for input %s", shaderId.GetText(), name.GetText()); + } + } + } else { + // If no connections were found, get the shader's input value directly + if (!getShaderInputValue(surface, name, input.value)) { + TF_WARN("Failed to get input value for %s", name.GetText()); + } + } +} + +bool +readUsdPreviewSurfaceMaterial(ReadLayerContext& ctx, + Material& material, + const UsdShadeShader& surface) +{ + TfToken infoIdToken; + surface.GetShaderId(&infoIdToken); + if (infoIdToken != AdobeTokens->UsdPreviewSurface) { + return false; + } + + readInput( + ctx, surface, UsdPreviewSurfaceTokens->useSpecularWorkflow, material.useSpecularWorkflow); + readInput(ctx, surface, UsdPreviewSurfaceTokens->diffuseColor, material.diffuseColor); + readInput(ctx, surface, UsdPreviewSurfaceTokens->emissiveColor, material.emissiveColor); + readInput(ctx, surface, UsdPreviewSurfaceTokens->specularColor, material.specularColor); + readInput(ctx, surface, UsdPreviewSurfaceTokens->normal, material.normal); + readInput(ctx, surface, UsdPreviewSurfaceTokens->metallic, material.metallic); + readInput(ctx, surface, UsdPreviewSurfaceTokens->roughness, material.roughness); + readInput(ctx, surface, UsdPreviewSurfaceTokens->clearcoat, material.clearcoat); + readInput( + ctx, surface, UsdPreviewSurfaceTokens->clearcoatRoughness, material.clearcoatRoughness); + readInput(ctx, surface, UsdPreviewSurfaceTokens->opacity, material.opacity); + readInput(ctx, surface, UsdPreviewSurfaceTokens->opacityThreshold, material.opacityThreshold); + readInput(ctx, surface, UsdPreviewSurfaceTokens->displacement, material.displacement); + readInput(ctx, surface, UsdPreviewSurfaceTokens->occlusion, material.occlusion); + readInput(ctx, surface, UsdPreviewSurfaceTokens->ior, material.ior); + + return true; +} + +bool +_readClearcoatModelsTransmissionTint(const UsdShadeShader& surface) +{ + bool value = false; + // Check for a custom attribute that carries an indicator where the clearcoat came from + surface.GetPrim().GetAttribute(AdobeTokens->clearcoatModelsTransmissionTint).Get(&value); + return value; +} + +bool +_readUnlit(const UsdShadeShader& surface) +{ + bool value = false; + // Check for a custom attribute that carries an indicator where the clearcoat came from + surface.GetPrim().GetAttribute(AdobeTokens->unlit).Get(&value); + return value; +} + +bool +readASMMaterial(ReadLayerContext& ctx, Material& material, const UsdShadeShader& surface) +{ + TfToken infoIdToken; + surface.GetShaderId(&infoIdToken); + if (infoIdToken != AdobeTokens->adobeStandardMaterial) { + return false; + } + + material.clearcoatModelsTransmissionTint = _readClearcoatModelsTransmissionTint(surface); + material.isUnlit = _readUnlit(surface); + + // Note, we currently only support fixed values for emissiveIntensity and sheenOpacity + // No texture support yet. + float emissiveIntensity = 0.0f; + float sheenOpacity = 0.0f; + bool scatter = false; + + auto getConstShaderInput = [&](const TfToken& inputName, auto& var) { + VtValue val; + if (getShaderInputValue(surface, inputName, val)) { + if (val.IsHolding>()) { + var = val.UncheckedGet>(); + } + } + }; + + getConstShaderInput(AsmTokens->emissiveIntensity, emissiveIntensity); + getConstShaderInput(AsmTokens->sheenOpacity, sheenOpacity); + getConstShaderInput(AsmTokens->scatter, scatter); + + readInput(ctx, surface, AsmTokens->baseColor, material.diffuseColor); + readInput(ctx, surface, AsmTokens->roughness, material.roughness); + readInput(ctx, surface, AsmTokens->metallic, material.metallic); + readInput(ctx, surface, AsmTokens->opacity, material.opacity); + // Note, this is a specially supported attribute from UsdPreviewSurface that we transport via + // ASM, so that we do not loose this information + readInput(ctx, surface, UsdPreviewSurfaceTokens->opacityThreshold, material.opacityThreshold); + readInput(ctx, surface, AsmTokens->specularLevel, material.specularLevel); + readInput(ctx, surface, AsmTokens->specularEdgeColor, material.specularColor); + readInput(ctx, surface, AsmTokens->normal, material.normal); + readInput(ctx, surface, AsmTokens->normalScale, material.normalScale); + readInput(ctx, surface, AsmTokens->height, material.displacement); + readInput(ctx, surface, AsmTokens->anisotropyLevel, material.anisotropyLevel); + readInput(ctx, surface, AsmTokens->anisotropyAngle, material.anisotropyAngle); + if (emissiveIntensity > 0.0f) { + readInput(ctx, surface, AsmTokens->emissive, material.emissiveColor); + applyInputMult(material.emissiveColor, emissiveIntensity); + } + if (sheenOpacity > 0.0f) { + readInput(ctx, surface, AsmTokens->sheenColor, material.sheenColor); + // XXX sheenOpacity can't really be multiplied into the color. We currently drop this value + } + readInput(ctx, surface, AsmTokens->sheenRoughness, material.sheenRoughness); + readInput(ctx, surface, AsmTokens->translucency, material.transmission); + readInput(ctx, surface, AsmTokens->IOR, material.ior); + readInput(ctx, surface, AsmTokens->absorptionColor, material.absorptionColor); + readInput(ctx, surface, AsmTokens->absorptionDistance, material.absorptionDistance); + if (scatter) { + readInput(ctx, surface, AsmTokens->scatteringColor, material.scatteringColor); + readInput(ctx, surface, AsmTokens->scatteringDistance, material.scatteringDistance); + readInput(ctx, surface, AsmTokens->scatteringDistanceScale, material.scatteringDistanceScale); + } + readInput(ctx, surface, AsmTokens->coatOpacity, material.clearcoat); + readInput(ctx, surface, AsmTokens->coatColor, material.clearcoatColor); + readInput(ctx, surface, AsmTokens->coatRoughness, material.clearcoatRoughness); + readInput(ctx, surface, AsmTokens->coatIOR, material.clearcoatIor); + readInput(ctx, surface, AsmTokens->coatSpecularLevel, material.clearcoatSpecular); + readInput(ctx, surface, AsmTokens->coatNormal, material.clearcoatNormal); + readInput(ctx, surface, AsmTokens->ambientOcclusion, material.occlusion); + readInput(ctx, surface, AsmTokens->volumeThickness, material.volumeThickness); + + return true; +} + +bool +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 + // UsdPreviewSurface + UsdShadeShader surface = usdMaterial.ComputeSurfaceSource({ AdobeTokens->adobe }); + bool success = false; + if (surface) { + success = readASMMaterial(ctx, material, surface); + if (!success) { + success = readUsdPreviewSurfaceMaterial(ctx, material, surface); + } + } else { + TF_WARN("No surface shader for material %s", prim.GetPath().GetText()); + } + + printMaterial("layer::read", prim.GetPath(), material, ctx.debugTag); + return success; +} + +} \ No newline at end of file diff --git a/utils/src/layerWriteMaterial.cpp b/utils/src/layerWriteMaterial.cpp index 3d4055a8..f89bbfc3 100644 --- a/utils/src/layerWriteMaterial.cpp +++ b/utils/src/layerWriteMaterial.cpp @@ -49,6 +49,13 @@ _createFallbackValue(const VtValue& value) } } +// Convert a TfToken to a VtValue, but keep the VtValue empty if the TfToken was empty +VtValue +_checkToken(const TfToken& token) +{ + return token.IsEmpty() ? VtValue() : VtValue(token); +} + SdfPath _createStReader(SdfAbstractData* sdfData, const SdfPath& parentPath, int uvIndex) { @@ -71,8 +78,7 @@ _createStTransform(SdfAbstractData* sdfData, const Input& input, const SdfPath& stReaderResultPath) { - if (input.transformRotation.IsEmpty() && input.transformScale.IsEmpty() && - input.transformTranslation.IsEmpty()) { + if (input.hasDefaultTransform()) { return stReaderResultPath; } @@ -81,9 +87,9 @@ _createStTransform(SdfAbstractData* sdfData, TfToken(name + "_stTransform"), AdobeTokens->UsdTransform2d, "result", - { { "rotation", input.transformRotation }, - { "scale", input.transformScale }, - { "translation", input.transformTranslation } }, + { { "rotation", input.uvRotation }, + { "scale", input.uvScale }, + { "translation", input.uvTranslation } }, { { "in", stReaderResultPath } }); } @@ -103,17 +109,22 @@ _createTextureReader(SdfAbstractData* sdfData, // attribute on the material and connect all corresponding texture readers to that attribute // value. - // Make sure the colorSpace is an empty VtValue if the TfToken for colorspace is empty - VtValue colorSpace = input.colorspace.IsEmpty() ? VtValue() : VtValue(input.colorspace); - + // Only emit scale and bias if they are not the default values + VtValue scale, bias; + if (input.scale != kDefaultTexScale) { + scale = input.scale; + } + if (input.bias != kDefaultTexBias) { + bias = input.bias; + } InputValues inputValues = { { "fallback", _createFallbackValue(input.value) }, - { "sourceColorSpace", colorSpace }, - { "wrapS", input.wrapS }, - { "wrapT", input.wrapT }, - { "minFilter", input.minFilter }, - { "magFilter", input.magFilter }, - { "scale", input.scale }, - { "bias", input.bias } }; + { "sourceColorSpace", _checkToken(input.colorspace) }, + { "wrapS", _checkToken(input.wrapS) }, + { "wrapT", _checkToken(input.wrapT) }, + { "minFilter", _checkToken(input.minFilter) }, + { "magFilter", _checkToken(input.magFilter) }, + { "scale", scale }, + { "bias", bias } }; InputConnections inputConnections = { { "st", stResultPath }, { "file", textureConnection } }; return createShader(sdfData, @@ -195,7 +206,7 @@ _setupInput(WriteSdfContext& ctx, void writeUsdPreviewSurface(WriteSdfContext& ctx, const SdfPath& materialPath, - const Material& material, + const OpenPbrMaterial& material, MaterialInputs& materialInputs) { SdfPath p; @@ -226,23 +237,30 @@ writeUsdPreviewSurface(WriteSdfContext& ctx, materialInputs); }; - writeInput(AdobeTokens->useSpecularWorkflow, material.useSpecularWorkflow); - writeInput(AdobeTokens->diffuseColor, material.diffuseColor); - writeInput(AdobeTokens->emissiveColor, material.emissiveColor); - writeInput(AdobeTokens->specularColor, material.specularColor); - writeInput(AdobeTokens->normal, material.normal); - writeInput(AdobeTokens->metallic, material.metallic); - writeInput(AdobeTokens->roughness, material.roughness); - writeInput(AdobeTokens->clearcoat, material.clearcoat); - writeInput(AdobeTokens->clearcoatRoughness, material.clearcoatRoughness); - writeInput(AdobeTokens->opacity, material.opacity); - writeInput(AdobeTokens->opacityThreshold, material.opacityThreshold); - writeInput(AdobeTokens->displacement, material.displacement); - writeInput(AdobeTokens->occlusion, material.occlusion); - writeInput(AdobeTokens->ior, material.ior); + writeInput(UsdPreviewSurfaceTokens->diffuseColor, material.base_color); + // XXX Multiply with emission_luminance? Also, what about the units (OpenPBR is in nits)? + writeInput(UsdPreviewSurfaceTokens->emissiveColor, material.emission_color); + if (material.useSpecularWorkflow) { + writeInput(UsdPreviewSurfaceTokens->useSpecularWorkflow, Input{ VtValue(1) }); + } + writeInput(UsdPreviewSurfaceTokens->specularColor, material.specular_color); + writeInput(UsdPreviewSurfaceTokens->metallic, material.base_metalness); + writeInput(UsdPreviewSurfaceTokens->roughness, material.specular_roughness); + writeInput(UsdPreviewSurfaceTokens->clearcoat, material.coat_weight); + writeInput(UsdPreviewSurfaceTokens->clearcoatRoughness, material.coat_roughness); + writeInput(UsdPreviewSurfaceTokens->opacity, material.geometry_opacity); + // UsdPreviewSurfaceTokens->opacityMode (no source data) + if (material.opacityThreshold > 0.0f) { + writeInput(UsdPreviewSurfaceTokens->opacityThreshold, + Input{ VtValue(material.opacityThreshold) }); + } + writeInput(UsdPreviewSurfaceTokens->ior, material.specular_ior); + writeInput(UsdPreviewSurfaceTokens->normal, material.geometry_normal); + writeInput(UsdPreviewSurfaceTokens->displacement, material.displacement); + writeInput(UsdPreviewSurfaceTokens->occlusion, material.occlusion); // If we don't have opacity, but we do have transmission, we wire it into opacity - if (material.opacity.isEmpty() && !material.transmission.isEmpty()) { - writeInput(AdobeTokens->opacity, invertInput(material.transmission)); + if (material.geometry_opacity.isEmpty() && !material.transmission_weight.isEmpty()) { + writeInput(UsdPreviewSurfaceTokens->opacity, invertInput(material.transmission_weight)); } // Create UsdPreviewSurface shader @@ -272,7 +290,7 @@ writeUsdPreviewSurface(WriteSdfContext& ctx, void writeAsmMaterial(WriteSdfContext& ctx, const SdfPath& materialPath, - const Material& material, + const OpenPbrMaterial& material, MaterialInputs& materialInputs) { SdfPath p; @@ -304,63 +322,66 @@ writeAsmMaterial(WriteSdfContext& ctx, // Currently unused inputs // Input useSpecularWorkflow; - writeInput(AdobeTokens->baseColor, material.diffuseColor); - writeInput(AdobeTokens->roughness, material.roughness); - writeInput(AdobeTokens->metallic, material.metallic); - writeInput(AdobeTokens->opacity, material.opacity); - - // Note, ASM does not support an opacityThreshold. But without storing it here, the - // information is lost and can't be round tripped. So we store it, even though we know it - // won't affect the result of the material - writeInput(AdobeTokens->opacityThreshold, material.opacityThreshold); - writeInput(AdobeTokens->specularLevel, material.specularLevel); - // XXX should this be gated by material.useSpecularWorkflow? - writeInput(AdobeTokens->specularEdgeColor, material.specularColor); - writeInput(AdobeTokens->normal, material.normal); - writeInput(AdobeTokens->normalScale, material.normalScale); + writeInput(AsmTokens->baseColor, material.base_color); + writeInput(AsmTokens->roughness, material.specular_roughness); + writeInput(AsmTokens->metallic, material.base_metalness); + writeInput(AsmTokens->opacity, material.geometry_opacity); + writeInput(AsmTokens->specularLevel, material.specular_weight); + writeInput(AsmTokens->specularEdgeColor, material.specular_color); + writeInput(AsmTokens->normal, material.geometry_normal); + if (material.normalScale != 1.0f) { + writeInput(AsmTokens->normalScale, Input{ VtValue(material.normalScale) }); + } // combineNormalAndHeight = false (flag) (no source info) - writeInput(AdobeTokens->height, material.displacement); + writeInput(AsmTokens->height, material.displacement); // heightScale (no source info) // heightLevel (no source info) - writeInput(AdobeTokens->anisotropyLevel, material.anisotropyLevel); - writeInput(AdobeTokens->anisotropyAngle, material.anisotropyAngle); - - // Turn on emission if we have a valid input - if (!material.emissiveColor.isEmpty()) { - // The intensity is part of the emissive `scale` or `value` of the emissiveColor input - inputValues.emplace_back("emissiveIntensity", 1.0f); - } - writeInput(AdobeTokens->emissive, material.emissiveColor); - if (!material.sheenColor.isEmpty()) { - // XXX We currently turn the sheen fully on if the asset has a sheen color specified - inputValues.emplace_back("sheenOpacity", 1.0f); - } - writeInput(AdobeTokens->sheenColor, material.sheenColor); - writeInput(AdobeTokens->sheenRoughness, material.sheenRoughness); - writeInput(AdobeTokens->translucency, material.transmission); - writeInput(AdobeTokens->IOR, material.ior); - // dispersion (no source info) - writeInput(AdobeTokens->absorptionColor, material.absorptionColor); - writeInput(AdobeTokens->absorptionDistance, material.absorptionDistance); - if (!material.scatteringColor.isEmpty() || !material.scatteringDistance.isEmpty()) { + writeInput(AsmTokens->anisotropyLevel, material.specular_roughness_anisotropy); + // Note, this is just a pass through. OpenPBR does not support an anisotropy angle input + writeInput(AsmTokens->anisotropyAngle, material.anisotropyAngle); + writeInput(AsmTokens->emissiveIntensity, material.emission_luminance); + writeInput(AsmTokens->emissive, material.emission_color); + writeInput(AsmTokens->sheenOpacity, material.fuzz_weight); + writeInput(AsmTokens->sheenColor, material.fuzz_color); + writeInput(AsmTokens->sheenRoughness, material.fuzz_roughness); + writeInput(AsmTokens->translucency, material.transmission_weight); + writeInput(AsmTokens->IOR, material.specular_ior); + // XXX This is only correct when transmission_dispersion_abbe_number is at the default of 20 + writeInput(AsmTokens->dispersion, material.transmission_dispersion_scale); + writeInput(AsmTokens->absorptionColor, material.transmission_color); + writeInput(AsmTokens->absorptionDistance, material.transmission_depth); + // XXX subsurface_weight could be a textured floating point value. We currently don't have a + // way to express that with ASM + if (!material.subsurface_weight.isEmpty()) { inputValues.emplace_back("scatter", true); } - writeInput(AdobeTokens->scatteringColor, material.scatteringColor); - writeInput(AdobeTokens->scatteringDistance, material.scatteringDistance); - // scatteringDistanceScale (the scale is part of the scatteringDistance `scale` or `value`) + writeInput(AsmTokens->scatteringColor, material.subsurface_color); + writeInput(AsmTokens->scatteringDistance, material.subsurface_radius); + // XXX a precise value conversion is rather complicated + writeInput(AsmTokens->scatteringDistanceScale, material.subsurface_radius_scale); // scatteringRedShift (no source info) // scatteringRayleigh (no source info) - writeInput(AdobeTokens->coatOpacity, material.clearcoat); - writeInput(AdobeTokens->coatColor, material.clearcoatColor); - writeInput(AdobeTokens->coatRoughness, material.clearcoatRoughness); - writeInput(AdobeTokens->coatIOR, material.clearcoatIor); - writeInput(AdobeTokens->coatSpecularLevel, material.clearcoatSpecular); - writeInput(AdobeTokens->coatNormal, material.clearcoatNormal); + writeInput(AsmTokens->coatOpacity, material.coat_weight); + writeInput(AsmTokens->coatColor, material.coat_color); + writeInput(AsmTokens->coatRoughness, material.coat_roughness); + writeInput(AsmTokens->coatIOR, material.coat_ior); + // Note, this is just a pass through. OpenPBR does not support a coatSpecularLevel input + writeInput(AsmTokens->coatSpecularLevel, material.coatSpecularLevel); + writeInput(AsmTokens->coatNormal, material.geometry_coat_normal); // coatNormalScale (the scale is part of the coatNormal `scale` or `value`) - writeInput(AdobeTokens->ambientOcclusion, material.occlusion); - writeInput(AdobeTokens->volumeThickness, material.volumeThickness); + writeInput(AsmTokens->ambientOcclusion, material.occlusion); + // Note, this is just a pass through. OpenPBR does not support a volumeThickness input + writeInput(AsmTokens->volumeThickness, material.volumeThickness); // volumeThicknessScale (the scale is part of the volumeThickness `scale` or `value`) + // Note, ASM does not support an opacityThreshold. But without storing it here, the + // information is lost and can't be round tripped. So we store it, even though we know it + // won't affect the result of the material + if (material.opacityThreshold > 0.0f) { + writeInput(UsdPreviewSurfaceTokens->opacityThreshold, + Input{ VtValue(material.opacityThreshold) }); + } + // Create Adobe Standard Material shader SdfPath outputPath = createShader(ctx.sdfData, parentPath, @@ -372,11 +393,11 @@ writeAsmMaterial(WriteSdfContext& ctx, createShaderOutput( ctx.sdfData, materialPath, "adobe:surface", SdfValueTypeNames->Token, outputPath); + SdfPath surfaceShaderPath = parentPath.AppendChild(AdobeTokens->ASM); if (material.isUnlit) { - SdfPath p = createAttributeSpec(ctx.sdfData, - parentPath.AppendChild(AdobeTokens->ASM), - AdobeTokens->unlit, - SdfValueTypeNames->Bool); + // Author a custom attribute to leave an indicator that this material should be unlit + SdfPath p = createAttributeSpec( + ctx.sdfData, surfaceShaderPath, AdobeTokens->unlit, SdfValueTypeNames->Bool); setAttributeMetadata(ctx.sdfData, p, SdfFieldKeys->Custom, VtValue(true)); setAttributeDefaultValue(ctx.sdfData, p, true); } @@ -384,7 +405,7 @@ writeAsmMaterial(WriteSdfContext& ctx, if (material.clearcoatModelsTransmissionTint) { // Author a custom attribute to leave an indicator where the clearcoat came from SdfPath p = createAttributeSpec(ctx.sdfData, - parentPath.AppendChild(AdobeTokens->ASM), + surfaceShaderPath, AdobeTokens->clearcoatModelsTransmissionTint, SdfValueTypeNames->Bool); setAttributeMetadata(ctx.sdfData, p, SdfFieldKeys->Custom, VtValue(true)); diff --git a/utils/src/layerWriteMaterialX.cpp b/utils/src/layerWriteOpenPBR.cpp similarity index 68% rename from utils/src/layerWriteMaterialX.cpp rename to utils/src/layerWriteOpenPBR.cpp index 05b824d7..21eaf098 100644 --- a/utils/src/layerWriteMaterialX.cpp +++ b/utils/src/layerWriteOpenPBR.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 +#include #include #include @@ -50,39 +50,35 @@ _createMaterialXUvTransform(SdfAbstractData* sdfData, const Input& input, const SdfPath& uvReaderResultPath) { - if (input.transformRotation.IsEmpty() && input.transformScale.IsEmpty() && - input.transformTranslation.IsEmpty()) { + if (input.hasDefaultTransform()) { return uvReaderResultPath; } // For the place2d node, the scale is not a multiplier, but the overall scale and so we need to // invert the value - VtValue scale; - if (input.transformScale.IsHolding()) { - GfVec2f s = input.transformScale.UncheckedGet(); - s[0] = s[0] != 0.0f ? 1.0f / s[0] : 0.0f; - s[1] = s[1] != 0.0f ? 1.0f / s[1] : 0.0f; - scale = s; - } + GfVec2f scale = input.uvScale; + scale[0] = scale[0] != 0.0f ? 1.0f / scale[0] : 0.0f; + scale[1] = scale[1] != 0.0f ? 1.0f / scale[1] : 0.0f; // Create UV transform by applying scale, rotation and transform, in that order // This matches what the UsdTransform2d node does - return createShader(sdfData, - parentPath, - TfToken(name + "_uv_transform"), - MtlXTokens->ND_place2d_vector2, - "out", - { { "scale", scale }, - { "rotate", input.transformRotation }, - { "offset", input.transformTranslation } }, - { { "texcoord", uvReaderResultPath } }); + return createShader( + sdfData, + parentPath, + TfToken(name + "_uv_transform"), + MtlXTokens->ND_place2d_vector2, + "out", + { { "scale", scale }, { "rotate", input.uvRotation }, { "offset", input.uvTranslation } }, + { { "texcoord", uvReaderResultPath } }); ; } std::string _toMaterialXAddressMode(const TfToken& wrapMode) { - if (wrapMode == AdobeTokens->repeat) { + if (wrapMode.IsEmpty()) { + return "periodic"; + } else if (wrapMode == AdobeTokens->repeat) { return "periodic"; } else if (wrapMode == AdobeTokens->clamp) { return "clamp"; @@ -255,9 +251,7 @@ _createMaterialXTextureReader(SdfAbstractData* sdfData, {}, { { "in", textureOutput } }); } else { - if (!input.scale.IsEmpty() || !input.bias.IsEmpty()) { - GfVec4f scale4 = input.scale.GetWithDefault(GfVec4f(1.0f)); - GfVec4f bias4 = input.bias.GetWithDefault(GfVec4f(0.0f)); + if (!input.hasDefaultScaleAndBias()) { bool isColor = shaderType == MtlXTokens->ND_image_color3; textureOutput = _createScaleAndBiasNodes(sdfData, parentPath, @@ -265,8 +259,8 @@ _createMaterialXTextureReader(SdfAbstractData* sdfData, textureOutput, numChannels, isColor, - scale4, - bias4); + input.scale, + input.bias); } } @@ -284,16 +278,16 @@ _createMaterialXTextureReader(SdfAbstractData* sdfData, } void -_setupMaterialXInput(WriteSdfContext& ctx, - const SdfPath& materialPath, - const SdfPath& parentPath, - const TfToken& name, - const Input& input, - std::unordered_map& uvReaderResultPathMap, - InputValues& inputValues, - InputConnections& inputConnections, - const InputToMaterialInputTypeMap& inputRemapping, - MaterialInputs& materialInputs) +_setupOpenPbrInput(WriteSdfContext& ctx, + const SdfPath& materialPath, + const SdfPath& parentPath, + const TfToken& name, + const Input& input, + std::unordered_map& uvReaderResultPathMap, + InputValues& inputValues, + InputConnections& inputConnections, + const InputToMaterialInputTypeMap& inputRemapping, + MaterialInputs& materialInputs) { auto remappingIt = inputRemapping.find(name); bool hasMapping = remappingIt != inputRemapping.cend(); @@ -354,41 +348,34 @@ _setupMaterialXInput(WriteSdfContext& ctx, inputConnections.emplace_back(name.GetString(), texResultPath); } } else if (!input.value.IsEmpty()) { - // Set constant value on the surface shader directly - if (name == OpenPbrTokens->geometry_opacity) { - // geometry_opacity expects a color, but our input opacity is a float input - if (input.value.IsHolding()) { - // NOTE that here, we are not creating a connection to a material level opacity - // input variable do to the type difference. - float opacity = input.value.UncheckedGet(); - inputValues.emplace_back(name.GetString(), VtValue(GfVec3f(opacity))); - } else { - TF_WARN("Expect float value for constant opacity. Got type %s", - input.value.GetTypeName().c_str()); - } - } else { + if (!materialInputName.IsEmpty()) { + // Set constant value on material input and connect surface shader to that input SdfPath connection = addMaterialInputValue( ctx.sdfData, materialPath, materialInputName, inputType, input.value, materialInputs); inputConnections.emplace_back(name.GetString(), connection); const MinMaxVtValuePair* range = ShaderRegistry::getInstance().getMaterialInputRange(materialInputName); - if (range) + if (range) { setRangeMetadata(ctx.sdfData, connection, *range); + } + } else { + // If the input name is not valid, then just set the value + inputValues.emplace_back(name.GetString(), input.value); } } } void -writeMaterialX(WriteSdfContext& ctx, - const SdfPath& materialPath, - const Material& material, - MaterialInputs& materialInputs) +writeOpenPBR(WriteSdfContext& ctx, + const SdfPath& materialPath, + const OpenPbrMaterial& material, + MaterialInputs& materialInputs) { SdfPath p; // This will create a NodeGraph parent prim for all the shading nodes in this network SdfPath parentPath = - createPrimSpec(ctx.sdfData, materialPath, MtlXTokens->MaterialX, UsdShadeTokens->NodeGraph); + createPrimSpec(ctx.sdfData, materialPath, MtlXTokens->OpenPBR, UsdShadeTokens->NodeGraph); TF_DEBUG_MSG(FILE_FORMAT_UTIL, "layer::write MaterialX network %s\n", parentPath.GetText()); @@ -396,115 +383,76 @@ writeMaterialX(WriteSdfContext& ctx, InputConnections inputConnections; std::unordered_map uvReaderResultPathMap; const InputToMaterialInputTypeMap& remapping = - ShaderRegistry::getInstance().getMaterialXInputRemapping(); + ShaderRegistry::getInstance().getOpenPbrInputRemapping(); auto writeInput = [&](const TfToken& name, const Input& input) { if (!input.isEmpty()) - _setupMaterialXInput(ctx, - materialPath, - parentPath, - name, - input, - uvReaderResultPathMap, - inputValues, - inputConnections, - remapping, - materialInputs); + _setupOpenPbrInput(ctx, + materialPath, + parentPath, + name, + input, + uvReaderResultPathMap, + inputValues, + inputConnections, + remapping, + materialInputs); }; - // OpenPBR spec: - // https://github.com/AcademySoftwareFoundation/OpenPBR/blob/main/reference/open_pbr_surface.mtlx - - // Currently unused inputs - // Input useSpecularWorkflow; - // Input clearcoatSpecular; - // Input displacement; - // Input opacityThreshold; - // Input occlusion; - // Input volumeThickness; - - // base - // base_weight (no source info) - writeInput(OpenPbrTokens->base_color, material.diffuseColor); - // XXX we're not setting base_roughness? Should we when metallic != 0? - // "Roughness of the diffuse reflection. Higher values cause the surface to appear flatter." - // writeInput(OpenPbrTokens->base_roughness, material.roughness); - writeInput(OpenPbrTokens->base_metalness, material.metallic); - - // specular - writeInput(OpenPbrTokens->specular_weight, material.specularLevel); - writeInput(OpenPbrTokens->specular_color, material.specularColor); - writeInput(OpenPbrTokens->specular_roughness, material.roughness); - writeInput(OpenPbrTokens->specular_ior, material.ior); - // specular_ior_level (no source info) - writeInput(OpenPbrTokens->specular_anisotropy, material.anisotropyLevel); - // XXX it's unclear if the angle we got for the ASM model works with the OpenPBR rotation - writeInput(OpenPbrTokens->specular_rotation, material.anisotropyAngle); - - // transmission - writeInput(OpenPbrTokens->transmission_weight, material.transmission); - writeInput(OpenPbrTokens->transmission_color, material.absorptionColor); - writeInput(OpenPbrTokens->transmission_depth, material.absorptionDistance); - // transmission_scatter (no source info) - // transmission_scatter_anisotropy (no source info) - // transmission_dispersion (no source info) - - // subsurface - if (!material.scatteringColor.isEmpty() || !material.scatteringDistance.isEmpty()) { - // XXX We currently turn the subsurface fully on if the asset has a scattering color or - // distance specified - inputValues.emplace_back("subsurface_weight", 1.0f); - } - writeInput(OpenPbrTokens->subsurface_color, material.scatteringColor); - writeInput(OpenPbrTokens->subsurface_radius, material.scatteringDistance); - // subsurface_radius_scale (no source info) (maps to ASM scatteringDistanceScale) - // subsurface_anisotropy (no source info) - - // fuzz - if (!material.sheenColor.isEmpty()) { - // XXX We currently turn the fuzz fully on if the asset has a sheen color specified - inputValues.emplace_back("fuzz_weight", 1.0f); - } - writeInput(OpenPbrTokens->fuzz_color, material.sheenColor); - writeInput(OpenPbrTokens->fuzz_roughness, material.sheenRoughness); - - // coat - // XXX How does clearcoatSpecular fit into this lobe? coat_ior_level? - writeInput(OpenPbrTokens->coat_weight, material.clearcoat); - writeInput(OpenPbrTokens->coat_color, material.clearcoatColor); - writeInput(OpenPbrTokens->coat_roughness, material.clearcoatRoughness); - // coat_anisotropy (no source info) - // coat_rotation (no source info) - writeInput(OpenPbrTokens->coat_ior, material.clearcoatIor); - // coat_ior_level (no source info) - - // thin_film - // thin_film_thickness (no source info) - // thin_film_ior (no source info) - - // emission - if (!material.emissiveColor.isEmpty()) { - // The luminance is currently part of of the `scale` or `value` of the - // emissiveColor input - inputValues.emplace_back("emission_luminance", 1.0f); - } - writeInput(OpenPbrTokens->emission_color, material.emissiveColor); - - // geometry - writeInput(OpenPbrTokens->geometry_opacity, material.opacity); - // geometry_thin_walled (no source info) - writeInput(OpenPbrTokens->geometry_normal, material.normal); - writeInput(OpenPbrTokens->geometry_coat_normal, material.clearcoatNormal); - // geometry_tangent (no source info) +#define INPUT(x) writeInput(OpenPbrTokens->x, material.x); + INPUT(base_weight); + INPUT(base_color); + INPUT(base_diffuse_roughness); + INPUT(base_metalness); + INPUT(specular_weight); + INPUT(specular_color); + INPUT(specular_roughness); + INPUT(specular_ior); + INPUT(specular_roughness_anisotropy); + INPUT(transmission_weight); + INPUT(transmission_color); + INPUT(transmission_depth); + INPUT(transmission_scatter); + INPUT(transmission_scatter_anisotropy); + INPUT(transmission_dispersion_scale); + INPUT(transmission_dispersion_abbe_number); + INPUT(subsurface_weight); + INPUT(subsurface_color); + INPUT(subsurface_radius); + INPUT(subsurface_radius_scale); + INPUT(subsurface_scatter_anisotropy); + INPUT(fuzz_weight); + INPUT(fuzz_color); + INPUT(fuzz_roughness); + INPUT(coat_weight); + INPUT(coat_color); + INPUT(coat_roughness); + INPUT(coat_roughness_anisotropy); + INPUT(coat_ior); + INPUT(coat_darkening); + INPUT(thin_film_weight); + INPUT(thin_film_thickness); + INPUT(thin_film_ior); + INPUT(emission_luminance); + INPUT(emission_color); + INPUT(geometry_opacity); + INPUT(geometry_thin_walled); + INPUT(geometry_normal); + INPUT(geometry_coat_normal); + INPUT(geometry_tangent); + INPUT(geometry_coat_tangent); +#undef INPUT // Create OpenPBR surface shader SdfPath outputPath = createShader(ctx.sdfData, parentPath, - MtlXTokens->MaterialX, + MtlXTokens->OpenPBR, MtlXTokens->ND_open_pbr_surface_surfaceshader, "out", inputValues, inputConnections); createShaderOutput( ctx.sdfData, materialPath, "mtlx:surface", SdfValueTypeNames->Token, outputPath); + + // TODO: create displacement setup } } diff --git a/utils/src/layerWriteSdfData.cpp b/utils/src/layerWriteSdfData.cpp index 4d07d3b2..881f2d66 100644 --- a/utils/src/layerWriteSdfData.cpp +++ b/utils/src/layerWriteSdfData.cpp @@ -15,7 +15,7 @@ governing permissions and limitations under the License. #include #include #include -#include +#include #include #include #include @@ -554,6 +554,7 @@ _writePrimvars(SdfAbstractData* sdfData, const SdfPath& primPath, const Mesh& me } _writePrimvar(sdfData, primPath, "normals", SdfValueTypeNames->Normal3fArray, mesh.normals); _writePrimvar(sdfData, primPath, "tangents", SdfValueTypeNames->Float4Array, mesh.tangents); + _writePrimvar(sdfData, primPath, "bitangents", SdfValueTypeNames->Float3Array, mesh.bitangents); } auto indexedName = [](const std::string& baseName, int index) -> std::string { @@ -890,11 +891,10 @@ _writeNurb(SdfAbstractData* sdfData, const SdfPath& parentPath, NurbData& nurb) } SdfPath -_writeCurve(WriteSdfContext& ctx, - const SdfPath& parentPath, - const Curve& curve) +_writeCurve(WriteSdfContext& ctx, const SdfPath& parentPath, const Curve& curve) { - SdfPath primPath = createPrimSpec(ctx.sdfData, parentPath, TfToken(curve.name), UsdGeomTokens->BasisCurves); + SdfPath primPath = + createPrimSpec(ctx.sdfData, parentPath, TfToken(curve.name), UsdGeomTokens->BasisCurves); TF_DEBUG_MSG(FILE_FORMAT_UTIL, "write curve: path=%s\n", primPath.GetString().c_str()); auto createAttr = [&](const TfToken& name, @@ -934,9 +934,10 @@ _writeCurve(WriteSdfContext& ctx, cvc[i] = 4; } createAttr(UsdGeomTokens->curveVertexCounts, SdfValueTypeNames->IntArray, cvc); - } - else { - createAttr(UsdGeomTokens->curveVertexCounts, SdfValueTypeNames->IntArray, PXR_NS::VtArray{npts}); + } else { + createAttr(UsdGeomTokens->curveVertexCounts, + SdfValueTypeNames->IntArray, + PXR_NS::VtArray{ npts }); } #if 0 @@ -1220,7 +1221,7 @@ _writeSkeletonAnimation(SdfAbstractData* sdfData, } SdfPath -_writeMaterial(WriteSdfContext& ctx, const SdfPath& parentPath, const Material& material) +_writeMaterial(WriteSdfContext& ctx, const SdfPath& parentPath, const OpenPbrMaterial& material) { SdfPath materialPath = createMaterialPrimSpec(ctx.sdfData, parentPath, TfToken(material.name)); @@ -1229,8 +1230,6 @@ _writeMaterial(WriteSdfContext& ctx, const SdfPath& parentPath, const Material& ctx.sdfData, materialPath, SdfFieldKeys->DisplayName, VtValue(material.displayName)); } - printMaterial("layer::write", materialPath, material, ctx.debugTag); - TF_DEBUG_MSG(FILE_FORMAT_UTIL, "layer::write material '%s' to %s\n", material.name.c_str(), @@ -1238,17 +1237,21 @@ _writeMaterial(WriteSdfContext& ctx, const SdfPath& parentPath, const Material& MaterialInputs materialInputs; - // Generate a UsdPreviewSurface based material network - writeUsdPreviewSurface(ctx, materialPath, material, materialInputs); + if (ctx.options->writeUsdPreviewSurface) { + // Generate a UsdPreviewSurface based material network + writeUsdPreviewSurface(ctx, materialPath, material, materialInputs); + } #ifdef USD_FILEFORMATS_ENABLE_ASM - // Generate a ASM based material network - writeAsmMaterial(ctx, materialPath, material, materialInputs); + if (ctx.options->writeASM) { + // Generate a ASM based material network + writeAsmMaterial(ctx, materialPath, material, materialInputs); + } #endif // USD_FILEFORMATS_ENABLE_ASM - if (ctx.options->writeMaterialX) { - // Generate a MaterialX based material network - writeMaterialX(ctx, materialPath, material, materialInputs); + if (ctx.options->writeOpenPBR) { + // Generate a MaterialX based material network for OpenPBR + writeOpenPBR(ctx, materialPath, material, materialInputs); } return materialPath; @@ -1457,7 +1460,12 @@ _writeLayerSdfData(const WriteLayerOptions& options, SdfPath materialsPath = createPrimSpec(sdfData, rootNodePath, materialsPrimName); int i = 0; for (const Material& material : usdData.materials) { - ctx.materialMap[i++] = _writeMaterial(ctx, materialsPath, material); + const OpenPbrMaterial openPbrMaterial = + mapMaterialStructToOpenPbrMaterialStruct(material); + ctx.materialMap[i++] = _writeMaterial(ctx, materialsPath, openPbrMaterial); + + SdfPath materialPath = materialsPath.AppendChild(TfToken(material.name)); + printMaterial("layer::write", materialPath, material, ctx.debugTag); } } @@ -1478,7 +1486,7 @@ _writeLayerSdfData(const WriteLayerOptions& options, } } - // Write skeletons after nodes, as we want skeletons to be parented to the nodes + // Write skeletons after nodes, as we sometimes want skeletons to be parented to the nodes if (!usdData.skeletons.empty()) { ctx.skeletonMap.resize(usdData.skeletons.size()); @@ -1487,7 +1495,10 @@ _writeLayerSdfData(const WriteLayerOptions& options, // Create a SkelRoot to host the skeleton, the skeleton animation (if present) and any // related meshes - const SdfPath& skelParentPath = ctx.nodeMap[skeleton.parent]; + // If the skeleton has a parent, use that- otherwise, don't use a parent (similar to a + // root node) + const SdfPath& skelParentPath = + (skeleton.parent == -1) ? rootNodePath : ctx.nodeMap[skeleton.parent]; std::string skelRootName = skeleton.name + "_SkelRoot"; SdfPath skelRootPath = createPrimSpec( diff --git a/utils/src/layerWriteShared.cpp b/utils/src/layerWriteShared.cpp index dadc1aba..84c24051 100644 --- a/utils/src/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 #include -#include #include #include @@ -115,4 +115,112 @@ createTexturePath(const std::string& srcAssetFilename, const std::string& imageU return srcAssetFilename.empty() ? imageUri : srcAssetFilename + "[" + imageUri + "]"; } +OpenPbrMaterial +mapMaterialStructToOpenPbrMaterialStruct(const Material& material) +{ + const bool scatter = + !material.scatteringColor.isEmpty() || !material.scatteringDistance.isEmpty(); + const bool fuzz = !material.sheenColor.isEmpty(); + const bool emission = !material.emissiveColor.isEmpty(); + + OpenPbrMaterial result; + result.name = material.name; + result.displayName = material.displayName; + + // OpenPBR spec: + /// This is based on OpenPBR 1.0 + /// https://github.com/AcademySoftwareFoundation/OpenPBR/blob/44fe76650880914980402221672446ad44df15bd/reference/open_pbr_surface.mtlx + /// + /// The latest version can be found here (currently at 1.1) + /// https://github.com/AcademySoftwareFoundation/OpenPBR/blob/main/reference/open_pbr_surface.mtlx + + // Julien Guertault and Peter Kutz have written a guide to convert from ASM to OpenPBR. + // Note, that the code below does not implement any value remapping as described in this + // document. We only use the rough input-to-input mapping that is derived from it. + + // base + // base_weight (no source info) + result.base_color = material.diffuseColor; + // base_diffuse_roughness (no source info) Note, this is a diffuse roughness + result.base_metalness = material.metallic; + + // specular + result.specular_weight = material.specularLevel; + result.specular_color = material.specularColor; + result.specular_roughness = material.roughness; + result.specular_ior = material.ior; + result.specular_roughness_anisotropy = material.anisotropyLevel; + + // transmission + // TODO consider scatter + result.transmission_weight = material.transmission; + result.transmission_color = material.absorptionColor; + result.transmission_depth = material.absorptionDistance; + // transmission_scatter (no source info) + // transmission_scatter_anisotropy (no source info) + // transmission_dispersion_scale (no source info) + // transmission_dispersion_abbe_number (no source info) + + // subsurface + result.subsurface_weight = Input{ scatter ? VtValue(1.0f) : VtValue() }; + result.subsurface_color = material.scatteringColor; + result.subsurface_radius = material.scatteringDistance; + result.subsurface_radius_scale = material.scatteringDistanceScale; + + // fuzz + result.fuzz_weight = Input{ fuzz ? VtValue(1.0f) : VtValue() }; + result.fuzz_color = material.sheenColor; + result.fuzz_roughness = material.sheenRoughness; + + // coat + result.coat_weight = material.clearcoat; + result.coat_color = material.clearcoatColor; + result.coat_roughness = material.clearcoatRoughness; + // coat_roughness_anisotropy (no source info) + result.coat_ior = material.clearcoatIor; + // coat_darkening (no source info) + + // thin_film + // thin_film_weight (no source info) + // thin_film_thickness (no source info) + // thin_film_ior (no source info) + + // emission + result.emission_luminance = Input{ emission ? VtValue(1.0f) : VtValue() }; + result.emission_color = material.emissiveColor; + + // geometry + result.geometry_opacity = material.opacity; + // geometry_thin_walled (no source info) + result.geometry_normal = material.normal; + result.geometry_coat_normal = material.clearcoatNormal; + // geometry_tangent (no source info) + // geometry_coat_tangent (no source info) + + // Non-OpenPBR inputs + result.displacement = material.displacement; + result.occlusion = material.occlusion; + result.anisotropyAngle = material.anisotropyAngle; + result.coatSpecularLevel = material.clearcoatSpecular; + result.volumeThickness = material.volumeThickness; + if (!material.normalScale.isEmpty() && material.normalScale.value.IsHolding()) { + result.normalScale = material.normalScale.value.UncheckedGet(); + } + if (!material.useSpecularWorkflow.isEmpty() && + material.useSpecularWorkflow.value.IsHolding()) { + result.useSpecularWorkflow = material.useSpecularWorkflow.value.UncheckedGet() != 0; + } + if (!material.opacityThreshold.isEmpty() && + material.opacityThreshold.value.IsHolding()) { + float threshold = material.opacityThreshold.value.UncheckedGet(); + if (threshold > 0.0f) { + result.opacityThreshold = threshold; + } + } + result.clearcoatModelsTransmissionTint = material.clearcoatModelsTransmissionTint; + result.isUnlit = material.isUnlit; + + return result; +} + } diff --git a/utils/src/materials.cpp b/utils/src/materials.cpp index 23d501f0..36ad2394 100644 --- a/utils/src/materials.cpp +++ b/utils/src/materials.cpp @@ -322,20 +322,8 @@ InputTranslator::translateToSingleAffine(const std::string& name, out = in; // apply scale and bias to source scale and bias - GfVec4f srcScale = in.scale.GetWithDefault(GfVec4f(1.0f)); - GfVec4f srcBias = in.bias.GetWithDefault(GfVec4f(0.0f)); - GfVec4f newscale = scale * srcScale; - GfVec4f newbias = scale * srcBias + GfVec4f(bias); - if (newscale != GfVec4f(1.0f)) { - out.scale = newscale; - } else { - out.scale = VtValue(); - } - if (newbias != GfVec4f(0.0f)) { - out.bias = newbias; - } else { - out.bias = VtValue(); - } + out.scale = scale * in.scale; + out.bias = scale * in.bias + GfVec4f(bias); return true; } return false; @@ -423,41 +411,33 @@ InputTranslator::translateFactor(const Input& in, translateDirect(in, out, intermediate); float f = factor.value.GetWithDefault(1.0f); if (f != 1.0f) { - if (out.scale.IsHolding()) { - GfVec4f scale = out.scale.UncheckedGet(); - scale *= f; - out.scale = scale; - } else { - out.scale = GfVec4f(f); - } + out.scale *= f; } } else if (factor.image >= 0) { // factor is an image and the in input is a constant value translateDirect(factor, out, intermediate); - GfVec4f scale = out.scale.GetWithDefault(GfVec4f(1.0f)); if (in.value.IsHolding()) { float f = in.value.UncheckedGet(); - scale *= f; + out.scale *= f; } else if (in.value.IsHolding()) { const GfVec2f& v = in.value.UncheckedGet(); - scale[0] *= v[0]; - scale[1] *= v[1]; + out.scale[0] *= v[0]; + out.scale[1] *= v[1]; } else if (in.value.IsHolding()) { const GfVec3f& v = in.value.UncheckedGet(); - scale[0] *= v[0]; - scale[1] *= v[1]; - scale[2] *= v[2]; + out.scale[0] *= v[0]; + out.scale[1] *= v[1]; + out.scale[2] *= v[2]; } else if (in.value.IsHolding()) { const GfVec4f& v = in.value.UncheckedGet(); - scale[0] *= v[0]; - scale[1] *= v[1]; - scale[2] *= v[2]; - scale[3] *= v[3]; + out.scale[0] *= v[0]; + out.scale[1] *= v[1]; + out.scale[2] *= v[2]; + out.scale[3] *= v[3]; } else { TF_DEBUG_MSG(FILE_FORMAT_UTIL, "translateFactor in input is not holding a float value\n"); } - out.scale = scale; } else { // Both inputs are constant values if (factor.value.IsHolding()) { @@ -499,11 +479,9 @@ InputTranslator::extractChannel(const std::string& name, return true; } - GfVec4f srcScale = in.scale.GetWithDefault(GfVec4f(1.0f)); - GfVec4f srcBias = in.bias.GetWithDefault(GfVec4f(0.0f)); // apply scale and bias to source channel scale and bias - float newscale = scale * srcScale[channelIndex]; - float newbias = scale * srcBias[channelIndex] + bias; + float newscale = scale * in.scale[channelIndex]; + float newbias = scale * in.bias[channelIndex] + bias; if (in.image >= 0) { const ImageAsset& inImageAsset = mImagesSrc[in.image]; @@ -525,8 +503,8 @@ InputTranslator::extractChannel(const std::string& name, bool result = translateDirect(in, out, false); if (result) { out.channel = AdobeTokens->r; - out.scale = VtValue(); - out.bias = VtValue(); + out.scale = kDefaultTexScale; + out.bias = kDefaultTexBias; } return result; } else { @@ -555,8 +533,8 @@ InputTranslator::extractChannel(const std::string& name, } // Clear the scale and bias since it was applied to the pixel values and constants - out.scale = VtValue(); - out.bias = VtValue(); + out.scale = kDefaultTexScale; + out.bias = kDefaultTexBias; return true; } @@ -602,8 +580,8 @@ InputTranslator::translateAffine(const std::string& name, } // Clear the scale and bias since it was applied to the pixel values and the constants - out.scale = VtValue(); - out.bias = VtValue(); + out.scale = kDefaultTexScale; + out.bias = kDefaultTexBias; return true; } @@ -835,19 +813,17 @@ bool InputTranslator::translateOpacity2Transparency(const Input& opacity, Input& transparency) { if (opacity.image >= 0) { - GfVec4f srcScale = opacity.scale.GetWithDefault(GfVec4f(1.0f)); - GfVec4f srcBias = opacity.bias.GetWithDefault(GfVec4f(0.0f)); int channelIndex = token2Channel(opacity.channel); if (channelIndex < 0) channelIndex = 0; - float newscale = -1.0f * srcScale[channelIndex]; - float newbias = 1.0 - srcBias[channelIndex]; + float newscale = -1.0f * opacity.scale[channelIndex]; + float newbias = 1.0 - opacity.bias[channelIndex]; // if there is already an inversion applied, we don't need to do anything if (newscale == 1.0f && newbias == 0.0f) { bool result = translateDirect(opacity, transparency, false); - transparency.scale = VtValue(); - transparency.bias = VtValue(); + transparency.scale = kDefaultTexScale; + transparency.bias = kDefaultTexBias; return result; } else { // invert the source scale/bias and apply to source opacity image to get new @@ -886,23 +862,28 @@ InputTranslator::translateAmbient2Occlusion(const Input& ambient, Input& occlusi void _collect2DTransformValues(const Input& input, - std::vector& rotations, - std::vector& scales, - std::vector& translations) + std::vector& rotations, + std::vector& scales, + std::vector& translations) { // We are only interested in 2d transform values when there is a texture if (input.image >= 0) { - rotations.push_back(input.transformRotation); - scales.push_back(input.transformScale); - translations.push_back(input.transformTranslation); + rotations.push_back(input.uvRotation); + scales.push_back(input.uvScale); + translations.push_back(input.uvTranslation); } } +template bool -_valuesAreEqual(const std::vector& values) +_valuesAreEqual(const std::vector& values) { + if (values.empty()) { + return true; + } + T firstValue = values[0]; for (size_t i = 1; i < values.size(); ++i) { - if (values[0] != values[i]) + if (firstValue != values[i]) return false; } return true; @@ -1000,32 +981,14 @@ InputTranslator::translateMix(const std::string& name, out.wrapS = AdobeTokens->repeat; out.wrapT = AdobeTokens->repeat; out.colorspace = colorspace; - if (!in0.scale.IsEmpty() || !in1.scale.IsEmpty() || !in2.scale.IsEmpty() || - !in3.scale.IsEmpty()) { - float scale0 = - in0.scale.IsHolding() ? in0.scale.UncheckedGet()[0] : 1; - float scale1 = - in1.scale.IsHolding() ? in1.scale.UncheckedGet()[1] : 1; - float scale2 = - in2.scale.IsHolding() ? in2.scale.UncheckedGet()[2] : 1; - float scale3 = - in3.scale.IsHolding() ? in3.scale.UncheckedGet()[3] : 1; - out.scale = GfVec4f(scale0, scale1, scale2, scale3); - } - if (!in0.bias.IsEmpty() || !in1.bias.IsEmpty() || !in2.bias.IsEmpty() || - !in3.bias.IsEmpty()) { - float bias0 = in0.bias.IsHolding() ? in0.bias.UncheckedGet()[0] : 0; - float bias1 = in1.bias.IsHolding() ? in1.bias.UncheckedGet()[1] : 0; - float bias2 = in2.bias.IsHolding() ? in2.bias.UncheckedGet()[2] : 0; - float bias3 = in3.bias.IsHolding() ? in3.bias.UncheckedGet()[3] : 0; - out.bias = GfVec4f(bias0, bias1, bias2, bias3); - } + out.scale = GfVec4f(in0.scale[0], in1.scale[1], in2.scale[2], in3.scale[3]); + out.bias = GfVec4f(in0.bias[0], in1.bias[1], in2.bias[2], in3.bias[3]); // collect all the 2d transforms for each input into separate arrays so we can // check each set for equality and then assign to the output or issue a warning. - std::vector rotations; - std::vector scales; - std::vector translations; + std::vector rotations; + std::vector scales; + std::vector translations; rotations.reserve(4); scales.reserve(4); translations.reserve(4); @@ -1035,19 +998,25 @@ InputTranslator::translateMix(const std::string& name, _collect2DTransformValues(in3, rotations, scales, translations); if (_valuesAreEqual(rotations)) { - out.transformRotation = (rotations.size() > 0) ? rotations[0] : VtValue(); + if (!rotations.empty()) { + out.uvRotation = rotations[0]; + } } else { - TF_WARN("Cannot copy transformRotation as inputs differ."); + TF_WARN("Cannot copy uvRotation as inputs differ."); } if (_valuesAreEqual(scales)) { - out.transformScale = (scales.size() > 0) ? scales[0] : VtValue(); + if (!scales.empty()) { + out.uvScale = scales[0]; + } } else { - TF_WARN("Cannot copy transformScale as inputs differ."); + TF_WARN("Cannot copy uvScale as inputs differ."); } if (_valuesAreEqual(translations)) { - out.transformTranslation = (translations.size() > 0) ? translations[0] : VtValue(); + if (!translations.empty()) { + out.uvTranslation = translations[0]; + } } else { - TF_WARN("Cannot copy transformTranslation as inputs differ."); + TF_WARN("Cannot copy uvTranslation as inputs differ."); } } return true; diff --git a/utils/src/sdfMaterialUtils.cpp b/utils/src/sdfMaterialUtils.cpp index bf7e256c..7eb8ee98 100644 --- a/utils/src/sdfMaterialUtils.cpp +++ b/utils/src/sdfMaterialUtils.cpp @@ -422,60 +422,48 @@ ShaderRegistry::ShaderRegistry() }, { { TfToken("outputs:out"), SdfValueTypeNames->Float3 } }}}, - // Note, the ND_adobe_standard_material will be retired soon in favor of the OpenPBR node - { MtlXTokens->ND_adobe_standard_material, {{ - { TfToken("inputs:base_color"), SdfValueTypeNames->Color3f }, - { TfToken("inputs:ambient_occlusion"), SdfValueTypeNames->Float }, - { TfToken("inputs:roughness"), SdfValueTypeNames->Float }, - { TfToken("inputs:metallic"), SdfValueTypeNames->Float }, - { TfToken("inputs:normal"), SdfValueTypeNames->Float3 }, - { TfToken("inputs:opacity"), SdfValueTypeNames->Float }, - { TfToken("inputs:emission_color"), SdfValueTypeNames->Color3f } - }, { - { TfToken("outputs:surface"), SdfValueTypeNames->Token } - }}}, { MtlXTokens->ND_open_pbr_surface_surfaceshader, {{ { TfToken("inputs:base_weight"), SdfValueTypeNames->Float }, { TfToken("inputs:base_color"), SdfValueTypeNames->Color3f }, - { TfToken("inputs:base_roughness"), SdfValueTypeNames->Float }, + { TfToken("inputs:base_diffuse_roughness"), SdfValueTypeNames->Float }, { TfToken("inputs:base_metalness"), SdfValueTypeNames->Float }, { TfToken("inputs:specular_weight"), SdfValueTypeNames->Float }, { TfToken("inputs:specular_color"), SdfValueTypeNames->Color3f }, { TfToken("inputs:specular_roughness"), SdfValueTypeNames->Float }, { TfToken("inputs:specular_ior"), SdfValueTypeNames->Float }, - { TfToken("inputs:specular_ior_level"), SdfValueTypeNames->Float }, - { TfToken("inputs:specular_anisotropy"), SdfValueTypeNames->Float }, - { TfToken("inputs:specular_rotation"), SdfValueTypeNames->Float }, + { TfToken("inputs:specular_roughness_anisotropy"), SdfValueTypeNames->Float }, { TfToken("inputs:transmission_weight"), SdfValueTypeNames->Float }, { TfToken("inputs:transmission_color"), SdfValueTypeNames->Color3f }, { TfToken("inputs:transmission_depth"), SdfValueTypeNames->Float }, { TfToken("inputs:transmission_scatter"), SdfValueTypeNames->Color3f }, { TfToken("inputs:transmission_scatter_anisotropy"), SdfValueTypeNames->Float }, - { TfToken("inputs:transmission_dispersion"), SdfValueTypeNames->Float }, + { TfToken("inputs:transmission_dispersion_scale"), SdfValueTypeNames->Float }, + { TfToken("inputs:transmission_dispersion_abbe_number"), SdfValueTypeNames->Float }, { TfToken("inputs:subsurface_weight"), SdfValueTypeNames->Float }, { TfToken("inputs:subsurface_color"), SdfValueTypeNames->Color3f }, { TfToken("inputs:subsurface_radius"), SdfValueTypeNames->Float }, { TfToken("inputs:subsurface_radius_scale"), SdfValueTypeNames->Color3f }, - { TfToken("inputs:subsurface_anisotropy"), SdfValueTypeNames->Float }, + { TfToken("inputs:subsurface_scatter_anisotropy"), SdfValueTypeNames->Float }, { TfToken("inputs:fuzz_weight"), SdfValueTypeNames->Float }, { TfToken("inputs:fuzz_color"), SdfValueTypeNames->Color3f }, { TfToken("inputs:fuzz_roughness"), SdfValueTypeNames->Float }, { TfToken("inputs:coat_weight"), SdfValueTypeNames->Float }, { TfToken("inputs:coat_color"), SdfValueTypeNames->Color3f }, { TfToken("inputs:coat_roughness"), SdfValueTypeNames->Float }, - { TfToken("inputs:coat_anisotropy"), SdfValueTypeNames->Float }, - { TfToken("inputs:coat_rotation"), SdfValueTypeNames->Float }, + { TfToken("inputs:coat_roughness_anisotropy"), SdfValueTypeNames->Float }, { TfToken("inputs:coat_ior"), SdfValueTypeNames->Float }, - { TfToken("inputs:coat_ior_level"), SdfValueTypeNames->Float }, + { TfToken("inputs:coat_darkening"), SdfValueTypeNames->Float }, + { TfToken("inputs:thin_film_weight"), SdfValueTypeNames->Float }, { TfToken("inputs:thin_film_thickness"), SdfValueTypeNames->Float }, { TfToken("inputs:thin_film_ior"), SdfValueTypeNames->Float }, { TfToken("inputs:emission_luminance"), SdfValueTypeNames->Float }, { TfToken("inputs:emission_color"), SdfValueTypeNames->Color3f }, - { TfToken("inputs:geometry_opacity"), SdfValueTypeNames->Color3f }, + { TfToken("inputs:geometry_opacity"), SdfValueTypeNames->Float }, { TfToken("inputs:geometry_thin_walled"), SdfValueTypeNames->Bool }, { TfToken("inputs:geometry_normal"), SdfValueTypeNames->Float3 }, { TfToken("inputs:geometry_coat_normal"), SdfValueTypeNames->Float3 }, { TfToken("inputs:geometry_tangent"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:geometry_coat_tangent"), SdfValueTypeNames->Float3 }, }, { { TfToken("outputs:out"), SdfValueTypeNames->Token } }}}, @@ -533,106 +521,137 @@ ShaderRegistry::ShaderRegistry() // Note, *Scale inputs don't have a range limit. Neither do absorptionDistance, // scatteringDistance, emissiveIntensity, scatteringRedShift, scatteringRayleigh m_inputRanges = { - { AdobeTokens->ambientOcclusion, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->anisotropyAngle, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->anisotropyLevel, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->coatIOR, { VtValue(1.0), VtValue(3.0) } }, - { AdobeTokens->coatOpacity, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->coatRoughness, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->coatSpecularLevel, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->dispersion, { VtValue(0.0), VtValue(1.0) } }, // Apparently it can go as high as 20 - { AdobeTokens->height, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->heightLevel, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->IOR, { VtValue(1.0), VtValue(3.0) } }, - { AdobeTokens->metallic, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->opacity, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->opacityThreshold, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->roughness, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->sheenOpacity, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->sheenRoughness, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->specularLevel, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->translucency, { VtValue(0.0), VtValue(1.0) } }, - { AdobeTokens->useSpecularWorkflow, { VtValue(0), VtValue(1) } }, - { AdobeTokens->volumeThickness, { VtValue(0.0),VtValue(1.0) } }, + { AsmTokens->ambientOcclusion, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->anisotropyAngle, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->anisotropyLevel, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->coatIOR, { VtValue(1.0), VtValue(3.0) } }, + { AsmTokens->coatOpacity, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->coatRoughness, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->coatSpecularLevel, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->dispersion, { VtValue(0.0), VtValue(1.0) } }, // Apparently it can go as high as 20 + { AsmTokens->height, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->heightLevel, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->IOR, { VtValue(1.0), VtValue(3.0) } }, + { AsmTokens->metallic, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->opacity, { VtValue(0.0), VtValue(1.0) } }, + { UsdPreviewSurfaceTokens->opacityThreshold, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->roughness, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->sheenOpacity, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->sheenRoughness, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->specularLevel, { VtValue(0.0), VtValue(1.0) } }, + { AsmTokens->translucency, { VtValue(0.0), VtValue(1.0) } }, + { UsdPreviewSurfaceTokens->useSpecularWorkflow, { VtValue(0), VtValue(1) } }, + { AsmTokens->volumeThickness, { VtValue(0.0),VtValue(1.0) } }, }; // Initialize usdPreviewSurfaceInputRemapping m_usdPreviewSurfaceInputRemapping = { - { AdobeTokens->clearcoat, { AdobeTokens->coatOpacity, SdfValueTypeNames->Float } }, - { AdobeTokens->clearcoatRoughness, { AdobeTokens->coatRoughness, SdfValueTypeNames->Float } }, - { AdobeTokens->diffuseColor, { AdobeTokens->baseColor, SdfValueTypeNames->Color3f } }, - { AdobeTokens->displacement, { AdobeTokens->height, SdfValueTypeNames->Float } }, - { AdobeTokens->emissiveColor, { AdobeTokens->emissive, SdfValueTypeNames->Color3f } }, - { AdobeTokens->ior, { AdobeTokens->IOR, SdfValueTypeNames->Float } }, - { AdobeTokens->metallic, { AdobeTokens->metallic, SdfValueTypeNames->Float } }, - { AdobeTokens->normal, { AdobeTokens->normal, SdfValueTypeNames->Normal3f } }, - { AdobeTokens->occlusion, { AdobeTokens->ambientOcclusion, SdfValueTypeNames->Float } }, - { AdobeTokens->opacity, { AdobeTokens->opacity, SdfValueTypeNames->Float } }, - { AdobeTokens->opacityThreshold, { AdobeTokens->opacityThreshold, SdfValueTypeNames->Float } }, - { AdobeTokens->roughness, { AdobeTokens->roughness, SdfValueTypeNames->Float } }, - { AdobeTokens->specularColor, { AdobeTokens->specularEdgeColor, SdfValueTypeNames->Color3f } }, - { AdobeTokens->useSpecularWorkflow, { AdobeTokens->useSpecularWorkflow, SdfValueTypeNames->Int } } + { UsdPreviewSurfaceTokens->clearcoat, { AsmTokens->coatOpacity, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->clearcoatRoughness, { AsmTokens->coatRoughness, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->diffuseColor, { AsmTokens->baseColor, SdfValueTypeNames->Color3f } }, + { UsdPreviewSurfaceTokens->displacement, { AsmTokens->height, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->emissiveColor, { AsmTokens->emissive, SdfValueTypeNames->Color3f } }, + { UsdPreviewSurfaceTokens->ior, { AsmTokens->IOR, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->metallic, { AsmTokens->metallic, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->normal, { AsmTokens->normal, SdfValueTypeNames->Normal3f } }, + { UsdPreviewSurfaceTokens->occlusion, { AsmTokens->ambientOcclusion, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->opacity, { AsmTokens->opacity, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->opacityThreshold, { UsdPreviewSurfaceTokens->opacityThreshold, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->roughness, { AsmTokens->roughness, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->specularColor, { AsmTokens->specularEdgeColor, SdfValueTypeNames->Color3f } }, + { UsdPreviewSurfaceTokens->useSpecularWorkflow, { UsdPreviewSurfaceTokens->useSpecularWorkflow, SdfValueTypeNames->Int } } }; // Initialize asmInputRemapping + // XXX This is incomplete m_asmInputRemapping = { - { AdobeTokens->absorptionColor, { AdobeTokens->absorptionColor, SdfValueTypeNames->Float3 } }, - { AdobeTokens->absorptionDistance, { AdobeTokens->absorptionDistance, SdfValueTypeNames->Float } }, - { AdobeTokens->ambientOcclusion, { AdobeTokens->ambientOcclusion, SdfValueTypeNames->Float } }, - { AdobeTokens->anisotropyAngle, { AdobeTokens->anisotropyAngle, SdfValueTypeNames->Float } }, - { AdobeTokens->anisotropyLevel, { AdobeTokens->anisotropyLevel, SdfValueTypeNames->Float } }, - { AdobeTokens->baseColor, { AdobeTokens->baseColor, SdfValueTypeNames->Float3 } }, - { AdobeTokens->coatColor, { AdobeTokens->coatColor, SdfValueTypeNames->Float3 } }, - { AdobeTokens->coatIOR, { AdobeTokens->coatIOR, SdfValueTypeNames->Float } }, - { AdobeTokens->coatNormal, { AdobeTokens->coatNormal, SdfValueTypeNames->Float3 } }, - { AdobeTokens->coatOpacity, { AdobeTokens->coatOpacity, SdfValueTypeNames->Float } }, - { AdobeTokens->coatRoughness, { AdobeTokens->coatRoughness, SdfValueTypeNames->Float } }, - { AdobeTokens->coatSpecularLevel, { AdobeTokens->coatSpecularLevel, SdfValueTypeNames->Float } }, - { AdobeTokens->emissive, { AdobeTokens->emissive, SdfValueTypeNames->Float3 } }, - { AdobeTokens->height, { AdobeTokens->height, SdfValueTypeNames->Float } }, - { AdobeTokens->heightScale, { AdobeTokens->heightScale, SdfValueTypeNames->Float } }, - { AdobeTokens->IOR, { AdobeTokens->IOR, SdfValueTypeNames->Float } }, - { AdobeTokens->metallic, { AdobeTokens->metallic, SdfValueTypeNames->Float } }, - { AdobeTokens->normal, { AdobeTokens->normal, SdfValueTypeNames->Float3 } }, - { AdobeTokens->normalScale, { AdobeTokens->normalScale, SdfValueTypeNames->Float} }, - { AdobeTokens->opacity, { AdobeTokens->opacity, SdfValueTypeNames->Float } }, - { AdobeTokens->opacityThreshold, { AdobeTokens->opacityThreshold, SdfValueTypeNames->Float } }, - { AdobeTokens->roughness, { AdobeTokens->roughness, SdfValueTypeNames->Float } }, - { AdobeTokens->scatteringColor, { AdobeTokens->scatteringColor, SdfValueTypeNames->Float3 } }, - { AdobeTokens->scatteringDistance, { AdobeTokens->scatteringDistance, SdfValueTypeNames->Float } }, - { AdobeTokens->sheenColor, { AdobeTokens->sheenColor, SdfValueTypeNames->Float3 } }, - { AdobeTokens->sheenRoughness, { AdobeTokens->sheenRoughness, SdfValueTypeNames->Float } }, - { AdobeTokens->specularEdgeColor, { AdobeTokens->specularEdgeColor, SdfValueTypeNames->Float3 } }, - { AdobeTokens->specularLevel, { AdobeTokens->specularLevel, SdfValueTypeNames->Float } }, - { AdobeTokens->translucency, { AdobeTokens->translucency, SdfValueTypeNames->Float } }, - { AdobeTokens->volumeThickness, { AdobeTokens->volumeThickness, SdfValueTypeNames->Float } }, + { AsmTokens->absorptionColor, { AsmTokens->absorptionColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->absorptionDistance, { AsmTokens->absorptionDistance, SdfValueTypeNames->Float } }, + { AsmTokens->ambientOcclusion, { AsmTokens->ambientOcclusion, SdfValueTypeNames->Float } }, + { AsmTokens->anisotropyAngle, { AsmTokens->anisotropyAngle, SdfValueTypeNames->Float } }, + { AsmTokens->anisotropyLevel, { AsmTokens->anisotropyLevel, SdfValueTypeNames->Float } }, + { AsmTokens->baseColor, { AsmTokens->baseColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->coatColor, { AsmTokens->coatColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->coatIOR, { AsmTokens->coatIOR, SdfValueTypeNames->Float } }, + { AsmTokens->coatNormal, { AsmTokens->coatNormal, SdfValueTypeNames->Float3 } }, + { AsmTokens->coatOpacity, { AsmTokens->coatOpacity, SdfValueTypeNames->Float } }, + { AsmTokens->coatRoughness, { AsmTokens->coatRoughness, SdfValueTypeNames->Float } }, + { AsmTokens->coatSpecularLevel, { AsmTokens->coatSpecularLevel, SdfValueTypeNames->Float } }, + { AsmTokens->dispersion, { AsmTokens->dispersion, SdfValueTypeNames->Float } }, + { AsmTokens->emissiveIntensity, { AsmTokens->emissiveIntensity, SdfValueTypeNames->Float } }, + { AsmTokens->emissive, { AsmTokens->emissive, SdfValueTypeNames->Float3 } }, + { AsmTokens->height, { AsmTokens->height, SdfValueTypeNames->Float } }, + { AsmTokens->heightScale, { AsmTokens->heightScale, SdfValueTypeNames->Float } }, + { AsmTokens->IOR, { AsmTokens->IOR, SdfValueTypeNames->Float } }, + { AsmTokens->metallic, { AsmTokens->metallic, SdfValueTypeNames->Float } }, + { AsmTokens->normal, { AsmTokens->normal, SdfValueTypeNames->Float3 } }, + { AsmTokens->normalScale, { AsmTokens->normalScale, SdfValueTypeNames->Float} }, + { AsmTokens->opacity, { AsmTokens->opacity, SdfValueTypeNames->Float } }, + // The reason why opacityThreshold is present in this mapping is as follows: + // We have an opacityThreshold input on the central Material struct, but there is no such field on ASM. + // By injecting an entry here, the rest of the material utilities will happily put a opacityThreshold + // value on an ASM shader. Eclair will just ignore it. + // There are materials in GLTF where we take the alphaCutoff and store it in the opacityThreshold, if + // we didn't store in on the ASM material, it would be lost if we were to write a GLTF material again. + // That is why we allow this extra attribute/value that means nothing to ASM itself, but it carries + // information that is otherwise lost. + { UsdPreviewSurfaceTokens->opacityThreshold, { UsdPreviewSurfaceTokens->opacityThreshold, SdfValueTypeNames->Float } }, + { AsmTokens->roughness, { AsmTokens->roughness, SdfValueTypeNames->Float } }, + { AsmTokens->scatteringColor, { AsmTokens->scatteringColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->scatteringDistance, { AsmTokens->scatteringDistance, SdfValueTypeNames->Float } }, + { AsmTokens->scatteringDistanceScale, { AsmTokens->scatteringDistanceScale, SdfValueTypeNames->Float3 } }, + { AsmTokens->sheenColor, { AsmTokens->sheenColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->sheenOpacity, { AsmTokens->sheenOpacity, SdfValueTypeNames->Float } }, + { AsmTokens->sheenRoughness, { AsmTokens->sheenRoughness, SdfValueTypeNames->Float } }, + { AsmTokens->specularEdgeColor, { AsmTokens->specularEdgeColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->specularLevel, { AsmTokens->specularLevel, SdfValueTypeNames->Float } }, + { AsmTokens->translucency, { AsmTokens->translucency, SdfValueTypeNames->Float } }, + { AsmTokens->volumeThickness, { AsmTokens->volumeThickness, SdfValueTypeNames->Float } }, }; - // Initialize materialXInputRemapping - m_materialXInputRemapping = { - { OpenPbrTokens->base_color, { AdobeTokens->baseColor, SdfValueTypeNames->Color3f } }, - { OpenPbrTokens->base_metalness, { AdobeTokens->metallic, SdfValueTypeNames->Float } }, - { OpenPbrTokens->coat_color, { AdobeTokens->coatColor, SdfValueTypeNames->Color3f } }, - { OpenPbrTokens->coat_ior, { AdobeTokens->coatIOR, SdfValueTypeNames->Float } }, - { OpenPbrTokens->coat_roughness, { AdobeTokens->coatRoughness, SdfValueTypeNames->Float } }, - { OpenPbrTokens->coat_weight, { AdobeTokens->coatOpacity, SdfValueTypeNames->Float } }, - { OpenPbrTokens->emission_color, { AdobeTokens->emissive, SdfValueTypeNames->Color3f } }, - { OpenPbrTokens->fuzz_color, { AdobeTokens->sheenColor, SdfValueTypeNames->Color3f } }, - { OpenPbrTokens->fuzz_roughness, { AdobeTokens->sheenRoughness, SdfValueTypeNames->Float } }, - { OpenPbrTokens->geometry_coat_normal, { AdobeTokens->coatNormal, SdfValueTypeNames->Float3 } }, - { OpenPbrTokens->geometry_normal, { AdobeTokens->normal, SdfValueTypeNames->Float3 } }, - { OpenPbrTokens->geometry_opacity, { AdobeTokens->opacity, SdfValueTypeNames->Color3f } }, - { OpenPbrTokens->specular_anisotropy, { AdobeTokens->anisotropyLevel, SdfValueTypeNames->Float } }, - { OpenPbrTokens->specular_color, { AdobeTokens->specularEdgeColor, SdfValueTypeNames->Color3f } }, - { OpenPbrTokens->specular_ior, { AdobeTokens->IOR, SdfValueTypeNames->Float } }, - { OpenPbrTokens->specular_rotation, { AdobeTokens->anisotropyAngle, SdfValueTypeNames->Float } }, - { OpenPbrTokens->specular_roughness, { AdobeTokens->roughness, SdfValueTypeNames->Float } }, - { OpenPbrTokens->specular_weight, { AdobeTokens->specularLevel, SdfValueTypeNames->Float } }, - { OpenPbrTokens->subsurface_color, { AdobeTokens->scatteringColor, SdfValueTypeNames->Color3f } }, - { OpenPbrTokens->subsurface_radius, { AdobeTokens->scatteringDistance, SdfValueTypeNames->Float } }, - { OpenPbrTokens->transmission_color, { AdobeTokens->absorptionColor, SdfValueTypeNames->Color3f } }, - { OpenPbrTokens->transmission_depth, { AdobeTokens->absorptionDistance, SdfValueTypeNames->Float } }, - { OpenPbrTokens->transmission_weight, { AdobeTokens->translucency, SdfValueTypeNames->Float } }, + // Initialize openPbrInputRemapping + m_openPbrInputRemapping = { + { OpenPbrTokens->base_weight, { OpenPbrMaterialInputTokens->baseWeight, SdfValueTypeNames->Float } }, + { OpenPbrTokens->base_color, { AsmTokens->baseColor, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->base_diffuse_roughness, { OpenPbrMaterialInputTokens->baseDiffuseRoughness, SdfValueTypeNames->Float } }, + { OpenPbrTokens->base_metalness, { AsmTokens->metallic, SdfValueTypeNames->Float } }, + { OpenPbrTokens->specular_weight, { OpenPbrMaterialInputTokens->specularWeight, SdfValueTypeNames->Float } }, + { OpenPbrTokens->specular_color, { AsmTokens->specularEdgeColor, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->specular_roughness, { AsmTokens->roughness, SdfValueTypeNames->Float } }, + { OpenPbrTokens->specular_ior, { AsmTokens->IOR, SdfValueTypeNames->Float } }, + { OpenPbrTokens->specular_roughness_anisotropy, { AsmTokens->anisotropyLevel, SdfValueTypeNames->Float } }, + { OpenPbrTokens->transmission_weight, { AsmTokens->translucency, SdfValueTypeNames->Float } }, + { OpenPbrTokens->transmission_color, { AsmTokens->absorptionColor, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->transmission_depth, { AsmTokens->absorptionDistance, SdfValueTypeNames->Float } }, + { OpenPbrTokens->transmission_scatter, { OpenPbrMaterialInputTokens->transmissionScatter, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->transmission_scatter_anisotropy, { OpenPbrMaterialInputTokens->transmissionScatterAnisotropy, SdfValueTypeNames->Float } }, + { OpenPbrTokens->transmission_dispersion_scale, { OpenPbrMaterialInputTokens->transmissionDispersionScale, SdfValueTypeNames->Float } }, + { OpenPbrTokens->transmission_dispersion_abbe_number, { OpenPbrMaterialInputTokens->transmissionDispersionAbbeNumber, SdfValueTypeNames->Float } }, + { OpenPbrTokens->subsurface_weight, { OpenPbrMaterialInputTokens->subsurfaceWeight, SdfValueTypeNames->Float } }, + { OpenPbrTokens->subsurface_color, { AsmTokens->scatteringColor, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->subsurface_radius, { AsmTokens->scatteringDistance, SdfValueTypeNames->Float } }, + { OpenPbrTokens->subsurface_radius_scale, { OpenPbrMaterialInputTokens->subsurfaceRadiusScale, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->subsurface_scatter_anisotropy, { OpenPbrMaterialInputTokens->subsurfaceScatterAnisotropy, SdfValueTypeNames->Float } }, + { OpenPbrTokens->fuzz_weight, { OpenPbrMaterialInputTokens->fuzzWeight, SdfValueTypeNames->Float } }, + { OpenPbrTokens->fuzz_color, { AsmTokens->sheenColor, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->fuzz_roughness, { AsmTokens->sheenRoughness, SdfValueTypeNames->Float } }, + { OpenPbrTokens->coat_weight, { AsmTokens->coatOpacity, SdfValueTypeNames->Float } }, + { OpenPbrTokens->coat_color, { AsmTokens->coatColor, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->coat_roughness, { AsmTokens->coatRoughness, SdfValueTypeNames->Float } }, + { OpenPbrTokens->coat_roughness_anisotropy, { OpenPbrMaterialInputTokens->coatRoughnessAnisotropy, SdfValueTypeNames->Float } }, + { OpenPbrTokens->coat_ior, { AsmTokens->coatIOR, SdfValueTypeNames->Float } }, + { OpenPbrTokens->coat_darkening, { OpenPbrMaterialInputTokens->coatDarkening, SdfValueTypeNames->Float } }, + { OpenPbrTokens->thin_film_weight, { OpenPbrMaterialInputTokens->thinFilmWeight, SdfValueTypeNames->Float } }, + { OpenPbrTokens->thin_film_thickness, { OpenPbrMaterialInputTokens->thinFilmThickness, SdfValueTypeNames->Float } }, + { OpenPbrTokens->thin_film_ior, { OpenPbrMaterialInputTokens->thinFilmIOR, SdfValueTypeNames->Float } }, + { OpenPbrTokens->emission_luminance, { OpenPbrMaterialInputTokens->emissionLuminance, SdfValueTypeNames->Float } }, + { OpenPbrTokens->emission_color, { AsmTokens->emissive, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->geometry_opacity, { AsmTokens->opacity, SdfValueTypeNames->Float } }, + { OpenPbrTokens->geometry_thin_walled, { OpenPbrMaterialInputTokens->thinWalled, SdfValueTypeNames->Bool } }, + { OpenPbrTokens->geometry_normal, { AsmTokens->normal, SdfValueTypeNames->Float3 } }, + { OpenPbrTokens->geometry_coat_normal, { AsmTokens->coatNormal, SdfValueTypeNames->Float3 } }, + { OpenPbrTokens->geometry_tangent, { OpenPbrMaterialInputTokens->tangent, SdfValueTypeNames->Float3 } }, + { OpenPbrTokens->geometry_coat_tangent, { OpenPbrMaterialInputTokens->coatTangent, SdfValueTypeNames->Float3 } }, }; // clang-format on } diff --git a/utils/src/sdfUtils.cpp b/utils/src/sdfUtils.cpp index 703632a3..a97727cd 100644 --- a/utils/src/sdfUtils.cpp +++ b/utils/src/sdfUtils.cpp @@ -337,4 +337,20 @@ addVariantSelection(SdfAbstractData* data, parentPath, SdfFieldKeys->VariantSelection, SdfAbstractDataConstTypedValue(&selections)); } -} \ No newline at end of file +} + +PXR_NAMESPACE_OPEN_SCOPE + +void +FileFormatDataBase::parseFromFileFormatArgs(const SdfLayer::FileFormatArguments& args, + const std::string& debugTag) +{ + using adobe::usd::argReadBool; + using adobe::usd::argReadString; + argReadBool(args, "writeUsdPreviewSurface", writeUsdPreviewSurface, debugTag); + argReadBool(args, "writeASM", writeASM, debugTag); + argReadBool(args, "writeOpenPBR", writeOpenPBR, debugTag); + argReadString(args, "assetsPath", assetsPath, debugTag); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/utils/src/test.cpp b/utils/src/test.cpp index 39d9397c..64b282c8 100644 --- a/utils/src/test.cpp +++ b/utils/src/test.cpp @@ -10,11 +10,14 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #include + #include + #include #include #include #include +#include #include #include #include @@ -23,9 +26,10 @@ governing permissions and limitations under the License. #include #include #include -#include #include +#include + PXR_NAMESPACE_OPEN_SCOPE TF_DEFINE_PUBLIC_TOKENS(TestTokens, TEST_TOKENS); PXR_NAMESPACE_CLOSE_SCOPE @@ -64,60 +68,95 @@ readPrimvar(UsdGeomPrimvarsAPI& api, const TfToken& name, Primvar& primvar) return false; } -#define ASSERT_ARRAY(...) assertArray(__VA_ARGS__) -template -void -assertArray(VtArray& actual, const ArrayData& expected, const std::string& name) // test a subset of the array +/** + * fuzzyEqual for integer types is just the == operator + */ +template +constexpr typename std::enable_if::value, bool>::type +fuzzyEqual(IntLike a, IntLike b) { - ASSERT_EQ(actual.size(), expected.size); - ASSERT_GE(actual.size(), expected.values.size()); - bool arraysMatch = true; - size_t i; - for (i = 0; i < expected.values.size(); i++) { - if (actual[i] != expected.values[i]) { - arraysMatch = false; - break; - } - } - ASSERT_TRUE(arraysMatch) << "Variable: " << name << ". Elements at [" << i << "] differ. Actual = " << actual[i] - << ", Expected = " << expected.values[i]; + return a == b; } -bool -floatsEqual(float a, float b, float epsilon = 1e-6) +/** + * fuzzyEqual for floating point types checks that the difference is less than an optional third + * parameter, epsilon, that defaults to 1e-6 + */ +template +constexpr typename std::enable_if::value, bool>::type +fuzzyEqual(FloatLike a, FloatLike b, FloatLike epsilon = 1e-6) { - // Ensure that floating point comparison doesn't result in a false negative return std::abs(a - b) < epsilon; } -bool -doublesEqual(double a, double b, double epsilon = 1e-6) +/** + * fuzzyEqual for GfVec types checks that each element is the same. An optional third parameter, + * if not null, will be set to the index of the non equal elements if the GfVecs differ. + * + * Vec is a valid class for this function iff: + * - Vec is a class + * - Vec has elements accessible with [] + * - Each element is either an arithmetic type or a reference to one + * - Vec has a static dimension field + */ +template +typename std::enable_if::value && + std::is_arithmetic()[0])>::type>::value, + bool>::type +fuzzyEqual(const Vec& a, const Vec& b, size_t* failingIndex = nullptr) { - // Ensure that floating point comparison doesn't result in a false negative - return std::abs(a - b) < epsilon; + for (size_t i = 0; i < Vec::dimension; ++i) { + if (!fuzzyEqual(a[i], b[i])) { + if (failingIndex) { + *failingIndex = i; + } + return false; + } + } + return true; } -#define ASSERT_VEC2F(...) assertVec2f(__VA_ARGS__) +#define ASSERT_ARRAY(...) assertArray(__VA_ARGS__) +template void -assertVec2f(const PXR_NS::GfVec2f& actual, - const PXR_NS::GfVec2f& expected, - std::string msg = "") // test a vector of 2 floats +assertArray(VtArray& actual, + const ArrayData& expected, + const std::string& name) // test a subset of the array { - bool valuesMatch = true; + ASSERT_EQ(actual.size(), expected.size) << name << " does not have the expected number of elements"; + ASSERT_GE(actual.size(), expected.values.size()) << "There are fewer " << name << " than elements to be checked."; + bool arraysMatch = true; size_t i; - for (i = 0; i < 2; ++i) { - if (!floatsEqual(actual[i], expected[i])) { - valuesMatch = false; + for (i = 0; i < expected.values.size(); i++) { + if (!fuzzyEqual(actual[i], expected.values[i])) { + arraysMatch = false; break; } } + ASSERT_TRUE(arraysMatch) << "Variable: " << name << ". Elements at [" << i + << "] differ. Actual = " << actual[i] + << ", Expected = " << expected.values[i]; +} +#define ASSERT_VEC(...) assertVec(__VA_ARGS__) +template +void +assertVec(const GfVec& actual, + const GfVec& expected, + std::string msg = "") +{ if (msg != "") { // Add a space after the message if it's not empty msg += ": "; } - ASSERT_TRUE(valuesMatch) << msg << "Elements at [" << i << "] differ. Actual = " << actual[i] - << ", Expected = " << expected[i]; + + // If the vectors are not equal, idx will be set to the index where they differ + size_t idx; + + ASSERT_TRUE(fuzzyEqual(actual, expected, &idx)) + << msg << "Elements at [" << idx << "] differ. Actual = " << actual[idx] + << ", Expected = " << expected[idx]; } #define ASSERT_QUATF(...) assertQuatf(__VA_ARGS__) @@ -126,71 +165,16 @@ assertQuatf(const PXR_NS::GfQuatf& actual, const PXR_NS::GfQuatf& expected, std::string msg = "") // test a quaternion of floats { - bool valuesMatch = true; if (msg != "") { // Add a space after the message if it's not empty msg += ": "; } - ASSERT_TRUE(floatsEqual(actual.GetReal(), expected.GetReal())) + ASSERT_TRUE(fuzzyEqual(actual.GetReal(), expected.GetReal())) << msg << "Real elements differ. Actual = " << actual.GetReal() << ", Expected = " << expected.GetReal(); - size_t i; - for (i = 0; i < 3; ++i) { - if (!floatsEqual(actual.GetImaginary()[i], expected.GetImaginary()[i])) { - valuesMatch = false; - break; - } - } - ASSERT_TRUE(valuesMatch) << msg << "Imaginary elements at [" << i - << "] differ. Actual = " << actual.GetImaginary()[i] - << ", Expected = " << expected.GetImaginary()[i]; -} - -#define ASSERT_VEC3F(...) assertVec3f(__VA_ARGS__) -void -assertVec3f(const PXR_NS::GfVec3f& actual, - const PXR_NS::GfVec3f& expected, - std::string msg = "") // test a vector of 3 floats -{ - bool valuesMatch = true; - size_t i; - for (i = 0; i < 3; ++i) { - if (!floatsEqual(actual[i], expected[i])) { - valuesMatch = false; - break; - } - } - - if (msg != "") { - // Add a space after the message if it's not empty - msg += ": "; - } - ASSERT_TRUE(valuesMatch) << msg << "Elements at [" << i << "] differ. Actual = " << actual[i] - << ", Expected = " << expected[i]; -} -#define ASSERT_VEC3D(...) assertVec3d(__VA_ARGS__) -void -assertVec3d(const PXR_NS::GfVec3d& actual, - const PXR_NS::GfVec3d& expected, - std::string msg = "") // test a vector of 3 doubles -{ - bool valuesMatch = true; - size_t i; - for (i = 0; i < 3; ++i) { - if (!doublesEqual(actual[i], expected[i])) { - valuesMatch = false; - break; - } - } - - if (msg != "") { - // Add a space after the message if it's not empty - msg += ": "; - } - ASSERT_TRUE(valuesMatch) << msg << "Elements at [" << i << "] differ. Actual = " << actual[i] - << ", Expected = " << expected[i]; + ASSERT_VEC(actual.GetImaginary(), expected.GetImaginary(), msg + "GfQuatf imaginary component"); } void @@ -293,7 +277,7 @@ assertAnimation(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Ani GfVec3f scaleValue; extractUsdAttribute( prim, TfToken("xformOp:scale"), &scaleValue, UsdTimeCode(time)); - ASSERT_VEC3F(scaleValue, + ASSERT_VEC(scaleValue, data.scale.at(time), std::string("xformOp:scale[") + std::to_string(time) + "]"); } @@ -304,7 +288,7 @@ assertAnimation(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Ani GfVec3d translateValue; extractUsdAttribute( prim, TfToken("xformOp:translate"), &translateValue, UsdTimeCode(time)); - ASSERT_VEC3D(translateValue, + ASSERT_VEC(translateValue, data.translate.at(time), std::string("xformOp:translate[") + std::to_string(time) + "]"); } @@ -329,7 +313,7 @@ assertCamera(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Camera GfVec3f scale; if (extractUsdAttribute(parent, TfToken("xformOp:translate"), &translation)) { - ASSERT_VEC3D( + ASSERT_VEC( translation, cameraData.translate, path + "'s parent translation does not match\n"); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No translation attribute found for %s\n", path.c_str()); @@ -340,7 +324,7 @@ assertCamera(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Camera TF_WARN("No rotation attribute found for %s\n", path.c_str()); } if (extractUsdAttribute(parent, TfToken("xformOp:scale"), &scale)) { - ASSERT_VEC3F(scale, cameraData.scale, path + "'s parent scale does not match\n"); + ASSERT_VEC(scale, cameraData.scale, path + "'s parent scale does not match\n"); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No scale attribute found for %s\n", path.c_str()); } @@ -352,21 +336,24 @@ assertCamera(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Camera GfVec2f clippingRange; if (camera.GetClippingRangeAttr().Get(&clippingRange)) { - ASSERT_VEC2F(clippingRange, cameraData.clippingRange, path + "'s clipping range does not match\n"); + ASSERT_VEC( + clippingRange, cameraData.clippingRange, path + "'s clipping range does not match\n"); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No clipping range attribute found for %s\n", path.c_str()); } float focalLength; if (camera.GetFocalLengthAttr().Get(&focalLength)) { - ASSERT_FLOAT_EQ(focalLength, cameraData.focalLength) << path << " focal length does not match\n"; + ASSERT_FLOAT_EQ(focalLength, cameraData.focalLength) + << path << " focal length does not match\n"; } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No focal length attribute found for %s\n", path.c_str()); } float focusDistance; if (camera.GetFocusDistanceAttr().Get(&focusDistance)) { - ASSERT_FLOAT_EQ(focusDistance, cameraData.focusDistance) << path << " focus distance does not match\n"; + ASSERT_FLOAT_EQ(focusDistance, cameraData.focusDistance) + << path << " focus distance does not match\n"; } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No focus distance attribute found for %s\n", path.c_str()); } @@ -388,7 +375,8 @@ assertCamera(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Camera TfToken projection; if (camera.GetProjectionAttr().Get(&projection)) { - ASSERT_EQ(projection, TfToken(cameraData.projection)) << path << " projection does not match\n"; + ASSERT_EQ(projection, TfToken(cameraData.projection)) + << path << " projection does not match\n"; } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No projection attribute found for %s\n", path.c_str()); } @@ -423,7 +411,7 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa ASSERT_TRUE( extractUsdAttribute(parent, TfToken("xformOp:translate"), &translation)) << "Expected translation attribute not found for " << path << "\n"; - ASSERT_VEC3D(translation, + ASSERT_VEC(translation, lightData.translation.value(), path + "'s parent translation does not match\n"); } @@ -436,7 +424,7 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa if (lightData.scale) { ASSERT_TRUE(extractUsdAttribute(parent, TfToken("xformOp:scale"), &scale)) << "Expected scale attribute not found for " << path << "\n"; - ASSERT_VEC3F(scale, lightData.scale.value(), path + "'s parent scale does not match\n"); + ASSERT_VEC(scale, lightData.scale.value(), path + "'s parent scale does not match\n"); } // Next, we check the light data itself @@ -449,7 +437,7 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa PXR_NS::GfVec3f color; ASSERT_TRUE(sphereLight.GetColorAttr().Get(&color)) << path << " is missing expected color attribute\n"; - ASSERT_VEC3F(color, lightData.color.value(), path + " color does not match\n"); + ASSERT_VEC(color, lightData.color.value(), path + " color does not match\n"); } if (lightData.intensity) { @@ -475,7 +463,7 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa PXR_NS::GfVec3f color; ASSERT_TRUE(distantLight.GetColorAttr().Get(&color)) << path << " is missing expected color attribute\n"; - ASSERT_VEC3F(color, lightData.color.value(), path + " color does not match\n"); + ASSERT_VEC(color, lightData.color.value(), path + " color does not match\n"); } if (lightData.intensity) { @@ -496,7 +484,7 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa PXR_NS::GfVec3f color; ASSERT_TRUE(diskLight.GetColorAttr().Get(&color)) << path << " is missing expected color attribute\n"; - ASSERT_VEC3F(color, lightData.color.value(), path + " color does not match\n"); + ASSERT_VEC(color, lightData.color.value(), path + " color does not match\n"); } if (lightData.intensity) { @@ -523,7 +511,7 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa UsdLuxRectLight rectLight(prim); ASSERT_TRUE(rectLight); - ASSERT_VEC3F(rectLight.color, lightData.color); + ASSERT_VEC(rectLight.color, lightData.color); ASSERT_FLOAT_EQ(rectLight.intensity, lightData.intensity); // Rectangle specific attributes @@ -539,7 +527,7 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa UsdLuxDomeLight domeLight(prim); ASSERT_TRUE(domeLight); - ASSERT_VEC3F(domeLight.color, lightData.color); + ASSERT_VEC(domeLight.color, lightData.color); ASSERT_FLOAT_EQ(domeLight.intensity, lightData.intensity); // Add texture test once we support this on import @@ -642,7 +630,9 @@ assertInputField(const UsdShadeShader& shader, const std::string& name, const T& if (valueAttrs.size()) { T actual; valueAttrs.front().Get(&actual); - ASSERT_EQ(actual, value); + ASSERT_EQ(actual, value) + << "Input field " << name << " for shader " << shader.GetPath().GetString() + << " doesn't match (" << typeid(value).name() << ")"; return; } } @@ -729,14 +719,12 @@ assertMaterial(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Mate UsdShadeShader(stage->GetPrimAtPath(sourcePath)); TfToken shaderId; stShader.GetShaderId(&shaderId); - if (!data.transformRotation.IsEmpty() || - !data.transformScale.IsEmpty() || - !data.transformTranslation.IsEmpty()) { + if (!data.uvRotation.IsEmpty() || !data.uvScale.IsEmpty() || + !data.uvTranslation.IsEmpty()) { ASSERT_TRUE(shaderId == TestTokens->UsdTransform2d); - ASSERT_INPUT_FIELD(stShader, "rotation", data.transformRotation); - ASSERT_INPUT_FIELD(stShader, "scale", data.transformScale); - ASSERT_INPUT_FIELD( - stShader, "translation", data.transformTranslation); + ASSERT_INPUT_FIELD(stShader, "rotation", data.uvRotation); + ASSERT_INPUT_FIELD(stShader, "scale", data.uvScale); + ASSERT_INPUT_FIELD(stShader, "translation", data.uvTranslation); } else { std::string shaderName = stShader.GetPrim().GetName().GetString(); if (shaderName == "texCoordReader") { @@ -776,7 +764,7 @@ assertRender(const std::string& filename, const std::string& imageFilename) { const std::string imageParentPath = TfGetPathName(imageFilename); TfMakeDirs(imageParentPath, -1, true); - const std::string command = "usdrecord \"" + filename + "\" \"" + imageFilename + "\""; + const std::string command = "HYDRA_ENABLE_HGIGL=0 usdrecord \"" + filename + "\" \"" + imageFilename + "\""; int result = archSystem(command); ASSERT_EQ(result, 0); } \ No newline at end of file diff --git a/utils/src/usdData.cpp b/utils/src/usdData.cpp index 39b8e068..10f71b44 100644 --- a/utils/src/usdData.cpp +++ b/utils/src/usdData.cpp @@ -63,11 +63,8 @@ bool Input::isZeroTexture() const { // If scale and bias are zero, the texture will only produce zero values - PXR_NS::GfVec4f scaleValue = scale.GetWithDefault(PXR_NS::GfVec4f(1.0f)); - PXR_NS::GfVec4f biasValue = bias.GetWithDefault(PXR_NS::GfVec4f(0.0f)); // Note, we're only checking the first three, since the multipliers are usually stored there - return scaleValue[0] == 0.0f && scaleValue[1] == 0.0f && scaleValue[2] == 0.0f && - biasValue == PXR_NS::GfVec4f(0.0f); + return scale[0] == 0.0f && scale[1] == 0.0f && scale[2] == 0.0f && bias == kDefaultTexBias; } bool @@ -95,10 +92,8 @@ invertInput(const Input& in) // Invert transformation is: (-1)y + (+1) = (-1)(scale)x + (-1)(bias) + (+1) // ----------- ----------------- // newScale newBias - GfVec4f oldScale = in.scale.GetWithDefault(GfVec4f(1.0f)); - GfVec4f oldBias = in.bias.GetWithDefault(GfVec4f(0.0f)); - out.scale = -oldScale; - out.bias = -oldBias + GfVec4f(1.0f); + out.scale = -in.scale; + out.bias = -in.bias + GfVec4f(1.0f); } else if (!in.value.IsEmpty()) { if (in.value.IsHolding()) { out.value = 1.0f - in.value.UncheckedGet(); @@ -120,9 +115,7 @@ printInput(const TfToken& name, const Input& input) ss << "\n " << std::setfill(' ') << std::setw(20) << std::left << name.GetString() << ": "; if (input.image >= 0) { ss << std::setfill(' ') << std::setw(3) << std::right << input.image - << ", ch: " << std::setfill(' ') << std::setw(4) << std::right - << input.channel - // << ", ch: " << input.channel + << ", ch: " << std::setfill(' ') << std::setw(4) << std::right << input.channel << ", uv: " << input.uvIndex; if (!input.wrapS.IsEmpty()) { ss << ", wrapS: " << input.wrapS; @@ -139,21 +132,11 @@ printInput(const TfToken& name, const Input& input) if (!input.colorspace.IsEmpty()) { ss << ", colorspace: " << input.colorspace; } - if (!input.bias.IsEmpty()) { - ss << ", bias: " << input.bias; - } - if (!input.scale.IsEmpty()) { - ss << ", scale: " << input.scale; - } - if (!input.transformRotation.IsEmpty()) { - ss << ", stRot: " << input.transformRotation; - } - if (!input.transformScale.IsEmpty()) { - ss << ", stScale: " << input.transformScale; - } - if (!input.transformTranslation.IsEmpty()) { - ss << ", stTrans: " << input.transformTranslation; - } + ss << ", scale: " << input.scale; + ss << ", bias: " << input.bias; + ss << ", stRot: " << input.uvRotation; + ss << ", stScale: " << input.uvScale; + ss << ", stTrans: " << input.uvTranslation; } else if (!input.value.IsEmpty()) { ss << std::setprecision(3); ss << "<"; @@ -202,36 +185,38 @@ printMaterial(const std::string& header, debugTag.c_str(), header.c_str(), path.GetAsString().c_str(), - printInput(AdobeTokens->useSpecularWorkflow, material.useSpecularWorkflow).c_str(), - printInput(AdobeTokens->diffuseColor, material.diffuseColor).c_str(), - printInput(AdobeTokens->emissiveColor, material.emissiveColor).c_str(), - printInput(AdobeTokens->specularLevel, material.specularLevel).c_str(), - printInput(AdobeTokens->specularColor, material.specularColor).c_str(), - printInput(AdobeTokens->normal, material.normal).c_str(), - printInput(AdobeTokens->normalScale, material.normalScale).c_str(), - printInput(AdobeTokens->metallic, material.metallic).c_str(), - printInput(AdobeTokens->roughness, material.roughness).c_str(), - printInput(AdobeTokens->coatOpacity, material.clearcoat).c_str(), - printInput(AdobeTokens->coatColor, material.clearcoatColor).c_str(), - printInput(AdobeTokens->coatRoughness, material.clearcoatRoughness).c_str(), - printInput(AdobeTokens->coatIOR, material.clearcoatIor).c_str(), - printInput(AdobeTokens->coatSpecularLevel, material.clearcoatSpecular).c_str(), - printInput(AdobeTokens->coatNormal, material.clearcoatNormal).c_str(), - printInput(AdobeTokens->sheenColor, material.sheenColor).c_str(), - printInput(AdobeTokens->sheenRoughness, material.sheenRoughness).c_str(), - printInput(AdobeTokens->anisotropyLevel, material.anisotropyLevel).c_str(), - printInput(AdobeTokens->anisotropyAngle, material.anisotropyAngle).c_str(), - printInput(AdobeTokens->opacity, material.opacity).c_str(), - printInput(AdobeTokens->opacityThreshold, material.opacityThreshold).c_str(), - printInput(AdobeTokens->displacement, material.displacement).c_str(), - printInput(AdobeTokens->occlusion, material.occlusion).c_str(), - printInput(AdobeTokens->ior, material.ior).c_str(), - printInput(AdobeTokens->translucency, material.transmission).c_str(), - printInput(AdobeTokens->volumeThickness, material.volumeThickness).c_str(), - printInput(AdobeTokens->absorptionDistance, material.absorptionDistance).c_str(), - printInput(AdobeTokens->absorptionColor, material.absorptionColor).c_str(), - printInput(AdobeTokens->scatteringDistance, material.scatteringDistance).c_str(), - printInput(AdobeTokens->scatteringColor, material.scatteringColor).c_str(), + printInput(UsdPreviewSurfaceTokens->useSpecularWorkflow, material.useSpecularWorkflow) + .c_str(), + printInput(UsdPreviewSurfaceTokens->diffuseColor, material.diffuseColor).c_str(), + printInput(UsdPreviewSurfaceTokens->emissiveColor, material.emissiveColor).c_str(), + printInput(AsmTokens->specularLevel, material.specularLevel).c_str(), + printInput(UsdPreviewSurfaceTokens->specularColor, material.specularColor).c_str(), + printInput(AsmTokens->normal, material.normal).c_str(), + printInput(AsmTokens->normalScale, material.normalScale).c_str(), + printInput(AsmTokens->metallic, material.metallic).c_str(), + printInput(AsmTokens->roughness, material.roughness).c_str(), + printInput(AsmTokens->coatOpacity, material.clearcoat).c_str(), + printInput(AsmTokens->coatColor, material.clearcoatColor).c_str(), + printInput(AsmTokens->coatRoughness, material.clearcoatRoughness).c_str(), + printInput(AsmTokens->coatIOR, material.clearcoatIor).c_str(), + printInput(AsmTokens->coatSpecularLevel, material.clearcoatSpecular).c_str(), + printInput(AsmTokens->coatNormal, material.clearcoatNormal).c_str(), + printInput(AsmTokens->sheenColor, material.sheenColor).c_str(), + printInput(AsmTokens->sheenRoughness, material.sheenRoughness).c_str(), + printInput(AsmTokens->anisotropyLevel, material.anisotropyLevel).c_str(), + printInput(AsmTokens->anisotropyAngle, material.anisotropyAngle).c_str(), + printInput(AsmTokens->opacity, material.opacity).c_str(), + printInput(UsdPreviewSurfaceTokens->opacityThreshold, material.opacityThreshold).c_str(), + printInput(UsdPreviewSurfaceTokens->displacement, material.displacement).c_str(), + printInput(UsdPreviewSurfaceTokens->occlusion, material.occlusion).c_str(), + printInput(UsdPreviewSurfaceTokens->ior, material.ior).c_str(), + printInput(AsmTokens->translucency, material.transmission).c_str(), + printInput(AsmTokens->volumeThickness, material.volumeThickness).c_str(), + printInput(AsmTokens->absorptionDistance, material.absorptionDistance).c_str(), + printInput(AsmTokens->absorptionColor, material.absorptionColor).c_str(), + printInput(AsmTokens->scatteringDistance, material.scatteringDistance).c_str(), + printInput(AsmTokens->scatteringDistanceScale, material.scatteringDistanceScale).c_str(), + printInput(AsmTokens->scatteringColor, material.scatteringColor).c_str(), printClearcoatModelsTransmissionTint(material).c_str(), printUnlit(material).c_str()); } @@ -747,6 +732,14 @@ UniqueNameEnforcer::enforceUniqueness(std::string& name) _makeUniqueAndAdd(namesMap, name); } +void +removeBrackets(std::string& name) +{ + name.erase(std::remove_if(name.begin(), name.end(), + [](char c) { return c == '[' || c == ']'; }), + name.end()); +} + void trimDegenerateNormals(Mesh& mesh) { diff --git a/utils/tests/CMakeLists.txt b/utils/tests/CMakeLists.txt new file mode 100644 index 00000000..2b7d87d2 --- /dev/null +++ b/utils/tests/CMakeLists.txt @@ -0,0 +1,21 @@ +include(GoogleTest) + +add_executable(utilsTests tests.cpp) + +usd_plugin_compile_config(utilsTests) + +target_link_libraries(utilsTests + PRIVATE + sdf + usd + usdGeom + usdSkel + usdShade + GTest::gtest + GTest::gtest_main + fileformatUtils +) + +add_test(NAME utilsTests + COMMAND utilsTests + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/utils/tests/data/baseline_writeASM.usda b/utils/tests/data/baseline_writeASM.usda new file mode 100644 index 00000000..12fdfa08 --- /dev/null +++ b/utils/tests/data/baseline_writeASM.usda @@ -0,0 +1,341 @@ +#usda 1.0 +( + customLayerData = { + } + defaultPrim = "Scene" +) + +def Xform "Scene" +{ + def "Materials" + { + def Material "GeneralTestMaterial" + { + float3 inputs:absorptionColor = (0.25, 0.5, 1) + float inputs:absorptionDistance = 111 + float inputs:ambientOcclusion = 0.01 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:anisotropyAngle = 0.777 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:anisotropyLevel = 0.321 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float3 inputs:baseColor = (1, 2, 3) + float3 inputs:coatColor = (1, 1, 0) + float inputs:coatIOR = 1.33 ( + customData = { + dictionary range = { + double max = 3 + double min = 1 + } + } + ) + float3 inputs:coatNormal = (0.66, 0, 0.66) + float inputs:coatOpacity = 0.55 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:coatRoughness = 0.66 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:coatSpecularLevel = 0.88 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float3 inputs:emissive = (1, 2, 3) + float inputs:emissiveIntensity = 1 + float inputs:height = 1.23 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:IOR = 1.55 ( + customData = { + dictionary range = { + double max = 3 + double min = 1 + } + } + ) + float inputs:metallic = 0.22 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float3 inputs:normal = (0.33, 0.33, 0.33) + float inputs:normalScale = 0.666 + float inputs:opacity = 0.8 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:opacityThreshold = 0.75 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:roughness = 0.44 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float3 inputs:scatteringColor = (1, 0.5, 1) + float inputs:scatteringDistance = 222 + float3 inputs:sheenColor = (0, 1, 1) + float inputs:sheenOpacity = 1 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:sheenRoughness = 0.99 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float3 inputs:specularEdgeColor = (1, 0, 1) + float inputs:specularLevel = 0.5 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:translucency = 0.123 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:volumeThickness = 0.987 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + token outputs:adobe:surface.connect = + + def NodeGraph "ASM" + { + def Shader "ASM" + { + uniform token info:id = "AdobeStandardMaterial_4_0" + float3 inputs:absorptionColor.connect = + float inputs:absorptionDistance.connect = + float inputs:ambientOcclusion.connect = + float inputs:anisotropyAngle.connect = + float inputs:anisotropyLevel.connect = + float3 inputs:baseColor.connect = + float3 inputs:coatColor.connect = + float inputs:coatIOR.connect = + float3 inputs:coatNormal.connect = + float inputs:coatOpacity.connect = + float inputs:coatRoughness.connect = + float inputs:coatSpecularLevel.connect = + float3 inputs:emissive.connect = + float inputs:emissiveIntensity.connect = + float inputs:height.connect = + float inputs:IOR.connect = + float inputs:metallic.connect = + float3 inputs:normal.connect = + float inputs:normalScale.connect = + float inputs:opacity.connect = + float inputs:opacityThreshold.connect = + float inputs:roughness.connect = + bool inputs:scatter = 1 + float3 inputs:scatteringColor.connect = + float inputs:scatteringDistance.connect = + float3 inputs:sheenColor.connect = + float inputs:sheenOpacity.connect = + float inputs:sheenRoughness.connect = + float3 inputs:specularEdgeColor.connect = + float inputs:specularLevel.connect = + float inputs:translucency.connect = + float inputs:volumeThickness.connect = + token outputs:surface + } + } + } + + def Material "TextureTestMaterial" + { + asset inputs:baseColorTexture = @textures/color.png@ + asset inputs:coatNormalTexture = @textures/normal.png@ + asset inputs:coatOpacityTexture = @textures/color.png@ + float inputs:emissiveIntensity = 1 + asset inputs:emissiveTexture = @textures/color.png@ + asset inputs:normalTexture = @textures/normal.png@ + asset inputs:roughnessTexture = @textures/greyscale.png@ + token outputs:adobe:surface.connect = + + def NodeGraph "ASM" + { + def Shader "texCoordReader" + { + uniform token info:id = "UsdPrimvarReader_float2" + string inputs:varname = "st" + float2 outputs:result + } + + def Shader "baseColor" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + token inputs:sourceColorSpace = "sRGB" + float2 inputs:st.connect = + float3 outputs:rgb + } + + def Shader "roughness" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" + float2 inputs:st.connect = + float outputs:r + } + + def Shader "normal" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" + float2 inputs:st.connect = + float3 outputs:rgb + } + + def Shader "emissive_stTransform" + { + uniform token info:id = "UsdTransform2d" + float2 inputs:in.connect = + float inputs:rotation = 15 + float2 inputs:scale = (1.5, 0.75) + float2 inputs:translation = (0.12, 3.45) + float2 outputs:result + } + + def Shader "emissive" + { + uniform token info:id = "UsdUVTexture" + float4 inputs:bias = (0.1, 0.2, 0.3, 0) + asset inputs:file.connect = + float4 inputs:scale = (1, 2, 0.5, 1) + token inputs:sourceColorSpace = "sRGB" + float2 inputs:st.connect = + token inputs:wrapS = "clamp" + token inputs:wrapT = "mirror" + float3 outputs:rgb + } + + def Shader "coatOpacity" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + float2 inputs:st.connect = + float outputs:g + } + + def Shader "coatNormal" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" + float2 inputs:st.connect = + float3 outputs:rgb + } + + def Shader "ASM" + { + uniform token info:id = "AdobeStandardMaterial_4_0" + float3 inputs:baseColor.connect = + float3 inputs:coatNormal.connect = + float inputs:coatOpacity.connect = + float3 inputs:emissive.connect = + float inputs:emissiveIntensity.connect = + float3 inputs:normal.connect = + float inputs:roughness.connect = + token outputs:surface + } + } + } + + def Material "TransmissionTestMaterial" + { + float inputs:translucency = 0.543 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + token outputs:adobe:surface.connect = + + def NodeGraph "ASM" + { + def Shader "ASM" + { + uniform token info:id = "AdobeStandardMaterial_4_0" + float inputs:translucency.connect = + token outputs:surface + } + } + } + } +} + diff --git a/utils/tests/data/baseline_writeOpenPBR.usda b/utils/tests/data/baseline_writeOpenPBR.usda new file mode 100644 index 00000000..527eb4dc --- /dev/null +++ b/utils/tests/data/baseline_writeOpenPBR.usda @@ -0,0 +1,323 @@ +#usda 1.0 +( + customLayerData = { + } + defaultPrim = "Scene" +) + +def Xform "Scene" +{ + def "Materials" + { + def Material "GeneralTestMaterial" + { + color3f inputs:absorptionColor = (0.25, 0.5, 1) + float inputs:absorptionDistance = 111 + float inputs:anisotropyLevel = 0.321 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + color3f inputs:baseColor = (1, 2, 3) + color3f inputs:coatColor = (1, 1, 0) + float inputs:coatIOR = 1.33 ( + customData = { + dictionary range = { + double max = 3 + double min = 1 + } + } + ) + float3 inputs:coatNormal = (0.66, 0, 0.66) + float inputs:coatOpacity = 0.55 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:coatRoughness = 0.66 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:emissionLuminance = 1 + color3f inputs:emissive = (1, 2, 3) + float inputs:fuzzWeight = 1 + float inputs:IOR = 1.55 ( + customData = { + dictionary range = { + double max = 3 + double min = 1 + } + } + ) + float inputs:metallic = 0.22 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float3 inputs:normal = (0.33, 0.33, 0.33) + float inputs:opacity = 0.8 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:roughness = 0.44 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + color3f inputs:scatteringColor = (1, 0.5, 1) + float inputs:scatteringDistance = 222 + color3f inputs:sheenColor = (0, 1, 1) + float inputs:sheenRoughness = 0.99 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + color3f inputs:specularEdgeColor = (1, 0, 1) + float inputs:specularWeight = 0.5 + float inputs:subsurfaceWeight = 1 + float inputs:translucency = 0.123 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + token outputs:mtlx:surface.connect = + + def NodeGraph "OpenPBR" + { + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = + float inputs:base_metalness.connect = + color3f inputs:coat_color.connect = + float inputs:coat_ior.connect = + float inputs:coat_roughness.connect = + float inputs:coat_weight.connect = + color3f inputs:emission_color.connect = + float inputs:emission_luminance.connect = + color3f inputs:fuzz_color.connect = + float inputs:fuzz_roughness.connect = + float inputs:fuzz_weight.connect = + float3 inputs:geometry_coat_normal.connect = + float3 inputs:geometry_normal.connect = + float inputs:geometry_opacity.connect = + color3f inputs:specular_color.connect = + float inputs:specular_ior.connect = + float inputs:specular_roughness.connect = + float inputs:specular_roughness_anisotropy.connect = + float inputs:specular_weight.connect = + color3f inputs:subsurface_color.connect = + float inputs:subsurface_radius.connect = + float inputs:subsurface_weight.connect = + color3f inputs:transmission_color.connect = + float inputs:transmission_depth.connect = + float inputs:transmission_weight.connect = + token outputs:out + } + } + } + + def Material "TextureTestMaterial" + { + asset inputs:baseColorTexture = @textures/color.png@ + asset inputs:coatNormalTexture = @textures/normal.png@ + asset inputs:coatOpacityTexture = @textures/color.png@ + float inputs:emissionLuminance = 1 + asset inputs:emissiveTexture = @textures/color.png@ + asset inputs:normalTexture = @textures/normal.png@ + asset inputs:roughnessTexture = @textures/greyscale.png@ + token outputs:mtlx:surface.connect = + + def NodeGraph "OpenPBR" + { + def Shader "texCoordReader" + { + uniform token info:id = "ND_texcoord_vector2" + float2 outputs:out + } + + def Shader "base_color" + { + uniform token info:id = "ND_image_color3" + asset inputs:file ( + colorSpace = "srgb_texture" + ) + asset inputs:file.connect = + float2 inputs:texcoord.connect = + string inputs:uaddressmode = "periodic" + string inputs:vaddressmode = "periodic" + color3f outputs:out + } + + def Shader "specular_roughness" + { + uniform token info:id = "ND_image_vector4" + asset inputs:file.connect = + float2 inputs:texcoord.connect = + string inputs:uaddressmode = "periodic" + string inputs:vaddressmode = "periodic" + float4 outputs:out + } + + def Shader "specular_roughness_to_float" + { + uniform token info:id = "ND_separate4_vector4" + float4 inputs:in.connect = + float outputs:outx + } + + def Shader "coat_weight" + { + uniform token info:id = "ND_image_vector4" + asset inputs:file.connect = + float2 inputs:texcoord.connect = + string inputs:uaddressmode = "periodic" + string inputs:vaddressmode = "periodic" + float4 outputs:out + } + + def Shader "coat_weight_to_float" + { + uniform token info:id = "ND_separate4_vector4" + float4 inputs:in.connect = + float outputs:outy + } + + def Shader "emission_color_uv_transform" + { + uniform token info:id = "ND_place2d_vector2" + float2 inputs:offset = (0.12, 3.45) + float inputs:rotate = 15 + float2 inputs:scale = (0.6666667, 1.3333334) + float2 inputs:texcoord.connect = + float2 outputs:out + } + + def Shader "emission_color" + { + uniform token info:id = "ND_image_color3" + asset inputs:file ( + colorSpace = "srgb_texture" + ) + asset inputs:file.connect = + float2 inputs:texcoord.connect = + string inputs:uaddressmode = "clamp" + string inputs:vaddressmode = "mirror" + color3f outputs:out + } + + def Shader "emission_color_scale" + { + uniform token info:id = "ND_multiply_color3" + color3f inputs:in1 = (1, 2, 0.5) + color3f inputs:in2.connect = + color3f outputs:out + } + + def Shader "emission_color_bias" + { + uniform token info:id = "ND_add_color3" + color3f inputs:in1 = (0.1, 0.2, 0.3) + color3f inputs:in2.connect = + color3f outputs:out + } + + def Shader "geometry_normal" + { + uniform token info:id = "ND_image_vector3" + asset inputs:file.connect = + float2 inputs:texcoord.connect = + string inputs:uaddressmode = "periodic" + string inputs:vaddressmode = "periodic" + float3 outputs:out + } + + def Shader "geometry_normal_to_world_space" + { + uniform token info:id = "ND_normalmap" + float3 inputs:in.connect = + float3 outputs:out + } + + def Shader "geometry_coat_normal" + { + uniform token info:id = "ND_image_vector3" + asset inputs:file.connect = + float2 inputs:texcoord.connect = + string inputs:uaddressmode = "periodic" + string inputs:vaddressmode = "periodic" + float3 outputs:out + } + + def Shader "geometry_coat_normal_to_world_space" + { + uniform token info:id = "ND_normalmap" + float3 inputs:in.connect = + float3 outputs:out + } + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = + float inputs:coat_weight.connect = + color3f inputs:emission_color.connect = + float inputs:emission_luminance.connect = + float3 inputs:geometry_coat_normal.connect = + float3 inputs:geometry_normal.connect = + float inputs:specular_roughness.connect = + token outputs:out + } + } + } + + def Material "TransmissionTestMaterial" + { + float inputs:translucency = 0.543 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + token outputs:mtlx:surface.connect = + + def NodeGraph "OpenPBR" + { + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + float inputs:transmission_weight.connect = + token outputs:out + } + } + } + } +} diff --git a/utils/tests/data/baseline_writeUsdPreviewSurface.usda b/utils/tests/data/baseline_writeUsdPreviewSurface.usda new file mode 100644 index 00000000..1028fe3a --- /dev/null +++ b/utils/tests/data/baseline_writeUsdPreviewSurface.usda @@ -0,0 +1,243 @@ +#usda 1.0 +( + customLayerData = { + } + defaultPrim = "Scene" +) + +def Xform "Scene" +{ + def "Materials" + { + def Material "GeneralTestMaterial" + { + float inputs:ambientOcclusion = 0.01 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + color3f inputs:baseColor = (1, 2, 3) + float inputs:coatOpacity = 0.55 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:coatRoughness = 0.66 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + color3f inputs:emissive = (1, 2, 3) + float inputs:height = 1.23 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:IOR = 1.55 ( + customData = { + dictionary range = { + double max = 3 + double min = 1 + } + } + ) + float inputs:metallic = 0.22 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + normal3f inputs:normal = (0.33, 0.33, 0.33) + float inputs:opacity = 0.8 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:opacityThreshold = 0.75 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + float inputs:roughness = 0.44 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + color3f inputs:specularEdgeColor = (1, 0, 1) + int inputs:useSpecularWorkflow = 1 ( + customData = { + dictionary range = { + int max = 1 + int min = 0 + } + } + ) + token outputs:displacement.connect = + token outputs:surface.connect = + + def NodeGraph "UsdPreviewSurface" + { + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + float inputs:clearcoat.connect = + float inputs:clearcoatRoughness.connect = + color3f inputs:diffuseColor.connect = + float inputs:displacement.connect = + color3f inputs:emissiveColor.connect = + float inputs:ior.connect = + float inputs:metallic.connect = + normal3f inputs:normal.connect = + float inputs:occlusion.connect = + float inputs:opacity.connect = + float inputs:opacityThreshold.connect = + float inputs:roughness.connect = + color3f inputs:specularColor.connect = + int inputs:useSpecularWorkflow.connect = + token outputs:displacement + token outputs:surface + } + } + } + + def Material "TextureTestMaterial" + { + asset inputs:baseColorTexture = @textures/color.png@ + asset inputs:coatOpacityTexture = @textures/color.png@ + asset inputs:emissiveTexture = @textures/color.png@ + asset inputs:normalTexture = @textures/normal.png@ + asset inputs:roughnessTexture = @textures/greyscale.png@ + token outputs:displacement.connect = + token outputs:surface.connect = + + def NodeGraph "UsdPreviewSurface" + { + def Shader "texCoordReader" + { + uniform token info:id = "UsdPrimvarReader_float2" + string inputs:varname = "st" + float2 outputs:result + } + + def Shader "diffuseColor" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + token inputs:sourceColorSpace = "sRGB" + float2 inputs:st.connect = + float3 outputs:rgb + } + + def Shader "emissiveColor_stTransform" + { + uniform token info:id = "UsdTransform2d" + float2 inputs:in.connect = + float inputs:rotation = 15 + float2 inputs:scale = (1.5, 0.75) + float2 inputs:translation = (0.12, 3.45) + float2 outputs:result + } + + def Shader "emissiveColor" + { + uniform token info:id = "UsdUVTexture" + float4 inputs:bias = (0.1, 0.2, 0.3, 0) + asset inputs:file.connect = + float4 inputs:scale = (1, 2, 0.5, 1) + token inputs:sourceColorSpace = "sRGB" + float2 inputs:st.connect = + token inputs:wrapS = "clamp" + token inputs:wrapT = "mirror" + float3 outputs:rgb + } + + def Shader "roughness" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" + float2 inputs:st.connect = + float outputs:r + } + + def Shader "clearcoat" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + float2 inputs:st.connect = + float outputs:g + } + + def Shader "normal" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" + float2 inputs:st.connect = + float3 outputs:rgb + } + + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + float inputs:clearcoat.connect = + color3f inputs:diffuseColor.connect = + color3f inputs:emissiveColor.connect = + normal3f inputs:normal.connect = + float inputs:roughness.connect = + token outputs:displacement + token outputs:surface + } + } + } + + def Material "TransmissionTestMaterial" + { + float inputs:opacity = 0.45700002 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + token outputs:displacement.connect = + token outputs:surface.connect = + + def NodeGraph "UsdPreviewSurface" + { + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + float inputs:opacity.connect = + token outputs:displacement + token outputs:surface + } + } + } + } +} + diff --git a/utils/tests/tests.cpp b/utils/tests/tests.cpp new file mode 100644 index 00000000..ac58bd5f --- /dev/null +++ b/utils/tests/tests.cpp @@ -0,0 +1,286 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +// Run with this turned on to (re-)generate the baselines +#define UPDATE_USDA_BASELINES 0 + +#if UPDATE_USDA_BASELINES +# define ASSERT_USDA(usdaLayer, baselinePath) \ + { \ + std::cout << "Updating USDA baseline " << baselinePath << std::endl; \ + usdaLayer->Export(baselinePath); \ + assertUsda(usdaLayer, baselinePath); \ + } +#else +# define ASSERT_USDA(usdaLayer, baselinePath) assertUsda(usdaLayer, baselinePath) +#endif + +PXR_NAMESPACE_USING_DIRECTIVE + +using namespace adobe::usd; + +// This class is here to expose the protected SdfFileFormat::_SetLayerData function to this test +class TestFileFormat : public SdfFileFormat +{ + public: + static void SetLayerData(SdfLayer* layer, SdfAbstractDataRefPtr& data) + { + SdfFileFormat::_SetLayerData(layer, data); + } +}; + +void +assertUsda(const SdfLayerHandle& sdfLayer, const std::string& baselinePath) +{ + ASSERT_TRUE(sdfLayer); + SdfLayerRefPtr baselineLayer = SdfLayer::FindOrOpen(baselinePath); + ASSERT_TRUE(baselineLayer) << "Failed to load baseline layer from " << baselinePath; + + std::string layerStr; + sdfLayer->ExportToString(&layerStr); + std::string baselineStr; + baselineLayer->ExportToString(&baselineStr); + + if (layerStr != baselineStr) { + EXPECT_TRUE(false) << "Output of layer " << sdfLayer->GetIdentifier() + << " does not match baseline " << baselinePath; + + std::cout << "Layer output has length: " << layerStr.size() + << "\nBaseline has length: " << baselineStr.size() << std::endl; + + std::filesystem::path basePath(baselinePath); + std::string dumpPath = basePath.filename().string(); + std::fstream out(dumpPath, std::ios::out); + out << layerStr; + out.close(); + std::cout << "Output dumped to " << dumpPath << std::endl; + + // Very poor person's diff operation. Can we do better without bringing + // in a diff library? + for (size_t i = 0; i < layerStr.size(); ++i) { + if (i >= baselineStr.size()) { + std::cout << "Size difference. Output has more characters than baseline" + << std::endl; + break; + } + if (layerStr[i] != baselineStr[i]) { + std::cout << "Mismatch at char " << i << std::endl; + std::cout << "Remainder in output:\n" << &layerStr[i] << std::endl; + std::cout << "Remainder in baseline:\n" << &baselineStr[i] << std::endl; + break; + } + } + } +} + +void +fillGeneralTestMaterial(UsdData& data) +{ + Material& m = data.addMaterial().second; + m.name = "GeneralTestMaterial"; + // Set every input to a constant value + m.useSpecularWorkflow = Input{ VtValue(1) }; + m.diffuseColor = Input{ VtValue(GfVec3f(1.0f, 2.0f, 3.0f)) }; + m.emissiveColor = Input{ VtValue(GfVec3f(1.0f, 2.0f, 3.0f)) }; + m.specularLevel = Input{ VtValue(0.5f) }; + m.specularColor = Input{ VtValue(GfVec3f(1.0f, 0.0f, 1.0f)) }; + m.normal = Input{ VtValue(GfVec3f(0.33f, 0.33f, 0.33f)) }; + m.normalScale = Input{ VtValue(0.666f) }; + m.metallic = Input{ VtValue(0.22f) }; + m.roughness = Input{ VtValue(0.44f) }; + m.clearcoat = Input{ VtValue(0.55f) }; + m.clearcoatColor = Input{ VtValue(GfVec3f(1.0f, 1.0f, 0.0f)) }; + m.clearcoatRoughness = Input{ VtValue(0.66f) }; + m.clearcoatIor = Input{ VtValue(1.33f) }; + m.clearcoatSpecular = Input{ VtValue(0.88f) }; + m.clearcoatNormal = Input{ VtValue(GfVec3f(0.66f, 0.0f, 0.66f)) }; + m.sheenColor = Input{ VtValue(GfVec3f(0.0f, 1.0f, 1.0f)) }; + m.sheenRoughness = Input{ VtValue(0.99f) }; + m.anisotropyLevel = Input{ VtValue(0.321f) }; + m.anisotropyAngle = Input{ VtValue(0.777f) }; + m.opacity = Input{ VtValue(0.8f) }; + m.opacityThreshold = Input{ VtValue(0.75f) }; + m.displacement = Input{ VtValue(1.23f) }; + m.occlusion = Input{ VtValue(0.01f) }; + m.ior = Input{ VtValue(1.55f) }; + m.transmission = Input{ VtValue(0.123f) }; + m.volumeThickness = Input{ VtValue(0.987f) }; + m.absorptionDistance = Input{ VtValue(111.0f) }; + m.absorptionColor = Input{ VtValue(GfVec3f(0.25f, 0.5f, 1.0f)) }; + m.scatteringDistance = Input{ VtValue(222.0f) }; + m.scatteringColor = Input{ VtValue(GfVec3f(1.0f, 0.5f, 1.0f)) }; +} + +void +fillTextureTestMaterial(UsdData& data) +{ + // Add some images to use + auto [colorId, colorImage] = data.addImage(); + colorImage.name = "color.png"; + colorImage.uri = "textures/color.png"; + colorImage.format = ImageFormat::ImageFormatPng; + auto [normalId, normalImage] = data.addImage(); + normalImage.name = "normal.png"; + normalImage.uri = "textures/normal.png"; + normalImage.format = ImageFormat::ImageFormatPng; + auto [greyscaleId, greyscaleImage] = data.addImage(); + greyscaleImage.name = "greyscale.png"; + greyscaleImage.uri = "textures/greyscale.png"; + greyscaleImage.format = ImageFormat::ImageFormatPng; + + Material& m = data.addMaterial().second; + m.name = "TextureTestMaterial"; + // Set different inputs to specific texture setups + + // Color textures + { + Input input; + input.image = colorId; + input.channel = AdobeTokens->rgb; + input.colorspace = AdobeTokens->sRGB; + m.diffuseColor = input; + + // Wrap mode, scale & bias, UV transform + input.wrapS = AdobeTokens->clamp; + input.wrapT = AdobeTokens->mirror; + input.scale = GfVec4f(1.0f, 2.0f, 0.5, 1.0f); + input.bias = GfVec4f(0.1f, 0.2f, 0.3f, 0.0f); + input.uvRotation = 15.0f; + input.uvScale = GfVec2f(1.5f, 0.75f); + input.uvTranslation = GfVec2f(0.12f, 3.45f); + m.emissiveColor = input; + } + + // Normal maps + { + Input input; + input.image = normalId; + input.channel = AdobeTokens->rgb; + input.colorspace = AdobeTokens->raw; + m.normal = input; + m.clearcoatNormal = input; + } + + // Greyscale maps + { + Input input; + input.image = greyscaleId; + input.channel = AdobeTokens->r; + input.colorspace = AdobeTokens->raw; + m.roughness = input; + } + + // Single channel from RGB map + { + Input input; + input.image = colorId; + input.channel = AdobeTokens->g; + m.clearcoat = input; + } +} + +void +fillTransmissionMaterial(UsdData& data) +{ + Material& m = data.addMaterial().second; + m.name = "TransmissionTestMaterial"; + + // Set transmission, but not opacity. For UsdPreviewSurface this should be mapped as an inverse + // to opacity + m.transmission = Input{ VtValue(0.543f) }; +} + +TEST(FileFormatUtilsTests, writeUsdPreviewSurface) +{ + SdfLayerRefPtr layer = SdfLayer::CreateAnonymous("Scene.usda"); + SdfAbstractDataRefPtr sdfData(new SdfData()); + UsdData data; + + fillGeneralTestMaterial(data); + fillTextureTestMaterial(data); + fillTransmissionMaterial(data); + + WriteLayerOptions options; + options.writeUsdPreviewSurface = true; + options.writeASM = false; + options.writeOpenPBR = false; + + writeLayer( + options, data, &*layer, sdfData, "Test Data", "Testing", TestFileFormat::SetLayerData); + // Clear the doc string, since it contains the date and version number and hence would have to + // be updated all the time + layer->SetDocumentation(""); + + ASSERT_USDA(layer, "data/baseline_writeUsdPreviewSurface.usda"); +} + +TEST(FileFormatUtilsTests, writeASM) +{ + SdfLayerRefPtr layer = SdfLayer::CreateAnonymous("Scene.usda"); + SdfAbstractDataRefPtr sdfData(new SdfData()); + UsdData data; + + fillGeneralTestMaterial(data); + fillTextureTestMaterial(data); + fillTransmissionMaterial(data); + + WriteLayerOptions options; + options.writeUsdPreviewSurface = false; + options.writeASM = true; + options.writeOpenPBR = false; + + writeLayer( + options, data, &*layer, sdfData, "Test Data", "Testing", TestFileFormat::SetLayerData); + // Clear the doc string, since it contains the date and version number and hence would have to + // be updated all the time + layer->SetDocumentation(""); + + ASSERT_USDA(layer, "data/baseline_writeASM.usda"); +} + +TEST(FileFormatUtilsTests, writeOpenPBR) +{ + SdfLayerRefPtr layer = SdfLayer::CreateAnonymous("Scene.usda"); + SdfAbstractDataRefPtr sdfData(new SdfData()); + UsdData data; + + fillGeneralTestMaterial(data); + fillTextureTestMaterial(data); + fillTransmissionMaterial(data); + + WriteLayerOptions options; + options.writeUsdPreviewSurface = false; + options.writeASM = false; + options.writeOpenPBR = true; + + writeLayer( + options, data, &*layer, sdfData, "Test Data", "Testing", TestFileFormat::SetLayerData); + // Clear the doc string, since it contains the date and version number and hence would have to + // be updated all the time + layer->SetDocumentation(""); + + ASSERT_USDA(layer, "data/baseline_writeOpenPBR.usda"); +} \ No newline at end of file diff --git a/version b/version index 8cfbc905..8428158d 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.1.1 \ No newline at end of file +1.1.2 \ No newline at end of file