diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..4d9f4864 --- /dev/null +++ b/.clang-format @@ -0,0 +1,31 @@ +--- +BasedOnStyle: Mozilla +TabWidth: '4' +IndentWidth: '4' +AccessModifierOffset: -4 +ColumnLimit: 100 +AlignAfterOpenBracket: Align +AlignEscapedNewlines: Left +MaxEmptyLinesToKeep: 1 +AllowShortBlocksOnASingleLine: Empty +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Never + AfterEnum: true + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfa3f67a..a6bed32b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,6 +180,7 @@ jobs: FBX_OUTFILE_CORRECTED=$(echo "${{ env.FBX_OUTFILE }}" | sed 's/_mac.pkg/_macos.pkg/') sudo installer -pkg $FBX_OUTFILE_CORRECTED -target / -verboseR sudo mv "/Applications/Autodesk/FBX SDK/" "/Applications/Autodesk/FBXSDK" + sudo sed -i '.bak' 's/mLefttChild/mLeftChild/g' "/Applications/Autodesk/FBXSDK/2020.3.7/include/fbxsdk/core/base/fbxredblacktree.h" else # Linux FBX_INSTALL_DIR=${{ github.workspace }}/FBX_SDK_INSTALL chmod +r ${{ env.FBX_OUTFILE }}.tar.gz @@ -205,8 +206,7 @@ jobs: "-DUSD_FILEFORMATS_ENABLE_FBX=$([[ "${{ matrix.config }}" == "ALL" || "${{ matrix.config }}" == "FBX" ]] && echo "ON" || echo "OFF")" "-DFBXSDK_ROOT=${{ env.FBX_INSTALL_DIR }}" "-DUSD_FILEFORMATS_BUILD_TESTS=ON" - "-DOpenImageIO_INCLUDE_DIR=${{ github.workspace }}/usd_build/include" - "-DOpenImageIO_INCLUDES=${{ github.workspace }}/usd_build/include" + "-DOpenImageIO_ROOT=${{ github.workspace }}/usd_build" "-DPython3_LIBRARY=" "-DPython3_INCLUDE_DIR=" "-DPython3_VERSION=3.10.11" @@ -218,12 +218,8 @@ jobs: else libFolder="lib" fi - platformArgs=( - "-DOpenImageIO_DIR=${{ github.workspace }}/usd_build/${libFolder}/cmake/OpenImageIO" - "-DOpenImageIO_LIB_DIR=${{ github.workspace }}/usd_build/${libFolder}/cmake/OpenImageIO" - ) - fullCmakeArgs="$baseArgs ${commonArgs[@]} ${platformArgs[@]}" + fullCmakeArgs="$baseArgs ${commonArgs[@]}" cmake $fullCmakeArgs - name: Build and Display Linker Command diff --git a/.github/workflows/create-usd-release.yml b/.github/workflows/create-usd-release.yml index 70144a15..f415aad1 100644 --- a/.github/workflows/create-usd-release.yml +++ b/.github/workflows/create-usd-release.yml @@ -9,7 +9,7 @@ on: usd_version: description: "USD Version to build" required: true - default: "2411" + default: "2511" jobs: prepare-matrix: @@ -19,9 +19,9 @@ jobs: steps: - id: set-matrix run: | - OS_LIST="[\"windows-2022\",\"macOS-13\"" + OS_LIST="[\"windows-2022\", \"windows-2025\"" if [[ "${{ github.event.inputs.usd_version }}" -gt 2400 ]]; then - OS_LIST="$OS_LIST,\"macOS-14\"" + OS_LIST="$OS_LIST,\"macOS-14\", \"macOS-15\", \"macos-26\"" fi OS_LIST="$OS_LIST,\"ubuntu-22.04\"]" echo "matrix=$OS_LIST" >> $GITHUB_OUTPUT @@ -57,7 +57,7 @@ jobs: $releaseExists = $false "exists=false" | Out-File -FilePath $env:GITHUB_ENV -Append Write-Output "Release not found: $releaseName -- Creating new one" - gh release create "$releaseName" --title "$releaseName" --notes "USD built with the following parameters: --build-shared --openimageio --tools --python --debug-python --usd-imaging --build-variant release --use-cxx11-abi=0 (linux)" + gh release create "$releaseName" --title "$releaseName" --notes "USD built with the following parameters: --build-shared --openimageio --tools --usd-imaging --build-variant release --use-cxx11-abi=0 (linux)" } else { $releaseExists = $true } @@ -82,6 +82,12 @@ jobs: python-version: "3.10.11" id: setup-python + - name: Upgrade pip + setuptools + if: contains(matrix.os, 'macos-26-intel') + run: | + python -m pip install --upgrade pip setuptools + python -c "import ssl; print('SSL OK:', ssl.OPENSSL_VERSION)" + - name: Install Ninja (Unix) if: env.exists == 'false' && matrix.os != 'windows-2022' run: | @@ -102,13 +108,14 @@ jobs: brew cleanup - name: Install Additional Dependencies (Ubuntu) - if: env.exists == 'false' && matrix.os == 'ubuntu-22.04' + if: env.exists == 'false' && contains(matrix.os, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y build-essential libgl1-mesa-dev libglew-dev libxi-dev libxrandr-dev + sudo apt-get install -y libx11-dev libxinerama-dev libxcursor-dev libxi-dev - name: Install Additional Dependencies (Windows) - if: env.exists == 'false' && matrix.os == 'windows-2022' + if: env.exists == 'false' && contains(matrix.os, 'windows') run: | choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' @@ -179,18 +186,18 @@ jobs: if ("${{runner.os}}" -eq "Linux") { $abi_arg = "--use-cxx11-abi 0" } - $python_cmd = "python $file `${{ github.workspace }}/usd_build` --build-shared --openimageio --tools --python --debug-python --usd-imaging --build-variant release $abi_arg $generator" + $python_cmd = "python $file `${{ github.workspace }}/usd_build` --onetbb --no-examples --draco --openimageio --no-materialx --tools --build-variant release $abi_arg $generator" Invoke-Expression $python_cmd - name: Remove Specific Folders Unix - if: env.exists == 'false' && matrix.os != 'windows-2022' + if: env.exists == 'false' && contains(matrix.os, 'windows') == 'false' run: | rm -rf ${{ github.workspace }}/usd_build/build rm -rf ${{ github.workspace }}/usd_build/share rm -rf ${{ github.workspace }}/usd_build/src - name: Remove Specific Folders Windows - if: env.exists == 'false' && matrix.os == 'windows-2022' + if: env.exists == 'false' && contains(matrix.os, 'windows') run: | powershell -Command "& { Remove-Item -Path ${{ github.workspace }}\usd_build\build -Recurse -Force @@ -222,13 +229,13 @@ jobs: } - name: Package Build Artifacts Unix - if: env.exists == 'false' && matrix.os != 'windows-2022' + if: env.exists == 'false' && contains(matrix.os, 'windows') == 'false' run: | cd ${{ github.workspace }}/usd_build zip -r ../usd-${{ github.event.inputs.usd_version }}-${{ matrix.os }}.zip * - name: Package Build Artifacts Windows - if: env.exists == 'false' && matrix.os == 'windows-2022' + if: env.exists == 'false' && contains(matrix.os, 'windows') run: | powershell -Command "& { Set-Location ${{ github.workspace }}\usd_build diff --git a/.gitignore b/.gitignore index 71ce32ce..b38885de 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ *.out *.app +# Build artifacts *build*/ build-meta @@ -44,11 +45,13 @@ build-meta .vscode/* .idea +# All python cache directories and bytecode files +**/__pycache__ + # Generated documentation docs Testing bin -test/__pycache__ test/output test/assets/fbx/*.usd test/assets/gltf/*.usd @@ -56,5 +59,8 @@ test/assets/obj/*/*.usd test/assets/ply/*.usd test/assets/stl/*.usd -# .DS_Store +# macOS system files .DS_Store + +# local cache +*.cache diff --git a/CMakeLists.txt b/CMakeLists.txt index e91c7033..0f26d6e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,9 @@ -cmake_minimum_required(VERSION 3.16.0) +cmake_minimum_required(VERSION 3.19.0) + project(usdFileFormats) -file(READ "version" VERSION) +file(READ "version.json" VERSION_JSON) +string(JSON VERSION GET "${VERSION_JSON}" version) string(REGEX MATCH "([0-9]*).([0-9]*).([0-9]*)" _ ${VERSION}) set(CPACK_PACKAGE_VERSION_MAJOR ${CMAKE_MATCH_1}) set(CPACK_PACKAGE_VERSION_MINOR ${CMAKE_MATCH_2}) @@ -97,26 +99,44 @@ endif () add_subdirectory(utils) +# Add a new file format to the build. This macro will add the relevant subdirectory and set the +# installation destination needed by that plugin's CMakeLists.txt so it installs into +# usd-fileformats-plugins/bin/plugin/usd +# +# New variables: +# - USD${FILEFORMAT}_DESTINATION: Where the fileformat libraries will be installed. This will +# typically be "plugin/usd" +# Example: USDFBX_DESTINATION +# +# @param SUBDIRECTORY_NAME The name of the subdirectory to add. This should be the same name as +# directory of the plugin, and will typically be all lowercase +macro(add_usd_fileformat SUBDIRECTORY_NAME) + string(TOUPPER ${SUBDIRECTORY_NAME} FILEFORMAT) + + set(USD${FILEFORMAT}_DESTINATION "plugin/usd") + add_subdirectory(${SUBDIRECTORY_NAME}) +endmacro() + if (USD_FILEFORMATS_ENABLE_FBX) - add_subdirectory(fbx) + add_usd_fileformat(fbx) endif() if (USD_FILEFORMATS_ENABLE_GLTF) - add_subdirectory(gltf) + add_usd_fileformat(gltf) endif() if (USD_FILEFORMATS_ENABLE_OBJ) - add_subdirectory(obj) + add_usd_fileformat(obj) endif() if (USD_FILEFORMATS_ENABLE_PLY) - add_subdirectory(ply) + add_usd_fileformat(ply) endif() if (USD_FILEFORMATS_ENABLE_SBSAR) - add_subdirectory(sbsar) + add_usd_fileformat(sbsar) endif() if (USD_FILEFORMATS_ENABLE_SPZ) - add_subdirectory(spz) + add_usd_fileformat(spz) endif() if (USD_FILEFORMATS_ENABLE_STL) - add_subdirectory(stl) + add_usd_fileformat(stl) endif() if (UNIX AND NOT APPLE) diff --git a/README.md b/README.md index d585dcab..e78d5be4 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The following tools are needed: The following dependencies are needed: |Dependency|Version|Affects|Optional| |--|--|--|--| -| [Pixar USD](https://github.com/PixarAnimationStudios/USD) | 23.08 | all | no | +| [Pixar USD](https://github.com/PixarAnimationStudios/USD) | 23.08-25.11 | all | no | | [GTest](https://github.com/google/googletest.git) | 1.11.0 | all tests | yes | | [Eigen](https://gitlab.com/libeigen/eigen) | 3.4.0 | usdply, usdspz | no | | [FBX SDK](https://aps.autodesk.com/developer/overview/fbx-sdk) | 2020.3.7 | usdfbx | no | @@ -57,6 +57,13 @@ The following dependencies are needed: | [Spz](https://github.com/nianticlabs/spz) | fd4e2a5 | usdspz | no | | [Substance](https://developer.adobe.com/substance3d-sdk/) | 9.1.2 | usdsbsar | no | + +## Coding Standards +Linting standards are defined within ./.clang-format. All changes within pull requests are expected to now follow these standards. To ensure this, it is advised setup IDEs with format on save features. Alternatively, formatting can be run on all cpp and header files by running the following bash command: +``` +find . -iname '*.h' -o -iname '*.cpp' | xargs clang-format -i +``` + ## Build ### 1. Setup dependencies @@ -83,6 +90,8 @@ The following dependencies are needed: * `/lib64` to `LD_LIBRARY_PATH` in linux * `/lib/python` to `PYTHONPATH` +If USD was built with Python (default behavior with the build script), ensure the environment has access to the Python it was built with. In particular, on Windows, ensure that PATH includes the location of the Python dll used. (Alternatively, the Python dll can also be copied into `/bin`). + In linux you may need these other dependencies: ``` sudo apt update @@ -224,7 +233,7 @@ Our GitHub Actions setup includes two main workflows to support continuous integ ### 1. CI Build Workflow This workflow is triggered by any push or pull request to the main branch and ensures compatibility with Universal Scene Description (USD) versions: -- **Versions Tested:** Builds against the oldest (23.08) and newest (24.05) supported USD versions regularly. +- **Versions Tested:** Builds against the oldest (23.08) and newest (25.11) supported USD versions regularly. - **Weekly Builds:** The workflow builds against all supported USD versions to confirm ongoing compatibility. - **Post-Build Testing:** Following the build, each plugin undergoes sanity testing, including loading a cube to check basic functionality. - **Supported Plugins:** Currently supports FBXm GLTF, OBJ, PLY, and STL. Note: SBSAR plugin is not supported due to SDK constraints. diff --git a/_config.yml b/_config.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/changelog.txt b/changelog.txt index 3d16a962..70904e08 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,96 @@ +v2026.03.0 March 6th, 2026 + +Features + fbx + - add generator metadata to USD + - support bitangents/tangents during import/export + gltf + - support khr_materials_volume_scatter extension + - add support for textures with brackets in their file names + - support EXT_materials_specular_edge_color & EXT_materials_clearcoat_color + - support bitangents/tangents during import/export + - add support for KHR_materials_coat extension + - Skip invalid IOR values < 1.0 + obj + - allow single value for ke material setting + sbsar + - expose uv texture repeat controls + - panorama support + - allow for unlimited cache + - relative image path resolution + stl + - support empty normals on import + utils + - increase MaterialX OpenPBR support + +Fixes + 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 + - fix material property mapping for non lambert/phong shader models + - fix FBX standard material import + - skeleton index validation / avoid default indexes / out of bounds checks + - fix skeletal animation for joints that only have curves on individual channels + - Handle color spaces correctly according to declared parameters + - Armature scale is applied only once through the USD hierarchy + gltf + - fix material index lookup when material is missing + - improved support for gltf scattering extension + - add generator metadata to USD + - fix various crashes + - fix inverted normal maps + - add input validation to prevent memory corruption vulnerabilities + - update readme to fix param name + obj + - replace backslash with slash in texture filepath + - fix crash on loading a file > 2gb + - adding computeNormals to SDF_FORMAT_ARG + - cpp 20 compilation fix + - Adjust FMT library dependency in the OBJ plugin to solve linker issues + 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 + sbsar + - switching the default normal format to sbsar + - specifying substance engine/framework for arm64 + - folder/subfolder support when parsing SBSAR files + - filter SBSAR by graph type + - increase default cache size to 2GB for smooth 4K texture handling + - tests fix double free error on Linux by removing getRenderThreadState() in destructor + - tag texture attributes with color space information for MaterialX/OpenPBR + - Fix conversion of sbsar displacement to OpenPBR + - Revamp heuristic fallbacks for procedural input image path resolution + - Add inverted uv scale for use with openpbr + - Correct normal scale / bias in directx style sbsar files for OpenPBR + spz + - remove clipping to SH0 for Gsplat + - fix GSplat import and export and add support to SH4 + stl + - reverse normals on export + - calculate geometric normals on import + utils + - improve shared file format args + - refactor input struct & material processing + - fix for crash in smooth normals computation + - add asset path to input:file property of UsdPreviewSurface and ASM shaders + - switch to `ND_UsdUVTexture_23` for texture reads in OpenPBR/MaterialX networks + - add type checking to `setAttributeDefaultValue()` to catch invalid data + - check for empty values when setting the default value and forgo the type check + - readLayer to correctly processes instancing setups + - OpenPBR oriented material reading + - add general `preserveExtraMaterialInfo` file format argument + - improvements on ASM to OpenPBR conversion + - updating third-party dependencies + +Build System + utils + - update test baseline images for 24.11 renderer changes + - updated the baselines images for the fbx plugins to match colorspace changes + v1.2.0 October 22nd, 2025 fbx: - fix mesh import when fbx mesh is a root node diff --git a/cmake/FindGTest.cmake b/cmake/FindGTest.cmake index f79a635f..f5334d43 100644 --- a/cmake/FindGTest.cmake +++ b/cmake/FindGTest.cmake @@ -38,7 +38,7 @@ if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_GTEST) CPMAddPackage( NAME googletest # using GTest here triggers errors GIT_REPOSITORY "https://github.com/google/googletest.git" - GIT_TAG "release-1.11.0" + GIT_TAG "v1.17.0" ) set(BUILD_SHARED_LIBS ON) @@ -49,4 +49,4 @@ else() else() find_package(GTest CONFIG) endif() -endif() \ No newline at end of file +endif() diff --git a/cmake/FindLibXml2.cmake b/cmake/FindLibXml2.cmake index 447e20dc..ff3f72f6 100644 --- a/cmake/FindLibXml2.cmake +++ b/cmake/FindLibXml2.cmake @@ -40,7 +40,7 @@ if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_LIBXML2) CPMAddPackage( NAME LibXml2 GIT_REPOSITORY "https://github.com/GNOME/libxml2.git" - GIT_TAG "ae383bdb74523ddaf831d7db0690173c25e483b3" # Release v2.10.0 + GIT_TAG "v2.13.0" ) set(LibXml2_FOUND TRUE) else() @@ -50,4 +50,4 @@ else() else() find_package(LibXml2 CONFIG) endif() -endif() \ No newline at end of file +endif() diff --git a/cmake/FindOpenImageIO.cmake b/cmake/FindOpenImageIO.cmake deleted file mode 100644 index 4a74f0ad..00000000 --- a/cmake/FindOpenImageIO.cmake +++ /dev/null @@ -1,57 +0,0 @@ -#[=======================================================================[.rst: ----- - -Finds the OpenImageIO library. -This find module will simply redirect to a find_package(CONFIG) hinted to look -into the pxr_ROOT. - - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module provides the following imported targets, if found: - -``OpenImageIO::OpenImageIO`` - The OpenImageIO library - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``OpenImageIO_FOUND`` - True if the system has the OpenImageIO library. - - - -#]=======================================================================] -if (TARGET OpenImageIO::OpenImageIO AND TARGET OpenImageIO::OpenImageIO_Util) - return() -endif() - -if(${OpenImageIO_FIND_REQUIRED}) - find_package(OpenImageIO PATHS ${pxr_ROOT} ${pxr_ROOT}/lib64 REQUIRED) -else() - 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/FindTinyGLTF.cmake b/cmake/FindTinyGLTF.cmake index 664f0fca..7220dd18 100644 --- a/cmake/FindTinyGLTF.cmake +++ b/cmake/FindTinyGLTF.cmake @@ -29,22 +29,31 @@ if(TARGET tinygltf::tinygltf) endif() if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_TINYGLTF) + + find_package(nlohmann_json REQUIRED) + message(STATUS "Fetching TinyGLTF") include(CPM) set(TINYGLTF_BUILD_LOADER_EXAMPLE OFF) - set(TINYGLTF_INSTALL OFF) - set(TINYGLTF_HEADER_ONLY ON) + set(TINYGLTF_INSTALL ON) + set(TINYGLTF_HEADER_ONLY OFF) + set(_saved_BUILD_SHARED_LIBS_TinyGLTF ${BUILD_SHARED_LIBS}) + set(BUILD_SHARED_LIBS OFF) + CPMAddPackage( NAME TinyGLTF GIT_REPOSITORY "https://github.com/syoyo/tinygltf.git" - GIT_TAG "v2.8.21" # 4bfc1fc1807e2e2cf3d3111f67d6ebd957514c80 + GIT_TAG "v2.8.21" ) set(TinyGLTF_FOUND TRUE) add_library(tinygltf::tinygltf ALIAS tinygltf) + set(BUILD_SHARED_LIBS ${_saved_BUILD_SHARED_LIBS_TinyGLTF}) else() if (${TinyGLTF_FIND_REQUIRED}) find_package(TinyGLTF CONFIG REQUIRED) else() find_package(TinyGLTF CONFIG) endif() -endif() \ No newline at end of file +endif() + +target_link_libraries(tinygltf INTERFACE nlohmann_json::nlohmann_json) \ No newline at end of file diff --git a/cmake/FindZLIB.cmake b/cmake/FindZLIB.cmake index 87c31dfd..dfed929c 100644 --- a/cmake/FindZLIB.cmake +++ b/cmake/FindZLIB.cmake @@ -46,7 +46,7 @@ if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_ZLIB) CPMAddPackage( NAME ZLIB GIT_REPOSITORY "https://github.com/madler/zlib.git" - GIT_TAG "cacf7f1d4e3d44d871b605da3b647f07d718623f" # /tag/v1.2.11 + GIT_TAG "v1.3.1" ) set(ZLIB_FOUND TRUE) add_library(ZLIB::ZLIB ALIAS zlib) @@ -114,4 +114,4 @@ else() elseif(${ZLIB_FIND_REQUIRED}) message(FATAL_ERROR "Could not find ZLIB") endif() -endif() \ No newline at end of file +endif() diff --git a/cmake/Finddraco.cmake b/cmake/Finddraco.cmake index 5f3e7df1..94be1688 100644 --- a/cmake/Finddraco.cmake +++ b/cmake/Finddraco.cmake @@ -87,4 +87,4 @@ else() elseif(${draco_FIND_REQUIRED}) message(FATAL_ERROR "Could not find draco") endif() -endif() \ No newline at end of file +endif() diff --git a/cmake/Findfmt.cmake b/cmake/Findfmt.cmake index 7d1ed4b3..7e931f0a 100644 --- a/cmake/Findfmt.cmake +++ b/cmake/Findfmt.cmake @@ -34,7 +34,7 @@ if(USD_FILEFORMATS_FORCE_FETCHCONTENT OR USD_FILEFORMATS_FETCH_FMT) CPMAddPackage( NAME fmt GIT_REPOSITORY "https://github.com/fmtlib/fmt.git" - GIT_TAG "10.1.1" # f5e54359df4c26b6230fc61d38aa294581393084 + GIT_TAG "10.1.1" ) set(fmt_FOUND TRUE) else() diff --git a/cmake/Findnlohmann_json.cmake b/cmake/Findnlohmann_json.cmake new file mode 100644 index 00000000..a4d5ca61 --- /dev/null +++ b/cmake/Findnlohmann_json.cmake @@ -0,0 +1,46 @@ +# +# Copyright 2020 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. +# +if(TARGET nlohmann_json::nlohmann_json) + return() +endif() + +message(STATUS "Third-party (external): creating target 'nlohmann_json::nlohmann_json'") + +# nlohmann_json is a big repo for a single header, so we just download the release archive +set(NLOHMANNJSON_VERSION "v3.11.3") + +include(CPM) +CPMAddPackage( + NAME nlohmann_json + URL "https://github.com/nlohmann/json/releases/download/${NLOHMANNJSON_VERSION}/include.zip" + URL_HASH SHA256=a22461d13119ac5c78f205d3df1db13403e58ce1bb1794edc9313677313f4a9d +) + +add_library(nlohmann_json INTERFACE) +add_library(nlohmann_json::nlohmann_json ALIAS nlohmann_json) + +include(GNUInstallDirs) +target_include_directories(nlohmann_json INTERFACE + "$/include" + "$" +) +target_compile_features(nlohmann_json INTERFACE cxx_std_11) + +set(nlohmann_json_FOUND TRUE) + +# Install rules +set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME nlohmann_json) +install(DIRECTORY ${nlohmann_json_SOURCE_DIR}/include/nlohmann DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(TARGETS nlohmann_json EXPORT NlohmannJson_Targets) +install(EXPORT NlohmannJson_Targets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/nlohmann_json NAMESPACE nlohmann_json::) +export(EXPORT NlohmannJson_Targets FILE "${CMAKE_CURRENT_BINARY_DIR}/NlohmannJsonTargets.cmake") + diff --git a/fbx/README.md b/fbx/README.md index 6cefd8ce..1f9f33e4 100644 --- a/fbx/README.md +++ b/fbx/README.md @@ -117,6 +117,14 @@ Note that PBR materials are not supported on export, only Phong The material network uses `MaterialX` nodes to express individual operations and has an `OpenPBR` surface, which has rich support for PBR oriented materials. +* `preserveExtraMaterialInfo`: Generate shading networks with extra data for transcoding. Default is `true` + When this is enabled, the generated shading networks might contain extra inputs that are outside of the respective + material surface schema, that are useful for transcoding purposes. For example, the `OpenPBR` surface does not have + an `occlusion` input for ambient occlusion, but we might want to express such a signal, if it was present in the + source asset, so that an exporter can pick-up said signal and use it when generating an output asset. + When `preserveExtraMaterialInfo` is `false`, the code will not generate these extra fields that are outside of the + schema, which won't affect renders, but can affect the transcoding abilities. + * `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. @@ -128,14 +136,23 @@ Note that PBR materials are not supported on export, only Phong 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`: Convert colors from sRGB to linear. Default: `""` +* `fbxOriginalColorSpace`: Specify the color space of the FBX data. Default: `""` (no conversion) + **Default behavior:** When not specified, the plugin performs **no color conversion** — color values + are passed through as-is and annotated as `raw` (unknown colorspace). This allows the client + application to handle color management according to its needs. + **When set to `sRGB`:** The plugin converts sRGB color values to linear on import, as USD expects + linear color data for rendering. The converted data is annotated as `raw` (linear). 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. + The user can set which one the data was in during import. Exporting will also consider the + original color space. See Export -> `outputColorSpace` for details. ``` from pxr import Usd + # No conversion (default) - data passed through as-is + stage = Usd.Stage.Open("cube.fbx") + + # Convert sRGB to linear stage = Usd.Stage.Open("cube.fbx:SDF_FORMAT_ARGS:fbxOriginalColorSpace=sRGB") ``` diff --git a/fbx/src/CMakeLists.txt b/fbx/src/CMakeLists.txt index 8a6b2787..e35c23b8 100644 --- a/fbx/src/CMakeLists.txt +++ b/fbx/src/CMakeLists.txt @@ -22,6 +22,7 @@ PRIVATE target_include_directories(usdFbx PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" ) @@ -39,32 +40,6 @@ PRIVATE fbxsdk::fbxsdk ) -target_precompile_headers(usdFbx -PRIVATE - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" -) - # Installation of plugin files mimics the file structure that USD has for plugins, # so it is easy to deploy it in a pre-existing USD build, if one chooses to do so. @@ -79,17 +54,19 @@ set_target_properties(usdFbx PROPERTIES RESOURCE ${CMAKE_CURRENT_BINARY_DIR}/plu set_target_properties(usdFbx PROPERTIES RESOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json:plugInfo.json") +# USDFBX_DESTINATION is set in the parent scope by the add_usd_fileformat macro + if(USDFBX_ENABLE_INSTALL) install( TARGETS usdFbx - RUNTIME DESTINATION plugin/usd COMPONENT Runtime - LIBRARY DESTINATION plugin/usd COMPONENT Runtime - RESOURCE DESTINATION plugin/usd/usdFbx/resources COMPONENT Runtime + RUNTIME DESTINATION ${USDFBX_DESTINATION} COMPONENT Runtime + LIBRARY DESTINATION ${USDFBX_DESTINATION} COMPONENT Runtime + RESOURCE DESTINATION ${USDFBX_DESTINATION}/usdFbx/resources COMPONENT Runtime ) install( FILES plugInfo.root.json - DESTINATION plugin/usd + DESTINATION ${USDFBX_DESTINATION} RENAME plugInfo.json COMPONENT Runtime ) diff --git a/fbx/src/api.h b/fbx/src/api.h index eb1d40a6..07dc65da 100644 --- a/fbx/src/api.h +++ b/fbx/src/api.h @@ -14,19 +14,19 @@ governing permissions and limitations under the License. #include "pxr/base/arch/export.h" #if defined(PXR_STATIC) -# define USDFBX_API -# define USDFBX_API_TEMPLATE_CLASS(...) -# define USDFBX_API_TEMPLATE_STRUCT(...) -# define USDFBX_LOCAL +#define USDFBX_API +#define USDFBX_API_TEMPLATE_CLASS(...) +#define USDFBX_API_TEMPLATE_STRUCT(...) +#define USDFBX_LOCAL #else -# if defined(USDFBX_EXPORTS) -# define USDFBX_API ARCH_EXPORT -# define USDFBX_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) -# define USDFBX_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) -# else -# define USDFBX_API ARCH_IMPORT -# define USDFBX_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) -# define USDFBX_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) -# endif -# define USDFBX_LOCAL ARCH_HIDDEN +#if defined(USDFBX_EXPORTS) +#define USDFBX_API ARCH_EXPORT +#define USDFBX_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define USDFBX_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#else +#define USDFBX_API ARCH_IMPORT +#define USDFBX_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define USDFBX_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#endif +#define USDFBX_LOCAL ARCH_HIDDEN #endif diff --git a/fbx/src/fbx.cpp b/fbx/src/fbx.cpp index 4a02965d..8e9875fe 100644 --- a/fbx/src/fbx.cpp +++ b/fbx/src/fbx.cpp @@ -12,8 +12,8 @@ governing permissions and limitations under the License. #include "fbx.h" #include "debugCodes.h" #include -#include #include +#include #include #include #include diff --git a/fbx/src/fbx.h b/fbx/src/fbx.h index cb81acbc..107977a4 100644 --- a/fbx/src/fbx.h +++ b/fbx/src/fbx.h @@ -11,15 +11,14 @@ governing permissions and limitations under the License. */ #pragma once #include +#include #include #include #include #include #include -#include #include - // Dev Notes // * FBX's `GetDirectArray()` can be troublesome when paired with `auto`! Better specify the full // type: diff --git a/fbx/src/fbxExport.cpp b/fbx/src/fbxExport.cpp index 94df7316..66b32307 100644 --- a/fbx/src/fbxExport.cpp +++ b/fbx/src/fbxExport.cpp @@ -952,7 +952,7 @@ exportFbxInput(ExportFbxContext& ctx, const ImageAsset& image = inputTranslator.getImage(input.image); FbxFileTexture* fbxTexture = FbxFileTexture::Create(ctx.fbx->scene, image.name.c_str()); - std::string path = ctx.exportParentPath + image.uri; + std::string path = ctx.exportParentPath + TfGetBaseName(image.uri); fbxTexture->SetFileName(path.c_str()); // File is in current directory. fbxTexture->SetTextureUse(textureUse); fbxTexture->SetWrapMode(getWrapMode(input.wrapS), getWrapMode(input.wrapT)); @@ -975,7 +975,7 @@ exportFbxInput(ExportFbxContext& ctx, return true; } else if (!input.value.IsEmpty()) { // using the sRGB colorspace should only be used for vec3 values - if (colorSpace == AdobeTokens->sRGB) { + if (ctx.convertColorSpaceToSRGB && colorSpace == AdobeTokens->sRGB) { exportFbxPropertyAsSRGB(input.value, property); } else { exportFbxProperty(input.value, property); diff --git a/fbx/src/fbxImport.cpp b/fbx/src/fbxImport.cpp index eaad37e8..84e915d0 100644 --- a/fbx/src/fbxImport.cpp +++ b/fbx/src/fbxImport.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 "fbxImport.h" + #include "debugCodes.h" #include #include @@ -38,6 +39,7 @@ struct ImportedFbxSkeleton { FbxNode* fbxParent = nullptr; std::vector fbxSkeletons; + FbxAMatrix parentGlobalInverse; }; struct ImportFbxContext @@ -61,6 +63,8 @@ struct ImportFbxContext std::unordered_map bonesMap; // Maps an FbxNode* to a skeleton index. No repeated entries expected. std::unordered_map skeletonsMap; + // Tracks which skeleton nodes have animations and will be handled by skeletal animation system + std::unordered_set animatedSkeletonNodes; // Stores Fbx data related to a skeleton. One per USD skeletonIndex std::vector skeletons; @@ -150,7 +154,7 @@ importFbxSettings(ImportFbxContext& ctx) // https://forums.autodesk.com/t5/fbx-forum/evaluating-with-animation-turned-off/td-p/7052419 class ScopedAnimStackDisabler { - public: +public: ScopedAnimStackDisabler(ImportFbxContext& ctx) : mCtx(ctx) { @@ -166,7 +170,7 @@ class ScopedAnimStackDisabler } } - private: +private: ImportFbxContext& mCtx; }; @@ -191,95 +195,101 @@ importFbxTransform(ImportFbxContext& ctx, scale = scaleH; }; - for (int animationStackIndex = 0; animationStackIndex < ctx.animationStacks.size(); - animationStackIndex++) { - // Set the current animation stack so that EvaluateLocalTransform will return the correct - // value - ctx.scene->SetCurrentAnimationStack(ctx.animationStacks[animationStackIndex].stack); - - AnimationTrack& track = ctx.usd->animationTracks[animationStackIndex]; - const ImportedFbxStack& fbxStack = ctx.animationStacks[animationStackIndex]; - - std::set keyFrameTimes; - - // Helper function to get the times of every keyframe from a particular animation curve - auto addFrameTimes = [&keyFrameTimes](const FbxAnimCurve* curve) { - if (curve != nullptr) { - // We found animation data, so we extract every keyframe to process below - int keyCount = curve->KeyGetCount(); - for (int keyIndex = 0; keyIndex < keyCount; ++keyIndex) { - keyFrameTimes.insert(curve->KeyGetTime(keyIndex)); + // Only import regular node animations if this is not an animated skeleton node + // Animated skeleton nodes get their animations from the skeletal animation system + bool isAnimatedSkeletonNode = + (ctx.animatedSkeletonNodes.find(fbxNode) != ctx.animatedSkeletonNodes.end()); + if (!isAnimatedSkeletonNode) { + for (int animationStackIndex = 0; animationStackIndex < ctx.animationStacks.size(); + animationStackIndex++) { + // Set the current animation stack so that EvaluateLocalTransform will return the + // correct value + ctx.scene->SetCurrentAnimationStack(ctx.animationStacks[animationStackIndex].stack); + + AnimationTrack& track = ctx.usd->animationTracks[animationStackIndex]; + const ImportedFbxStack& fbxStack = ctx.animationStacks[animationStackIndex]; + + std::set keyFrameTimes; + + // Helper function to get the times of every keyframe from a particular animation curve + auto addFrameTimes = [&keyFrameTimes](const FbxAnimCurve* curve) { + if (curve != nullptr) { + // We found animation data, so we extract every keyframe to process below + int keyCount = curve->KeyGetCount(); + for (int keyIndex = 0; keyIndex < keyCount; ++keyIndex) { + keyFrameTimes.insert(curve->KeyGetTime(keyIndex)); + } } - } - }; + }; - // For each animation layer, check every property for animation curves and extract the - // keyframes to process - for (FbxAnimLayer* animLayer : fbxStack.animLayers) { - for (auto property = fbxNode->GetFirstProperty(); property.IsValid(); - property = fbxNode->GetNextProperty(property)) { + // For each animation layer, check every property for animation curves and extract the + // keyframes to process + for (FbxAnimLayer* animLayer : fbxStack.animLayers) { + for (auto property = fbxNode->GetFirstProperty(); property.IsValid(); + property = fbxNode->GetNextProperty(property)) { - if (!property.IsAnimated(animLayer)) { - continue; - } + if (!property.IsAnimated(animLayer)) { + continue; + } - FbxAnimCurve* curve = property.GetCurve(animLayer); - FbxAnimCurveNode* curveNode = property.GetCurveNode(animLayer); + FbxAnimCurve* curve = property.GetCurve(animLayer); + FbxAnimCurveNode* curveNode = property.GetCurveNode(animLayer); - // Usually the curve has animation data, but sometimes it is null but at least one - // of the curveNode's channels do. For this reason, we check them all - addFrameTimes(curve); - int numChannels = curveNode ? curveNode->GetChannelsCount() : 0; - for (int channelIndex = 0; channelIndex < numChannels; ++channelIndex) { - addFrameTimes(curveNode->GetCurve(channelIndex)); + // Usually the curve has animation data, but sometimes it is null but at least + // one of the curveNode's channels do. For this reason, we check them all + addFrameTimes(curve); + int numChannels = curveNode ? curveNode->GetChannelsCount() : 0; + for (int channelIndex = 0; channelIndex < numChannels; ++channelIndex) { + addFrameTimes(curveNode->GetCurve(channelIndex)); + } } } - } - size_t numKeyFrames = keyFrameTimes.size(); - if (numKeyFrames > 0) { - node.animations.resize(ctx.animationStacks.size()); - } else { - continue; - } + size_t numKeyFrames = keyFrameTimes.size(); + if (numKeyFrames > 0) { + node.animations.resize(ctx.animationStacks.size()); + } else { + continue; + } - track.hasTimepoints = true; - ctx.usd->hasAnimations = true; + track.hasTimepoints = true; + ctx.usd->hasAnimations = true; - NodeAnimation& nodeAnimation = node.animations[animationStackIndex]; + NodeAnimation& nodeAnimation = node.animations[animationStackIndex]; - nodeAnimation.translations.times.clear(); - nodeAnimation.translations.times.reserve(numKeyFrames); - nodeAnimation.translations.values.reserve(numKeyFrames); + nodeAnimation.translations.times.clear(); + nodeAnimation.translations.times.reserve(numKeyFrames); + nodeAnimation.translations.values.reserve(numKeyFrames); - nodeAnimation.rotations.times.clear(); - nodeAnimation.rotations.times.reserve(numKeyFrames); - nodeAnimation.rotations.values.reserve(numKeyFrames); + nodeAnimation.rotations.times.clear(); + nodeAnimation.rotations.times.reserve(numKeyFrames); + nodeAnimation.rotations.values.reserve(numKeyFrames); - nodeAnimation.scales.times.clear(); - nodeAnimation.scales.times.reserve(numKeyFrames); - nodeAnimation.scales.values.reserve(numKeyFrames); + nodeAnimation.scales.times.clear(); + nodeAnimation.scales.times.reserve(numKeyFrames); + nodeAnimation.scales.values.reserve(numKeyFrames); - for (auto keyFrameTime : keyFrameTimes) { - GfVec3f translation; - GfQuatf rotation; - GfVec3f scale; - float time = keyFrameTime.GetSecondDouble(); - decomposeTransformation(translation, - rotation, - scale, - useGlobalTransform - ? fbxNode->EvaluateGlobalTransform(keyFrameTime) - : fbxNode->EvaluateLocalTransform(keyFrameTime)); + for (auto keyFrameTime : keyFrameTimes) { + GfVec3f translation; + GfQuatf rotation; + GfVec3f scale; + float time = keyFrameTime.GetSecondDouble(); + decomposeTransformation(translation, + rotation, + scale, + useGlobalTransform + ? fbxNode->EvaluateGlobalTransform(keyFrameTime) + : fbxNode->EvaluateLocalTransform(keyFrameTime)); - nodeAnimation.translations.times.push_back(time); - nodeAnimation.translations.values.push_back(translation); + nodeAnimation.translations.times.push_back(time); + nodeAnimation.translations.values.push_back(translation); - nodeAnimation.rotations.times.push_back(time); - nodeAnimation.rotations.values.push_back(rotation); + nodeAnimation.rotations.times.push_back(time); + nodeAnimation.rotations.values.push_back(rotation); - nodeAnimation.scales.times.push_back(time); - nodeAnimation.scales.values.push_back(scale); + nodeAnimation.scales.times.push_back(time); + nodeAnimation.scales.values.push_back(scale); + } } } @@ -422,9 +432,9 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) 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]) }; + static_cast(tangent[1]), + static_cast(tangent[2]), + static_cast(tangent[3]) }; } } else { // FbxGeometryElement::eIndexToDirect size_t tangentCount = tangentElement->GetIndexArray().GetCount(); @@ -433,9 +443,9 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) 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]) }; + static_cast(tangent[1]), + static_cast(tangent[2]), + static_cast(tangent[3]) }; } } } @@ -450,8 +460,8 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) 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]) }; + static_cast(binormal[1]), + static_cast(binormal[2]) }; } } else { // FbxGeometryElement::eIndexToDirect size_t binormalCount = binormalElement->GetIndexArray().GetCount(); @@ -460,8 +470,8 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) 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]) }; + static_cast(binormal[1]), + static_cast(binormal[2]) }; } } } @@ -562,7 +572,26 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) TF_WARN("Skin: %d first cluster does not have a first link.\n", i); continue; } - size_t skeletonIndex = ctx.skeletonsMap[firstlink]; + + // Use find() instead of operator[] to avoid creating default entries + // for nodes not in any skeleton, and validate skeleton index bounds + auto skelIt = ctx.skeletonsMap.find(firstlink); + if (skelIt == ctx.skeletonsMap.end()) { + TF_WARN("Skin %d first link node '%s' not found in skeletons map\n", + i, + firstlink->GetName()); + continue; + } + size_t skeletonIndex = skelIt->second; + + // Validate skeleton index before accessing + if (skeletonIndex >= ctx.usd->skeletons.size()) { + TF_WARN("Skeleton index %zu out of bounds (size %zu) for skin %d\n", + skeletonIndex, + ctx.usd->skeletons.size(), + i); + continue; + } ctx.meshSkinsMap[meshIndex] = skeletonIndex; ctx.usd->skeletons[skeletonIndex].meshSkinningTargets.push_back(meshIndex); @@ -570,8 +599,12 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) // set the mesh geomBindTransform based on the transform matrix // For some reason, FBX put this matrix on the cluster, but we should get the same // result no matter which cluster we look at. + // Factor out the skeleton parent's global transform to make the bind transform + // relative to the parent (armature) node. FbxAMatrix geomBindTransform; firstCluster->GetTransformMatrix(geomBindTransform); + const FbxAMatrix& parentInv = ctx.skeletons[skeletonIndex].parentGlobalInverse; + geomBindTransform = parentInv * geomBindTransform; mesh.geomBindTransform = GetUSDMatrixFromFBX(geomBindTransform); Skeleton& skeleton = ctx.usd->skeletons[skeletonIndex]; @@ -588,7 +621,19 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) continue; } - size_t jointIndex = ctx.bonesMap[link]; + // Use find() instead of operator[] to avoid creating default entries + // for nodes not in the skeleton, and validate bounds before accessing + // bindTransforms + auto boneIt = ctx.bonesMap.find(link); + if (boneIt == ctx.bonesMap.end()) { + TF_WARN("Cluster link node '%s' not found in skeleton bones map for skin %d " + "cluster %d\n", + link->GetName(), + i, + j); + continue; + } + size_t jointIndex = boneIt->second; // if the linkMode for any cluster is not eNormalize, then we will disable weight // normalization @@ -596,22 +641,37 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) if (FbxCluster::ELinkMode::eNormalize != clusterLinkMode) linkMode = clusterLinkMode; - // Set the bindTransform for the joint + // Set the bindTransform for the joint, factoring out the skeleton parent's + // global transform to avoid double-application through the USD hierarchy. FbxAMatrix linkTransform; cluster->GetTransformLinkMatrix(linkTransform); + linkTransform = parentInv * linkTransform; // XXX In theory different meshes could have different link transforms in the case // where they share the same skeleton/links. If that can happen, we'd end up with a // conflict trying to share the skeleton as USD stores the bindTransform on the // skeleton, not on the mesh. We haven't seen this yet though, so we don't try to // un-share the skeletons in this case, and instead just ovewrite the bindTransforms + + // Validate jointIndex is within bounds before writing + if (jointIndex >= skeleton.bindTransforms.size()) { + TF_WARN( + "Joint index %zu exceeds bind transforms size %zu for skin %d cluster %d\n", + jointIndex, + skeleton.bindTransforms.size(), + i, + j); + continue; + } skeleton.bindTransforms[jointIndex] = GetUSDMatrixFromFBX(linkTransform); int clusterControlPointIndicesCount = cluster->GetControlPointIndicesCount(); int* clusterControlPointIndices = cluster->GetControlPointIndices(); double* pointsWeights = cluster->GetControlPointWeights(); if (clusterControlPointIndices == nullptr) { - TF_WARN("No cluster control point indices for skin cluster: %d.\n", j); + // This is normal for some meshes, so don't warn about it as it can spam the + // console + // TF_WARN("No cluster control point indices for skin cluster: %d.\n", j); continue; } if (pointsWeights == nullptr) { @@ -620,9 +680,12 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) } for (int k = 0; k < clusterControlPointIndicesCount; k++) { int controlPointIndex = clusterControlPointIndices[k]; - if (controlPointIndex > indexes.size() || controlPointIndex > weights.size()) { + // Use >= instead of > to prevent off-by-one out-of-bounds access + if (controlPointIndex < 0 || + static_cast(controlPointIndex) >= indexes.size() || + static_cast(controlPointIndex) >= weights.size()) { TF_WARN("Control Point Index outside of index or weight bounds. index: %d " - " Index Size: %d Weight Size: %d", + " Index Size: %zu Weight Size: %zu", controlPointIndex, indexes.size(), weights.size()); @@ -692,8 +755,8 @@ importFbxMesh(ImportFbxContext& ctx, FbxMesh* fbxMesh, int parent) // 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()); + "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); @@ -884,7 +947,8 @@ importPropTexture(ImportFbxContext& ctx, if (!FbxProperty::HasDefaultValue(prop)) { input.value = readPropValue(prop); } - if (colorSpace == AdobeTokens->sRGB) { + bool convertToLinear = (ctx.originalColorSpace == AdobeTokens->sRGB); + if (convertToLinear && colorSpace == AdobeTokens->sRGB) { input.value = srgbToLinear(input.value); } // It's handy to also print the value here, besides the texture information @@ -897,7 +961,11 @@ importPropTexture(ImportFbxContext& ctx, printPropValue(prop).c_str(), colorSpace == AdobeTokens->sRGB ? "(sRGB)" : "(raw)", textureFilename.c_str()); - input.colorspace = colorSpace; + // Set the colorspace annotation: + // - If conversion happened, data is now linear (raw) + // - If no conversion and originalColorSpace not set, use raw (unknown colorspace) + // - Otherwise, keep the semantic colorspace annotation + input.colorspace = convertToLinear ? AdobeTokens->raw : colorSpace; } static const FbxImplementation* @@ -1011,28 +1079,33 @@ _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, TF_DEBUG_MSG(FILE_FORMAT_FBX, "Checking if %s is an Autodesk Standard Surface Material\n", fbxMaterial->GetName()); + // Determine the effective colorspace for color properties based on the originalColorSpace + // option. If originalColorSpace is set to sRGB, color data will be converted to linear and + // stored as raw. If originalColorSpace is not set, no conversion happens and data is passed + // through as raw (unknown colorspace - let the client application handle color management). + const TfToken& colorPropertySpace = + (ctx.originalColorSpace == AdobeTokens->sRGB) ? AdobeTokens->sRGB : AdobeTokens->raw; + // This will contain the properties that are directly mapped from the standard surface exactly - // as is. We need to note if they are One or Three channels and if they are sRGB or raw for - // later usage. - std::unordered_map> + // as is. We need to note if they are One or Three channels and the colorspace for later usage. + std::unordered_map> standardSurfToUsdProperty = { { "base_color", - { usdMaterial.diffuseColor, FbxPropertyNumChannels::Three, AdobeTokens->sRGB } }, + { usdMaterial.diffuseColor, FbxPropertyNumChannels::Three, colorPropertySpace } }, { "specular_color", - { usdMaterial.specularColor, FbxPropertyNumChannels::Three, AdobeTokens->sRGB } }, + { usdMaterial.specularColor, FbxPropertyNumChannels::Three, colorPropertySpace } }, { "metalness", { usdMaterial.metallic, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "specular_roughness", { usdMaterial.roughness, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "coat", { usdMaterial.clearcoat, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "coat_color", - { usdMaterial.clearcoatColor, FbxPropertyNumChannels::Three, AdobeTokens->sRGB } }, + { usdMaterial.clearcoatColor, FbxPropertyNumChannels::Three, colorPropertySpace } }, { "coat_roughness", { usdMaterial.clearcoatRoughness, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "coat_IOR", { usdMaterial.clearcoatIor, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "sheen_color", - { usdMaterial.sheenColor, FbxPropertyNumChannels::Three, AdobeTokens->sRGB } }, + { usdMaterial.sheenColor, FbxPropertyNumChannels::Three, colorPropertySpace } }, { "sheen_roughness", { usdMaterial.sheenRoughness, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "specular_anisotropy", @@ -1045,9 +1118,9 @@ _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, { "transmission_depth", { usdMaterial.absorptionDistance, FbxPropertyNumChannels::One, AdobeTokens->raw } }, { "transmission_color", - { usdMaterial.absorptionColor, FbxPropertyNumChannels::Three, AdobeTokens->sRGB } }, + { usdMaterial.absorptionColor, FbxPropertyNumChannels::Three, colorPropertySpace } }, { "subsurface_color", - { usdMaterial.scatteringColor, FbxPropertyNumChannels::Three, AdobeTokens->sRGB } }, + { usdMaterial.scatteringColor, FbxPropertyNumChannels::Three, colorPropertySpace } }, }; // Make a set that has all the properties we want to validate to confirm this is a standard @@ -1096,19 +1169,22 @@ _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, // If we got here then we assume this is one of the standard shader variants because it had all // of the properties we are expecting to see and use to map to USD for (auto& it : standardSurfToUsdProperty) { - auto property = getProp(it.first); - auto numChannels = std::get<1>(it.second); - auto colorSpace = std::get<2>(it.second); + FbxProperty property = getProp(it.first); + Input& input = std::get<0>(it.second); + FbxPropertyNumChannels numChannels = std::get<1>(it.second); + const TfToken& colorSpace = std::get<2>(it.second); if (numChannels == FbxPropertyNumChannels::One) { auto typedProp = static_cast>(property); - Input input; - importPropTexture(ctx, textures, fbxMaterial, typedProp, input, "r", colorSpace); - inputTranslator.translateDirect(input, std::get<0>(it.second)); + Input tempInput; + importPropTexture(ctx, textures, fbxMaterial, typedProp, tempInput, "r", colorSpace); + inputTranslator.translateDirect(tempInput, input); } else if (numChannels == FbxPropertyNumChannels::Three) { auto typedProp = static_cast>(property); - Input input; - importPropTexture(ctx, textures, fbxMaterial, typedProp, input, "rgb", colorSpace); - inputTranslator.translateDirect(input, std::get<0>(it.second)); + Input tempInput; + importPropTexture(ctx, textures, fbxMaterial, typedProp, tempInput, "rgb", colorSpace); + inputTranslator.translateDirect(tempInput, input); + } else { + TF_CODING_ERROR("Unknown number of channels"); } } @@ -1147,7 +1223,7 @@ _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, emissionColorProperty, emissionColorInput, "rgb", - AdobeTokens->sRGB); + colorPropertySpace); // XXX @dcoffey I believe a more proper way to do this is to keep the emissive intensity as a // separate input because in UIs that use a color picker to modify this input you will lose @@ -1159,10 +1235,10 @@ _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, auto opacityProperty = getProp(kOpacity); if (opacityProperty.IsValid()) { auto opacityTypedProp = static_cast>(opacityProperty); - FbxDouble3 opacityColor = opacityTypedProp.Get(); + GfVec3f opacityColor = readPropValue(opacityTypedProp); // Convert the opacity color to grayscale and use that as the opacity value - double grayscaleOpacity = (opacityColor[0] + opacityColor[1] + opacityColor[2]) / 3.0; + float grayscaleOpacity = (opacityColor[0] + opacityColor[1] + opacityColor[2]) / 3.0f; usdMaterial.opacity.value = grayscaleOpacity; usdMaterial.opacity.colorspace = AdobeTokens->raw; } @@ -1172,47 +1248,57 @@ _mapAutodeskStandardMaterial(const FbxSurfaceMaterial* fbxMaterial, bool _processHardwareShaderMaterial(const FbxSurfaceMaterial* fbxMaterial, - ImportFbxContext& ctx, - const std::unordered_map& textures, - Material& usdMaterial, - InputTranslator& inputTranslator) + 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()); - + TF_DEBUG_MSG(FILE_FORMAT_FBX, + "Attempting hardware shader material processing for '%s'\n", + fbxMaterial->GetName()); + + // Determine colorspace based on originalColorSpace option + const TfToken& colorPropertySpace = + (ctx.originalColorSpace == AdobeTokens->sRGB) ? AdobeTokens->sRGB : AdobeTokens->raw; + 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()) { + 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]); + usdMaterial.diffuseColor.colorspace = colorPropertySpace; + 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 +// 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 @@ -1222,31 +1308,43 @@ _processUnknownShadingModel(const FbxSurfaceMaterial* fbxMaterial, Material& usdMaterial, InputTranslator& inputTranslator) { - TF_DEBUG_MSG(FILE_FORMAT_FBX, - "Processing material '%s' with unknown ShadingModel using property-based approach\n", - fbxMaterial->GetName()); - + TF_DEBUG_MSG( + FILE_FORMAT_FBX, + "Processing material '%s' with unknown ShadingModel using property-based approach\n", + fbxMaterial->GetName()); + + // Determine colorspace based on originalColorSpace option + const TfToken& colorPropertySpace = + (ctx.originalColorSpace == AdobeTokens->sRGB) ? AdobeTokens->sRGB : AdobeTokens->raw; + bool foundAnyProperties = false; - + // Helper to safely extract color properties with multiple naming conventions - auto extractColorProperty = [&](const std::vector& names, Input& targetInput) -> bool { + 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()); - + 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]); + targetInput.colorspace = colorPropertySpace; + 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) { @@ -1256,22 +1354,32 @@ _processUnknownShadingModel(const FbxSurfaceMaterial* fbxMaterial, 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]); + targetInput.colorspace = colorPropertySpace; + 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()); + 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 { + 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) { @@ -1279,88 +1387,101 @@ _processUnknownShadingModel(const FbxSurfaceMaterial* fbxMaterial, 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); + 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()); + 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()); + 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 + "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" + "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" - }; - + 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" + "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" - }; - + 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()); + 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()); + + TF_DEBUG_MSG(FILE_FORMAT_FBX, + "No recognizable properties found for material '%s'\n", + fbxMaterial->GetName()); return false; } @@ -1379,7 +1500,8 @@ normalizePathFromAnyOS(const std::string& path) std::replace(normalized.begin(), normalized.end(), '\\', '/'); // Remove any . or .. in the path, and fix up cases where we have consecutive separators - normalized = std::filesystem::u8path(path).lexically_normal().u8string(); + // and then convert path to string using utf8 representation + normalized = convertPathToString(std::filesystem::u8path(path).lexically_normal()); // Replace all backslashes with forward slashes again, as lexically_normal() will convert // to backslashes on Windows. @@ -1395,7 +1517,7 @@ normalizePathFromAnyOS(const std::string& path) static bool isAbsolutePathFromAnyOS(const std::filesystem::path& path) { - std::string filename = path.u8string(); + std::string filename = convertPathToString(path); // 1. Manually check for POSIX absolute paths (starting with '/') if (!filename.empty() && filename[0] == '/') { @@ -1442,7 +1564,7 @@ importFbxMaterials(ImportFbxContext& ctx) std::string baseName; std::string absFileName; // Only used when image is not embedded if (isEmbedded) { - baseName = normalizePathFromAnyOS(origAbsFileName).filename().u8string(); + baseName = convertPathToString(normalizePathFromAnyOS(origAbsFileName).filename()); } else { // GetRelativeFileName() will use the original OS path delimiters. We must normalize it // before adding it to the metadata. @@ -1451,7 +1573,7 @@ importFbxMaterials(ImportFbxContext& ctx) normalizePathFromAnyOS(fileTexture->GetRelativeFileName()); // Add the path to the metadata even if the file is not present on disk. - ctx.usd->importedFileNames.insert(filePathNormalized.u8string()); + ctx.usd->importedFileNames.insert(convertPathToString(filePathNormalized)); std::filesystem::path absFilePath; if (isAbsolutePathFromAnyOS(filePathNormalized)) { @@ -1478,8 +1600,8 @@ importFbxMaterials(ImportFbxContext& ctx) } } - absFileName = absFilePath.u8string(); - baseName = absFilePath.filename().u8string(); + absFileName = convertPathToString(absFilePath); + baseName = convertPathToString(absFilePath.filename()); } textures[texture] = i; @@ -1540,11 +1662,11 @@ importFbxMaterials(ImportFbxContext& ctx) // Try traditional Lambert/Phong casting FbxSurfaceLambert* lambert = FbxCast(material); FbxSurfacePhong* phong = FbxCast(material); - + 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()); + TF_DEBUG_MSG( + FILE_FORMAT_FBX, "Processing '%s' as Lambert/Phong material\n", material->GetName()); Input ambientFactor; Input diffuse; @@ -1561,157 +1683,166 @@ importFbxMaterials(ImportFbxContext& ctx) Input reflectionFactor; if (lambert) { - importPropTexture(ctx, - textures, - material, - lambert->AmbientFactor, - ambientFactor, - "r", - AdobeTokens->raw); - importPropTexture(ctx, textures, material, lambert->Diffuse, diffuse, "rgb"); - importPropTexture(ctx, - textures, - material, - lambert->DiffuseFactor, - diffuseFactor, - "r", - AdobeTokens->raw); - importPropTexture(ctx, textures, material, lambert->Emissive, emissive, "rgb"); - importPropTexture(ctx, - textures, - material, - lambert->EmissiveFactor, - emissiveFactor, - "r", - AdobeTokens->raw); - importPropTexture( - ctx, textures, material, lambert->NormalMap, normal, "rgb", AdobeTokens->raw); - importPropTexture(ctx, textures, material, lambert->Bump, bump, "r", AdobeTokens->raw); - - // For transparent textures, we only capture the R channel of the texture as we will - // map this directly to opacity if the texture exists. We do this because the USD - // Preview Surface only has a single-valued opacity property. - // HOWEVER, using the 'r' channel of the TransparentColor texture as an opacity value - // worked for some fbx scenes with separate opacity textures but some fbx scenes - // (possibly incorrectly) used the DiffuseColor texture as the TransparentColor texture. - // This lead to strange results. As a consequence, we are currently ignoring the - // TransparentColor property and will only use the TransparencyFactor and Opacity fbx - // properties on the material to map to the USD opacity property. importPropTexture(ctx, - // textures, material, lambert->TransparentColor, transparentColor, "r", - // AdobeTokens->raw); - - importPropTexture(ctx, - textures, - material, - lambert->TransparencyFactor, - transparencyFactor, - "r", - AdobeTokens->raw); - } - if (phong) { - importPropTexture(ctx, textures, material, phong->Specular, specular, "rgb"); - importPropTexture( - ctx, textures, material, phong->Shininess, shininess, "rgb", AdobeTokens->raw); - importPropTexture(ctx, - textures, - material, - phong->SpecularFactor, - specularFactor, - "r", - AdobeTokens->raw); - importPropTexture(ctx, - textures, - material, - phong->ReflectionFactor, - reflectionFactor, - "r", - AdobeTokens->raw); - } + importPropTexture(ctx, + textures, + material, + lambert->AmbientFactor, + ambientFactor, + "r", + AdobeTokens->raw); + importPropTexture(ctx, textures, material, lambert->Diffuse, diffuse, "rgb"); + importPropTexture(ctx, + textures, + material, + lambert->DiffuseFactor, + diffuseFactor, + "r", + AdobeTokens->raw); + importPropTexture(ctx, textures, material, lambert->Emissive, emissive, "rgb"); + importPropTexture(ctx, + textures, + material, + lambert->EmissiveFactor, + emissiveFactor, + "r", + AdobeTokens->raw); + importPropTexture( + ctx, textures, material, lambert->NormalMap, normal, "rgb", AdobeTokens->raw); + importPropTexture( + ctx, textures, material, lambert->Bump, bump, "r", AdobeTokens->raw); + + // For transparent textures, we only capture the R channel of the texture as we will + // map this directly to opacity if the texture exists. We do this because the USD + // Preview Surface only has a single-valued opacity property. + // HOWEVER, using the 'r' channel of the TransparentColor texture as an opacity + // value worked for some fbx scenes with separate opacity textures but some fbx + // scenes (possibly incorrectly) used the DiffuseColor texture as the + // TransparentColor texture. This lead to strange results. As a consequence, we are + // currently ignoring the TransparentColor property and will only use the + // TransparencyFactor and Opacity fbx properties on the material to map to the USD + // opacity property. importPropTexture(ctx, textures, material, + // lambert->TransparentColor, transparentColor, "r", AdobeTokens->raw); + + importPropTexture(ctx, + textures, + material, + lambert->TransparencyFactor, + transparencyFactor, + "r", + AdobeTokens->raw); + } + if (phong) { + importPropTexture(ctx, textures, material, phong->Specular, specular, "rgb"); + importPropTexture( + ctx, textures, material, phong->Shininess, shininess, "rgb", AdobeTokens->raw); + importPropTexture(ctx, + textures, + material, + phong->SpecularFactor, + specularFactor, + "r", + AdobeTokens->raw); + importPropTexture(ctx, + textures, + material, + phong->ReflectionFactor, + reflectionFactor, + "r", + AdobeTokens->raw); + } - if (ctx.options->importPhong) { - inputTranslator.translatePhong2PBR( - diffuse, specular, shininess, um.diffuseColor, um.metallic, um.roughness); - } else { - inputTranslator.translateDirect(diffuse, um.diffuseColor); - // Note, using reflectionFactor for metallic, and specularFactor for roughness, are very - // crude approximations for a Phong to PBR conversion. - inputTranslator.translateDirect(reflectionFactor, um.metallic); - inputTranslator.translateDirect(specularFactor, um.roughness); - } + if (ctx.options->importPhong) { + inputTranslator.translatePhong2PBR( + diffuse, specular, shininess, um.diffuseColor, um.metallic, um.roughness); + } else { + inputTranslator.translateDirect(diffuse, um.diffuseColor); + // Note, using reflectionFactor for metallic, and specularFactor for roughness, are + // very crude approximations for a Phong to PBR conversion. + inputTranslator.translateDirect(reflectionFactor, um.metallic); + inputTranslator.translateDirect(specularFactor, um.roughness); + } - inputTranslator.translateFactor(emissive, emissiveFactor, um.emissiveColor); + inputTranslator.translateFactor(emissive, emissiveFactor, um.emissiveColor); - // ignore specular color if there is a specular factor texture but no specular color - if ((specular.image >= 0) || (specularFactor.image < 0)) { - inputTranslator.translateFactor(specular, specularFactor, um.specularColor); - } + // ignore specular color if there is a specular factor texture but no specular color + if ((specular.image >= 0) || (specularFactor.image < 0)) { + inputTranslator.translateFactor(specular, specularFactor, um.specularColor); + } - // NOTE: as commented above, we are ignoring TransparentColor values so the - // condition in the 'if' statement below should always be false, in which case - // the 'else' block will be executed. + // NOTE: as commented above, we are ignoring TransparentColor values so the + // condition in the 'if' statement below should always be false, in which case + // the 'else' block will be executed. - // If there is a TransparentColor texture, we use it directly as the opacity channel - if (transparentColor.image >= 0) { - inputTranslator.translateDirect(transparentColor, um.opacity); - } else { - // There are FBX files where both the Opacity and TransparencyFactor properties are - // present (even though the Opacity property has been phased out and is not defined as a - // property of FbxSurfaceLambert). In some cases, both properties are present in the - // material definition and so it's unclear which should be used. We use the - // "TransparencyFactor" (ie 1.0) as is when both values are present and both equal 1.0. - // Otherwise, we convert TransparencyFactor to an opacity value by computing 1.0 - - // TransparencyFactor - FbxProperty opacityProp = material->FindProperty("Opacity", FbxDoubleDT, true); - FbxProperty transparencyFactorProp = - material->FindProperty("TransparencyFactor", FbxDoubleDT, true); - if (opacityProp.IsValid() && transparencyFactorProp.IsValid() && - 1.0 == opacityProp.Get() && 1.0 == transparencyFactorProp.Get()) { - // Use the transparencyFactor as is and treat it like an opacity value - inputTranslator.translateDirect(transparencyFactor, um.opacity); + // If there is a TransparentColor texture, we use it directly as the opacity channel + if (transparentColor.image >= 0) { + inputTranslator.translateDirect(transparentColor, um.opacity); } else { - // invert transparencyFactor and assign to usd opacity - inputTranslator.translateTransparency2Opacity(transparencyFactor, um.opacity); + // There are FBX files where both the Opacity and TransparencyFactor properties are + // present (even though the Opacity property has been phased out and is not defined + // as a property of FbxSurfaceLambert). In some cases, both properties are present + // in the material definition and so it's unclear which should be used. We use the + // "TransparencyFactor" (ie 1.0) as is when both values are present and both + // equal 1.0. Otherwise, we convert TransparencyFactor to an opacity value by + // computing 1.0 - TransparencyFactor + FbxProperty opacityProp = material->FindProperty("Opacity", FbxDoubleDT, true); + FbxProperty transparencyFactorProp = + material->FindProperty("TransparencyFactor", FbxDoubleDT, true); + if (opacityProp.IsValid() && transparencyFactorProp.IsValid() && + 1.0 == opacityProp.Get() && + 1.0 == transparencyFactorProp.Get()) { + // Use the transparencyFactor as is and treat it like an opacity value + inputTranslator.translateDirect(transparencyFactor, um.opacity); + } else { + // invert transparencyFactor and assign to usd opacity + inputTranslator.translateTransparency2Opacity(transparencyFactor, um.opacity); + } } - } 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()); - + // 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, "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, " 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()); - + 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()); + 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()); + + 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()); + 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()); + TF_WARN("Unable to process material '%s' - no recognizable properties found\n", + material->GetName()); } } ctx.usd->images = std::move(inputTranslator.getImages()); @@ -2129,13 +2260,16 @@ importFbxSkeleton(ImportFbxContext& ctx, const ImportedFbxSkeleton& importedSkel std::vector> framesInEachStack; framesInEachStack.resize(ctx.animationStacks.size()); - std::vector> animatedNodes; + // Track animated joints, avoiding duplicates across animation layers + std::map animatedJointsMap; size_t jointCount = 0; + // clang-format off std::function + size_t skeletonIndex, Skeleton& skeleton, FbxNode* fbxNode, const SdfPath& parentPath)> importFbxBone; + // clang-format on importFbxBone = [&](size_t skeletonIndex, Skeleton& skeleton, FbxNode* fbxNode, @@ -2164,9 +2298,13 @@ importFbxSkeleton(ImportFbxContext& ctx, const ImportedFbxSkeleton& importedSkel skeleton.jointNames.push_back(stem); skeleton.restTransforms.push_back(GetUSDMatrixFromFBX(localTransform)); - // The bindTransforms will be updated later when the skelelon clusters + // The bindTransforms will be updated later when the skeleton clusters // are processed but we still set them using the default global joint transform. - skeleton.bindTransforms.push_back(GetUSDMatrixFromFBX(globalTransform)); + // Factor out the skeleton parent's global transform so bind transforms are + // relative to the parent (armature) node, avoiding double-application through + // the USD hierarchy. + FbxAMatrix adjustedGlobal = importedSkeleton.parentGlobalInverse * globalTransform; + skeleton.bindTransforms.push_back(GetUSDMatrixFromFBX(adjustedGlobal)); // Here also register which nodes are animated, // and accumulate in a map the animation keys' times. @@ -2183,13 +2321,61 @@ importFbxSkeleton(ImportFbxContext& ctx, const ImportedFbxSkeleton& importedSkel fbxNode->LclTranslation.GetCurve(animLayer); const FbxAnimCurve* rotationCurve = fbxNode->LclRotation.GetCurve(animLayer); const FbxAnimCurve* scalingCurve = fbxNode->LclScaling.GetCurve(animLayer); + + // Also check curve nodes for individual channels (X, Y, Z) + // Sometimes GetCurve() returns null but individual channels have curves + FbxAnimCurveNode* translationCurveNode = + fbxNode->LclTranslation.GetCurveNode(animLayer); + FbxAnimCurveNode* rotationCurveNode = + fbxNode->LclRotation.GetCurveNode(animLayer); + FbxAnimCurveNode* scalingCurveNode = + fbxNode->LclScaling.GetCurveNode(animLayer); + + bool hasTranslationAnim = + (translationCurve != nullptr) || + (translationCurveNode && translationCurveNode->GetChannelsCount() > 0); + bool hasRotationAnim = + (rotationCurve != nullptr) || + (rotationCurveNode && rotationCurveNode->GetChannelsCount() > 0); + bool hasScalingAnim = + (scalingCurve != nullptr) || + (scalingCurveNode && scalingCurveNode->GetChannelsCount() > 0); + addAnimCurveFrameTimes(translationCurve, frames); addAnimCurveFrameTimes(rotationCurve, frames); addAnimCurveFrameTimes(scalingCurve, frames); - if (translationCurve != nullptr || rotationCurve != nullptr || - scalingCurve != nullptr) { - animatedNodes.emplace_back(fbxNode, jointPathToken); + // Also add frame times from curve node channels + if (translationCurveNode) { + for (unsigned int channeld = 0; + channeld < translationCurveNode->GetChannelsCount(); + ++channeld) { + addAnimCurveFrameTimes(translationCurveNode->GetCurve(channeld), + frames); + } + } + if (rotationCurveNode) { + for (unsigned int channeld = 0; + channeld < rotationCurveNode->GetChannelsCount(); + ++channeld) { + addAnimCurveFrameTimes(rotationCurveNode->GetCurve(channeld), frames); + } + } + if (scalingCurveNode) { + for (unsigned int channeld = 0; + channeld < scalingCurveNode->GetChannelsCount(); + ++channeld) { + addAnimCurveFrameTimes(scalingCurveNode->GetCurve(channeld), frames); + } + } + + // Add to map to track animated joints (avoids duplicates across layers) + // Use the more robust channel check instead of just checking for curves + if (hasTranslationAnim || hasRotationAnim || hasScalingAnim) { + // Only add if not already in the map + if (animatedJointsMap.find(fbxNode) == animatedJointsMap.end()) { + animatedJointsMap[fbxNode] = jointPathToken; + } } } } @@ -2216,19 +2402,36 @@ importFbxSkeleton(ImportFbxContext& ctx, const ImportedFbxSkeleton& importedSkel importFbxBone(skeletonIndex, skeleton, skel->GetNode(), SdfPath()); } - for (const auto& i : animatedNodes) { - skeleton.animatedJoints.push_back(i.second); + // Populate skeleton.animatedJoints. + // Don't add these to ctx.animatedSkeletonNodes yet because we don't know + // if this skeleton has skinned meshes. That happens during importFbxNodeHierarchy. + // We'll add them later in markAnimatedSkeletonNodes() if the skeleton has skinned meshes. + for (const auto& pair : animatedJointsMap) { + skeleton.animatedJoints.push_back(pair.second); } for (int animationStackIndex = 0; animationStackIndex < framesInEachStack.size(); animationStackIndex++) { AnimationTrack& track = ctx.usd->animationTracks[animationStackIndex]; + std::set& frames = framesInEachStack[animationStackIndex]; + + // Extend animation to cover the full stack duration by adding a final hold frame + // This ensures animations hold their final values to match the declared animation length + if (!frames.empty()) { + FbxTime lastKeyframeTime = *frames.rbegin(); + double lastKeyframeSeconds = lastKeyframeTime.GetSecondDouble(); + + // Allow a small tolerance + if (track.maxTime > lastKeyframeSeconds + 0.001) { + FbxTime stackEndTime; + stackEndTime.SetSecondDouble(track.maxTime); + frames.insert(stackEndTime); + } + } // Set the current animation stack so that EvaluateLocalTransform will return the correct // value ctx.scene->SetCurrentAnimationStack(ctx.animationStacks[animationStackIndex].stack); - - std::set& frames = framesInEachStack[animationStackIndex]; if (!frames.empty()) { track.hasTimepoints = true; ctx.usd->hasAnimations = true; @@ -2238,12 +2441,13 @@ importFbxSkeleton(ImportFbxContext& ctx, const ImportedFbxSkeleton& importedSkel SkeletonAnimation& skeletonAnimation = skeleton.skeletonAnimations[animationStackIndex]; skeletonAnimation.times.resize(frames.size()); skeletonAnimation.translations.resize(frames.size(), - VtArray(animatedNodes.size())); + VtArray(animatedJointsMap.size())); skeletonAnimation.rotations.resize(frames.size(), - VtArray(animatedNodes.size())); - skeletonAnimation.scales.resize(frames.size(), VtArray(animatedNodes.size())); + VtArray(animatedJointsMap.size())); + skeletonAnimation.scales.resize(frames.size(), + VtArray(animatedJointsMap.size())); size_t i = 0; - for (const auto& nodePair : animatedNodes) { + for (const auto& nodePair : animatedJointsMap) { FbxNode* fbxNode = nodePair.first; size_t j = 0; for (const FbxTime& frameTime : frames) { @@ -2268,6 +2472,43 @@ importFbxSkeleton(ImportFbxContext& ctx, const ImportedFbxSkeleton& importedSkel return true; } +// After skeleton and mesh import, mark which skeleton nodes should have their animations +// handled by the skeletal animation system (and thus skip node animation import). +// Only mark nodes for skeletons that have skinned meshes. +void +markAnimatedSkeletonNodes(ImportFbxContext& ctx) +{ + for (size_t skeletonIndex = 0; skeletonIndex < ctx.usd->skeletons.size(); skeletonIndex++) { + const Skeleton& skeleton = ctx.usd->skeletons[skeletonIndex]; + + // Only mark animated nodes for skeletons that have skinned meshes + // If there are no skinned meshes, the skeletal animation won't be exported, + // so we should let the regular node animation system handle it + bool hasSkinnedMeshes = !skeleton.meshSkinningTargets.empty(); + if (hasSkinnedMeshes) { + // This skeleton has skinned meshes, so mark its animated nodes + // to skip regular node animation import + for (const auto& pair : ctx.bonesMap) { + FbxNode* fbxNode = pair.first; + size_t jointIndex = pair.second; + size_t skeletonMapIndex = ctx.skeletonsMap[fbxNode]; + + if (skeletonMapIndex == skeletonIndex) { + // This bone belongs to this skeleton + // Check if it's in the animated joints list + TfToken jointToken = skeleton.joints[jointIndex]; + bool isAnimated = std::find(skeleton.animatedJoints.begin(), + skeleton.animatedJoints.end(), + jointToken) != skeleton.animatedJoints.end(); + if (isAnimated) { + ctx.animatedSkeletonNodes.insert(fbxNode); + } + } + } + } + } +} + // Import skeletons from fbx. // The only way of recognizing a skeleton is to check whether an fbx node has a skeleton attribute. // So traverse all nodes here, but only look at the skeleton roots for further processing. @@ -2306,6 +2547,16 @@ importFBXSkeletons(ImportFbxContext& ctx) } } + { + ScopedAnimStackDisabler animStackDisabler(ctx); + for (auto& skel : ctx.skeletons) { + if (skel.fbxParent) { + FbxAMatrix parentGlobal = skel.fbxParent->EvaluateGlobalTransform(); + skel.parentGlobalInverse = parentGlobal.Inverse(); + } + } + } + for (const ImportedFbxSkeleton& skeleton : ctx.skeletons) { importFbxSkeleton(ctx, skeleton); } @@ -2558,6 +2809,7 @@ importFbx(const ImportFbxOptions& options, Fbx& fbx, UsdData& usd) loadAnimLayers(ctx); importFBXSkeletons(ctx); importFbxNodeHierarchy(ctx); + markAnimatedSkeletonNodes(ctx); setSkeletonParents(ctx); } diff --git a/fbx/src/fbxResolver.cpp b/fbx/src/fbxResolver.cpp index d8343685..d71345e4 100644 --- a/fbx/src/fbxResolver.cpp +++ b/fbx/src/fbxResolver.cpp @@ -26,8 +26,7 @@ static std::mutex mutex; FbxResolver::FbxResolver() : Resolver("FbxResolver") -{ -} +{} void FbxResolver::readCache(const std::string& filename, std::vector& images) @@ -36,11 +35,13 @@ FbxResolver::readCache(const std::string& filename, std::vector& ima Fbx fbx; UsdData usd; TfStopwatch watch; - TF_DEBUG_MSG(FBX_PACKAGE_RESOLVER, "START TOTAL: %ld\n", static_cast(watch.GetMilliseconds())); + TF_DEBUG_MSG( + FBX_PACKAGE_RESOLVER, "START TOTAL: %ld\n", static_cast(watch.GetMilliseconds())); watch.Start(); VOID_GUARD(readFbx(fbx, filename, true, true), "Error reading FBX from %s\n", filename.c_str()); watch.Stop(); - TF_DEBUG_MSG(FBX_PACKAGE_RESOLVER, "STOP TOTAL: %ld\n", static_cast(watch.GetMilliseconds())); + TF_DEBUG_MSG( + FBX_PACKAGE_RESOLVER, "STOP TOTAL: %ld\n", static_cast(watch.GetMilliseconds())); ImportFbxOptions options; options.importGeometry = false; options.importMaterials = true; diff --git a/fbx/src/fbxResolver.h b/fbx/src/fbxResolver.h index 42a40f57..bbbd872b 100644 --- a/fbx/src/fbxResolver.h +++ b/fbx/src/fbxResolver.h @@ -18,10 +18,10 @@ namespace adobe::usd { /// \brief usdfbx custom asset resolver. class FbxResolver : public Resolver { - public: +public: FbxResolver(); - private: +private: /// \ingroup usdfbx /// \brief Reads images from the FBX file, /// opening it only with IMP_FBX_MATERIAL and IMP_FBX_TEXTURE diff --git a/fbx/src/fileFormat.cpp b/fbx/src/fileFormat.cpp index 52b9b1d0..41aa89c4 100644 --- a/fbx/src/fileFormat.cpp +++ b/fbx/src/fileFormat.cpp @@ -23,8 +23,6 @@ governing permissions and limitations under the License. #include #include -#include - using namespace adobe::usd; PXR_NAMESPACE_OPEN_SCOPE @@ -160,14 +158,16 @@ UsdFbxFileFormat::WriteToString(const SdfLayer& layer, const std::string& comment) const { // Write as USDA because we don't implement FBX export. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToString(layer, str, comment); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToString(layer, str, comment); } bool UsdFbxFileFormat::WriteToStream(const SdfSpecHandle& spec, std::ostream& out, size_t indent) const { // Write as USDA because we don't implement FBX export. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToStream(spec, out, indent); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToStream(spec, out, indent); } bool diff --git a/fbx/src/fileFormat.h b/fbx/src/fileFormat.h index 8fc518cc..9d5450b5 100644 --- a/fbx/src/fileFormat.h +++ b/fbx/src/fileFormat.h @@ -14,11 +14,11 @@ governing permissions and limitations under the License. #ifdef _MSC_VER // Disable warnings in the pxr headers. // conversion from 'double' to 'float', possible loss of data -# pragma warning(disable : 4244) +#pragma warning(disable : 4244) // conversion from 'size_t' to 'int', possible loss of data -# pragma warning(disable : 4267) +#pragma warning(disable : 4267) // truncation from 'double' to 'float' -# pragma warning(disable : 4305) +#pragma warning(disable : 4305) #endif // _MSC_VER #include "api.h" @@ -46,7 +46,7 @@ TF_DECLARE_WEAK_AND_REF_PTRS(FbxData); /// \brief SdfData specialization for working with FBX files. class FbxData : public FileFormatDataBase { - public: +public: bool animationStacks = false; bool phong = false; bool triangulateMeshes = true; @@ -60,7 +60,7 @@ class USDFBX_API UsdFbxFileFormat : public SdfFileFormat , public PcpDynamicFileFormatInterface { - public: +public: friend class FbxData; virtual SdfAbstractDataRefPtr InitData(const FileFormatArguments& args) const override; @@ -98,7 +98,7 @@ class USDFBX_API UsdFbxFileFormat const std::string& comment = std::string(), const FileFormatArguments& args = FileFormatArguments()) const override; - protected: +protected: static const TfToken animationStacksToken; static const TfToken assetsPathToken; static const TfToken originalColorSpaceToken; diff --git a/fbx/src/precompiled.h b/fbx/src/precompiled.h deleted file mode 100644 index 2d12ece9..00000000 --- a/fbx/src/precompiled.h +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2023 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/fbx/tests/sanityTests.cpp b/fbx/tests/sanityTests.cpp index cc7de75a..52875b58 100644 --- a/fbx/tests/sanityTests.cpp +++ b/fbx/tests/sanityTests.cpp @@ -47,4 +47,4 @@ TEST(Sanity, ExportCube) // Cleanup scene->Destroy(); -} \ No newline at end of file +} diff --git a/fbx/tests/util.cpp b/fbx/tests/util.cpp index c05bcf7c..0c41ad9b 100644 --- a/fbx/tests/util.cpp +++ b/fbx/tests/util.cpp @@ -181,9 +181,7 @@ getFbxNodeByPath(FbxScene* scene, std::string nodePath) // 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()); + TF_WARN("Root node \"%s\" not found in path %s", rootNode->GetName(), nodePath.c_str()); return nullptr; } diff --git a/fbx/tests/util.h b/fbx/tests/util.h index 7e77cc3c..1d434a33 100644 --- a/fbx/tests/util.h +++ b/fbx/tests/util.h @@ -21,7 +21,7 @@ governing permissions and limitations under the License. class FbxLoaderSingleton { - public: +public: /** * Get the singleton instance of FbxLoaderSingleton. */ @@ -47,7 +47,7 @@ class FbxLoaderSingleton return scene ? scene->GetFbxManager() : nullptr; } - private: +private: FbxLoaderSingleton(); std::recursive_mutex mFbxLoaderMutex; diff --git a/gltf/README.md b/gltf/README.md index 13698956..075ba63c 100644 --- a/gltf/README.md +++ b/gltf/README.md @@ -88,6 +88,7 @@ During material import, the ASM shading model is used as an intermediate transpo | ADOBE_materials_clearcoat_specular |✅| | ADOBE_materials_clearcoat_tint |✅| | EXT_materials_clearcoat_color |✅| +| KHR_materials_coat |✅| | KHR_materials_pbrSpecularGlossiness |✅| Anisotropy @@ -176,7 +177,15 @@ Mesh bounding box exported as min and max accessor bounds in glTF. 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` +* `preserveExtraMaterialInfo`: Generate shading networks with extra data for transcoding. Default is `true` + When this is enabled, the generated shading networks might contain extra inputs that are outside of the respective + material surface schema, that are useful for transcoding purposes. For example, the `OpenPBR` surface does not have + an `occlusion` input for ambient occlusion, but we might want to express such a signal, if it was present in the + source asset, so that an exporter can pick-up said signal and use it when generating an output asset. + When `preserveExtraMaterialInfo` is `false`, the code will not generate these extra fields that are outside of the + schema, which won't affect renders, but can affect the transcoding abilities. + +* `gltfAnimationTracks`: 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 @@ -187,7 +196,7 @@ Mesh bounding box exported as min and max accessor bounds in glTF. 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 = Usd.Stage.Open("animAsset.gltf:SDF_FORMAT_ARGS:gltfAnimationTracks=true") stage.Export("animAsset.fbx") ``` diff --git a/gltf/src/CMakeLists.txt b/gltf/src/CMakeLists.txt index 7ba1ab90..4805da5a 100644 --- a/gltf/src/CMakeLists.txt +++ b/gltf/src/CMakeLists.txt @@ -1,5 +1,4 @@ add_library(usdGltf SHARED) - usd_plugin_compile_config(usdGltf) target_compile_definitions(usdGltf PRIVATE USDGLTF_EXPORTS) @@ -28,6 +27,7 @@ PRIVATE target_include_directories(usdGltf PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" ) @@ -52,69 +52,6 @@ if(USD_FILEFORMATS_ENABLE_DRACO) ) endif() -target_precompile_headers(usdGltf - PRIVATE - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" -) - # Installation of plugin files mimics the file structure that USD has for plugins, # so it is easy to deploy it in a pre-existing USD build, if one chooses to do so. @@ -129,18 +66,20 @@ set_target_properties(usdGltf PROPERTIES RESOURCE ${CMAKE_CURRENT_BINARY_DIR}/pl set_target_properties(usdGltf PROPERTIES RESOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json:plugInfo.json") +# USDGLTF_DESTINATION is set in the parent scope by the add_usd_fileformat macro + if(USDGLTF_ENABLE_INSTALL) install( TARGETS usdGltf - RUNTIME DESTINATION plugin/usd COMPONENT Runtime - LIBRARY DESTINATION plugin/usd COMPONENT Runtime - RESOURCE DESTINATION plugin/usd/usdGltf/resources COMPONENT Runtime + RUNTIME DESTINATION ${USDGLTF_DESTINATION} COMPONENT Runtime + LIBRARY DESTINATION ${USDGLTF_DESTINATION} COMPONENT Runtime + RESOURCE DESTINATION ${USDGLTF_DESTINATION}/usdGltf/resources COMPONENT Runtime ) install( FILES plugInfo.root.json - DESTINATION plugin/usd + DESTINATION ${USDGLTF_DESTINATION} RENAME plugInfo.json COMPONENT Runtime ) -endif() \ No newline at end of file +endif() diff --git a/gltf/src/api.h b/gltf/src/api.h index ae923228..c3bc24e1 100644 --- a/gltf/src/api.h +++ b/gltf/src/api.h @@ -14,19 +14,19 @@ governing permissions and limitations under the License. #include "pxr/base/arch/export.h" #if defined(PXR_STATIC) -# define USDGLTF_API -# define USDGLTF_API_TEMPLATE_CLASS(...) -# define USDGLTF_API_TEMPLATE_STRUCT(...) -# define USDGLTF_LOCAL +#define USDGLTF_API +#define USDGLTF_API_TEMPLATE_CLASS(...) +#define USDGLTF_API_TEMPLATE_STRUCT(...) +#define USDGLTF_LOCAL #else -# if defined(USDGLTF_EXPORTS) -# define USDGLTF_API ARCH_EXPORT -# define USDGLTF_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) -# define USDGLTF_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) -# else -# define USDGLTF_API ARCH_IMPORT -# define USDGLTF_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) -# define USDGLTF_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) -# endif -# define USDGLTF_LOCAL ARCH_HIDDEN +#if defined(USDGLTF_EXPORTS) +#define USDGLTF_API ARCH_EXPORT +#define USDGLTF_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define USDGLTF_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#else +#define USDGLTF_API ARCH_IMPORT +#define USDGLTF_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define USDGLTF_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#endif +#define USDGLTF_LOCAL ARCH_HIDDEN #endif \ No newline at end of file diff --git a/gltf/src/fileFormat.cpp b/gltf/src/fileFormat.cpp index 6d637156..0d92965e 100644 --- a/gltf/src/fileFormat.cpp +++ b/gltf/src/fileFormat.cpp @@ -25,8 +25,9 @@ governing permissions and limitations under the License. // USD #include #include +#include #include -#include +#include PXR_NAMESPACE_OPEN_SCOPE @@ -287,7 +288,8 @@ UsdGltfFileFormat::WriteToString(const SdfLayer& layer, const std::string& comment) const { // Write USD as glTF: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToString(layer, str, comment); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToString(layer, str, comment); } bool diff --git a/gltf/src/fileFormat.h b/gltf/src/fileFormat.h index a86b733a..ff0eb55e 100644 --- a/gltf/src/fileFormat.h +++ b/gltf/src/fileFormat.h @@ -41,7 +41,7 @@ class ArAsset; /// \brief SdfData specialization for working with glTF files. class GltfData : public FileFormatDataBase { - public: +public: bool animationTracks = false; bool computeBitangents = false; static GltfDataRefPtr InitData(const SdfFileFormat::FileFormatArguments& args); @@ -53,7 +53,7 @@ class USDGLTF_API UsdGltfFileFormat : public SdfFileFormat , public PcpDynamicFileFormatInterface { - public: +public: friend class GltfData; // If successful, returns true and fills in the assetPtr, baseDir and the isAscii flag @@ -98,7 +98,7 @@ class USDGLTF_API UsdGltfFileFormat const std::string& comment = std::string(), const FileFormatArguments& args = FileFormatArguments()) const override; - protected: +protected: static const TfToken assetsPathToken; static const TfToken animationTracksToken; static const TfToken computeBitangentsToken; diff --git a/gltf/src/gltf.cpp b/gltf/src/gltf.cpp index f666aa57..7eaaa3f6 100644 --- a/gltf/src/gltf.cpp +++ b/gltf/src/gltf.cpp @@ -11,19 +11,14 @@ governing permissions and limitations under the License. */ #include "gltf.h" #include "debugCodes.h" -#include -#include #include #include +#include +#include +#include #include #include #include -#include -#include -#include -#include -#include -#include #include #include @@ -135,24 +130,23 @@ 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]); + 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\""); @@ -162,21 +156,25 @@ preValidateGLB(const unsigned char* buffer, size_t bufferSize) 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); + 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); + 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; } @@ -525,24 +523,28 @@ void readAccessorData(const tinygltf::Model& model, int accessorIndex, uint8_t* dst) { if (accessorIndex < 0) { - TF_CODING_ERROR("Accessor index %d is invalid (< 0). File should be rejected.", accessorIndex); + 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()); + 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()) { + 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); + TF_WARN( + "Buffer view %d has invalid buffer index %d", accessor.bufferView, bufferView.buffer); return; } const tinygltf::Buffer& buffer = model.buffers[bufferView.buffer]; @@ -555,24 +557,35 @@ readAccessorData(const tinygltf::Model& model, int accessorIndex, uint8_t* dst) // 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()); + 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()); + 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); + 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); + TF_WARN("Accessor %d data extends beyond buffer view bounds (accessor offset %zu + size " + "%zu > view length %zu)", + accessorIndex, + accessorStartOffset, + accessorTotalSize, + bufferView.byteLength); return; } @@ -613,24 +626,28 @@ void readAccessorDataToFloat(const tinygltf::Model& model, int accessorIndex, float* dst) { if (accessorIndex < 0) { - TF_CODING_ERROR("Accessor index %d is invalid (< 0). File should be rejected.", accessorIndex); + 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()); + 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()) { + 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); + TF_WARN( + "Buffer view %d has invalid buffer index %d", accessor.bufferView, bufferView.buffer); return; } const tinygltf::Buffer& buffer = model.buffers[bufferView.buffer]; @@ -644,24 +661,35 @@ readAccessorDataToFloat(const tinygltf::Model& model, int accessorIndex, float* // 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()); + 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()); + 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); + 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); + TF_WARN("Accessor %d data extends beyond buffer view bounds (accessor offset %zu + size " + "%zu > view length %zu)", + accessorIndex, + accessorStartOffset, + accessorTotalSize, + bufferView.byteLength); return; } @@ -850,37 +878,53 @@ readColor(const tinygltf::Model& model, // Incurs a double copy but handles reading accessors holding integer data with unknown size void -readAccessorInts(const tinygltf::Model& model, int accessorIndex, PXR_NS::VtArray& dst) +readAccessorInts(const tinygltf::Model& model, + int accessorIndex, + PXR_NS::VtArray& dst, + bool isScalar) { if (accessorIndex < 0) { - TF_CODING_ERROR("Accessor index %d is invalid (< 0). File should be rejected.", accessorIndex); + 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()); + 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); + + // Accessor type can only be either SCALAR or VEC4, depending on + // whether mesh face indices or joint indices are being accessed. + if (isScalar && accessor.type != TINYGLTF_TYPE_SCALAR) { + TF_WARN("Accessor %d used as mesh index has invalid type %d (expected SCALAR type %d).", + accessorIndex, + accessor.type, + TINYGLTF_TYPE_SCALAR); + return; + } else if (!isScalar && accessor.type != TINYGLTF_TYPE_VEC4) { + TF_WARN("Accessor %d used as joint index has invalid type %d (expected VECTOR type %d).", + accessorIndex, + accessor.type, + TINYGLTF_TYPE_VEC4); 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); + 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/gltf.h b/gltf/src/gltf.h index 1f3fe690..2f510405 100644 --- a/gltf/src/gltf.h +++ b/gltf/src/gltf.h @@ -11,6 +11,8 @@ governing permissions and limitations under the License. */ #pragma once #include +#include +#include #include namespace adobe::usd { @@ -91,7 +93,10 @@ readColor(const tinygltf::Model& model, PXR_NS::VtArray& color, PXR_NS::VtArray& opacity); void -readAccessorInts(const tinygltf::Model& model, int accessorIndex, PXR_NS::VtArray& dst); +readAccessorInts(const tinygltf::Model& model, + int accessorIndex, + PXR_NS::VtArray& dst, + bool isScalar = false); void addToTimeMap(std::vector& globalTime, const PXR_NS::VtArray& time); diff --git a/gltf/src/gltfAnisotropy.cpp b/gltf/src/gltfAnisotropy.cpp index 962355f4..2c465e53 100644 --- a/gltf/src/gltfAnisotropy.cpp +++ b/gltf/src/gltfAnisotropy.cpp @@ -299,7 +299,8 @@ importAnisotropyData(ImportGltfContext& ctx, 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); + 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, @@ -342,7 +343,8 @@ 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()); + TF_DEBUG_MSG( + FILE_FORMAT_GLTF, "importAnisotropyTexture for material '%s'\n", m.displayName.c_str()); // Get Roughness image const tinygltf::Image* roughnessImage = nullptr; // Validate texture index before use to prevent signed/unsigned comparison bug @@ -385,8 +387,8 @@ importAnisotropyTexture(ImportGltfContext& ctx, if (usdAnisoLevelImageIndex < 0 && usdAnisoAngleImageIndex < 0) { processAnisotropyPixels(anisotropySrcImage, roughnessImage, - bilinearRoughnessSampling, roughness, + bilinearRoughnessSampling, anisotropyData, anisoLevelImage, anisoAngleImage); diff --git a/gltf/src/gltfExport.cpp b/gltf/src/gltfExport.cpp index 6ba1ee91..d586b5b7 100644 --- a/gltf/src/gltfExport.cpp +++ b/gltf/src/gltfExport.cpp @@ -17,45 +17,7 @@ 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include // TODO refine this description /** @@ -1180,6 +1142,31 @@ exportClearcoatExtension(ExportGltfContext& ctx, return false; } +bool +exportCoatExtension(ExportGltfContext& ctx, + InputTranslator& inputTranslator, + const Material& m, + tinygltf::Material& gm) +{ + ExtMap ext; + if (addTextureToExt(ctx, inputTranslator, ext, m.clearcoat, "coatTexture", "coatFactor")) { + addTextureToExt(ctx, + inputTranslator, + ext, + m.clearcoatRoughness, + "coatRoughnessTexture", + "coatRoughnessFactor"); + addTextureToExt(ctx, inputTranslator, ext, m.clearcoatNormal, "coatNormalTexture"); + addTextureToExt( + ctx, inputTranslator, ext, m.clearcoatColor, "coatColorTexture", "coatColorFactor"); + addFloatValueToExt(ext, "coatIor", m.clearcoatIor.value, 1.5f); + addMaterialExt(ctx, gm, "KHR_materials_coat", ext); + return true; + } + + return false; +} + bool exportEmissiveStrengthExtension(ExportGltfContext& ctx, InputTranslator& inputTranslator, @@ -1251,7 +1238,8 @@ exportSpecularExtension(ExportGltfContext& ctx, // 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. + // 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); @@ -1324,19 +1312,19 @@ exportAdobeClearcoatSpecularExtension(ExportGltfContext& ctx, } bool -exportClearcoatColorExtension(ExportGltfContext& ctx, - InputTranslator& inputTranslator, - const Material& m, - tinygltf::Material& gm) +exportAdobeClearcoatColorExtension(ExportGltfContext& ctx, + InputTranslator& inputTranslator, + const Material& m, + tinygltf::Material& gm) { ExtMap ext; if (addTextureToExt(ctx, inputTranslator, ext, m.clearcoatColor, - "clearcoatColorTexture", - "clearcoatColorFactor")) { - addMaterialExt(ctx, gm, "EXT_materials_clearcoat_color", ext); + "clearcoatTintTexture", + "clearcoatTintFactor")) { + addMaterialExt(ctx, gm, "ADOBE_materials_clearcoat_tint", ext); return true; } @@ -1748,9 +1736,11 @@ exportMaterials(ExportGltfContext& ctx) // by default and the clearcoat is redundant at best, if not wrong. bool exportClearcoat = !m.clearcoatModelsTransmissionTint; if (exportClearcoat) { - exportClearcoatExtension(ctx, inputTranslator, m, gm); - exportAdobeClearcoatSpecularExtension(ctx, inputTranslator, m, gm); - exportClearcoatColorExtension(ctx, inputTranslator, m, gm); + if (exportClearcoatExtension(ctx, inputTranslator, m, gm)) { + exportAdobeClearcoatSpecularExtension(ctx, inputTranslator, m, gm); + exportAdobeClearcoatColorExtension(ctx, inputTranslator, m, gm); + } + exportCoatExtension(ctx, inputTranslator, m, gm); } } @@ -1805,7 +1795,7 @@ exportMaterials(ExportGltfContext& ctx) ui->uri.c_str(), gi.bufferView); } else { - gi.uri = ui->uri; + gi.uri = TfGetBaseName(ui->uri); // Store the image in the tinygltf image struct, so that it will be written to the // location of the URI gi.image.resize(ui->image.size()); @@ -1930,66 +1920,76 @@ exportMeshes(ExportGltfContext& ctx) 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 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]; + 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) { + 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); + "(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); + + 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); + "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); + "tangents", + TINYGLTF_TARGET_ARRAY_BUFFER, + TINYGLTF_TYPE_VEC4, + TINYGLTF_COMPONENT_TYPE_FLOAT, + mesh.tangents.values.size(), + mesh.tangents.values.data(), + true); } } diff --git a/gltf/src/gltfExport.h b/gltf/src/gltfExport.h index 4892b3a0..e712feb3 100644 --- a/gltf/src/gltfExport.h +++ b/gltf/src/gltfExport.h @@ -11,9 +11,9 @@ governing permissions and limitations under the License. */ #pragma once #include "gltf.h" -#include #include #include +#include namespace adobe::usd { diff --git a/gltf/src/gltfImport.cpp b/gltf/src/gltfImport.cpp index ef4042eb..481199e7 100644 --- a/gltf/src/gltfImport.cpp +++ b/gltf/src/gltfImport.cpp @@ -22,8 +22,7 @@ governing permissions and limitations under the License. #include #include -#include -#include +#include using namespace PXR_NS; @@ -300,7 +299,9 @@ importImage(ImportGltfContext& ctx, // 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); + textureIndex, + materialName.c_str(), + ctx.gltf->textures.size() - 1); return -1; } @@ -323,6 +324,15 @@ importImage(ImportGltfContext& ctx, textureIndex); return -1; } + // Validate image index to prevent out-of-bounds array access + if (static_cast(imageIndex) >= ctx.gltf->images.size()) { + TF_WARN("Invalid image index %d for texture %d in material '%s' (valid range: 0-%zu)", + imageIndex, + textureIndex, + materialName.c_str(), + ctx.gltf->images.size() - 1); + return -1; + } const tinygltf::Image& image = ctx.gltf->images[imageIndex]; const std::string uriStem = TfStringGetBeforeSuffix(TfGetBaseName(image.uri)); @@ -394,9 +404,15 @@ importTexture(const tinygltf::Model* gltf, const TfToken& channel, const TfToken& colorSpace) { + // Validate texture index to prevent out-of-bounds array access + if (textureIndex < 0 || static_cast(textureIndex) >= gltf->textures.size()) { + // Invalid texture index - skip texture import silently + // (importImage already logged a warning) + return false; + } tinygltf::Texture texture = gltf->textures[textureIndex]; int samplerIndex = texture.sampler; - if (samplerIndex >= 0) { + if (samplerIndex >= 0 && static_cast(samplerIndex) < gltf->samplers.size()) { tinygltf::Sampler sampler = gltf->samplers[samplerIndex]; switch (sampler.wrapS) { case TINYGLTF_TEXTURE_WRAP_REPEAT: @@ -636,6 +652,48 @@ importClearcoat(const tinygltf::ExtensionMap& extensions, Clearcoat* clearcoat) return false; } +struct Coat +{ + double factor = 0.0; + tinygltf::TextureInfo texture; // r channel + double roughnessFactor = 0.0; + tinygltf::TextureInfo roughnessTexture; // g channel + tinygltf::NormalTextureInfo normalTexture; // rgb channels + double ior = 1.5; + double colorFactor[3] = { 1.0, 1.0, 1.0 }; + tinygltf::TextureInfo colorTexture; // rgb channels +}; + +bool +importCoat(const tinygltf::ExtensionMap& extensions, Coat* coat, const std::string& materialName) +{ + auto extIt = extensions.find("KHR_materials_coat"); + if (extIt != extensions.end()) { + const tinygltf::Value& coatExt = extIt->second; + readDoubleValue(coatExt.Get("coatFactor"), coat->factor); + readTextureInfo(coatExt.Get("coatTexture"), coat->texture); + readDoubleValue(coatExt.Get("coatRoughnessFactor"), coat->roughnessFactor); + readTextureInfo(coatExt.Get("coatRoughnessTexture"), coat->roughnessTexture); + readNormalTextureInfo(coatExt.Get("coatNormalTexture"), coat->normalTexture); + double coatIor = coat->ior; // preserve default + readDoubleValue(coatExt.Get("coatIor"), coatIor); + // Per spec, IOR must be >= 1.0 + if (coatIor >= 1.0) { + coat->ior = coatIor; + } else { + TF_WARN("Material '%s': Skipping invalid IOR value %f in KHR_materials_coat (must be " + ">= 1.0)", + materialName.c_str(), + coatIor); + } + readDoubleArray(coatExt.Get("coatColorFactor"), coat->colorFactor, 3); + readTextureInfo(coatExt.Get("coatColorTexture"), coat->colorTexture); + return true; + } + + return false; +} + bool importEmissionStrength(const tinygltf::ExtensionMap& extensions, double* emissiveStrength) { @@ -649,11 +707,21 @@ importEmissionStrength(const tinygltf::ExtensionMap& extensions, double* emissiv } bool -importIor(const tinygltf::ExtensionMap& extensions, double* ior) +importIor(const tinygltf::ExtensionMap& extensions, double* ior, const std::string& materialName) { if (auto extIt = extensions.find("KHR_materials_ior"); extIt != extensions.end()) { const tinygltf::Value& iorExt = extIt->second; - readDoubleValue(iorExt.Get("ior"), *ior); + double iorValue = *ior; // preserve default + readDoubleValue(iorExt.Get("ior"), iorValue); + // Per KHR_materials_ior spec, IOR must be >= 1.0 + if (iorValue >= 1.0) { + *ior = iorValue; + } else { + TF_WARN("Material '%s': Skipping invalid IOR value %f in KHR_materials_ior (must be >= " + "1.0)", + materialName.c_str(), + iorValue); + } return true; } return false; @@ -760,12 +828,23 @@ struct AdobeClearcoatSpecular bool importAdobeClearcoatSpecular(const tinygltf::ExtensionMap& extensions, - AdobeClearcoatSpecular* clearcoatSpecular) + AdobeClearcoatSpecular* clearcoatSpecular, + const std::string& materialName) { auto extIt = extensions.find("ADOBE_materials_clearcoat_specular"); if (extIt != extensions.end()) { const tinygltf::Value& coatExt = extIt->second; - readDoubleValue(coatExt.Get("clearcoatIor"), clearcoatSpecular->ior); + double clearcoatIor = clearcoatSpecular->ior; // preserve default + readDoubleValue(coatExt.Get("clearcoatIor"), clearcoatIor); + // Per spec, IOR must be >= 1.0 + if (clearcoatIor >= 1.0) { + clearcoatSpecular->ior = clearcoatIor; + } else { + TF_WARN("Material '%s': Skipping invalid IOR value %f in " + "ADOBE_materials_clearcoat_specular (must be >= 1.0)", + materialName.c_str(), + clearcoatIor); + } readDoubleValue(coatExt.Get("clearcoatSpecularFactor"), clearcoatSpecular->factor); readTextureInfo(coatExt.Get("clearcoatSpecularTexture"), clearcoatSpecular->texture); return true; @@ -774,28 +853,18 @@ importAdobeClearcoatSpecular(const tinygltf::ExtensionMap& extensions, return false; } -// Multi-vendor extension for supporting colored tinting of clearcoat -struct ClearcoatColor +// Extension for supporting colored tinting of clearcoat +struct AdobeClearcoatColor { double factor[3] = { 1.0, 1.0, 1.0 }; tinygltf::TextureInfo texture; // rgb channels }; bool -importClearcoatColor(const tinygltf::ExtensionMap& extensions, - ClearcoatColor* clearcoatColor) +importAdobeClearcoatColor(const tinygltf::ExtensionMap& extensions, + AdobeClearcoatColor* clearcoatColor) { - // 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"); + auto extIt = extensions.find("ADOBE_materials_clearcoat_tint"); if (extIt != extensions.end()) { const tinygltf::Value& coatExt = extIt->second; readDoubleArray(coatExt.Get("clearcoatTintFactor"), clearcoatColor->factor, 3); @@ -1133,7 +1202,7 @@ importMaterials(ImportGltfContext& ctx) } double ior = 1.5; - if (importIor(gm.extensions, &ior)) { + if (importIor(gm.extensions, &ior, m.displayName)) { importValue1(m.ior, ior); } @@ -1182,7 +1251,33 @@ importMaterials(ImportGltfContext& ctx) } Clearcoat clearcoat; - if (importClearcoat(gm.extensions, &clearcoat)) { + Coat coat; + if (importCoat(gm.extensions, &coat, m.displayName)) { + importInput(ctx, + m.displayName, + "coat", + m.clearcoat, + coat.texture, + AdobeTokens->r, + &coat.factor); + importInput(ctx, + m.displayName, + "coatRoughness", + m.clearcoatRoughness, + coat.roughnessTexture, + AdobeTokens->g, + &coat.roughnessFactor); + importNormalInput( + ctx, m.displayName, "coatNormal", m.clearcoatNormal, coat.normalTexture); + importValue1(m.clearcoatIor, coat.ior); + importColorInput(ctx, + m.displayName, + "coatColor", + m.clearcoatColor, + coat.colorTexture, + coat.colorFactor, + 1.0); + } else if (importClearcoat(gm.extensions, &clearcoat)) { importInput(ctx, m.displayName, "clearcoat", @@ -1202,30 +1297,30 @@ importMaterials(ImportGltfContext& ctx) "clearcoatNormal", m.clearcoatNormal, clearcoat.normalTexture); - } - - AdobeClearcoatSpecular clearcoatSpecular; - if (importAdobeClearcoatSpecular(gm.extensions, &clearcoatSpecular)) { - importValue1(m.clearcoatIor, clearcoatSpecular.ior); - importInput(ctx, - m.displayName, - "clearcoatSpecular", - m.clearcoatSpecular, - clearcoatSpecular.texture, - AdobeTokens->b, - &clearcoatSpecular.factor, - 1.0); - } - ClearcoatColor clearcoatColor; - if (importClearcoatColor(gm.extensions, &clearcoatColor)) { - importColorInput(ctx, - m.displayName, - "clearcoatColor", - m.clearcoatColor, - clearcoatColor.texture, - clearcoatColor.factor, - 1.0); + AdobeClearcoatSpecular clearcoatSpecular; + if (importAdobeClearcoatSpecular( + gm.extensions, &clearcoatSpecular, m.displayName)) { + importValue1(m.clearcoatIor, clearcoatSpecular.ior); + importInput(ctx, + m.displayName, + "clearcoatSpecular", + m.clearcoatSpecular, + clearcoatSpecular.texture, + AdobeTokens->b, + &clearcoatSpecular.factor, + 1.0); + } + AdobeClearcoatColor clearcoatColor; + if (importAdobeClearcoatColor(gm.extensions, &clearcoatColor)) { + importColorInput(ctx, + m.displayName, + "clearcoatColor", + m.clearcoatColor, + clearcoatColor.texture, + clearcoatColor.factor, + 1.0); + } } Sheen sheen; @@ -1342,8 +1437,9 @@ importMaterials(ImportGltfContext& ctx) 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. + // 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); @@ -1485,13 +1581,17 @@ importMeshJointWeights(const tinygltf::Model& model, 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()); + 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()); + jointsIndices[i], + jointAccessor.type, + mesh.displayName.c_str()); return; } } @@ -1499,13 +1599,17 @@ importMeshJointWeights(const tinygltf::Model& model, 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()); + 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()); + weightsIndices[i], + weightAccessor.type, + mesh.displayName.c_str()); return; } } @@ -1579,7 +1683,9 @@ getIndices(const tinygltf::Model& model, { if (indicesIndex >= 0) { dst.resize(getAccessorElementCount(model, indicesIndex)); - readAccessorInts(model, indicesIndex, dst); + // Mesh indices can only be scalar + constexpr bool isScalar = true; + readAccessorInts(model, indicesIndex, dst, isScalar); } else { dst.resize(numVertices); @@ -1603,38 +1709,42 @@ 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); + 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; @@ -1654,8 +1764,8 @@ importMeshes(ImportGltfContext& ctx) // NORMAL is optional - only read if present if (normalsIndex >= 0) { - mesh.normals.values = - PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, normalsIndex)); + 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; @@ -1663,8 +1773,8 @@ importMeshes(ImportGltfContext& ctx) // TANGENT is optional - only read if present if (tangentsIndex >= 0) { - mesh.tangents.values = - PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, tangentsIndex)); + 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; @@ -1672,35 +1782,40 @@ importMeshes(ImportGltfContext& ctx) // 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()) { + 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); + 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 + 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()); + 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()); } } @@ -1710,6 +1825,25 @@ importMeshes(ImportGltfContext& ctx) PXR_NS::VtArray(getAccessorElementCount(*ctx.gltf, uvsIndex)); readAccessorDataToFloat( *ctx.gltf, uvsIndex, reinterpret_cast(mesh.uvs.values.data())); + + // Validate UV coordinates - clean out NaN/Inf values + size_t invalidCount = 0; + for (auto& uv : mesh.uvs.values) { + bool invalid = std::isnan(uv[0]) || std::isinf(uv[0]) || std::isnan(uv[1]) || + std::isinf(uv[1]); + if (invalid) { + uv[0] = 0.0f; + uv[1] = 0.0f; + invalidCount++; + } + } + if (invalidCount > 0) { + TF_WARN("Mesh '%s' has %zu invalid UV coordinates (NaN/Inf). " + "These have been reset to (0,0) to prevent rendering issues.", + mesh.displayName.c_str(), + invalidCount); + } + // Flip V coordinates for glTF files to match USD convention for (auto& uv : mesh.uvs.values) { uv[1] = 1.0f - uv[1]; @@ -1733,6 +1867,26 @@ importMeshes(ImportGltfContext& ctx) getAccessorElementCount(*ctx.gltf, uvsIndex)); readAccessorDataToFloat( *ctx.gltf, uvsIndex, reinterpret_cast(uvs.values.data())); + + // Validate UV coordinates for extra UV sets - clean out NaN/Inf values + size_t invalidCount = 0; + for (auto& uv : uvs.values) { + bool invalid = std::isnan(uv[0]) || std::isinf(uv[0]) || + std::isnan(uv[1]) || std::isinf(uv[1]); + if (invalid) { + uv[0] = 0.0f; + uv[1] = 0.0f; + invalidCount++; + } + } + if (invalidCount > 0) { + TF_WARN("Mesh '%s' TEXCOORD_%d has %zu invalid UV coordinates (NaN/Inf). " + "These have been reset to (0,0).", + mesh.displayName.c_str(), + n, + invalidCount); + } + // Flip V coordinates for additional UV sets as well for (auto& uv : uvs.values) { uv[1] = 1.0f - uv[1]; @@ -1752,7 +1906,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; @@ -1830,9 +1984,14 @@ importMeshes(ImportGltfContext& ctx) } } -// Traverses the glTF nodes to construct names appropriate for UsdSkel API consumption +// 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) { +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; @@ -1881,11 +2040,12 @@ importSkeletons(ImportGltfContext& ctx) } } - // ctx.usd->skeletons was resized at the very start to match the size of ctx.gltf->skins, + // 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()); + ctx.usd->skeletons.size(), + ctx.gltf->skins.size()); } // Then build the skeletons @@ -1907,14 +2067,16 @@ importSkeletons(ImportGltfContext& ctx) // 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()); - + 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.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; @@ -1927,7 +2089,8 @@ importSkeletons(ImportGltfContext& ctx) } 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, + TF_WARN("USD node index %d out of bounds (length %zu)", + nodeIt->second, ctx.usd->nodes.size()); continue; } @@ -1935,10 +2098,10 @@ importSkeletons(ImportGltfContext& ctx) usdNode.isJoint = true; const tinygltf::Node& node = ctx.gltf->nodes[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. - + PXR_NS::GfVec3d t = node.translation.size() ? PXR_NS::GfVec3d(node.translation[0], node.translation[1], node.translation[2]) @@ -1962,38 +2125,105 @@ importSkeletons(ImportGltfContext& ctx) skeleton.restTransforms[jointIdx] = m; } - // Validate inverse bind matrices accessor to prevent type confusion attacks + // Validate and read inverse bind matrices accessor + // Only process if inverseBindMatrices is provided (>= 0) if (skin.inverseBindMatrices >= 0) { + // Validate accessor index bounds 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()); + 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]; + + // Validate accessor type - must be MAT4 for inverse bind matrices 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()); + 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; } + + // Validate accessor count matches joints count 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()); + 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 jointIdx = 0; jointIdx < skin.joints.size(); jointIdx++) { - skeleton.bindTransforms[jointIdx] = - PXR_NS::GfMatrix4d(inverseBindMatricesFloat[jointIdx]).GetInverse(); + // Validate buffer view index to prevent NULL pointer dereference + if (ibmAccessor.bufferView < 0 || + static_cast(ibmAccessor.bufferView) >= ctx.gltf->bufferViews.size()) { + TF_WARN("Inverse bind matrices accessor %d has invalid buffer view index %d for " + "skin '%s'", + skin.inverseBindMatrices, + ibmAccessor.bufferView, + skeleton.displayName.c_str()); + continue; + } + + // Validate buffer index + const tinygltf::BufferView& bufferView = ctx.gltf->bufferViews[ibmAccessor.bufferView]; + if (bufferView.buffer < 0 || + static_cast(bufferView.buffer) >= ctx.gltf->buffers.size()) { + TF_WARN( + "Inverse bind matrices buffer view %d has invalid buffer index %d for skin '%s'", + ibmAccessor.bufferView, + bufferView.buffer, + skeleton.displayName.c_str()); + continue; + } + + // Read inverse bind matrices and compute bind transforms + PXR_NS::VtArray inverseBindMatricesFloat( + getAccessorElementCount(*ctx.gltf, skin.inverseBindMatrices)); + readAccessorData(*ctx.gltf, + skin.inverseBindMatrices, + reinterpret_cast(inverseBindMatricesFloat.data())); + for (size_t jointIdx = 0; jointIdx < skin.joints.size(); jointIdx++) { + skeleton.bindTransforms[jointIdx] = + PXR_NS::GfMatrix4d(inverseBindMatricesFloat[jointIdx]).GetInverse(); + } } + // If inverseBindMatrices is not provided (-1), bind transforms remain at their default + // (identity) } } +// Helper function to get the expected GLTF type for animation output values +// Returns the expected TINYGLTF_TYPE_* constant for the given USD type +template +constexpr int +getExpectedGltfType() +{ + // Default: unsupported type + return -1; +} + +template<> +constexpr int +getExpectedGltfType() +{ + return TINYGLTF_TYPE_VEC3; // For translation and scale +} + +template<> +constexpr int +getExpectedGltfType() +{ + return TINYGLTF_TYPE_VEC4; // For rotation (quaternion) +} + template bool importChannel(const tinygltf::Model& gltf, @@ -2007,40 +2237,77 @@ importChannel(const tinygltf::Model& gltf, 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()); + 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()); + 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; + } + + // Validate input accessor type - must be SCALAR for animation timestamps + // This prevents buffer overflow when reading timestamps into float array + const tinygltf::Accessor& inputAccessor = gltf.accessors[sampler.input]; + if (inputAccessor.type != TINYGLTF_TYPE_SCALAR) { + TF_WARN("Animation sampler input accessor %d has invalid type %d (expected SCALAR type " + "%d) for channel '%s'", + sampler.input, + inputAccessor.type, + TINYGLTF_TYPE_SCALAR, + name.c_str()); + return false; + } + + // Validate output accessor type - must match expected type for the animation channel + // This prevents buffer overflow when reading values into typed array + const tinygltf::Accessor& outputAccessor = gltf.accessors[sampler.output]; + int expectedType = getExpectedGltfType(); + if (outputAccessor.type != expectedType) { + TF_WARN("Animation sampler output accessor %d has invalid type %d (expected type %d) " + "for channel '%s'", + sampler.output, + outputAccessor.type, + expectedType, + 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()); + 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()); + 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() + offset)); + readAccessorDataToFloat( + gltf, sampler.input, reinterpret_cast(values.times.data() + offset)); readAccessorDataToFloat( 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]); @@ -2073,8 +2340,9 @@ importNodeAnimations(ImportGltfContext& ctx) 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()); + 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]; @@ -2084,7 +2352,8 @@ importNodeAnimations(ImportGltfContext& ctx) continue; } if (nodeIt->second < 0 || nodeIt->second >= ctx.usd->nodes.size()) { - TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + TF_WARN("USD node index %d out of bounds (length %zu)", + nodeIt->second, ctx.usd->nodes.size()); continue; } @@ -2156,20 +2425,22 @@ importSkeletonAnimations(ImportGltfContext& ctx) continue; } if (nodeIt->second < 0 || nodeIt->second >= ctx.usd->nodes.size()) { - TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + 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, + 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()); + "Found non skeleton node %d %s\n", + channel.target_node, + node.name.c_str()); } continue; } @@ -2182,16 +2453,17 @@ importSkeletonAnimations(ImportGltfContext& ctx) return; } - // ctx.usd->skeletons was resized at the very start to match the size of ctx.gltf->skins, + // 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()); + 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 @@ -2214,7 +2486,7 @@ importSkeletonAnimations(ImportGltfContext& ctx) 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", + TF_WARN("Could not find skeleton node name for glTF node %d", skelAnimNodes[skelAnimIdx]); continue; } @@ -2226,8 +2498,7 @@ importSkeletonAnimations(ImportGltfContext& ctx) 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"); @@ -2239,7 +2510,8 @@ importSkeletonAnimations(ImportGltfContext& ctx) continue; } if (nodeIt->second < 0 || nodeIt->second >= ctx.usd->nodes.size()) { - TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + TF_WARN("USD node index %d out of bounds (length %zu)", + nodeIt->second, ctx.usd->nodes.size()); continue; } @@ -2284,14 +2556,16 @@ importSkeletonAnimations(ImportGltfContext& ctx) continue; } if (nodeIt->second < 0 || nodeIt->second >= ctx.usd->nodes.size()) { - TF_WARN("USD node index %d out of bounds (length %zu)", nodeIt->second, + 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, + TF_WARN("Node index %d out of bounds (length %zu)", + nodeIndex, ctx.gltf->nodes.size()); continue; } @@ -2325,15 +2599,14 @@ importSkeletonAnimations(ImportGltfContext& ctx) node.translation[1], node.translation[2]) : PXR_NS::GfVec3f(0); - definitiveTranslations[skelAnimIdx].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[skelAnimIdx]); + interpolateData(definitiveTimes, + na.scales.times, + na.scales.values, + definitiveScales[skelAnimIdx]); } else { PXR_NS::GfVec3f restScale = node.scale.size() @@ -2351,18 +2624,18 @@ importSkeletonAnimations(ImportGltfContext& ctx) skeletonAnimation.scales.resize(definitiveTimes.size(), PXR_NS::VtArray(skelAnimNodes.size())); 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.rotations[defTimeIdx][skelAnimIdx] = + definitiveRotations[skelAnimIdx][defTimeIdx]; skeletonAnimation.translations[defTimeIdx][skelAnimIdx] = - PXR_NS::GfVec3f(definitiveTranslations[skelAnimIdx][defTimeIdx]); + PXR_NS::GfVec3f(definitiveTranslations[skelAnimIdx][defTimeIdx]); - skeletonAnimation.scales[defTimeIdx][skelAnimIdx] = - PXR_NS::GfVec3h(definitiveScales[skelAnimIdx][defTimeIdx]); + skeletonAnimation.scales[defTimeIdx][skelAnimIdx] = + PXR_NS::GfVec3h(definitiveScales[skelAnimIdx][defTimeIdx]); } } } @@ -2528,9 +2801,15 @@ importNgpExtension(const tinygltf::Value& ngp, NgpData& ngpData) // We traverse the glTF nodes recursively from root to children and assign each node a usd 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) { - +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); @@ -2541,7 +2820,7 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& return -1; } traversedNodes.insert(nodeIndex); - + // Get the next slot in the ctx.usd->nodes vector int usdNodeIndex = curUsdIndex; curUsdIndex++; @@ -2568,7 +2847,7 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& 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()); @@ -2581,7 +2860,7 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& n.parent = usdParentIndex; return usdNodeIndex; } - + const tinygltf::Node& node = ctx.gltf->nodes[nodeIndex]; Node& n = ctx.usd->nodes[usdNodeIndex]; @@ -2590,20 +2869,24 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& 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]); + 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()); + 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]); + 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()); + 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); @@ -2612,8 +2895,9 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& 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()); + 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); @@ -2623,14 +2907,17 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& 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()); + 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); + 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; } @@ -2638,20 +2925,24 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& // 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); + 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; } } 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); + 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 @@ -2678,7 +2969,7 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& 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 } @@ -2690,7 +2981,8 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& int validCount = 0; n.children.resize(validChildren.size()); for (auto childIndex : validChildren) { - rtnIndex = _traverseNodes(ctx, skinnedNodes, curUsdIndex, nodeIndex, childIndex, traversedNodes); + rtnIndex = + _traverseNodes(ctx, skinnedNodes, curUsdIndex, nodeIndex, childIndex, traversedNodes); if (rtnIndex >= 0) { n.children[validCount] = rtnIndex; validCount++; @@ -2700,7 +2992,7 @@ int _traverseNodes(ImportGltfContext& ctx, std::vector& skinnedNodes, int& return usdNodeIndex; } -// Import nodes from tinygltf Model to UsdData. We traverse the glTF nodes recursively +// 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) @@ -2710,7 +3002,7 @@ importNodes(ImportGltfContext& ctx) 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); @@ -2726,7 +3018,8 @@ importNodes(ImportGltfContext& ctx) std::unordered_set traversedNodes; for (const tinygltf::Scene& scene : ctx.gltf->scenes) { for (int rootNodeIndex : scene.nodes) { - rtnIndex = _traverseNodes(ctx, skinnedNodes, curUsdIndex, -1, rootNodeIndex, traversedNodes); + rtnIndex = + _traverseNodes(ctx, skinnedNodes, curUsdIndex, -1, rootNodeIndex, traversedNodes); if (rtnIndex >= 0) { ctx.usd->rootNodes.push_back(rtnIndex); } @@ -2741,13 +3034,11 @@ importNodes(ImportGltfContext& ctx) 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()); + 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()); + TF_WARN("Mesh index %d is out of bounds (max: %zu)", node.mesh, ctx.meshes.size()); continue; } @@ -2755,8 +3046,8 @@ importNodes(ImportGltfContext& ctx) // If the skin has a skeleton, find the parent node of the skeleton if (gltfSkeletonNodeIndex >= 0) { auto parentIt = ctx.parentMap.find(gltfSkeletonNodeIndex); - int gltfSkeletonNodeParentIndex = (parentIt != ctx.parentMap.end()) ? - parentIt->second : -1; + int gltfSkeletonNodeParentIndex = + (parentIt != ctx.parentMap.end()) ? parentIt->second : -1; // Check if the parent of the skeleton exists if (gltfSkeletonNodeParentIndex != -1) { @@ -2806,8 +3097,9 @@ checkMeshInstancing(ImportGltfContext& ctx) 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()); + TF_WARN("Primitive index %d is out of bounds (max: %zu)", + primitiveIdx, + ctx.usd->meshes.size()); continue; } ctx.usd->meshes[primitiveIdx].instanceable = true; @@ -2848,7 +3140,6 @@ 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(), @@ -2858,6 +3149,7 @@ static const std::set supportedExtension = { // In-development extensions "KHR_materials_diffuse_transmission", "KHR_materials_volume_scatter", + "KHR_materials_coat", "KHR_materials_subsurface", // previous incarnation of KHR_materials_volume_scatter "KHR_materials_sss" // previous name of KHR_materials_subsurface }; diff --git a/gltf/src/gltfImport.h b/gltf/src/gltfImport.h index f9c566bc..247b85b7 100644 --- a/gltf/src/gltfImport.h +++ b/gltf/src/gltfImport.h @@ -12,8 +12,8 @@ governing permissions and limitations under the License. #pragma once #include "gltf.h" #include "importGltfContext.h" -#include #include +#include namespace adobe::usd { diff --git a/gltf/src/gltfResolver.cpp b/gltf/src/gltfResolver.cpp index 8211f0bd..f86d153a 100644 --- a/gltf/src/gltfResolver.cpp +++ b/gltf/src/gltfResolver.cpp @@ -15,6 +15,7 @@ governing permissions and limitations under the License. #include "gltf.h" #include "gltfImport.h" +#include #include using namespace PXR_NS; @@ -24,8 +25,7 @@ AR_DEFINE_PACKAGE_RESOLVER(GltfResolver, ArPackageResolver); GltfResolver::GltfResolver() : Resolver("GltfResolver") -{ -} +{} void GltfResolver::readCache(const std::string& resolvedPath, std::vector& images) diff --git a/gltf/src/gltfResolver.h b/gltf/src/gltfResolver.h index 6665219e..92f860e2 100644 --- a/gltf/src/gltfResolver.h +++ b/gltf/src/gltfResolver.h @@ -18,10 +18,10 @@ namespace adobe::usd { /// \brief usdgltf custom asset resolver. class GltfResolver : public Resolver { - public: +public: GltfResolver(); - private: +private: virtual void readCache(const std::string& filename, std::vector& images) override; }; diff --git a/gltf/src/gltfSpecGloss.h b/gltf/src/gltfSpecGloss.h index 6d3a95fb..69177e40 100644 --- a/gltf/src/gltfSpecGloss.h +++ b/gltf/src/gltfSpecGloss.h @@ -15,7 +15,6 @@ governing permissions and limitations under the License. #include #include - namespace adobe::usd { /// Convert a specular-glossiness material to a metallic-roughness material diff --git a/gltf/src/importGltfContext.h b/gltf/src/importGltfContext.h index b1f1d527..43e5ac76 100644 --- a/gltf/src/importGltfContext.h +++ b/gltf/src/importGltfContext.h @@ -25,9 +25,10 @@ struct ImportGltfContext const tinygltf::Model* gltf = nullptr; UsdData* usd = nullptr; std::string path; - std::unordered_map nodeMap; // maps glTF node index to USD node index + 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::unordered_map + skeletonNodeNames; // maps glTF node index to skeleton node name std::vector> meshes; std::vector meshUseCount; diff --git a/gltf/src/precompiled.h b/gltf/src/precompiled.h deleted file mode 100644 index cc28696e..00000000 --- a/gltf/src/precompiled.h +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2023 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/obj/README.md b/obj/README.md index ea8acd6b..96e3b4bd 100644 --- a/obj/README.md +++ b/obj/README.md @@ -48,7 +48,7 @@ The generated USD will keep default units and up axis (1cm, +y). -Allows importing obj from ZBrush with vertex color (#MRGB tag) +Allows importing OBJ files with vertex color (#MRGB tag) * `objOriginalColorSpace`: USD uses linear colorspace, however, OBJ 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. @@ -120,6 +120,14 @@ Also, the resulting meshes are unitless (obj does not support units). No adjustm The material network uses `MaterialX` nodes to express individual operations and has an `OpenPBR` surface, which has rich support for PBR oriented materials. +* `preserveExtraMaterialInfo`: Generate shading networks with extra data for transcoding. Default is `true` + When this is enabled, the generated shading networks might contain extra inputs that are outside of the respective + material surface schema, that are useful for transcoding purposes. For example, the `OpenPBR` surface does not have + an `occlusion` input for ambient occlusion, but we might want to express such a signal, if it was present in the + source asset, so that an exporter can pick-up said signal and use it when generating an output asset. + When `preserveExtraMaterialInfo` is `false`, the code will not generate these extra fields that are outside of the + schema, which won't affect renders, but can affect the transcoding abilities. + * `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: @@ -132,6 +140,29 @@ Also, the resulting meshes are unitless (obj does not support units). No adjustm Keep in mind it is a lossy conversion. > Note: currently this only works when also providing assetsPath (TODO fix). +* `computeNormals`: Generate smooth vertex normals for meshes that don't have explicit normals in the OBJ file. Default is `false` + By default, the plugin only imports normals if they are present in the OBJ file (as `vn` lines). If an OBJ file has no explicit normals, + meshes will be imported without normal data, and renderers will compute normals at render time. You can force the generation of smooth vertex normals during import: + ``` + from pxr import Usd + stage = Usd.Stage.Open("asset.obj:SDF_FORMAT_ARGS:computeNormals=true") + stage.Export("asset.usda") + ``` + The generated normals are vertex-interpolated and computed by averaging face normals at shared vertices. This is useful for OBJ files exported without normals, + though be aware that smooth normals will not preserve high-frequency detail that would be captured in per-face-vertex normals. +* `groupOptions`: Control how OBJ groups are imported into USD. Default is `separateGroupsAsMeshes` + OBJ files can contain groups (`g` lines) that organize faces into logical collections. The `groupOptions` argument controls how these groups are translated to USD: + - `separateGroupsAsMeshes` (default): Each OBJ group becomes a separate USD Mesh prim. This preserves the original group structure but can be slow for files with many groups. + + - `combineGroups`: All groups are merged into a single USD Mesh. This is much faster for files with many groups and results in better rendering performance. Group information is discarded. + + - `separateGroupsAsSubsets`: All groups are merged into a single USD Mesh, but each group is preserved as a GeomSubset. This maintains group information while having a single mesh, though creating many subsets can still be slow. + Example using `combineGroups` for a file with many groups: + ``` + from pxr import Usd + stage = Usd.Stage.Open("sculpt.obj:SDF_FORMAT_ARGS:groupOptions=combineGroups") + stage.Export("sculpt.usda") + ``` ## Debug codes * `FILE_FORMAT_OBJ`: Common debug messages. * OBJ_PACKAGE_RESOLVER diff --git a/obj/src/CMakeLists.txt b/obj/src/CMakeLists.txt index 37a9d13a..43dce85c 100644 --- a/obj/src/CMakeLists.txt +++ b/obj/src/CMakeLists.txt @@ -40,68 +40,10 @@ PUBLIC target_include_directories(usdObj PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}/src" ) -target_precompile_headers(usdObj -PRIVATE - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" -) - # Installation of plugin files mimics the file structure that USD has for plugins, # so it is easy to deploy it in a pre-existing USD build, if one chooses to do so. @@ -116,18 +58,19 @@ set_target_properties(usdObj PROPERTIES RESOURCE ${CMAKE_CURRENT_BINARY_DIR}/plu set_target_properties(usdObj PROPERTIES RESOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json:plugInfo.json") +# USDOBJ_DESTINATION is set in the parent scope by the add_usd_fileformat macro if(USDOBJ_ENABLE_INSTALL) install( TARGETS usdObj - RUNTIME DESTINATION plugin/usd COMPONENT Runtime - LIBRARY DESTINATION plugin/usd COMPONENT Runtime - RESOURCE DESTINATION plugin/usd/usdObj/resources COMPONENT Runtime + RUNTIME DESTINATION ${USDOBJ_DESTINATION} COMPONENT Runtime + LIBRARY DESTINATION ${USDOBJ_DESTINATION} COMPONENT Runtime + RESOURCE DESTINATION ${USDOBJ_DESTINATION}/usdObj/resources COMPONENT Runtime ) install( FILES plugInfo.root.json - DESTINATION plugin/usd + DESTINATION ${USDOBJ_DESTINATION} RENAME plugInfo.json COMPONENT Runtime ) diff --git a/obj/src/api.h b/obj/src/api.h index 2880b5a8..ff8225a5 100644 --- a/obj/src/api.h +++ b/obj/src/api.h @@ -14,19 +14,19 @@ governing permissions and limitations under the License. #include "pxr/base/arch/export.h" #if defined(PXR_STATIC) -# define USDOBJ_API -# define USDOBJ_API_TEMPLATE_CLASS(...) -# define USDOBJ_API_TEMPLATE_STRUCT(...) -# define USDOBJ_LOCAL +#define USDOBJ_API +#define USDOBJ_API_TEMPLATE_CLASS(...) +#define USDOBJ_API_TEMPLATE_STRUCT(...) +#define USDOBJ_LOCAL #else -# if defined(USDOBJ_EXPORTS) -# define USDOBJ_API ARCH_EXPORT -# define USDOBJ_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) -# define USDOBJ_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) -# else -# define USDOBJ_API ARCH_IMPORT -# define USDOBJ_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) -# define USDOBJ_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) -# endif -# define USDOBJ_LOCAL ARCH_HIDDEN +#if defined(USDOBJ_EXPORTS) +#define USDOBJ_API ARCH_EXPORT +#define USDOBJ_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define USDOBJ_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#else +#define USDOBJ_API ARCH_IMPORT +#define USDOBJ_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define USDOBJ_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#endif +#define USDOBJ_LOCAL ARCH_HIDDEN #endif \ No newline at end of file diff --git a/obj/src/fileFormat.cpp b/obj/src/fileFormat.cpp index 6b852db5..44b6dc47 100644 --- a/obj/src/fileFormat.cpp +++ b/obj/src/fileFormat.cpp @@ -17,15 +17,12 @@ governing permissions and limitations under the License. #include "objImport.h" #include -#include #include #include #include #include #include -#include - PXR_NAMESPACE_OPEN_SCOPE using namespace adobe::usd; @@ -33,6 +30,8 @@ using namespace adobe::usd; const TfToken UsdObjFileFormat::assetsPathToken("objAssetsPath", TfToken::Immortal); const TfToken UsdObjFileFormat::phongToken("objPhong", TfToken::Immortal); const TfToken UsdObjFileFormat::originalColorSpaceToken("objOriginalColorSpace", TfToken::Immortal); +const TfToken UsdObjFileFormat::computeNormalsToken("computeNormals", TfToken::Immortal); +const TfToken UsdObjFileFormat::groupOptionsToken("groupOptions", TfToken::Immortal); TF_DEFINE_PUBLIC_TOKENS(UsdObjFileFormatTokens, USDOBJ_FILE_FORMAT_TOKENS); TF_REGISTRY_FUNCTION(TfType) @@ -68,6 +67,8 @@ UsdObjFileFormat::InitData(const FileFormatArguments& args) const argReadBool(args, phongToken.GetString(), pd->phong, DEBUG_TAG); argReadString(args, originalColorSpaceToken.GetString(), pd->originalColorSpace, DEBUG_TAG); + argReadBool(args, computeNormalsToken.GetString(), pd->computeNormals, DEBUG_TAG); + argReadString(args, groupOptionsToken.GetString(), pd->groupOptions, DEBUG_TAG); return pd; } void @@ -79,6 +80,8 @@ UsdObjFileFormat::ComposeFieldsForFileFormatArguments(const std::string& assetPa argComposeString(context, args, assetsPathToken, DEBUG_TAG); argComposeBool(context, args, phongToken, DEBUG_TAG); argComposeString(context, args, originalColorSpaceToken, DEBUG_TAG); + argComposeBool(context, args, computeNormalsToken, DEBUG_TAG); + argComposeString(context, args, groupOptionsToken, DEBUG_TAG); } bool @@ -115,11 +118,32 @@ UsdObjFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool me options.importMaterials = true; options.importImages = readImages; options.importPhong = data->phong; + options.groupOptions = data->groupOptions; 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"); + + // Generate normals if requested and missing + if (data->computeNormals) { + int processedCount = 0; + for (adobe::usd::Mesh& mesh : usd.meshes) { + if (mesh.normals.values.size() == 0) { + TF_DEBUG_MSG( + FILE_FORMAT_OBJ, "Computing smooth normals for mesh %s\n", mesh.name.c_str()); + adobe::usd::computeSmoothNormals(mesh); + processedCount++; + } + } + if (processedCount > 0) { + TF_DEBUG_MSG(FILE_FORMAT_OBJ, + "Computed normals for %d/%zu meshes\n", + processedCount, + usd.meshes.size()); + } + } + GUARD(writeLayer( layerOptions, usd, layer, layerData, fileType, DEBUG_TAG, SdfFileFormat::_SetLayerData), "Error writing to the USD layer\n"); @@ -151,8 +175,29 @@ UsdObjFileFormat::ReadFromString(SdfLayer* layer, const std::string& input) cons options.importMaterials = true; options.importImages = readImages; options.importPhong = data->phong; + options.groupOptions = data->groupOptions; 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"); + + // Generate normals if requested and missing + if (data->computeNormals) { + int processedCount = 0; + for (adobe::usd::Mesh& mesh : usd.meshes) { + if (mesh.normals.values.size() == 0) { + TF_DEBUG_MSG( + FILE_FORMAT_OBJ, "Computing smooth normals for mesh %s\n", mesh.name.c_str()); + adobe::usd::computeSmoothNormals(mesh); + processedCount++; + } + } + if (processedCount > 0) { + TF_DEBUG_MSG(FILE_FORMAT_OBJ, + "Computed normals for %d/%zu meshes\n", + processedCount, + usd.meshes.size()); + } + } + GUARD(writeLayer( layerOptions, usd, layer, layerData, "obj", DEBUG_TAG, SdfFileFormat::_SetLayerData), "Error writing to the USD stage\n"); @@ -193,14 +238,16 @@ UsdObjFileFormat::WriteToString(const SdfLayer& layer, const std::string& comment) const { // Write USD as OBJ: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToString(layer, str, comment); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToString(layer, str, comment); } bool UsdObjFileFormat::WriteToStream(const SdfSpecHandle& spec, std::ostream& out, size_t indent) const { // Write USD as OBJ: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToStream(spec, out, indent); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToStream(spec, out, indent); } PXR_NAMESPACE_CLOSE_SCOPE diff --git a/obj/src/fileFormat.h b/obj/src/fileFormat.h index c2dd9d70..0eaca66d 100644 --- a/obj/src/fileFormat.h +++ b/obj/src/fileFormat.h @@ -18,7 +18,6 @@ governing permissions and limitations under the License. #include #include -#include #include #include @@ -33,8 +32,11 @@ TF_DECLARE_WEAK_AND_REF_PTRS(UsdObjFileFormat); /// \brief SdfData specialization for working with obj files. class ObjData : public FileFormatDataBase { - public: +public: bool phong = false; + bool computeNormals = false; + TfToken groupOptions; // "separateGroupsAsMeshes" (default), "separateGroupsAsSubsets", + // "combineGroups" TfToken originalColorSpace; static ObjDataRefPtr InitData(const SdfFileFormat::FileFormatArguments& args); }; @@ -45,7 +47,7 @@ class USDOBJ_API UsdObjFileFormat : public SdfFileFormat , public PcpDynamicFileFormatInterface { - public: +public: friend class ObjData; virtual SdfAbstractDataRefPtr InitData(const FileFormatArguments& args) const override; @@ -83,15 +85,17 @@ class USDOBJ_API UsdObjFileFormat std::string* str, const std::string& comment = std::string()) const override; - protected: +protected: SDF_FILE_FORMAT_FACTORY_ACCESS; virtual ~UsdObjFileFormat(); UsdObjFileFormat(); - private: +private: static const TfToken assetsPathToken; static const TfToken phongToken; static const TfToken originalColorSpaceToken; + static const TfToken computeNormalsToken; + static const TfToken groupOptionsToken; bool ReadFromStream(SdfLayer* layer, std::istream& input, diff --git a/obj/src/obj.cpp b/obj/src/obj.cpp index 0802668c..225e638d 100644 --- a/obj/src/obj.cpp +++ b/obj/src/obj.cpp @@ -61,11 +61,11 @@ governing permissions and limitations under the License. using namespace PXR_NS; #if defined(_WIN32) && !defined(__CYGWIN__) - #define ftell64 _ftelli64 - #define fseek64 _fseeki64 +#define ftell64 _ftelli64 +#define fseek64 _fseeki64 #else - #define ftell64 ftello - #define fseek64 fseeko +#define ftell64 ftello +#define fseek64 fseeko #endif namespace adobe::usd { @@ -304,15 +304,15 @@ 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 +/// 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 { + } else { x[2] = x[1] = x[0]; return true; } @@ -320,7 +320,6 @@ nextFloat1or3(const char*& p, const char* end, GfVec3f& x) return false; } - /// Helper parsing function. `p` is the moving pointer into the data. bool nextInteger(const char*& p, const char* end, int& x) @@ -699,6 +698,13 @@ readObjIntermediate(ObjIntermediate& inter) // Don't care about comments // rep.comments.push_back(std::string()); // nextText(rep.comments.back()); + } else if (c0 == 'v' && c1 >= '0' && c1 <= '9') { + // Detect malformed vertex lines like "v56 ..." instead of "v 56 ..." + // This is corrupted data - missing space after 'v' command + TF_WARN("Malformed vertex line at offset %td: line starts with 'v%c' instead of 'v ' - " + "vertex will be skipped. This may cause face index errors.", + p - inter.data, + c1); } else { } lineCount++; @@ -837,16 +843,25 @@ reindexObjIntermediate(Obj& obj, size_t vOutOfRangeCount = 0; size_t vtOutOfRangeCount = 0; size_t vnOutOfRangeCount = 0; + size_t vSkippedNoVerticesCount = 0; // Track skipped points when no vertices exist // This needs to be called when ever we start a new object or group auto checkOutOfRange = [&]() { if (g) { + if (vSkippedNoVerticesCount) { + TF_WARN( + "Object '%s', group '%s': %zu face points reference vertices but no vertices " + "exist in the file - these points were skipped.", + o->name.c_str(), + g->name.c_str(), + vSkippedNoVerticesCount); + } if (vOutOfRangeCount) { - TF_DEBUG_MSG(FILE_FORMAT_OBJ, - "Object %s, group %s: Invalid vertex indices: %lu\n", - o->name.c_str(), - g->name.c_str(), - vOutOfRangeCount); + TF_WARN("Object '%s', group '%s': %zu out-of-range vertex indices replaced with " + "fallback vertex 0. This may cause visual artifacts.", + o->name.c_str(), + g->name.c_str(), + vOutOfRangeCount); } size_t numVertexIndices = g->indices.size(); if (vtOutOfRangeCount) { @@ -898,6 +913,7 @@ reindexObjIntermediate(Obj& obj, vOutOfRangeCount = 0; vtOutOfRangeCount = 0; vnOutOfRangeCount = 0; + vSkippedNoVerticesCount = 0; }; auto addObject = [&]() { checkOutOfRange(); @@ -978,9 +994,16 @@ reindexObjIntermediate(Obj& obj, const GfVec3i& p = sum.points[pOffset + pointId]; if (p[0] != 0) { int index = p[0] > 0 ? p[0] - 1 : vOffset + p[0]; - if (static_cast(index) >= sum.vertices.size()) { + if (index < 0 || static_cast(index) >= sum.vertices.size()) { vOutOfRangeCount++; - continue; + // Use vertex 0 as fallback to preserve mesh topology. + // Using 'continue' here would skip adding an index, causing + // face vertex count mismatch (inconsistent mesh data). + if (sum.vertices.empty()) { + vSkippedNoVerticesCount++; + continue; // No valid fallback available + } + index = 0; } if (verticesMap[index]) { int existingIndex = verticesIndexMap[index]; @@ -1002,11 +1025,15 @@ reindexObjIntermediate(Obj& obj, } if (p[1] != 0) { int index = p[1] > 0 ? p[1] - 1 : vtOffset + p[1]; - if (static_cast(index) >= sum.uvs.size()) { + if (index < 0 || static_cast(index) >= sum.uvs.size()) { vtOutOfRangeCount++; - continue; - } - if (uvsMap[index]) { + // Use UV 0 as fallback to preserve array consistency. + // Using 'continue' here would skip adding an index, causing + // UV index count mismatch with vertex indices. + if (!g->uvs.empty()) { + g->uvIndices.push_back(0); + } + } else if (uvsMap[index]) { int existingIndex = uvsIndexMap[index]; g->uvIndices.push_back(existingIndex); } else { @@ -1041,11 +1068,15 @@ reindexObjIntermediate(Obj& obj, } if (p[2] != 0) { int index = p[2] > 0 ? p[2] - 1 : vnOffset + p[2]; - if (static_cast(index) >= sum.normals.size()) { + if (index < 0 || static_cast(index) >= sum.normals.size()) { vnOutOfRangeCount++; - continue; - } - if (normalsMap[index]) { + // Use normal 0 as fallback to preserve array consistency. + // Using 'continue' here would skip adding an index, causing + // normal index count mismatch with vertex indices. + if (!g->normals.empty()) { + g->normalIndices.push_back(0); + } + } else if (normalsMap[index]) { int existingIndex = normalsIndexMap[index]; g->normalIndices.push_back(existingIndex); } else { @@ -1691,7 +1722,7 @@ class BufferControl int flushCount; std::fstream& file; - public: +public: BufferControl(size_t bufferSize, std::fstream& file) : bufferSize(bufferSize) , flushCount(0) @@ -1717,7 +1748,7 @@ class BufferControl file.write(buffer, p - buffer); p = buffer; } - auto result = fmt::format_to_n(p, maxLineSize, format, args...); + auto result = fmt::format_to_n(p, maxLineSize, fmt::runtime(format), args...); if (result.size <= maxLineSize) { p += result.size; return true; diff --git a/obj/src/obj.h b/obj/src/obj.h index c6eaede3..e5ac758e 100644 --- a/obj/src/obj.h +++ b/obj/src/obj.h @@ -19,10 +19,7 @@ governing permissions and limitations under the License. /// mtl: http://paulbourke.net/dataformats/mtl/ /// pbr-extension: http://exocortex.com/blog/extending_wavefront_mtl_to_support_pbr -#include -#include #include -#include #include #include #include diff --git a/obj/src/objExport.cpp b/obj/src/objExport.cpp index e1afa1e9..d55bf0bd 100644 --- a/obj/src/objExport.cpp +++ b/obj/src/objExport.cpp @@ -14,38 +14,7 @@ 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 -#include -#include using namespace PXR_NS; @@ -71,7 +40,7 @@ writeObjMap(const UsdData& usd, ObjMap& map, const Input& input) if (input.image >= 0) { const ImageAsset& image = usd.images[input.image]; map.defined = true; - map.filename = image.uri; + map.filename = TfGetBaseName(image.uri); map.image = input.image; // XXX note that mtl doesn't support uv rotation so we only handle translation and scale @@ -215,7 +184,7 @@ exportObj(const ExportObjOptions& options, const UsdData& usd, Obj& obj) const ImageAsset& usdImage = usd.images[i]; ImageAsset& image = obj.images[i]; image.name = usdImage.name; - image.uri = usdImage.uri; + image.uri = TfGetBaseName(usdImage.uri); image.format = usdImage.format; image.image = usdImage.image; } diff --git a/obj/src/objImport.cpp b/obj/src/objImport.cpp index e5b93af7..0140af94 100644 --- a/obj/src/objImport.cpp +++ b/obj/src/objImport.cpp @@ -14,24 +14,23 @@ 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 using namespace PXR_NS; + +PXR_NAMESPACE_OPEN_SCOPE +// clang-format off +TF_DEFINE_PRIVATE_TOKENS(_groupOptionsTokens, + (combineGroups) + (separateGroupsAsSubsets) + (separateGroupsAsMeshes) +); +// clang-format on +PXR_NAMESPACE_CLOSE_SCOPE + namespace adobe::usd { void @@ -238,74 +237,328 @@ importObj(const ImportObjOptions& options, Obj& obj, UsdData& usd) if (options.importGeometry) { int current_material = -1; bool convertToLinear = (obj.originalColorSpace == AdobeTokens->sRGB); + + // Determine group handling mode + // Default is "separateGroupsAsMeshes" if empty or unspecified (backwards compatible) + bool useCombineGroups = options.groupOptions == _groupOptionsTokens->combineGroups; + bool useSeparateGroupsAsSubsets = + options.groupOptions == _groupOptionsTokens->separateGroupsAsSubsets; + bool useSeparateGroupsAsMeshes = + options.groupOptions.IsEmpty() || + options.groupOptions == _groupOptionsTokens->separateGroupsAsMeshes; + + // Warn if an unrecognized groupOptions value was provided and default to + // separateGroupsAsMeshes + if (!options.groupOptions.IsEmpty() && !useCombineGroups && !useSeparateGroupsAsSubsets && + !useSeparateGroupsAsMeshes) { + TF_WARN("Unrecognized groupOptions value '%s'. Expected 'combineGroups', " + "'separateGroupsAsSubsets', or 'separateGroupsAsMeshes'. Defaulting to " + "'separateGroupsAsMeshes'.", + options.groupOptions.GetText()); + useSeparateGroupsAsMeshes = true; + } + for (const ObjObject& o : obj.objects) { auto [nodeIndex, node] = usd.addNode(-1); node.name = o.name; - for (const ObjGroup& g : o.groups) { - // Skip empty groups - if (g.faces.empty()) { - TF_DEBUG_MSG( - FILE_FORMAT_OBJ, - "Skipping empty group %s on node %s - %zu verts, %zu faces, %zu indices\n", - g.name.c_str(), - node.name.c_str(), - g.vertices.size(), - g.faces.size(), - g.indices.size()); + if (useCombineGroups || useSeparateGroupsAsSubsets) { + // Combine all groups into a single mesh + // With separateSubgroups, also create GeomSubsets for each group + // First, count total sizes to pre-allocate + size_t totalVertices = 0; + size_t totalFaces = 0; + size_t totalIndices = 0; + size_t totalUvs = 0; + size_t totalUvIndices = 0; + size_t totalNormals = 0; + size_t totalNormalIndices = 0; + size_t totalColors = 0; + // Track if any group has these attributes (groups without them get placeholder + // values) + bool hasUvs = false; + bool hasNormals = false; + bool hasColors = false; + + for (const ObjGroup& g : o.groups) { + if (g.faces.empty()) + continue; + totalVertices += g.vertices.size(); + totalFaces += g.faces.size(); + totalIndices += g.indices.size(); + if (g.uvs.size()) { + hasUvs = true; + totalUvs += g.uvs.size(); + totalUvIndices += g.uvIndices.size(); + } + if (g.normals.size()) { + hasNormals = true; + totalNormals += g.normals.size(); + totalNormalIndices += g.normalIndices.size(); + } + if (g.colors.size()) { + hasColors = true; + totalColors += g.colors.size(); + } + } + + // Skip if no geometry + if (totalFaces == 0) { + TF_DEBUG_MSG(FILE_FORMAT_OBJ, + "Skipping object %s - no faces after combining groups\n", + o.name.c_str()); continue; } + auto [meshIndex, mesh] = usd.addMesh(); node.staticMeshes.push_back(meshIndex); - mesh.name = g.name; + // Leave name empty - uniquifyNames() will set it to "Mesh" as the default, since + // all meshes are combined into a single mesh mesh.doubleSided = true; - mesh.faces = g.faces; - mesh.indices = g.indices; - mesh.points = g.vertices; - if (g.uvs.size()) { - mesh.uvs.indices = g.uvIndices; - mesh.uvs.values = g.uvs; + + // Pre-allocate + mesh.points.reserve(totalVertices); + mesh.faces.reserve(totalFaces); + mesh.indices.reserve(totalIndices); + if (hasUvs) { + // Reserve +1 for placeholder UV at index 0 (used by groups without UVs) + mesh.uvs.values.reserve(totalUvs + 1); + mesh.uvs.indices.reserve(totalUvIndices); + // Add placeholder UV at index 0 for groups that don't have UVs + mesh.uvs.values.push_back(GfVec2f(0.0f, 0.0f)); + } + if (hasNormals) { + // Reserve +1 for placeholder normal at index 0 (used by groups without normals) + mesh.normals.values.reserve(totalNormals + 1); + mesh.normals.indices.reserve(totalNormalIndices); + // Add placeholder normal at index 0 for groups that don't have normals + mesh.normals.values.push_back(GfVec3f(0.0f, 1.0f, 0.0f)); + } + + VtVec3fArray combinedColors; + if (hasColors) { + combinedColors.reserve(totalColors); + } + + // Track offsets for index remapping + int vertexOffset = 0; + int uvOffset = hasUvs ? 1 : 0; // Start at 1 if we added a placeholder UV + int normalOffset = + hasNormals ? 1 : 0; // Start at 1 if we added a placeholder normal + int faceOffset = 0; + + // Track which faces belong to each group (for creating GeomSubsets) + struct GroupFaceRange + { + int startFace; + int faceCount; + int material; + }; + std::vector groupFaceRanges; + + // Combine all groups + for (const ObjGroup& g : o.groups) { + if (g.faces.empty()) + continue; + + // Track group face range for subset creation + if (useSeparateGroupsAsSubsets) { + groupFaceRanges.push_back( + { faceOffset, static_cast(g.faces.size()), g.material }); + } + + // Append vertices (using push_back for USD version compatibility) + for (const auto& v : g.vertices) { + mesh.points.push_back(v); + } + + // Append faces + for (const auto& f : g.faces) { + mesh.faces.push_back(f); + } + + // Append indices with offset + for (int idx : g.indices) { + mesh.indices.push_back(idx + vertexOffset); + } + + // Append UVs if present + if (hasUvs) { + if (g.uvs.size()) { + for (const auto& uv : g.uvs) { + mesh.uvs.values.push_back(uv); + } + for (int idx : g.uvIndices) { + mesh.uvs.indices.push_back(idx + uvOffset); + } + uvOffset += g.uvs.size(); + } else { + // Group has no UVs but others do - add placeholder indices + for (size_t i = 0; i < g.indices.size(); i++) { + mesh.uvs.indices.push_back(0); + } + } + } + + // Append normals if present + if (hasNormals) { + if (g.normals.size()) { + for (const auto& n : g.normals) { + mesh.normals.values.push_back(n); + } + for (int idx : g.normalIndices) { + mesh.normals.indices.push_back(idx + normalOffset); + } + normalOffset += g.normals.size(); + } else { + // Group has no normals but others do - add placeholder indices + for (size_t i = 0; i < g.indices.size(); i++) { + mesh.normals.indices.push_back(0); + } + } + } + + // Append colors if present + if (hasColors) { + if (g.colors.size()) { + if (convertToLinear) { + for (const auto& c : g.colors) { + combinedColors.push_back(GfVec3f( + srgbToLinear(c[0]), srgbToLinear(c[1]), srgbToLinear(c[2]))); + } + } else { + for (const auto& c : g.colors) { + combinedColors.push_back(c); + } + } + } else { + // Group has no colors but others do - add default white + for (size_t i = 0; i < g.vertices.size(); i++) { + combinedColors.push_back(GfVec3f(1.0f, 1.0f, 1.0f)); + } + } + } + + // Track material for the combined mesh + if (g.material >= 0) { + current_material = g.material; + } + + vertexOffset += g.vertices.size(); + faceOffset += g.faces.size(); + } + + // Set interpolation modes + if (hasUvs) { mesh.uvs.interpolation = UsdGeomTokens->faceVarying; } - if (g.normals.size()) { - mesh.normals.indices = g.normalIndices; - mesh.normals.values = g.normals; + if (hasNormals) { mesh.normals.interpolation = UsdGeomTokens->faceVarying; } - if (g.colors.size()) { + if (hasColors) { auto [colorSetIndex, color] = usd.addColorSet(meshIndex); - if (convertToLinear) { - VtVec3fArray mutableColors = g.colors; - for (auto& c : mutableColors) { - c[0] = srgbToLinear(c[0]); - c[1] = srgbToLinear(c[1]); - c[2] = srgbToLinear(c[2]); - } - color.values = mutableColors; - } else { - color.values = g.colors; - } + color.values = std::move(combinedColors); color.interpolation = UsdGeomTokens->vertex; } - // Set extent. - // SetExtent(vertexValues, mesh); - if (g.subsets.size() == 0) { - mesh.material = g.material; - } else if (g.subsets.size() == 1 && g.faces.size() == g.subsets[0].faces.size()) { - mesh.material = g.subsets.back().material; - } else { - for (const ObjSubset& s : g.subsets) { - // const auto& material = obj.materials[s.material]; + + // Create GeomSubsets for separateGroups mode + if (useSeparateGroupsAsSubsets && groupFaceRanges.size() > 1) { + for (const auto& range : groupFaceRanges) { auto [subsetIndex, subset] = usd.addSubset(meshIndex); - subset.material = s.material; - subset.faces = s.faces; + subset.material = range.material; + // Create face indices for this subset + subset.faces.resize(range.faceCount); + for (int i = 0; i < range.faceCount; i++) { + subset.faces[i] = range.startFace + i; + } } - } - if (mesh.material < 0) { + TF_DEBUG_MSG(FILE_FORMAT_OBJ, + "Created %zu subsets for mesh '%s': %zu verts, %zu faces\n", + groupFaceRanges.size(), + mesh.name.c_str(), + mesh.points.size(), + mesh.faces.size()); + } else { + // Set material (use the last encountered material for the combined mesh) mesh.material = current_material; + TF_DEBUG_MSG( + FILE_FORMAT_OBJ, + "Combined %zu groups into single mesh '%s': %zu verts, %zu faces\n", + o.groups.size(), + mesh.name.c_str(), + mesh.points.size(), + mesh.faces.size()); + } + + } else if (useSeparateGroupsAsMeshes) { + // Legacy behavior: create a mesh per group + for (const ObjGroup& g : o.groups) { + // Skip empty groups + if (g.faces.empty()) { + TF_DEBUG_MSG(FILE_FORMAT_OBJ, + "Skipping empty group %s on node %s - %zu verts, %zu faces, " + "%zu indices\n", + g.name.c_str(), + node.name.c_str(), + g.vertices.size(), + g.faces.size(), + g.indices.size()); + continue; + } + auto [meshIndex, mesh] = usd.addMesh(); + node.staticMeshes.push_back(meshIndex); + + mesh.name = g.name; + mesh.doubleSided = true; + mesh.faces = g.faces; + mesh.indices = g.indices; + mesh.points = g.vertices; + if (g.uvs.size()) { + mesh.uvs.indices = g.uvIndices; + mesh.uvs.values = g.uvs; + mesh.uvs.interpolation = UsdGeomTokens->faceVarying; + } + if (g.normals.size()) { + mesh.normals.indices = g.normalIndices; + mesh.normals.values = g.normals; + mesh.normals.interpolation = UsdGeomTokens->faceVarying; + } + if (g.colors.size()) { + auto [colorSetIndex, color] = usd.addColorSet(meshIndex); + if (convertToLinear) { + VtVec3fArray mutableColors = g.colors; + for (auto& c : mutableColors) { + c[0] = srgbToLinear(c[0]); + c[1] = srgbToLinear(c[1]); + c[2] = srgbToLinear(c[2]); + } + color.values = mutableColors; + } else { + color.values = g.colors; + } + color.interpolation = UsdGeomTokens->vertex; + } + // Set extent. + // SetExtent(vertexValues, mesh); + if (g.subsets.size() == 0) { + mesh.material = g.material; + } else if (g.subsets.size() == 1 && + g.faces.size() == g.subsets[0].faces.size()) { + mesh.material = g.subsets.back().material; + } else { + for (const ObjSubset& s : g.subsets) { + // const auto& material = obj.materials[s.material]; + auto [subsetIndex, subset] = usd.addSubset(meshIndex); + subset.material = s.material; + subset.faces = s.faces; + } + } + if (mesh.material < 0) { + mesh.material = current_material; + } + current_material = mesh.material; } - current_material = mesh.material; } } } diff --git a/obj/src/objImport.h b/obj/src/objImport.h index f1afca61..10b6ac40 100644 --- a/obj/src/objImport.h +++ b/obj/src/objImport.h @@ -21,6 +21,8 @@ struct ImportObjOptions bool importMaterials; bool importImages; bool importPhong; + PXR_NS::TfToken groupOptions; // "separateGroupsAsMeshes (default)" , "separateGroupsAsSubsets", + // "combineGroups" }; /// \ingroup usdobj diff --git a/obj/src/objResolver.cpp b/obj/src/objResolver.cpp index bc56c5ce..c78de822 100644 --- a/obj/src/objResolver.cpp +++ b/obj/src/objResolver.cpp @@ -10,10 +10,9 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #include "objResolver.h" -#include "obj.h" #include "objImport.h" -#include #include +#include using namespace PXR_NS; namespace adobe::usd { @@ -22,8 +21,7 @@ AR_DEFINE_PACKAGE_RESOLVER(ObjResolver, ArPackageResolver); ObjResolver::ObjResolver() : Resolver("ObjResolver") -{ -} +{} void ObjResolver::readCache(const std::string& filename, std::vector& images) diff --git a/obj/src/objResolver.h b/obj/src/objResolver.h index 0fe88f90..23cf4e7d 100644 --- a/obj/src/objResolver.h +++ b/obj/src/objResolver.h @@ -16,10 +16,10 @@ namespace adobe::usd { class ObjResolver : public Resolver { - public: +public: ObjResolver(); - private: +private: virtual void readCache(const std::string& filename, std::vector& images) override; }; diff --git a/obj/src/plugInfo.json.in b/obj/src/plugInfo.json.in index 0eb5c8d4..f2ca4c6a 100644 --- a/obj/src/plugInfo.json.in +++ b/obj/src/plugInfo.json.in @@ -20,6 +20,18 @@ "displayGroup": "Core", "documentation:": "Is the original colorspace in linear or sRGB", "type": "string" + }, + "computeNormals": { + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Generate smooth normals if missing from OBJ file", + "type": "bool" + }, + "groupOptions": { + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "How to handle OBJ groups: 'separateGroupsAsMeshes' (default) - each group as separate mesh, 'combineGroups' - merge all groups into single mesh, 'separateGroupsAsSubsets' - single mesh with groups as GeomSubsets", + "type": "string" } }, "Types": { diff --git a/obj/src/precompiled.h b/obj/src/precompiled.h deleted file mode 100644 index 3cb6d03a..00000000 --- a/obj/src/precompiled.h +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2023 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/ply/src/CMakeLists.txt b/ply/src/CMakeLists.txt index 788207b2..1c71c36a 100644 --- a/ply/src/CMakeLists.txt +++ b/ply/src/CMakeLists.txt @@ -20,6 +20,7 @@ PRIVATE target_include_directories(usdPly PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" ) @@ -35,65 +36,6 @@ PRIVATE fileformatUtils ) -target_precompile_headers(usdPly -PRIVATE - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" -) - # Installation of plugin files mimics the file structure that USD has for plugins, # so it is easy to deploy it in a pre-existing USD build, if one chooses to do so. @@ -108,17 +50,18 @@ set_target_properties(usdPly PROPERTIES RESOURCE ${CMAKE_CURRENT_BINARY_DIR}/plu set_target_properties(usdPly PROPERTIES RESOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json:plugInfo.json") +# USDPLY_DESTINATION is set in the parent scope by the add_usd_fileformat macro if(USDPLY_ENABLE_INSTALL) install( TARGETS usdPly - RUNTIME DESTINATION plugin/usd COMPONENT Runtime - LIBRARY DESTINATION plugin/usd COMPONENT Runtime - RESOURCE DESTINATION plugin/usd/usdPly/resources COMPONENT Runtime + RUNTIME DESTINATION ${USDPLY_DESTINATION} COMPONENT Runtime + LIBRARY DESTINATION ${USDPLY_DESTINATION} COMPONENT Runtime + RESOURCE DESTINATION ${USDPLY_DESTINATION}/usdPly/resources COMPONENT Runtime ) install( FILES plugInfo.root.json - DESTINATION plugin/usd + DESTINATION ${USDPLY_DESTINATION} RENAME plugInfo.json COMPONENT Runtime ) diff --git a/ply/src/api.h b/ply/src/api.h index 19ad933f..307044f0 100644 --- a/ply/src/api.h +++ b/ply/src/api.h @@ -14,19 +14,19 @@ governing permissions and limitations under the License. #include "pxr/base/arch/export.h" #if defined(PXR_STATIC) -# define USDPLY_API -# define USDPLY_API_TEMPLATE_CLASS(...) -# define USDPLY_API_TEMPLATE_STRUCT(...) -# define USDPLY_LOCAL +#define USDPLY_API +#define USDPLY_API_TEMPLATE_CLASS(...) +#define USDPLY_API_TEMPLATE_STRUCT(...) +#define USDPLY_LOCAL #else -# if defined(USDPLY_EXPORTS) -# define USDPLY_API ARCH_EXPORT -# define USDPLY_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) -# define USDPLY_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) -# else -# define USDPLY_API ARCH_IMPORT -# define USDPLY_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) -# define USDPLY_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) -# endif -# define USDPLY_LOCAL ARCH_HIDDEN +#if defined(USDPLY_EXPORTS) +#define USDPLY_API ARCH_EXPORT +#define USDPLY_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define USDPLY_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#else +#define USDPLY_API ARCH_IMPORT +#define USDPLY_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define USDPLY_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#endif +#define USDPLY_LOCAL ARCH_HIDDEN #endif \ No newline at end of file diff --git a/ply/src/fileFormat.cpp b/ply/src/fileFormat.cpp index 566833bb..13dad3d6 100644 --- a/ply/src/fileFormat.cpp +++ b/ply/src/fileFormat.cpp @@ -24,7 +24,6 @@ governing permissions and limitations under the License. #include #include -#include #include #include @@ -196,14 +195,16 @@ UsdPlyFileFormat::WriteToString(const SdfLayer& layer, const std::string& comment) const { // Write USD as PLY: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToString(layer, str, comment); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToString(layer, str, comment); } bool UsdPlyFileFormat::WriteToStream(const SdfSpecHandle& spec, std::ostream& out, size_t indent) const { // Write USD as PLY: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToStream(spec, out, indent); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToStream(spec, out, indent); } PXR_NAMESPACE_CLOSE_SCOPE diff --git a/ply/src/fileFormat.h b/ply/src/fileFormat.h index 481a9fb9..85f268b2 100644 --- a/ply/src/fileFormat.h +++ b/ply/src/fileFormat.h @@ -11,8 +11,8 @@ governing permissions and limitations under the License. */ #pragma once #include "api.h" -#include #include +#include #include #include #include @@ -39,7 +39,7 @@ TF_DECLARE_WEAK_AND_REF_PTRS(UsdPlyFileFormat); /// \brief SdfData specialization for working with ply files. class PlyData : public FileFormatDataBase { - public: +public: bool points = false; bool withUpAxisCorrection = true; PXR_NS::VtFloatArray gsplatsClippingBox = { -2, -2, -2, 2, 2, 2 }; @@ -53,7 +53,7 @@ class USDPLY_API UsdPlyFileFormat : public SdfFileFormat , public PcpDynamicFileFormatInterface { - public: +public: friend class PlyData; virtual SdfAbstractDataRefPtr InitData(const FileFormatArguments& args) const override; @@ -91,7 +91,7 @@ class USDPLY_API UsdPlyFileFormat std::string* str, const std::string& comment = std::string()) const override; - protected: +protected: static const TfToken pointsToken; static const TfToken pointWidthToken; static const TfToken withUpAxisCorrectionToken; @@ -101,7 +101,7 @@ class USDPLY_API UsdPlyFileFormat virtual ~UsdPlyFileFormat(); UsdPlyFileFormat(); - private: +private: bool ReadFromStream(SdfLayer* layer, std::istream& input, bool metadataOnly, diff --git a/ply/src/plyExport.cpp b/ply/src/plyExport.cpp index d5f4ae11..ad1869e5 100644 --- a/ply/src/plyExport.cpp +++ b/ply/src/plyExport.cpp @@ -11,44 +11,16 @@ governing permissions and limitations under the License. */ #include "plyExport.h" #include "debugCodes.h" +#include +#include #include #include #include #include #include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include using namespace PXR_NS; @@ -317,7 +289,8 @@ traverseNodesAndFindMaxNumSHCoeffs(UsdData& usd, int nodeIndex) } for (size_t i = 0; i < node.children.size(); i++) { - maxNumSHCoeffs = std::max(maxNumSHCoeffs, traverseNodesAndFindMaxNumSHCoeffs(usd, node.children[i])); + maxNumSHCoeffs = + std::max(maxNumSHCoeffs, traverseNodesAndFindMaxNumSHCoeffs(usd, node.children[i])); } return maxNumSHCoeffs; } diff --git a/ply/src/plyExport.h b/ply/src/plyExport.h index 33322632..56b27a0d 100644 --- a/ply/src/plyExport.h +++ b/ply/src/plyExport.h @@ -10,8 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once -#include #include +#include namespace adobe::usd { diff --git a/ply/src/plyImport.cpp b/ply/src/plyImport.cpp index 55c3ab10..4f779b98 100644 --- a/ply/src/plyImport.cpp +++ b/ply/src/plyImport.cpp @@ -11,29 +11,17 @@ governing permissions and limitations under the License. */ #include "plyImport.h" #include "debugCodes.h" -#include -#include -#include #include #include #include #include +#include #include -#include -#include #include -#include -#include -#include -#include #include -#include #include -#include -#include -#include -#include #include +#include #include using namespace PXR_NS; @@ -93,7 +81,7 @@ struct FloatOrHalfLoader this->getPropertyDataPtr(element, target); } - private: +private: std::vector scratchData; std::vector* dataPtr = nullptr; }; @@ -157,7 +145,8 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) 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. + // Will check later. An asset is a Gsplat only if it contains points and has all the + // Gsplat-related fields. mesh.asGsplats = mesh.asPoints; int numHighOrderSHCoeffs = 0; @@ -205,7 +194,8 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) gsColorCoeff1 = gsColorCoeff1Loader.getPropertyDataPtr(element, "f_dc_1"); gsColorCoeff2 = gsColorCoeff2Loader.getPropertyDataPtr(element, "f_dc_2"); } catch (std::exception& e) { - TF_DEBUG_MSG(FILE_FORMAT_PLY, "Invalid Gaussian splatting color data: %s\n", e.what()); + TF_DEBUG_MSG( + FILE_FORMAT_PLY, "Invalid Gaussian splatting color data: %s\n", e.what()); mesh.asGsplats = false; } } else { @@ -240,8 +230,7 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) } else { mesh.asGsplats = false; } - if (mesh.asGsplats && element.hasProperty("opacity")) - { + if (mesh.asGsplats && element.hasProperty("opacity")) { try { gsOpacity = gsOpacityLoader.getPropertyDataPtr(element, "opacity"); } catch (std::exception& e) { @@ -456,23 +445,20 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) // We filter out useful convention info from the comment. bool useZup = false; - // The input source is probably Z-up if the comment contains these words. - const std::vector zUpTokens = { - std::regex("\\bZ-axis up\\b"), - std::regex("\\bBlender\\b"), - std::regex("\\bArtec\\b"), - std::regex("\\bRhinoceros\\b") - }; + // The input source is probably Z-up if the comment contains these words. + const std::vector zUpTokens = { std::regex("\\bZ-axis up\\b"), + std::regex("\\bBlender\\b"), + std::regex("\\bArtec\\b"), + std::regex("\\bRhinoceros\\b") }; - for (const std::string& comment : ply.comments) - { + for (const std::string& comment : ply.comments) { if (!useZup) { for (const std::regex& pattern : zUpTokens) { if (std::regex_search(comment, pattern)) { useZup = true; break; } - } + } } } @@ -482,8 +468,7 @@ importPly(const ImportPlyOptions& options, PLYData& ply, UsdData& usd) usd.upAxis = UsdGeomTokens->y; } - if (mesh.asGsplats && options.importGsplatClippingBox.size() >= 6) - { + if (mesh.asGsplats && options.importGsplatClippingBox.size() >= 6) { PXR_NS::GfVec3f minPos(std::numeric_limits::max()); PXR_NS::GfVec3f maxPos(-std::numeric_limits::max()); for (size_t i = 0; i < mesh.points.size(); i++) { diff --git a/ply/src/plyImport.h b/ply/src/plyImport.h index 02c5bf47..7c4f440e 100644 --- a/ply/src/plyImport.h +++ b/ply/src/plyImport.h @@ -10,8 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once -#include #include +#include namespace adobe::usd { diff --git a/sbsar/CMakeLists.txt b/sbsar/CMakeLists.txt index fb117931..2297e75c 100644 --- a/sbsar/CMakeLists.txt +++ b/sbsar/CMakeLists.txt @@ -11,7 +11,9 @@ option(USDSBSAR_ENABLE_FIX_STORM_16BIT "Enables fix storm 16bit textures issues" # 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) +# Asset cache: Stores rendered texture data. Increase for high-resolution workflows. +# Recommended: 1GB for HD, 2GB for 2K/4K, 4GB+ for 8K +set(USDSBSAR_CACHE_SIZE 2000000000 CACHE "" STRING) # 2GB for high-res rendering support set(USDSBSAR_IMAGE_CACHE_SIZE 1000000000 CACHE "" STRING) set(USDSBSAR_PACKAGE_LIMIT 10 CACHE "" STRING) @@ -81,9 +83,18 @@ endif() # find dependent packages find_package(ZLIB REQUIRED) + +# The engine has a default if this is not set +# Manually setting this for arm64 mac os as +# the default was not being set correctly +if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64") + set(SUBSTANCE_FRAMEWORK_ENGINE_VARIANT neon_blend) +endif() + include(substance_engine) add_subdirectory(src) if(USD_FILEFORMATS_BUILD_TESTS) add_subdirectory(test) endif(USD_FILEFORMATS_BUILD_TESTS) + diff --git a/sbsar/README.md b/sbsar/README.md index 0484b0e2..28c31301 100644 --- a/sbsar/README.md +++ b/sbsar/README.md @@ -121,6 +121,50 @@ def Material "SbsarGraphName" ( The thumbnail path format of the graph can be `./path/sbsar.sbsar[thumbnails/{graphName}.png]`. You can also specify it with the file name and the `thumbnail.png` (i.e. `./path/sbsar.sbsar[thumbnail.png]`), which returns the thumbnail of the material graph that matches the name of the SBSAR. If no such graph exists the thumbnail of the first graph is returned. +### Graph Type Filtering + +SBSAR files can contain different types of graphs (materials, environment lights, etc.). To prevent importing the wrong type of SBSAR in the wrong context, you can use the `graphTypeFilter` file format argument to filter which graphs are loaded. + +**Valid filter values:** +- `material` - Only load material graphs +- `light` - Only load light/environment graphs +- (no filter) - Load all graphs (default behavior) + +**Example: Loading a Material SBSAR with Filter** +```usda +def Material "WoodMaterial" ( + prepend references=@./wood.sbsar:SDF_FORMAT_ARGS:graphTypeFilter=material@ +) +{ + # This will only succeed if wood.sbsar contains material graphs +} +``` + +**Example: Loading an Environment SBSAR with Filter** +```usda +def DomeLight "SkyDome" ( + prepend references=@./sky.sbsar:SDF_FORMAT_ARGS:graphTypeFilter=light@ +) +{ + # This will only succeed if sky.sbsar contains light/environment graphs +} +``` + +**Error Handling:** +If the SBSAR file doesn't contain graphs of the requested type, the import will fail with a clear error message: +``` +SBSAR package 'wood.sbsar' does not contain any light/environment graphs. +Package contains: 3 material graph(s). +This SBSAR file cannot be used in this context. +``` + +This feature is especially useful when: +- Building import tools that need to validate SBSAR types before use +- Preventing user errors when dragging/dropping SBSAR files +- Creating asset libraries with type-safe SBSAR references + +**Note:** When no filter is specified, the plugin loads all graphs in the SBSAR file (backward compatible behavior). + ## Sample data There are samples in the data directory that show how you can interact with Substance materials in USD. diff --git a/sbsar/src/CMakeLists.txt b/sbsar/src/CMakeLists.txt index ff3b1d49..5a3a32d7 100644 --- a/sbsar/src/CMakeLists.txt +++ b/sbsar/src/CMakeLists.txt @@ -1,7 +1,6 @@ set(PLUGIN_NAME usdSbsar) add_library(${PLUGIN_NAME} SHARED ${SRC}) - set(PUBLIC_HEADERS api.h) target_sources(${PLUGIN_NAME} @@ -42,7 +41,11 @@ target_sources(${PLUGIN_NAME} target_include_directories(${PLUGIN_NAME} PUBLIC .) # target properties -set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_STANDARD 17) +if (CMAKE_CXX_STANDARD) + set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_STANDARD ${CMAKE_CXX_STANDARD}) +else() + set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_STANDARD 17) +endif() if (USDSBSAR_FORCE_EXTERNAL_USD) set(_boost_include_dir "${_usd_testing_root}/include/boost-1_78") @@ -157,13 +160,15 @@ set_target_properties(${PLUGIN_NAME} PROPERTIES RESOURCE ${CMAKE_CURRENT_BINARY_ set(_resource_list ${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json:plugInfo.json generatedSchema.usda schema.usda) set_target_properties(${PLUGIN_NAME} PROPERTIES RESOURCE_FILES "${_resource_list}") +# USDSBSAR_DESTINATION is set in the parent scope by the add_usd_fileformat macro + if(USDSBSAR_ENABLE_INSTALL) # Install the plugInfo.json file for the specific plugin install( TARGETS ${PLUGIN_NAME} - RUNTIME DESTINATION plugin/usd COMPONENT Runtime - LIBRARY DESTINATION plugin/usd COMPONENT Runtime - RESOURCE DESTINATION plugin/usd/${PLUGIN_NAME}/resources COMPONENT Runtime + RUNTIME DESTINATION ${USDSBSAR_DESTINATION} COMPONENT Runtime + LIBRARY DESTINATION ${USDSBSAR_DESTINATION} COMPONENT Runtime + RESOURCE DESTINATION ${USDSBSAR_DESTINATION}/${PLUGIN_NAME}/resources COMPONENT Runtime PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} COMPONENT Devel) # Install the master plugInfo.json file for the install directory Note that this @@ -172,7 +177,7 @@ if(USDSBSAR_ENABLE_INSTALL) # /resources/plugInfo.json install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/plugInfo.root.json - DESTINATION plugin/usd + DESTINATION ${USDSBSAR_DESTINATION} RENAME plugInfo.json COMPONENT Runtime) endif() diff --git a/sbsar/src/api.h b/sbsar/src/api.h index 67b647da..3c85f5b0 100644 --- a/sbsar/src/api.h +++ b/sbsar/src/api.h @@ -22,16 +22,12 @@ governing permissions and limitations under the License. #else #if defined(USDSBSAR_EXPORTS) #define USDSBSAR_API ARCH_EXPORT -#define USDSBSAR_API_TEMPLATE_CLASS(...) \ - ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) -#define USDSBSAR_API_TEMPLATE_STRUCT(...) \ - ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#define USDSBSAR_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define USDSBSAR_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) #else #define USDSBSAR_API ARCH_IMPORT -#define USDSBSAR_API_TEMPLATE_CLASS(...) \ - ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) -#define USDSBSAR_API_TEMPLATE_STRUCT(...) \ - ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#define USDSBSAR_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define USDSBSAR_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) #endif #define USDSBSAR_LOCAL ARCH_HIDDEN #endif diff --git a/sbsar/src/assetPath/assetPathParser.cpp b/sbsar/src/assetPath/assetPathParser.cpp index 9c97e9b9..37bf08b1 100644 --- a/sbsar/src/assetPath/assetPathParser.cpp +++ b/sbsar/src/assetPath/assetPathParser.cpp @@ -72,7 +72,7 @@ parsePath(const std::string& packagedPath, ParsePathResult& output) // TF_STATUS("Trimmed Path: %s", trimmedPath.c_str()); std::vector delimiter_split; splitByDelimiter(trimmedPath, '/', delimiter_split); - if (delimiter_split.size() != 3) { + if (delimiter_split.size() < 3) { TF_RUNTIME_ERROR("Path format error, invalid path count %lu: %s", delimiter_split.size(), trimmedPath.c_str()); @@ -82,10 +82,11 @@ parsePath(const std::string& packagedPath, ParsePathResult& output) TF_RUNTIME_ERROR("Path format error, only assets at /graphs supported"); return ParsePathResult::PE_INVALID_FORMAT; } - output.graphName = delimiter_split[1]; + // The last component should be "images?..." + // Everything between "graphs" and "images" is the graph name, which may include subfolders std::vector parameter_string_split; - splitByDelimiter(delimiter_split[2], '?', parameter_string_split); + splitByDelimiter(delimiter_split.back(), '?', parameter_string_split); if (parameter_string_split.size() != 2) { TF_RUNTIME_ERROR("Path format error, only a single ? support %zu", parameter_string_split.size()); @@ -96,6 +97,13 @@ parsePath(const std::string& packagedPath, ParsePathResult& output) return ParsePathResult::PE_INVALID_ASSET_TYPE; } + // Reconstruct graph name from middle components (may include subfolders) + // e.g., "graphs/folder/subfolder/graphname/images" -> "folder/subfolder/graphname" + output.graphName = delimiter_split[1]; + for (size_t i = 2; i < delimiter_split.size() - 1; ++i) { + output.graphName += "/" + delimiter_split[i]; + } + std::vector parameter_split; splitByDelimiter(parameter_string_split[1], '#', parameter_split); output.inputParameters = ""; diff --git a/sbsar/src/assetPath/assetPathParser.h b/sbsar/src/assetPath/assetPathParser.h index cfac0c38..55c5e4c6 100644 --- a/sbsar/src/assetPath/assetPathParser.h +++ b/sbsar/src/assetPath/assetPathParser.h @@ -55,17 +55,17 @@ USDSBSAR_API ParsePathResult::ParseError parsePath(const std::string& packagedPath, ParsePathResult& output); // generate a path from a parsed path -ParsePathResult::ParseError +USDSBSAR_API ParsePathResult::ParseError generatePath(const ParsePathResult& parsedResult, std::string& output); //! Helper to read JSValue -bool +USDSBSAR_API bool getAsFloat(const PXR_NS::JsValue& v, float& res); -bool +USDSBSAR_API bool getAsInt(const PXR_NS::JsValue& v, int& res); -bool +USDSBSAR_API bool getAsDoubleArray(const PXR_NS::JsValue& v, std::vector& res); -bool +USDSBSAR_API bool getAsIntArray(const PXR_NS::JsValue& v, std::vector& res); } diff --git a/sbsar/src/assetResolver/sbsarAsset.cpp b/sbsar/src/assetResolver/sbsarAsset.cpp index ad59dd41..905d48ff 100644 --- a/sbsar/src/assetResolver/sbsarAsset.cpp +++ b/sbsar/src/assetResolver/sbsarAsset.cpp @@ -11,81 +11,43 @@ governing permissions and limitations under the License. */ #include -#include #include PXR_NAMESPACE_USING_DIRECTIVE namespace adobe::usd::sbsar { -namespace { -uint32_t -_computePixelBufferSize(const SubstanceTexture& texture) -{ - size_t bytePerPixel = SbsarImage::getBytePerPixel(texture.pixelFormat); - return texture.level0Height * texture.level0Width * bytePerPixel; -} - -std::shared_ptr -copyBuffer(const SubstanceAir::RenderResultImage& img) -{ - auto tex = img.getTexture(); - size_t data_size = _computePixelBufferSize(tex); - size_t buffer_size = sizeof(SbsarAsset::AssetHeader) + data_size; - auto buffer = std::shared_ptr(new char[buffer_size], std::default_delete()); - auto* header = reinterpret_cast(buffer.get()); - char* data = buffer.get() + sizeof(SbsarAsset::AssetHeader); - header->level0Width = tex.level0Width; - header->level0Height = tex.level0Height; - header->pixelFormat = tex.pixelFormat; - header->channelsOrder = Substance_ChanOrder_RGBA; - header->mipmapCount = tex.mipmapCount; - - memcpy(data, tex.buffer, data_size); - return buffer; -} -} - -SbsarAsset::SbsarAsset(const std::shared_ptr& renderResultImage) - : mRenderResultImage(renderResultImage) -{ - size_t data_size = _computePixelBufferSize(mRenderResultImage->getTexture()); - mBufferSize = sizeof(SbsarAsset::AssetHeader) + data_size; -} - -const SubstanceTexture& -SbsarAsset::getSubstanceTexture() const -{ - return mRenderResultImage->getTexture(); -} +SbsarAsset::SbsarAsset(const std::string& packagePath, const std::string& packagedPathNoExt) + : mPackagePath(packagePath) + , mPackagedPathNoExt(packagedPathNoExt) +{} size_t SbsarAsset::GetSize() const { - return mBufferSize; + TF_CODING_ERROR("SbsarAsset::GetSize not implemented"); + return 0; } std::shared_ptr SbsarAsset::GetBuffer() const { - if (!mBuffer) { - mBuffer = copyBuffer(*mRenderResultImage); - } - return mBuffer; + TF_CODING_ERROR("SbsarAsset::GetBuffer not implemented"); + return nullptr; } std::pair SbsarAsset::GetFileUnsafe() const { - TF_RUNTIME_ERROR("SbsarAsset::GetFileUnsafe not implemented"); + TF_CODING_ERROR("SbsarAsset::GetFileUnsafe not implemented"); return { nullptr, 0 }; } size_t SbsarAsset::Read(void* buffer, size_t count, size_t offset) const { - TF_RUNTIME_ERROR("SbsarAsset::Read not implemented"); + TF_CODING_ERROR("SbsarAsset::Read not implemented"); return 0; } diff --git a/sbsar/src/assetResolver/sbsarAsset.h b/sbsar/src/assetResolver/sbsarAsset.h index fcecbd34..9d5984e4 100644 --- a/sbsar/src/assetResolver/sbsarAsset.h +++ b/sbsar/src/assetResolver/sbsarAsset.h @@ -10,42 +10,29 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once +#include #include -#include +#include +#include namespace adobe::usd::sbsar { -//! Asset representing a substance texture. -//! If GetBuffer() is called, the buffer will be copied from the RenderResultImage. -class SbsarAsset final : public PXR_NS::ArAsset -{ - public: - struct AssetHeader - { - unsigned short level0Width; - unsigned short level0Height; - unsigned char pixelFormat; - unsigned char channelsOrder; - unsigned char mipmapCount; - }; - explicit SbsarAsset(const std::shared_ptr& renderResultImage); +//! Asset representing the parameters to render a sbsar texture. +class USDSBSAR_API SbsarAsset final : public PXR_NS::ArAsset +{ +public: + explicit SbsarAsset(const std::string& packagePath, const std::string& packagedPathNoExt); - const SubstanceTexture& getSubstanceTexture() const; + const std::string& GetPackagePath() const { return mPackagePath; } + const std::string& GetPackagedPathNoExt() const { return mPackagedPathNoExt; } size_t GetSize() const override; - //! This function makes a copy of the buffer from the RenderResultImage. - //! Prefere use getSubstanceTexture() to access to the texture data. std::shared_ptr GetBuffer() const override; size_t Read(void* buffer, size_t count, size_t offset) const override; std::pair GetFileUnsafe() const override; - private: - std::shared_ptr mRenderResultImage; - //! Buffer containing the header + image data in a continuous buffer. - //! It is mutable because in GetBuffer(), the first call will copy the buffer in - //! mRenderResultImage to mBuffer. - mutable std::shared_ptr mBuffer; - //! Buffer size in bytes - size_t mBufferSize; +private: + std::string mPackagePath; + std::string mPackagedPathNoExt; }; } diff --git a/sbsar/src/assetResolver/sbsarImage.cpp b/sbsar/src/assetResolver/sbsarImage.cpp index f4e01d33..0ae72012 100644 --- a/sbsar/src/assetResolver/sbsarImage.cpp +++ b/sbsar/src/assetResolver/sbsarImage.cpp @@ -152,8 +152,7 @@ SbsarImage::getBytePerPixel(unsigned char pixelFormat) SbsarImage::SbsarImage() : mFilename() -{ -} +{} SbsarImage::~SbsarImage() {} @@ -166,13 +165,13 @@ SbsarImage::GetFilename() const int SbsarImage::GetWidth() const { - return mSbsarAsset->getSubstanceTexture().level0Width; + return mRenderResultImage->getTexture().level0Width; } int SbsarImage::GetHeight() const { - return mSbsarAsset->getSubstanceTexture().level0Height; + return mRenderResultImage->getTexture().level0Height; } PXR_NS::HioFormat @@ -324,6 +323,10 @@ SbsarImage::_OpenForReading(const std::string& filename, // Store the file name mFilename = filename; + // Render the textures + mRenderResultImage = adobe::usd::sbsar::renderSbsarAsset(mSbsarAsset->GetPackagePath(), + mSbsarAsset->GetPackagedPathNoExt()); + unsigned char pixelFormat = _GetPixelFormat(); const bool isSRGB = [&]() -> bool { switch (sourceColorSpace) { @@ -356,13 +359,13 @@ SbsarImage::_OpenForWriting(const std::string& /*filename*/) const char* SbsarImage::_GetBuffer() const { - return reinterpret_cast(mSbsarAsset->getSubstanceTexture().buffer); + return reinterpret_cast(mRenderResultImage->getTexture().buffer); } unsigned char SbsarImage::_GetPixelFormat() const { - return mSbsarAsset->getSubstanceTexture().pixelFormat; + return mRenderResultImage->getTexture().pixelFormat; } TF_REGISTRY_FUNCTION(TfType) diff --git a/sbsar/src/assetResolver/sbsarImage.h b/sbsar/src/assetResolver/sbsarImage.h index b1579c4c..d5b28058 100644 --- a/sbsar/src/assetResolver/sbsarImage.h +++ b/sbsar/src/assetResolver/sbsarImage.h @@ -16,10 +16,13 @@ governing permissions and limitations under the License. #include #include +#include #include #include +#include + PXR_NAMESPACE_OPEN_SCOPE class ArAsset; PXR_NAMESPACE_CLOSE_SCOPE @@ -34,7 +37,7 @@ PXR_NAMESPACE_CLOSE_SCOPE class SbsarImage final : public PXR_NS::HioImage { - public: +public: static uint32_t getBytePerPixel(unsigned char pixelFormat); using Base = HioImage; @@ -62,7 +65,7 @@ class SbsarImage final : public PXR_NS::HioImage const StorageSpec& storage) override; bool Write(const StorageSpec& storage, const PXR_NS::VtDictionary& metadata) override; - protected: +protected: virtual bool _OpenForReading(std::string const& filename, int subimage, int mip, @@ -71,13 +74,15 @@ class SbsarImage final : public PXR_NS::HioImage virtual bool _OpenForWriting(std::string const& filename) override; //! @} HioImage overrides - private: +private: const char* _GetBuffer() const; unsigned char _GetPixelFormat() const; std::string mFilename; bool mIsColorSpaceSRGB; - std::shared_ptr mSbsarAsset; PXR_NS::HioFormat mFormat; int mBytePerPixel; + + std::shared_ptr mSbsarAsset; + std::shared_ptr mRenderResultImage; }; diff --git a/sbsar/src/assetResolver/sbsarPackageResolver.cpp b/sbsar/src/assetResolver/sbsarPackageResolver.cpp index 3de6943c..f00eaf72 100644 --- a/sbsar/src/assetResolver/sbsarPackageResolver.cpp +++ b/sbsar/src/assetResolver/sbsarPackageResolver.cpp @@ -12,8 +12,10 @@ governing permissions and limitations under the License. #include "sbsarPackageResolver.h" #include "sbsarDebug.h" #include +#include #include #include +#include #include #include #include @@ -141,9 +143,8 @@ SBSARPackageResolver::OpenSbsarAsset(const std::string& packagePath, .Msg("Opening sbsar asset %s %s\n", packagePath.c_str(), fixedPackagedPath.c_str()); std::string packagedPath_no_ext = fixedPackagedPath.substr(0, fixedPackagedPath.size() - 11); - std::string cache_path = packagePath + packagedPath_no_ext; - return renderSbsarAsset(packagePath, packagedPath_no_ext); + return std::make_shared(packagePath, packagedPath_no_ext); } std::shared_ptr diff --git a/sbsar/src/assetResolver/sbsarPackageResolver.h b/sbsar/src/assetResolver/sbsarPackageResolver.h index 5e2d37a7..5435c110 100644 --- a/sbsar/src/assetResolver/sbsarPackageResolver.h +++ b/sbsar/src/assetResolver/sbsarPackageResolver.h @@ -25,7 +25,7 @@ class ArAsset; //! the interface to read the underlying texture. class SBSARPackageResolver : public PXR_NS::ArPackageResolver { - public: +public: SBSARPackageResolver(); virtual ~SBSARPackageResolver(); diff --git a/sbsar/src/assetResolver/sbsarResolverCache.cpp b/sbsar/src/assetResolver/sbsarResolverCache.cpp index 3b4b6c94..2b88d318 100644 --- a/sbsar/src/assetResolver/sbsarResolverCache.cpp +++ b/sbsar/src/assetResolver/sbsarResolverCache.cpp @@ -27,8 +27,7 @@ SBSARResolverCache::SBSARResolverCache() = default; struct SBSARResolverCache::_Cache { - using _Map = - tbb::concurrent_hash_map>; + using _Map = tbb::concurrent_hash_map>; _Map _pathToEntryMap; }; @@ -70,8 +69,7 @@ SBSARResolverCache::findCachedAsset(const std::string& path) } void -SBSARResolverCache::addCachedAsset(std::string& path, - std::shared_ptr& asset) +SBSARResolverCache::addCachedAsset(std::string& path, std::shared_ptr& asset) { _CachePtr currentCache = _GetCurrentCache(); if (currentCache) { @@ -89,7 +87,6 @@ void SBSARResolverCache::dumpStats() { _CachePtr currentCache = _GetCurrentCache(); - if (currentCache) { - } + if (currentCache) {} } } diff --git a/sbsar/src/assetResolver/sbsarResolverCache.h b/sbsar/src/assetResolver/sbsarResolverCache.h index 3d77df5e..b9a6e7e5 100644 --- a/sbsar/src/assetResolver/sbsarResolverCache.h +++ b/sbsar/src/assetResolver/sbsarResolverCache.h @@ -20,7 +20,7 @@ PXR_NAMESPACE_CLOSE_SCOPE namespace adobe::usd::sbsar { class SBSARResolverCache { - public: +public: static SBSARResolverCache& GetInstance(); SBSARResolverCache(const SBSARResolverCache&) = delete; @@ -28,11 +28,10 @@ class SBSARResolverCache void BeginCacheScope(PXR_NS::VtValue* cacheScopeData); void EndCacheScope(PXR_NS::VtValue* cacheScopeData); std::shared_ptr findCachedAsset(const std::string& path); - void addCachedAsset(std::string& path, - std::shared_ptr& asset); + void addCachedAsset(std::string& path, std::shared_ptr& asset); void dumpStats(); - private: +private: SBSARResolverCache(); struct _Cache; diff --git a/sbsar/src/config/sbsarConfig.cpp b/sbsar/src/config/sbsarConfig.cpp index dc1fd2cd..1d424b31 100644 --- a/sbsar/src/config/sbsarConfig.cpp +++ b/sbsar/src/config/sbsarConfig.cpp @@ -64,16 +64,17 @@ SbsarConfig::~SbsarConfig() = default; void SbsarConfig::init() { - m_assetCacheSize = 1'000'000'000; - m_inputImageCacheSize = 1'000'000'000; - m_packageCacheSize = 10; + m_assetCacheSize = 2'000'000'000; // 2 GB - increased for high-res rendering support + m_inputImageCacheSize = 1'000'000'000; // 1 GB + m_packageCacheSize = 10; // 10 packages } void SbsarConfig::setAssetCacheSize(std::size_t size) { if (size == 0) { - TF_STATUS("SbsarConfig: Asset cache size is 0, which means the cache is unlimited and never cleared!"); + 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/config/sbsarConfig.h b/sbsar/src/config/sbsarConfig.h index 5f1f90ff..059479eb 100644 --- a/sbsar/src/config/sbsarConfig.h +++ b/sbsar/src/config/sbsarConfig.h @@ -31,7 +31,7 @@ class SbsarConfig : public TfRefBase , public TfWeakBase { - public: +public: SbsarConfig(); virtual ~SbsarConfig(); @@ -46,7 +46,7 @@ class SbsarConfig USDSBSAR_API std::size_t getInputImageCacheSize() const; USDSBSAR_API std::size_t getPackageCacheSize() const; - private: +private: std::atomic m_assetCacheSize; //! In bytes std::atomic m_inputImageCacheSize; //! In bytes std::atomic m_packageCacheSize; //! Max number of packages diff --git a/sbsar/src/config/sbsarConfigFactory.h b/sbsar/src/config/sbsarConfigFactory.h index 9a4f0c5f..42ba7264 100644 --- a/sbsar/src/config/sbsarConfigFactory.h +++ b/sbsar/src/config/sbsarConfigFactory.h @@ -24,7 +24,7 @@ PXR_NAMESPACE_OPEN_SCOPE class USDSBSAR_API SbsarConfigFactory : public TfType::FactoryBase { - public: +public: virtual ~SbsarConfigFactory(); SbsarConfigRefPtr New() { return TfCreateRefPtr(new SbsarConfig); } }; diff --git a/sbsar/src/config/sbsarConfigRegistry.h b/sbsar/src/config/sbsarConfigRegistry.h index 531633bf..90f42de4 100644 --- a/sbsar/src/config/sbsarConfigRegistry.h +++ b/sbsar/src/config/sbsarConfigRegistry.h @@ -28,11 +28,11 @@ class USDSBSAR_API SbsarConfigRegistry SbsarConfigRegistry(const SbsarConfigRegistry&) = delete; SbsarConfigRegistry& operator=(const SbsarConfigRegistry&) = delete; - public: +public: SbsarConfigRegistry(); SbsarConfigRefPtr getSbsarConfig(); - private: +private: SbsarConfigRefPtr m_sbsarConfig; }; diff --git a/sbsar/src/plugInfo.json.in b/sbsar/src/plugInfo.json.in index 401e05cd..2a3de84a 100644 --- a/sbsar/src/plugInfo.json.in +++ b/sbsar/src/plugInfo.json.in @@ -37,6 +37,12 @@ "displayGroup": "Core", "documentation:": "Whether to write MaterialX shader", "type": "bool" + }, + "preserveExtraMaterialInfo": { + "appliesTo": [ "prims" ], + "displayGroup": "Core", + "documentation:": "Whether to include extra inputs for transcoding", + "type": "bool" } }, "Types": { @@ -72,7 +78,8 @@ }, "SbsarImage" : { "bases": ["HioImage"], - "imageTypes": ["sbsarimage"] + "imageTypes": ["sbsarimage"], + "precedence": 1 }, "SbsarConfig": { "assetCacheSize": ${USDSBSAR_CACHE_SIZE}, diff --git a/sbsar/src/sbsarEngine/sbsarAssetCache.cpp b/sbsar/src/sbsarEngine/sbsarAssetCache.cpp index 66ff33b1..b3b008b3 100644 --- a/sbsar/src/sbsarEngine/sbsarAssetCache.cpp +++ b/sbsar/src/sbsarEngine/sbsarAssetCache.cpp @@ -25,7 +25,16 @@ governing permissions and limitations under the License. PXR_NAMESPACE_USING_DIRECTIVE namespace adobe::usd::sbsar { + namespace { + +uint32_t +_computePixelBufferSize(const SubstanceTexture& texture) +{ + size_t bytePerPixel = SbsarImage::getBytePerPixel(texture.pixelFormat); + return texture.level0Height * texture.level0Width * bytePerPixel; +} + std::string computeKey(const adobe::usd::sbsar::ParsePathResult& pathResult) { @@ -34,8 +43,8 @@ computeKey(const adobe::usd::sbsar::ParsePathResult& pathResult) } } -std::shared_ptr -RenderResultCache::getAsset(const std::string& usage) +std::shared_ptr +RenderResultCache::getRenderResultImage(const std::string& usage) { auto it = m_assets.find(usage); if (it == m_assets.end()) { @@ -47,7 +56,9 @@ RenderResultCache::getAsset(const std::string& usage) } void -RenderResultCache::addAsset(const std::string& usage, const std::shared_ptr& asset) +RenderResultCache::addRenderResultImage( + const std::string& usage, + const std::shared_ptr& asset) { m_assets[usage] = asset; } @@ -80,7 +91,7 @@ RenderResultCache::computeSize() { m_size = 0; for (const auto& asset : m_assets) { - m_size += asset.second->GetSize(); + m_size += _computePixelBufferSize(asset.second->getTexture()); } } @@ -108,15 +119,15 @@ AssetCache::hasRenderResult(const adobe::usd::sbsar::ParsePathResult& pathResult return m_assets.find(hash) != m_assets.end(); } -std::shared_ptr -AssetCache::getAsset(const adobe::usd::sbsar::ParsePathResult& pathResult) +std::shared_ptr +AssetCache::getRenderResultImage(const adobe::usd::sbsar::ParsePathResult& pathResult) { std::string hash = computeKey(pathResult); auto asset = m_assets.find(hash); if (asset == m_assets.end()) return nullptr; asset->second.updateLastAccessTime(); - return asset->second.getAsset(pathResult.usage); + return asset->second.getRenderResultImage(pathResult.usage); } VtValue @@ -137,7 +148,8 @@ 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 (getSbsarConfig()->getAssetCacheSize() > 0 && (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/sbsarAssetCache.h b/sbsar/src/sbsarEngine/sbsarAssetCache.h index 26697185..e663a813 100644 --- a/sbsar/src/sbsarEngine/sbsarAssetCache.h +++ b/sbsar/src/sbsarEngine/sbsarAssetCache.h @@ -13,8 +13,6 @@ governing permissions and limitations under the License. #pragma once #include -#include - #include #include #include @@ -31,13 +29,14 @@ struct ParsePathResult; struct CacheStats; //! \brief class to store a full render result for a specific graph and parameters. -class RenderResultCache +class USDSBSAR_API RenderResultCache { - public: +public: void updateLastAccessTime(); std::chrono::time_point getLastAccessTime() const; - std::shared_ptr getAsset(const std::string& usage); - void addAsset(const std::string& usage, const std::shared_ptr& asset); + std::shared_ptr getRenderResultImage(const std::string& usage); + void addRenderResultImage(const std::string& usage, + const std::shared_ptr& asset); PXR_NS::VtValue getNumericalValue(const std::string& usage); void addNumericalValue(const std::string& usage, const PXR_NS::VtValue& value); @@ -45,9 +44,9 @@ class RenderResultCache void computeSize(); std::size_t getAssetCount(); - private: +private: //! Key : usage of the asset - std::unordered_map> m_assets; + std::unordered_map> m_assets; //! Key : usage of the value std::unordered_map m_numericalValues; //! Time of creation of the assets or the last time it was used. @@ -61,16 +60,17 @@ class RenderResultCache //! The cache size is controled by CacheSize. When the cache is full, 10% of the oldest render //! result are erased. //! @see RenderResultCache, CacheSize -class AssetCache +class USDSBSAR_API AssetCache { - public: +public: AssetCache() = default; ~AssetCache() = default; //! Check is a render result for a combo graph + parameters exist in the cache. bool hasRenderResult(const ParsePathResult& pathResult); //! Return corresponding asset if it exist in the cache, return nullptr otherwise. //! Update time creation of the corresponding render result. - std::shared_ptr getAsset(const ParsePathResult& pathResult); + std::shared_ptr getRenderResultImage( + const ParsePathResult& pathResult); //! Return corresponding asset if it exist in the cache, return nullptr otherwise. //! Update time creation of the corresponding render result. PXR_NS::VtValue getNumericalValue(const ParsePathResult& pathResult); @@ -80,7 +80,7 @@ class AssetCache //! Erase all the cache. void clearCache(); - private: +private: //! Erase 10% of the cache. void cleanCache(); //! Key: Package hash + graph name + input parameters. diff --git a/sbsar/src/sbsarEngine/sbsarEngine.cpp b/sbsar/src/sbsarEngine/sbsarEngine.cpp index ac88edb5..a79bf171 100644 --- a/sbsar/src/sbsarEngine/sbsarEngine.cpp +++ b/sbsar/src/sbsarEngine/sbsarEngine.cpp @@ -23,12 +23,12 @@ governing permissions and limitations under the License. #ifdef _WIN32 // Windows engine selection code -# include -# include +#include +#include EXTERN_C IMAGE_DOS_HEADER __ImageBase; #else // _WIN32 // Linux and mac -# include +#include #endif // _WIN32 #ifdef _WIN32 diff --git a/sbsar/src/sbsarEngine/sbsarInputImageCache.cpp b/sbsar/src/sbsarEngine/sbsarInputImageCache.cpp index de185465..cc9f32a3 100644 --- a/sbsar/src/sbsarEngine/sbsarInputImageCache.cpp +++ b/sbsar/src/sbsarEngine/sbsarInputImageCache.cpp @@ -37,8 +37,7 @@ struct InputImageCacheData std::size_t size; explicit InputImageCacheData() : lastAccessTime(std::chrono::steady_clock::now()) - { - } + {} void updateLastAccessTime() { lastAccessTime = std::chrono::steady_clock::now(); } }; diff --git a/sbsar/src/sbsarEngine/sbsarPackageCache.cpp b/sbsar/src/sbsarEngine/sbsarPackageCache.cpp index c667e600..12e84e29 100644 --- a/sbsar/src/sbsarEngine/sbsarPackageCache.cpp +++ b/sbsar/src/sbsarEngine/sbsarPackageCache.cpp @@ -169,8 +169,7 @@ struct PackageCacheData explicit PackageCacheData() : lastAccessTime(std::chrono::steady_clock::now()) - { - } + {} void updateLastAccessTime() { lastAccessTime = std::chrono::steady_clock::now(); } }; @@ -268,8 +267,7 @@ GraphInstanceData::GraphInstanceData(std::shared_ptr : m_package(package) , m_instance(graphDesc) , m_lastInputParameters(inputParameters) -{ -} +{} SubstanceAir::GraphInstance& GraphInstanceData::getGraphInstance() diff --git a/sbsar/src/sbsarEngine/sbsarPackageCache.h b/sbsar/src/sbsarEngine/sbsarPackageCache.h index 1f5120ed..d2b71ed3 100644 --- a/sbsar/src/sbsarEngine/sbsarPackageCache.h +++ b/sbsar/src/sbsarEngine/sbsarPackageCache.h @@ -31,7 +31,7 @@ namespace adobe::usd::sbsar { //! package is removed. Since the returned shared_ptr will presist even when the cache is cleared. //! \param resolvedPackagePath The complete path to the package the should be opened //! \param outContentHash If valid, will be used to return the hash of the package -std::shared_ptr +USDSBSAR_API std::shared_ptr getSbsarFromPackageCache(const std::string& resolvedPackagePath, size_t* outContentHash = nullptr); using ParameterListPtr = std::shared_ptr>; @@ -40,7 +40,7 @@ using ParameterListPtr = std::shared_ptr package, const SubstanceAir::GraphDesc& graphDesc, const std::string& inputParameters); @@ -60,7 +60,7 @@ class GraphInstanceData const std::string& getLastInputParameters() const; void setLastInputParameters(const std::string& inputParameters); - private: +private: // Keep a reference to the package to avoid it being deleted while the graph instance is used. std::shared_ptr m_package; SubstanceAir::GraphInstance m_instance; @@ -74,11 +74,11 @@ class GraphInstanceData //! when the cache is cleared. //! \param resolvedPackagePath The complete path to the package the should be opened. //! \param sbsarParameters Graph name and other sbsar's input parameters. -std::shared_ptr +USDSBSAR_API std::shared_ptr getGraphInstanceFromPackageCache(const std::string& resolvedPackagePath, const ParsePathResult& sbsarParameters); //! \brief Find a graph in the package with a given name -const SubstanceAir::GraphDesc* +USDSBSAR_API const SubstanceAir::GraphDesc* findSelectedGraph(const std::string& graphName, const SubstanceAir::Graphs& graphs); } diff --git a/sbsar/src/sbsarEngine/sbsarRender.cpp b/sbsar/src/sbsarEngine/sbsarRender.cpp index a51e22ad..42629624 100644 --- a/sbsar/src/sbsarEngine/sbsarRender.cpp +++ b/sbsar/src/sbsarEngine/sbsarRender.cpp @@ -10,7 +10,6 @@ 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 @@ -61,7 +60,9 @@ applyParameterValue(InputInstanceBase* i, SubstanceIOType type, const JsValue& v std::vector a; getAsDoubleArray(v, a); if (a.size() != 2) { - TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Float2', incorrect data size, the size is {}", a.size()); + TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Float2', incorrect data " + "size, the size is {}", + a.size()); return false; } f->setValue(Vec2Float(static_cast(a[0]), static_cast(a[1]))); @@ -76,7 +77,9 @@ applyParameterValue(InputInstanceBase* i, SubstanceIOType type, const JsValue& v std::vector a; getAsDoubleArray(v, a); if (a.size() != 3) { - TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Float3', incorrect data size, the size is {}", a.size()); + TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Float3', incorrect data " + "size, the size is {}", + a.size()); return false; } f->setValue(Vec3Float( @@ -92,7 +95,9 @@ applyParameterValue(InputInstanceBase* i, SubstanceIOType type, const JsValue& v std::vector a; getAsDoubleArray(v, a); if (a.size() != 4) { - TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Float4', incorrect data size, the size is {}", a.size()); + TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Float4', incorrect data " + "size, the size is {}", + a.size()); return false; } f->setValue(Vec4Float(static_cast(a[0]), @@ -124,7 +129,9 @@ applyParameterValue(InputInstanceBase* i, SubstanceIOType type, const JsValue& v std::vector a; getAsIntArray(v, a); if (a.size() != 2) { - TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Integer2', incorrect data size, the size is {}", a.size()); + TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Integer2', incorrect data " + "size, the size is {}", + a.size()); return false; } ii->setValue(Vec2Int(a[0], a[1])); @@ -139,7 +146,9 @@ applyParameterValue(InputInstanceBase* i, SubstanceIOType type, const JsValue& v std::vector a; getAsIntArray(v, a); if (a.size() != 3) { - TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Integer3', incorrect data size, the size is {}", a.size()); + TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Integer3', incorrect data " + "size, the size is {}", + a.size()); return false; } ii->setValue(Vec3Int(a[0], a[1], a[2])); @@ -154,7 +163,9 @@ applyParameterValue(InputInstanceBase* i, SubstanceIOType type, const JsValue& v std::vector a; getAsIntArray(v, a); if (a.size() != 4) { - TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Integer4', incorrect data size, the size is {}", a.size()); + TF_RUNTIME_ERROR("SbsarRender: cast 'Substance_IOType_Integer4', incorrect data " + "size, the size is {}", + a.size()); return false; } ii->setValue(Vec4Int(a[0], a[1], a[2], a[3])); @@ -336,8 +347,8 @@ renderGraph(Renderer& renderer, for (const SubstanceAir::string& usage : o->mDesc.mChannelsStr) { lastSbsarParameters.usage = usage; - if (auto previousAsset = assetCache.getAsset(lastSbsarParameters)) - renderResult.addAsset(usage.c_str(), previousAsset); + if (auto previousAsset = assetCache.getRenderResultImage(lastSbsarParameters)) + renderResult.addRenderResultImage(usage.c_str(), previousAsset); else { VtValue previousValue = assetCache.getNumericalValue(lastSbsarParameters); if (!previousValue.IsEmpty()) @@ -358,10 +369,10 @@ renderGraph(Renderer& renderer, std::shared_ptr renderResultImage( dynamic_cast(res.release()), SubstanceAir::deleter()); - std::shared_ptr asset = std::make_shared(renderResultImage); + TF_AXIOM(renderResultImage); for (const SubstanceAir::string& usage : o->mDesc.mChannelsStr) { - renderResult.addAsset(usage.c_str(), asset); + renderResult.addRenderResultImage(usage.c_str(), renderResultImage); } } } diff --git a/sbsar/src/sbsarEngine/sbsarRender.h b/sbsar/src/sbsarEngine/sbsarRender.h index e49fd32f..cadd8f13 100644 --- a/sbsar/src/sbsarEngine/sbsarRender.h +++ b/sbsar/src/sbsarEngine/sbsarRender.h @@ -21,7 +21,7 @@ namespace adobe::usd::sbsar { //! \param instanceData Graph instance to renderer. //! \param sbsarParameters Input parameters that will be set to the graph instance. //! \param assetCache Cache where all the render's result are stored. -void +void USDSBSAR_API renderGraph(SubstanceAir::Renderer& renderer, GraphInstanceData& instanceData, const ParsePathResult& sbsarParameters, diff --git a/sbsar/src/sbsarEngine/sbsarRenderThread.cpp b/sbsar/src/sbsarEngine/sbsarRenderThread.cpp index 8a856673..ba703e65 100644 --- a/sbsar/src/sbsarEngine/sbsarRenderThread.cpp +++ b/sbsar/src/sbsarEngine/sbsarRenderThread.cpp @@ -62,7 +62,7 @@ std::unique_ptr g_state; RenderThreadState* getRenderThreadState() { - if (!g_state) { + { std::lock_guard _l(renderInitMutex); if (!g_state) { // Jumping through some hoops because of some kind of overridden @@ -154,7 +154,7 @@ template bool resultIsValid(const T& elem) { - if constexpr (std::is_same_v>) + if constexpr (std::is_same_v>) return elem != nullptr; else if constexpr (std::is_same_v) return !elem.IsEmpty(); @@ -165,8 +165,8 @@ template T findResultInCache(const ParsePathResult& parseOutput, RenderThreadState* state) { - if constexpr (std::is_same_v>) - return state->assetCache.getAsset(parseOutput); + if constexpr (std::is_same_v>) + return state->assetCache.getRenderResultImage(parseOutput); if constexpr (std::is_same_v) return state->assetCache.getNumericalValue(parseOutput); } @@ -176,10 +176,11 @@ template bool resultExistInTheOtherCache(const ParsePathResult& parseOutput, RenderThreadState* state) { - if constexpr (std::is_same_v>) + if constexpr (std::is_same_v>) return resultIsValid(state->assetCache.getNumericalValue(parseOutput)); if constexpr (std::is_same_v) - return resultIsValid>(state->assetCache.getAsset(parseOutput)); + return resultIsValid>( + state->assetCache.getRenderResultImage(parseOutput)); } //! Ask to the cache if the asset or a value is already exist for the given paths, if not request a @@ -243,10 +244,11 @@ requestRender(const std::string& packagePath, const std::string& packagedPath) } } -std::shared_ptr +std::shared_ptr renderSbsarAsset(const std::string& packagePath, const std::string& packagedPath) { - return requestRender>(packagePath, packagedPath); + return requestRender>(packagePath, + packagedPath); } VtValue @@ -296,7 +298,6 @@ RenderThreadState::RenderThreadState() RenderThreadState::~RenderThreadState() { TF_DEBUG(SBSAR_RENDER).Msg("SbsarRenderThread: Releasing\n"); - RenderThreadState* s = getRenderThreadState(); std::unique_lock guard(lock); shutDown = true; guard.unlock(); diff --git a/sbsar/src/sbsarEngine/sbsarRenderThread.h b/sbsar/src/sbsarEngine/sbsarRenderThread.h index 60555631..e36fe716 100644 --- a/sbsar/src/sbsarEngine/sbsarRenderThread.h +++ b/sbsar/src/sbsarEngine/sbsarRenderThread.h @@ -30,7 +30,7 @@ namespace adobe::usd::sbsar { //! \param packagedPath A complexe string generate by generateSbsarInfoPath() that //! containt all information to run a rendering with the substance engine. //! \see generateSbsarInfoPath() -USDSBSAR_API std::shared_ptr +USDSBSAR_API std::shared_ptr renderSbsarAsset(const std::string& packagePath, const std::string& packagedPath); //! \brief Resolve a request coming from the USD asset system: render a sbsar output value with the diff --git a/sbsar/src/sbsarfileformat.cpp b/sbsar/src/sbsarfileformat.cpp index 6d167dd2..085b2f80 100644 --- a/sbsar/src/sbsarfileformat.cpp +++ b/sbsar/src/sbsarfileformat.cpp @@ -23,11 +23,11 @@ governing permissions and limitations under the License. #include #include +#include #include #include #include #include -#include #include #include @@ -55,8 +55,7 @@ SBSARFileFormat::SBSARFileFormat() SBSARFileFormatTokens->Version, // versionString SBSARFileFormatTokens->Target, // target SBSARFileFormatTokens->Extension) // extension -{ -} +{} SBSARFileFormat::~SBSARFileFormat() = default; @@ -91,6 +90,62 @@ stringsMatchIgnoreCase(const std::string& packageName, const std::string& graphN [](unsigned char l, unsigned char r) { return std::tolower(l) == std::tolower(r); }); } +//! Validate graph type filter and check if package contains matching graphs. +//! Returns the expected GraphType and whether to continue processing. +std::pair +validateGraphTypeFilter(const std::string& graphTypeFilter, + const SubstanceAir::PackageDesc* packageDesc, + const std::string& packageName) +{ + GraphType expectedType = GraphType::Unknown; + + // Parse and validate the filter value + if (!graphTypeFilter.empty()) { + if (graphTypeFilter == "material") { + expectedType = GraphType::Material; + } else if (graphTypeFilter == "light") { + expectedType = GraphType::Light; + } else { + TF_WARN( + "SBSAR: Invalid graphTypeFilter value '%s'. Valid values are 'material' or 'light'.", + graphTypeFilter.c_str()); + } + } + + // If no valid filter, continue processing + if (expectedType == GraphType::Unknown) { + return { expectedType, true }; + } + + // Pre-scan graphs to validate against filter + std::vector foundTypes; + bool hasMatchingGraph = false; + + for (const SubstanceAir::GraphDesc& graphDesc : packageDesc->getGraphs()) { + GraphType graphType = guessGraphType(graphDesc); + foundTypes.push_back(graphType); + if (graphType == expectedType) { + hasMatchingGraph = true; + } + } + + if (!hasMatchingGraph) { + // Generate helpful error message + std::string foundTypesStr = describeGraphTypes(foundTypes); + std::string expectedTypeStr = graphTypeToString(expectedType); + + TF_RUNTIME_ERROR("SBSAR package '%s' does not contain any %s graphs. " + "Package contains: %s. " + "This SBSAR file cannot be used in this context.", + packageName.c_str(), + expectedTypeStr.c_str(), + foundTypesStr.c_str()); + return { expectedType, false }; + } + + return { expectedType, true }; +} + bool SBSARFileFormat::CreateLayerData(const SdfAbstractDataRefPtr& sdfDataPtr, const std::string& resolvedPath, @@ -114,6 +169,13 @@ SBSARFileFormat::CreateLayerData(const SdfAbstractDataRefPtr& sdfDataPtr, } std::string packageName = TfStringGetBeforeSuffix(TfGetBaseName(resolvedPath)); + // Validate graph types if filter is set + auto [expectedType, shouldContinue] = + validateGraphTypeFilter(sbsarData.graphTypeFilter, packageDesc.get(), packageName); + if (!shouldContinue) { + return false; + } + // Create all prim SdfPath defaultPrimPath; // Create a class prim for the materials in the package @@ -125,6 +187,17 @@ SBSARFileFormat::CreateLayerData(const SdfAbstractDataRefPtr& sdfDataPtr, const MappedSymbol graphName = symbolMapper.GetSymbol(getGraphName(graphDesc)); GraphType graphType = guessGraphType(graphDesc); + + // Skip graphs that don't match filter + if (expectedType != GraphType::Unknown && graphType != expectedType) { + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("SBSARFileFormat: Skipping graph %s (type %s, expected %s)\n", + graphName.usdName.c_str(), + graphTypeToString(graphType).c_str(), + graphTypeToString(expectedType).c_str()); + continue; + } + SdfPath primPath; if (graphType == GraphType::Material) { @@ -187,10 +260,130 @@ parseFileFormatArguments(const SBSARFileFormat::FileFormatArguments& args) argReadBool(args, "writeUsdPreviewSurface", data.writeUsdPreviewSurface, "SBSAR"); argReadBool(args, "writeASM", data.writeASM, "SBSAR"); argReadBool(args, "writeOpenPBR", data.writeOpenPBR, "SBSAR"); + argReadBool(args, "preserveExtraMaterialInfo", data.preserveExtraMaterialInfo, "SBSAR"); + + // Parse graphTypeFilter + argReadString(args, "graphTypeFilter", data.graphTypeFilter, "SBSAR"); return data; } +/// Resolve an image asset path authored on a parameter of an SBSAR file. +/// +/// Resolution strategy: +/// 1. Use SdfAssetPath::GetResolvedPath() if available (USD-correct). +/// 2. Resolve package-relative paths if the SBSAR itself is packaged (USDZ). +/// 3. Accept absolute filesystem paths directly. +/// 4. Apply BEST-EFFORT filesystem heuristics: +/// - relative to the SBSAR file +/// - relative to current working directory (CWD) +/// +/// IMPORTANT: +/// Anchor-relative paths (./foo.png) cannot be resolved correctly without +/// the authoring layer context. +/// XXX : Heuristic fallbacks are NOT guaranteed +/// to be correct and should be removed once USD exposes the layer context. +/// +/// \return empty string on failure. +std::string +resolveSbsarImageInputAssetPath(const SdfAssetPath& imageAssetPath, + const std::string& resolvedSbsarPath, + const std::string& parameterName) +{ + std::string resolvedPath; + // 1. Prefer USD-resolved path if already available + if (!imageAssetPath.GetResolvedPath().empty()) { + resolvedPath = imageAssetPath.GetResolvedPath(); + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("SBSAR: Using pre-resolved image path: %s\n", resolvedPath.c_str()); + return resolvedPath; + } + const std::string& imagePath = imageAssetPath.GetAssetPath(); + // 2. Reject unsupported package-relative image paths + if (ArIsPackageRelativePath(imagePath)) { + TF_WARN("SBSAR: Image path '%s' for parameter '%s' is package-relative " + "and cannot be resolved without authoring layer context.", + imagePath.c_str(), + parameterName.c_str()); + return {}; + } + // 3. SBSAR inside a package (USDZ) + if (ArIsPackageRelativePath(resolvedSbsarPath)) { + const std::string& packageAnchor = ArSplitPackageRelativePathOuter(resolvedSbsarPath).first; + std::string imageIdentifier = ArJoinPackageRelativePath(packageAnchor, imagePath); + if (std::string resolved = ArGetResolver().Resolve(imageIdentifier); !resolved.empty()) { + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("SBSAR: Resolved image inside package: %s\n", resolved.c_str()); + return resolved; + } + TF_WARN("SBSAR: Failed to resolve image '%s' relative to packaged SBSAR '%s'", + imagePath.c_str(), + resolvedSbsarPath.c_str()); + return {}; + } + // 4. Absolute filesystem paths are accepted directly + try { + std::filesystem::path fsImagePath(imagePath); + if (fsImagePath.is_absolute()) { + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("SBSAR: Using absolute image path: %s\n", imagePath.c_str()); + return imagePath; + } + } catch (const std::exception& e) { + TF_WARN( + "SBSAR: Image path '%s' is not a valid filesystem path: %s", imagePath.c_str(), e.what()); + return {}; + } + // 5. HEURISTIC FALLBACKS (best-effort, NOT USD-correct) + // 5.1 Relative to SBSAR file location + { + std::filesystem::path sbsarPath(resolvedSbsarPath); + std::filesystem::path baseDir = sbsarPath.parent_path(); + std::filesystem::path candidate = baseDir / imagePath; + std::error_code ec; + candidate = std::filesystem::weakly_canonical(candidate, ec); + if (!ec && std::filesystem::exists(candidate)) { + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("SBSAR: HEURISTIC resolved image relative to SBSAR: %s\n", + candidate.string().c_str()); + return candidate.string(); + } + } + // 5.2 Relative to current working directory (CWD) + { + std::filesystem::path candidate = std::filesystem::current_path() / imagePath; + std::error_code ec; + candidate = std::filesystem::weakly_canonical(candidate, ec); + if (!ec && std::filesystem::exists(candidate)) { + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("SBSAR: HEURISTIC (CWD) resolved image: %s\n", candidate.string().c_str()); + return candidate.string(); + } + } + // 5.3 Relative to parent of SBSAR directory + { + std::filesystem::path sbsarPath(resolvedSbsarPath); + std::filesystem::path baseDir = sbsarPath.parent_path().parent_path(); + + std::filesystem::path candidate = baseDir / imagePath; + + std::error_code ec; + candidate = std::filesystem::weakly_canonical(candidate, ec); + + if (!ec && std::filesystem::exists(candidate)) { + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("SBSAR: HEURISTIC resolved image relative to SBSAR parent dir: %s\n", + candidate.string().c_str()); + return candidate.string(); + } + } + TF_WARN("SBSAR: Failed to resolve image '%s' for parameter '%s' (SBSAR: %s)", + imagePath.c_str(), + parameterName.c_str(), + resolvedSbsarPath.c_str()); + return {}; +} + bool SBSARFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool metadataOnly) const { @@ -229,6 +422,15 @@ SBSARFileFormat::ComposeFieldsForFileFormatArguments(const std::string& assetPat FileFormatArguments arguments; SdfLayer::SplitIdentifier(assetPath, &sbsarPath, &arguments); + // Ensure the SBSAR path is fully resolved for use as an anchor + std::string resolvedSbsarPath = ArGetResolver().Resolve(sbsarPath); + if (resolvedSbsarPath.empty()) { + resolvedSbsarPath = sbsarPath; // Fall back to original if resolution fails + } + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("SBSARFileFormat::ComposeFieldsForFileFormatArguments: resolved SBSAR path : %s\n", + resolvedSbsarPath.c_str()); + ParameterListPtr sbsarParameters = getParameterListFromPackageCache(sbsarPath); SymbolMapper symbolMapper; VtDictionary dict; @@ -245,7 +447,7 @@ SBSARFileFormat::ComposeFieldsForFileFormatArguments(const std::string& assetPat if (parameter->isImage()) { const auto& imageAssetPath = paramValue.Get(); std::string resolvedImageAssetPath = - ArGetResolver().Resolve(imageAssetPath.GetAssetPath()); + resolveSbsarImageInputAssetPath(imageAssetPath, resolvedSbsarPath, parameterName); std::size_t hash = addImageToInputImageCache(resolvedImageAssetPath); dict[parameterName] = VtValue(hash); } else { @@ -309,14 +511,16 @@ SBSARFileFormat::WriteToString(const SdfLayer& layer, const std::string& comment) const { // Fall back to USDA - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToString(layer, str, comment); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToString(layer, str, comment); } bool SBSARFileFormat::WriteToStream(const SdfSpecHandle& spec, std::ostream& out, size_t indent) const { // Fall back to USDA - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToStream(spec, out, indent); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToStream(spec, out, indent); } bool diff --git a/sbsar/src/sbsarfileformat.h b/sbsar/src/sbsarfileformat.h index 188e8401..7080d818 100644 --- a/sbsar/src/sbsarfileformat.h +++ b/sbsar/src/sbsarfileformat.h @@ -12,6 +12,7 @@ governing permissions and limitations under the License. #pragma once #include "api.h" +#include #include #include #include @@ -21,17 +22,24 @@ governing permissions and limitations under the License. namespace adobe::usd::sbsar { struct SBSAROptions { + SBSAROptions() + { + adobe::usd::applyMaterialModelDefaults(writeUsdPreviewSurface, writeASM, writeOpenPBR); + } + PXR_NS::VtDictionary sbsarParameters; std::uint32_t depth = 0; bool writeUsdPreviewSurface = true; bool writeASM = true; bool writeOpenPBR = false; + bool preserveExtraMaterialInfo = true; + std::string graphTypeFilter = ""; // "material", "light", or "" (no filter) }; } // To avoid trouble when registering class we use pixar name space. PXR_NAMESPACE_OPEN_SCOPE -#define SBSAR_FILE_FORMAT_TOKENS \ +#define SBSAR_FILE_FORMAT_TOKENS \ ((Id, "sbsar"))((Version, "1.0"))((Target, "usd"))((Extension, "sbsar")) TF_DECLARE_PUBLIC_TOKENS(SBSARFileFormatTokens, SBSAR_FILE_FORMAT_TOKENS); @@ -58,7 +66,7 @@ class SBSARFileFormat : public SdfFileFormat , public PcpDynamicFileFormatInterface { - public: +public: // SdfFileFormat API. USDSBSAR_API virtual bool IsPackage() const override; USDSBSAR_API virtual std::string GetPackageRootLayerPath( @@ -133,7 +141,7 @@ class SBSARFileFormat std::ostream& out, size_t indent) const override; - protected: +protected: SDF_FILE_FORMAT_FACTORY_ACCESS; virtual ~SBSARFileFormat(); SBSARFileFormat(); diff --git a/sbsar/src/tokens.cpp b/sbsar/src/tokens.cpp index eb7c5473..6ec83420 100644 --- a/sbsar/src/tokens.cpp +++ b/sbsar/src/tokens.cpp @@ -14,41 +14,38 @@ governing permissions and limitations under the License. PXR_NAMESPACE_OPEN_SCOPE -UsdSbsarTokensType::UsdSbsarTokensType() : - pngFormat("png", TfToken::Immortal), - RES_1024x1024("10", TfToken::Immortal), - RES_128x128("7", TfToken::Immortal), - RES_16x16("4", TfToken::Immortal), - RES_1x1("0", TfToken::Immortal), - RES_2048x2048("11", TfToken::Immortal), - RES_256x256("8", TfToken::Immortal), - RES_2x2("1", TfToken::Immortal), - RES_32x32("5", TfToken::Immortal), - RES_4096x4096("12", TfToken::Immortal), - RES_4x4("2", TfToken::Immortal), - RES_512x512("9", TfToken::Immortal), - RES_64x64("6", TfToken::Immortal), - RES_8x8("3", TfToken::Immortal), - sbsarFormat("sbsar", TfToken::Immortal), - allTokens({ - pngFormat, - RES_1024x1024, - RES_128x128, - RES_16x16, - RES_1x1, - RES_2048x2048, - RES_256x256, - RES_2x2, - RES_32x32, - RES_4096x4096, - RES_4x4, - RES_512x512, - RES_64x64, - RES_8x8, - sbsarFormat - }) -{ -} +UsdSbsarTokensType::UsdSbsarTokensType() + : pngFormat("png", TfToken::Immortal) + , RES_1024x1024("10", TfToken::Immortal) + , RES_128x128("7", TfToken::Immortal) + , RES_16x16("4", TfToken::Immortal) + , RES_1x1("0", TfToken::Immortal) + , RES_2048x2048("11", TfToken::Immortal) + , RES_256x256("8", TfToken::Immortal) + , RES_2x2("1", TfToken::Immortal) + , RES_32x32("5", TfToken::Immortal) + , RES_4096x4096("12", TfToken::Immortal) + , RES_4x4("2", TfToken::Immortal) + , RES_512x512("9", TfToken::Immortal) + , RES_64x64("6", TfToken::Immortal) + , RES_8x8("3", TfToken::Immortal) + , sbsarFormat("sbsar", TfToken::Immortal) + , allTokens({ pngFormat, + RES_1024x1024, + RES_128x128, + RES_16x16, + RES_1x1, + RES_2048x2048, + RES_256x256, + RES_2x2, + RES_32x32, + RES_4096x4096, + RES_4x4, + RES_512x512, + RES_64x64, + RES_8x8, + sbsarFormat }) +{} TfStaticData UsdSbsarTokens; diff --git a/sbsar/src/tokens.h b/sbsar/src/tokens.h index dd869ef0..6646d6d9 100644 --- a/sbsar/src/tokens.h +++ b/sbsar/src/tokens.h @@ -16,21 +16,20 @@ governing permissions and limitations under the License. /// \file usdSbsar/tokens.h // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -// +// // This is an automatically generated file (by usdGenSchema.py). // Do not hand-edit! -// +// // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -#include "pxr/pxr.h" #include "./api.h" #include "pxr/base/tf/staticData.h" #include "pxr/base/tf/token.h" +#include "pxr/pxr.h" #include PXR_NAMESPACE_OPEN_SCOPE - /// \class UsdSbsarTokensType /// /// \link UsdSbsarTokens \endlink provides static, efficient @@ -49,67 +48,68 @@ PXR_NAMESPACE_OPEN_SCOPE /// \code /// gprim.GetMyTokenValuedAttr().Set(UsdSbsarTokens->pngFormat); /// \endcode -struct UsdSbsarTokensType { +struct UsdSbsarTokensType +{ USDSBSAR_API UsdSbsarTokensType(); /// \brief "png" - /// - /// Png extension name + /// + /// Png extension name const TfToken pngFormat; /// \brief "10" - /// + /// /// Image resolution of 1024x1024 pixels const TfToken RES_1024x1024; /// \brief "7" - /// + /// /// Image resolution of 128x128 pixels const TfToken RES_128x128; /// \brief "4" - /// + /// /// Image resolution of 16x16 pixels const TfToken RES_16x16; /// \brief "0" - /// + /// /// Image resolution of 1x1 pixels const TfToken RES_1x1; /// \brief "11" - /// + /// /// Image resolution of 2048x2048 pixels const TfToken RES_2048x2048; /// \brief "8" - /// + /// /// Image resolution of 256x256 pixels const TfToken RES_256x256; /// \brief "1" - /// + /// /// Image resolution of 2x2 pixels const TfToken RES_2x2; /// \brief "5" - /// + /// /// Image resolution of 32x32 pixels const TfToken RES_32x32; /// \brief "12" - /// - /// Image resolution of 4096x4096 pixels + /// + /// Image resolution of 4096x4096 pixels const TfToken RES_4096x4096; /// \brief "2" - /// + /// /// Image resolution of 4x4 pixels const TfToken RES_4x4; /// \brief "9" - /// + /// /// Image resolution of 512x512 pixels const TfToken RES_512x512; /// \brief "6" - /// + /// /// Image resolution of 64x64 pixels const TfToken RES_64x64; /// \brief "3" - /// + /// /// Image resolution of 8x8 pixels const TfToken RES_8x8; /// \brief "sbsar" - /// - /// Sbsar extension name + /// + /// Sbsar extension name const TfToken sbsarFormat; /// A vector of all of the tokens listed above. const std::vector allTokens; diff --git a/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp b/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp index b8fbfb7a..8e31af81 100644 --- a/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp +++ b/sbsar/src/usdGeneration/sbsarLuxDomeLight.cpp @@ -104,7 +104,7 @@ addLuxDomeLight(SdfAbstractData* sdfData, SdfAssetPath path = SdfAssetPath(generateSbsarInfoPath(usageString, graphName, sbsarHash, params)); setAttributeMetadata(sdfData, texAttrPath, SdfFieldKeys->Hidden, VtValue(true)); - setAttributeDefaultValue(sdfData, texAttrPath, path); + setAttributeDefaultValue(sdfData, texAttrPath, path, SdfValueTypeNames->Asset); } return lightPath; diff --git a/sbsar/src/usdGeneration/sbsarMaterial.cpp b/sbsar/src/usdGeneration/sbsarMaterial.cpp index 370c493a..d1d5dc51 100644 --- a/sbsar/src/usdGeneration/sbsarMaterial.cpp +++ b/sbsar/src/usdGeneration/sbsarMaterial.cpp @@ -18,6 +18,7 @@ governing permissions and limitations under the License. #include #include +#include #include #include #include @@ -49,7 +50,7 @@ setupPysicalSize(SdfAbstractData* sdfData, // TODO the dynamically computed evaluation of the physical size is not yet implemented. GfVec3f physicalSize( graphDesc.mPhysicalSize.x, graphDesc.mPhysicalSize.y, graphDesc.mPhysicalSize.z); - setAttributeDefaultValue(sdfData, paramPath, physicalSize); + setAttributeDefaultValue(sdfData, paramPath, physicalSize, SdfValueTypeNames->Float3); TF_DEBUG(FILE_FORMAT_SBSAR) .Msg("setupPysicalSize: %f %f %f\n", graphDesc.mPhysicalSize.x, @@ -81,6 +82,17 @@ initDefaultMaterialInputs(SdfAbstractData* sdfData, SdfPath textureAssetPath = createShaderInput(sdfData, materialPath, textureAssetName, SdfValueTypeNames->Asset); setAttributeMetadata(sdfData, textureAssetPath, SdfFieldKeys->Hidden, VtValue(true)); + // We set the color space on the attribute that will carry the texture asset path + // This is an important clue for the OpenPBR/MaterialX shading network, as the texture + // reading nodes there do not have a field for the color space. That space is specified + // on the attribute that holds the asset path. + // XXX: In the future we should switch these tokens to GfColorSpaceNames, which + // specifies a larger set of color spaces. For now we stick to "srgb_texture" and "raw", + // which are MaterialX supported and recognized color space names. + const TfToken& colorSpace = + isColorUsage(usage) ? MtlXTokens->srgb_texture : AdobeTokens->raw; + setAttributeMetadata( + sdfData, textureAssetPath, SdfFieldKeys->ColorSpace, VtValue(colorSpace)); // Not setting a default value here, so that it has to be overwritten in the payload // reference } @@ -95,8 +107,8 @@ initDefaultMaterialInputs(SdfAbstractData* sdfData, setAttributeMetadata(sdfData, biasAttrPath, SdfFieldKeys->Hidden, VtValue(true)); const auto [scale, bias] = getNormalMapScaleAndBias(normalFormat); - setAttributeDefaultValue(sdfData, scaleAttrPath, scale); - setAttributeDefaultValue(sdfData, biasAttrPath, bias); + setAttributeDefaultValue(sdfData, scaleAttrPath, scale, SdfValueTypeNames->Float4); + setAttributeDefaultValue(sdfData, biasAttrPath, bias, SdfValueTypeNames->Float4); } } } @@ -127,7 +139,7 @@ setMaterialTexturePaths(SdfAbstractData* sdfData, // The "./" makes the path anchored on this layer and it is resolved relative to it // inside of the same SBSAR package. SdfAssetPath path = SdfAssetPath("./" + sbsarPath); - setAttributeDefaultValue(sdfData, textureAssetPath, path); + setAttributeDefaultValue(sdfData, textureAssetPath, path, SdfValueTypeNames->Asset); } } } @@ -152,8 +164,10 @@ setMaterialValues(SdfAbstractData* sdfData, std::string infoPath = generateSbsarInfoPath(usage, graphName, sbsarHash, jsParams); TF_DEBUG(FILE_FORMAT_SBSAR) .Msg("Using engine to get value for %s\n", usage.c_str()); - setAttributeDefaultValue( - sdfData, textureAssetPath, renderSbsarValue(packagePath, infoPath)); + setAttributeDefaultValue(sdfData, + textureAssetPath, + renderSbsarValue(packagePath, infoPath), + defaultIt->second.type); } } } @@ -163,7 +177,8 @@ void setMaterialNormalScaleAndBias(SdfAbstractData* sdfData, const SdfPath& materialPath, const SubstanceAir::GraphDesc& graphDesc, - const JsValue& jsParams) + const JsValue& jsParams, + const SBSAROptions& sbsarData) { // If we don't have concrete information on the normal format, we don't author an explict scale // and bias to adjust for that and instead rely on the default that was authored with the @@ -188,8 +203,20 @@ setMaterialNormalScaleAndBias(SdfAbstractData* sdfData, SdfPath biasAttrPath = createShaderInput(sdfData, materialPath, biasName, SdfValueTypeNames->Float4); const auto [scale, bias] = getNormalMapScaleAndBias(normalFormat); - setAttributeDefaultValue(sdfData, scaleAttrPath, scale); - setAttributeDefaultValue(sdfData, biasAttrPath, bias); + std::string materialPathStr = materialPath.GetString(); + TF_DEBUG(FILE_FORMAT_SBSAR) + .Msg("Adjusting normal map scale and bias for material %s\n", + materialPathStr.c_str()); + setAttributeDefaultValue(sdfData, scaleAttrPath, scale, SdfValueTypeNames->Float4); + setAttributeDefaultValue(sdfData, biasAttrPath, bias, SdfValueTypeNames->Float4); + // XXX @dcoffey There's a gap here with OpenPBR which doesn't use these input + // connections as it sets the scale and bias directly in the Shader node. This means + // that if the normal format is changed via the sbsar parameters, the change won't be + // impact the scale / bias. It's a little abmbigous what the user goal of toggling the + // normal format via the sbsar parameters even is here, so for now we're going to accept + // this gap. There's also an argument that we shouldn't even be adjusting the scale and + // bias if the user changes the normal format via the sbsar parameters, since the user + // may be trying to "correct" the render, which setting these fields will counteract } } } @@ -202,15 +229,21 @@ addMaterialTransform(SdfAbstractData* sdfData, const SdfPath& materialPath) { SdfPath uvScalePath = createShaderInput(sdfData, materialPath, uv_scale_input, SdfValueTypeNames->Float2); - setAttributeDefaultValue(sdfData, uvScalePath, GfVec2f(1.0f, 1.0f)); + setAttributeDefaultValue(sdfData, uvScalePath, GfVec2f(1.0f, 1.0f), SdfValueTypeNames->Float2); + + SdfPath uvScaleInversePath = + createShaderInput(sdfData, materialPath, uv_scale_inverse_input, SdfValueTypeNames->Float2); + setAttributeDefaultValue( + sdfData, uvScaleInversePath, GfVec2f(1.0f, 1.0f), SdfValueTypeNames->Float2); SdfPath uvRotationPath = createShaderInput(sdfData, materialPath, uv_rotation_input, SdfValueTypeNames->Float); - setAttributeDefaultValue(sdfData, uvRotationPath, 0.0f); + setAttributeDefaultValue(sdfData, uvRotationPath, 0.0f, SdfValueTypeNames->Float); SdfPath uvTranslationPath = createShaderInput(sdfData, materialPath, uv_translation_input, SdfValueTypeNames->Float2); - setAttributeDefaultValue(sdfData, uvTranslationPath, GfVec2f(0.0f, 0.0f)); + setAttributeDefaultValue( + sdfData, uvTranslationPath, GfVec2f(0.0f, 0.0f), SdfValueTypeNames->Float2); } //! \brief Add standard material networks according to the compilation options. @@ -231,7 +264,8 @@ addStandardMaterial(SdfAbstractData* sdfData, // Set the default UV channel name SdfPath uvChannelNamePath = createShaderInput(sdfData, materialPath, uv_channel_name, SdfValueTypeNames->String); - setAttributeDefaultValue(sdfData, uvChannelNamePath, std::string("st")); + setAttributeDefaultValue( + sdfData, uvChannelNamePath, std::string("st"), SdfValueTypeNames->String); setAttributeMetadata(sdfData, uvChannelNamePath, SdfFieldKeys->Hidden, VtValue(true)); // Expose the texture wrap modes for texture reading nodes. This is shared by ASM and @@ -241,8 +275,10 @@ addStandardMaterial(SdfAbstractData* sdfData, 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); + setAttributeDefaultValue( + sdfData, uvWrapSPath, AdobeTokens->repeat, SdfValueTypeNames->Token); + setAttributeDefaultValue( + sdfData, uvWrapTPath, AdobeTokens->repeat, SdfValueTypeNames->Token); VtTokenArray wrapModes = { AdobeTokens->repeat, AdobeTokens->mirror, AdobeTokens->clamp, AdobeTokens->black }; @@ -262,7 +298,8 @@ addStandardMaterial(SdfAbstractData* sdfData, // Add Refractive MaterialX Implementation if (options.writeOpenPBR) { - addOpenPbrShader(sdfData, materialPath, graphDesc); + NormalFormat initialNormalFormat = getDefaultNormalFormat(graphDesc); + addOpenPbrShader(sdfData, materialPath, graphDesc, initialNormalFormat); } } @@ -352,7 +389,7 @@ addMaterialPrim(SdfAbstractData* sdfData, setMaterialValues( sdfData, materialPath, graphDesc, graphName, sbsarHash, jsParams, packagePath); // Set normal scale and bias depending on the normal format - setMaterialNormalScaleAndBias(sdfData, materialPath, graphDesc, jsParams); + setMaterialNormalScaleAndBias(sdfData, materialPath, graphDesc, jsParams, sbsarData); } return materialPath; diff --git a/sbsar/src/usdGeneration/sbsarOpenPBR.cpp b/sbsar/src/usdGeneration/sbsarOpenPBR.cpp index 6459c8ac..87f482a8 100644 --- a/sbsar/src/usdGeneration/sbsarOpenPBR.cpp +++ b/sbsar/src/usdGeneration/sbsarOpenPBR.cpp @@ -15,8 +15,10 @@ governing permissions and limitations under the License. // File format utils #include +#include #include #include +#include #include @@ -36,6 +38,7 @@ TF_DEFINE_PRIVATE_TOKENS(_tokens, (WsNormal) (Surface) (Displacement) + (HeightLevel) ); // clang-format on @@ -138,10 +141,20 @@ static std::map _materialMapBindings = { // thin_film_ior (no source info) // * Emission - // emission_luminance (no source info) (is set to 1 if we have "emissive" input) + // emission_luminance (no source info) (is set to 1000 if we have "emissive" input) { "emissive", { OpenPbrTokens->emission_color, SdfValueTypeNames->Color3f, "out", AdobeTokens->sRGB } }, + // * Displacement + // height, heightLevel and heightScale are sbs inputs that have the same names as ASM inputs + // but not OpenPBR native. We keep the ASM naming of the material inputs which are connected + // to a seperate displacement shader. + { "height", { TfToken("height"), SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + { "heightLevel", + { TfToken("heightLevel"), SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + { "heightScale", + { TfToken("heightScale"), SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, + // * Geometry { "opacity", { OpenPbrTokens->geometry_opacity, SdfValueTypeNames->Float, "out", AdobeTokens->raw } }, @@ -153,75 +166,73 @@ static std::map _materialMapBindings = { // geometry_coat_tangent (no source info) }; +static const std::string heightStr = "height"; +static const std::string heightLevelStr = "heightLevel"; +static const std::string heightScaleStr = "heightScale"; + SdfPath bindTexture(SdfAbstractData* sdfData, const SdfPath& parentPath, const BindInfo& bindInfo, const SdfPath& uvOutputAttrPath, const SdfPath& textureAssetAttrPath, - const SdfPath& uAddressModeAttrPath, - const SdfPath& vAddressModeAttrPath) + const NormalFormat& initialNormalFormat) { TF_DEBUG(FILE_FORMAT_SBSAR) .Msg("bindTexture: Binding texture channel %s\n", bindInfo.name.GetText()); - TfToken shaderType; + auto name = bindInfo.name; + Input input; + // XXX hardcoding this to repeat since we don't have the wrap mode info from the SBSAR at this + // point and these are typically always repeating textures + input.wrapS = AdobeTokens->repeat; + input.wrapT = AdobeTokens->repeat; + input.scale = kDefaultTexScale; + input.bias = kDefaultTexBias; + if (name == OpenPbrTokens->geometry_normal || name == OpenPbrTokens->geometry_coat_normal) { + if (initialNormalFormat == NormalFormat::DirectX || + initialNormalFormat == NormalFormat::Unknown) { + input.scale = kDirectXNormalTexScale; + input.bias = kDirectXNormalTexBias; + } else { + input.scale = kOpenGLNormalTexScale; + input.bias = kOpenGLNormalTexBias; + } + } + if (bindInfo.sdfType == SdfValueTypeNames->Color3f) { - shaderType = MtlXTokens->ND_image_color3; + input.channel = AdobeTokens->rgb; + input.colorspace = AdobeTokens->sRGB; } else if (bindInfo.sdfType == SdfValueTypeNames->Float3) { - shaderType = MtlXTokens->ND_image_vector3; + input.channel = AdobeTokens->rgb; } else if (bindInfo.sdfType == SdfValueTypeNames->Float) { - shaderType = MtlXTokens->ND_image_float; + input.channel = AdobeTokens->r; } 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; + SdfPath textureOutput = createMaterialXTextureReader( + sdfData, parentPath, name, input, uvOutputAttrPath, textureAssetAttrPath); + + return textureOutput; } bool addUsdOpenPbrShaderImpl(SdfAbstractData* sdfData, const SdfPath& materialPath, const GraphDesc& graphDesc, - const std::map& mapBindings) + const std::map& mapBindings, + const NormalFormat& initialNormalFormat) { 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)); + SdfPath uvChannelNamePath = + createShaderInput(sdfData, materialPath, uv_channel_name, SdfValueTypeNames->String); + setAttributeDefaultValue( + sdfData, uvChannelNamePath, std::string("st"), SdfValueTypeNames->String); // Create a scope for the OpenPBR implementation SdfPath scopePath = @@ -231,13 +242,13 @@ addUsdOpenPbrShaderImpl(SdfAbstractData* sdfData, SdfPath txOutputPath = createShader(sdfData, scopePath, _tokens->TexCoordReader, - MtlXTokens->ND_texcoord_vector2, + MtlXTokens->ND_geompropvalue_vector2, "out", {}, - { { "index", uvChannelIndexPath } }); + { { "geomprop", uvChannelNamePath } }); #ifdef USDSBSAR_ENABLE_TEXTURE_TRANSFORM - SdfPath uvScaleInputPath = inputPath(materialPath, uv_scale_input); + SdfPath uvScaleInputPath = inputPath(materialPath, uv_scale_inverse_input); SdfPath uvRotationInputPath = inputPath(materialPath, uv_rotation_input); SdfPath uvTranslationInputPath = inputPath(materialPath, uv_translation_input); @@ -257,44 +268,81 @@ addUsdOpenPbrShaderImpl(SdfAbstractData* sdfData, SdfPath uvOutputPath = txOutputPath; #endif // USDSBSAR_ENABLE_TEXTURE_TRANSFORM + auto createTextureReader = [&](const std::string& usage, const BindInfo& bindInfo) -> SdfPath { + // 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, initialNormalFormat); + return texResultPath; + }; + + auto createMaterialInput = [&](const std::string& usage, float defaultValue) -> SdfPath { + SdfPath path; + if (hasUsage(usage, graphDesc)) { + auto it = mapBindings.find(usage); + if (it != mapBindings.end()) { + path = createTextureReader(usage, it->second); + } + } + if (path.IsEmpty()) { + path = createShaderInput(sdfData, materialPath, usage, SdfValueTypeNames->Float); + setAttributeDefaultValue( + sdfData, path, VtValue(defaultValue), SdfValueTypeNames->Float); + } + return path; + }; + + SdfPath heightLevelAttrPath; + SdfPath heightScaleAttrPath; + if (hasUsage(heightStr, graphDesc)) { + heightLevelAttrPath = createMaterialInput(heightLevelStr, 0.5f); + heightScaleAttrPath = createMaterialInput(heightScaleStr, 1.0f); + } + // Create texture sampling nodes InputValues inputValues; InputConnections inputConnections; bool enableSubsurface = false; for (const auto& usage : mapped_usages) { if (hasUsage(usage, graphDesc)) { + if (usage == heightLevelStr || usage == heightScaleStr) { + // these are handled above when "height" is present so skip + continue; + } + auto it = mapBindings.find(usage); if (it != mapBindings.end()) { const BindInfo& bindInfo = it->second; + SdfPath texResultPath = createTextureReader(usage, bindInfo); + + if (usage == heightStr) { + SdfPath heightLevel = + createShader(sdfData, + scopePath, + _tokens->HeightLevel, + MtlXTokens->ND_subtract_float, + "out", + {}, + { { "in1", texResultPath }, { "in2", heightLevelAttrPath } }); + + SdfPath displacementOutputPath = createShader( + sdfData, + scopePath, + _tokens->Displacement, + MtlXTokens->ND_displacement_float, + "out", + {}, + { { "displacement", heightLevel }, { "scale", heightScaleAttrPath } }); + + createShaderOutput(sdfData, + materialPath, + "mtlx:displacement", + SdfValueTypeNames->Token, + displacementOutputPath); - // 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); } @@ -306,9 +354,9 @@ addUsdOpenPbrShaderImpl(SdfAbstractData* sdfData, 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, + // Still we need to turn emission on by setting the luminance to 1000.0, // otherwise emission is turned off. - inputValues.emplace_back(OpenPbrTokens->emission_luminance, 1.0f); + inputValues.emplace_back(OpenPbrTokens->emission_luminance, 1000.0f); } } } @@ -341,35 +389,21 @@ addUsdOpenPbrShaderImpl(SdfAbstractData* sdfData, 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 namespace adobe::usd::sbsar { bool addOpenPbrShader(SdfAbstractData* sdfData, const SdfPath& materialPath, - const SubstanceAir::GraphDesc& graphDesc) + const SubstanceAir::GraphDesc& graphDesc, + const NormalFormat& initialNormalFormat) { - return addUsdOpenPbrShaderImpl(sdfData, materialPath, graphDesc, _materialMapBindings); + return addUsdOpenPbrShaderImpl( + sdfData, materialPath, graphDesc, _materialMapBindings, initialNormalFormat); } } diff --git a/sbsar/src/usdGeneration/sbsarOpenPBR.h b/sbsar/src/usdGeneration/sbsarOpenPBR.h index af4a17e8..ce5f6372 100644 --- a/sbsar/src/usdGeneration/sbsarOpenPBR.h +++ b/sbsar/src/usdGeneration/sbsarOpenPBR.h @@ -16,6 +16,8 @@ governing permissions and limitations under the License. #include +#include "usdGenerationHelpers.h" + namespace adobe::usd::sbsar { /// @brief Adds an OpenPBR/MaterialX material network to the material @@ -30,6 +32,7 @@ namespace adobe::usd::sbsar { bool addOpenPbrShader(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& materialPath, - const SubstanceAir::GraphDesc& graphDesc); + const SubstanceAir::GraphDesc& graphDesc, + const NormalFormat& initialNormalFormat); } diff --git a/sbsar/src/usdGeneration/sbsarSymbolMapper.h b/sbsar/src/usdGeneration/sbsarSymbolMapper.h index 259f0b72..8fdcf597 100644 --- a/sbsar/src/usdGeneration/sbsarSymbolMapper.h +++ b/sbsar/src/usdGeneration/sbsarSymbolMapper.h @@ -30,7 +30,7 @@ struct MappedSymbol /// @details Guarantees the same Usd Symbol doesn't occur multiple times in the same mapper. class SymbolMapper { - public: +public: SymbolMapper(); virtual ~SymbolMapper(); @@ -43,7 +43,7 @@ class SymbolMapper /// @return The mapped symbol. MappedSymbol GetSymbol(const std::string& substanceSymbol); - private: +private: // Existing mappings std::map mapped_symbols; diff --git a/sbsar/src/usdGeneration/usdGenerationHelpers.cpp b/sbsar/src/usdGeneration/usdGenerationHelpers.cpp index 3456864f..39927b71 100644 --- a/sbsar/src/usdGeneration/usdGenerationHelpers.cpp +++ b/sbsar/src/usdGeneration/usdGenerationHelpers.cpp @@ -95,6 +95,11 @@ const std::vector uniform_usages = { "IOR", const std::vector normal_usages = { "normal", "coatNormal" }; +const std::unordered_set color_usages = { "absorptionColor", "baseColor", + "coatColor", "emissive", + "scatteringColor", "scatteringDistanceScale", + "sheenColor", "specularEdgeColor" }; + const std::map reserved_label_map = { { "$time", "Time" }, { "$outputsize", "Output Size" }, { "$randomseed", "Random Seed" }, @@ -215,6 +220,7 @@ const std::map default_channels = { const std::vector default_resolutions = { 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }; const std::string uv_scale_input("uvscale"); +const std::string uv_scale_inverse_input("uvscaleinverse"); const std::string uv_rotation_input("uvrotation"); const std::string uv_translation_input("uvtranslation"); @@ -262,6 +268,50 @@ guessGraphType(const SubstanceAir::GraphDesc& graphDesc) return GraphType::Unknown; } +std::string +graphTypeToString(GraphType type) +{ + switch (type) { + case GraphType::Material: + return "material"; + case GraphType::Light: + return "light/environment"; + case GraphType::Unknown: + return "unknown"; + } + return "unknown"; +} + +std::string +describeGraphTypes(const std::vector& types) +{ + std::map counts; + for (GraphType type : types) { + counts[type]++; + } + + std::vector parts; + if (counts[GraphType::Material] > 0) { + parts.push_back(std::to_string(counts[GraphType::Material]) + " material graph(s)"); + } + if (counts[GraphType::Light] > 0) { + parts.push_back(std::to_string(counts[GraphType::Light]) + " light graph(s)"); + } + if (counts[GraphType::Unknown] > 0) { + parts.push_back(std::to_string(counts[GraphType::Unknown]) + " unknown graph(s)"); + } + + if (parts.empty()) { + return "no graphs"; + } + + std::string result = parts[0]; + for (size_t i = 1; i < parts.size(); ++i) { + result += ", " + parts[i]; + } + return result; +} + std::pair getDefaultValueNames(const std::string& channelName) { @@ -314,6 +364,12 @@ isNormal(const std::string& usage) return false; } +bool +isColorUsage(const std::string& usage) +{ + return color_usages.find(usage) != color_usages.end(); +} + JsValue convertSbsarParameters(const VtDictionary& sbsarParameters) { @@ -801,7 +857,7 @@ setupProceduralParameters(SdfAbstractData* sdfData, TfToken paramToken = getInputParamToken(symbolMapper, input->mIdentifier); SdfPath paramPath = createAttributeSpec(sdfData, primPath, paramToken, targetType); - setAttributeDefaultValue(sdfData, paramPath, defaultValue); + setAttributeDefaultValue(sdfData, paramPath, defaultValue, targetType); setAttributeMetadata(sdfData, paramPath, SdfFieldKeys->Custom, VtValue(true)); bool isHidden = @@ -857,8 +913,11 @@ convertStringToVtValue(const SubstanceAir::string& val_str) SrcType value; stringstream sstr(val_str); sstr >> value; - if constexpr (std::is_same_v || std::is_same_v || - std::is_same_v) + + if constexpr (std::is_same_v || + std::is_same_v) + return VtValue(std::string(val_str.data(), val_str.size())); + else if constexpr (std::is_same_v || std::is_same_v) return VtValue(value); else return VtValue(DstType(&value.x)); @@ -968,7 +1027,7 @@ addPresetVariant(SdfAbstractData* sdfData, TfToken paramToken = getInputParamToken(symbolMapper, val.mIdentifier); SdfPath paramPath = createAttributeSpec(sdfData, presetVariantPath, paramToken, targetType); - setAttributeDefaultValue(sdfData, paramPath, targetValue); + setAttributeDefaultValue(sdfData, paramPath, targetValue, targetType); setAttributeMetadata(sdfData, paramPath, SdfFieldKeys->Custom, VtValue(true)); } @@ -998,7 +1057,7 @@ addResolutionVariantSet(SdfAbstractData* sdfData, const TfToken paramToken = getInputParamToken(symbolMapper, std::string("$outputsize")); SdfPath paramPath = createAttributeSpec(sdfData, resVariantPath, paramToken, SdfValueTypeNames->Int2); - setAttributeDefaultValue(sdfData, paramPath, GfVec2i(xres, yres)); + setAttributeDefaultValue(sdfData, paramPath, GfVec2i(xres, yres), SdfValueTypeNames->Int2); setAttributeMetadata(sdfData, paramPath, SdfFieldKeys->Custom, VtValue(true)); addPresetVariant( diff --git a/sbsar/src/usdGeneration/usdGenerationHelpers.h b/sbsar/src/usdGeneration/usdGenerationHelpers.h index 47f2f535..8fe3a3f3 100644 --- a/sbsar/src/usdGeneration/usdGenerationHelpers.h +++ b/sbsar/src/usdGeneration/usdGenerationHelpers.h @@ -55,6 +55,11 @@ 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 scale inverse transformation. This parameter is only used for +/// OpenPBR networks since the Place2D works differently than used in the other ASM and +/// UsdPreviewSurface networks. +extern const std::string uv_scale_inverse_input; + /// Input parameter name for UV rotation transformation extern const std::string uv_rotation_input; @@ -97,6 +102,18 @@ enum class GraphType GraphType guessGraphType(const SubstanceAir::GraphDesc& graphDesc); +/// @brief Convert GraphType enum to string representation. +/// @param type GraphType to convert +/// @return String representation ("material", "light/environment", or "unknown") +USDSBSAR_API std::string +graphTypeToString(GraphType type); + +/// @brief Generate a human-readable description of a collection of graph types. +/// @param types Vector of GraphType values to describe +/// @return String like "2 material graph(s), 1 light graph(s)" +USDSBSAR_API std::string +describeGraphTypes(const std::vector& types); + /// @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 @@ -124,12 +141,20 @@ hasUsage(const std::string& usage, const SubstanceAir::GraphDesc& graphDesc); bool hasInput(const std::string& identifier, const SubstanceAir::GraphDesc& graphDesc); -/// @brief Determine if a usage string represents a normal map. +/// @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 Determine if a usage string represents a color output +/// @param usage Usage string to check +/// @return True if the usage represents a color +/// +/// This is useful to choose the right color space +bool +isColorUsage(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 diff --git a/sbsar/test/test_sbsarConfig.cpp b/sbsar/test/test_sbsarConfig.cpp index 7cc00b3e..e7729fd4 100644 --- a/sbsar/test/test_sbsarConfig.cpp +++ b/sbsar/test/test_sbsarConfig.cpp @@ -19,7 +19,7 @@ using namespace adobe::usd::sbsar; class SbsarConfigFixure : public ::testing::Test { - protected: +protected: virtual void SetUp() {} virtual void TearDown() { PXR_NS::getSbsarConfig()->init(); } }; @@ -29,9 +29,9 @@ TEST(SbsarConfig, getCacheSize) PXR_NS::SbsarConfigRefPtr sbsarConfig = PXR_NS::getSbsarConfig(); ASSERT_TRUE(sbsarConfig); - EXPECT_EQ(sbsarConfig->getAssetCacheSize(), 1'000'000'000); - EXPECT_EQ(sbsarConfig->getInputImageCacheSize(), 1'000'000'000); - EXPECT_EQ(sbsarConfig->getPackageCacheSize(), 10); + EXPECT_EQ(sbsarConfig->getAssetCacheSize(), 2'000'000'000); // 2GB for high-res support + EXPECT_EQ(sbsarConfig->getInputImageCacheSize(), 1'000'000'000); // 1GB + EXPECT_EQ(sbsarConfig->getPackageCacheSize(), 10); // 10 packages } TEST_F(SbsarConfigFixure, setAssetCacheSize) diff --git a/scripts/requirements.in b/scripts/requirements.in new file mode 100644 index 00000000..1a11f9ab --- /dev/null +++ b/scripts/requirements.in @@ -0,0 +1,8 @@ +# Use this file to generate the frozen requirements in requirements.txt +# uv pip compile --emit-index-url --python 3.10 --universal requirements.in -o requirements.txt +# Pick the latest manylinux_2_28_x86_64 pyside6 wheel that is compatible with glibc 2.28 on RHEL8 +pyside6==6.9.3 +pyopengl +pytest +opencv-python +numpy diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 703146d5..8a58aff2 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,5 +1,47 @@ -pyside6 -pyopengl -pytest -opencv-python -numpy \ No newline at end of file +# This file was autogenerated by uv via the following command: +# uv pip compile --emit-index-url --python 3.10 --universal requirements.in -o requirements.txt +--index-url https://pypi.org/simple + +colorama==0.4.6 ; sys_platform == 'win32' + # via pytest +exceptiongroup==1.3.0 ; python_full_version < '3.11' + # via pytest +iniconfig==2.3.0 + # via pytest +numpy==2.2.6 ; python_full_version < '3.11' + # via + # -r requirements.in + # opencv-python +numpy==2.3.4 ; python_full_version >= '3.11' + # via + # -r requirements.in + # opencv-python +opencv-python==4.11.0.86 + # via -r requirements.in +packaging==25.0 + # via pytest +pluggy==1.6.0 + # via pytest +pygments==2.19.2 + # via pytest +pyopengl==3.1.10 + # via -r requirements.in +pyside6==6.9.3 + # via -r requirements.in +pyside6-addons==6.9.3 + # via pyside6 +pyside6-essentials==6.9.3 + # via + # pyside6 + # pyside6-addons +pytest==8.4.2 + # via -r requirements.in +shiboken6==6.9.3 + # via + # pyside6 + # pyside6-addons + # pyside6-essentials +tomli==2.3.0 ; python_full_version < '3.11' + # via pytest +typing-extensions==4.15.0 ; python_full_version < '3.11' + # via exceptiongroup diff --git a/spz/src/CMakeLists.txt b/spz/src/CMakeLists.txt index 55f428fe..f8c69977 100644 --- a/spz/src/CMakeLists.txt +++ b/spz/src/CMakeLists.txt @@ -18,6 +18,7 @@ PRIVATE target_include_directories(usdSpz PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" ) @@ -34,65 +35,6 @@ PRIVATE fileformatUtils ) -target_precompile_headers(usdSpz -PRIVATE - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" -) - # Installation of plugin files mimics the file structure that USD has for plugins, # so it is easy to deploy it in a pre-existing USD build, if one chooses to do so. @@ -107,17 +49,19 @@ set_target_properties(usdSpz PROPERTIES RESOURCE ${CMAKE_CURRENT_BINARY_DIR}/plu set_target_properties(usdSpz PROPERTIES RESOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json:plugInfo.json") +# USDSPZ_DESTINATION is set in the parent scope by the add_usd_fileformat macro + if(USDSPZ_ENABLE_INSTALL) install( TARGETS usdSpz - RUNTIME DESTINATION plugin/usd COMPONENT Runtime - LIBRARY DESTINATION plugin/usd COMPONENT Runtime - RESOURCE DESTINATION plugin/usd/usdSpz/resources COMPONENT Runtime + RUNTIME DESTINATION ${USDSPZ_DESTINATION} COMPONENT Runtime + LIBRARY DESTINATION ${USDSPZ_DESTINATION} COMPONENT Runtime + RESOURCE DESTINATION ${USDSPZ_DESTINATION}/usdSpz/resources COMPONENT Runtime ) install( FILES plugInfo.root.json - DESTINATION plugin/usd + DESTINATION ${USDSPZ_DESTINATION} RENAME plugInfo.json COMPONENT Runtime ) diff --git a/spz/src/api.h b/spz/src/api.h index 163eb236..77046219 100644 --- a/spz/src/api.h +++ b/spz/src/api.h @@ -14,19 +14,19 @@ governing permissions and limitations under the License. #include "pxr/base/arch/export.h" #if defined(PXR_STATIC) -# define USDSPZ_API -# define USDSPZ_API_TEMPLATE_CLASS(...) -# define USDSPZ_API_TEMPLATE_STRUCT(...) -# define USDSPZ_LOCAL +#define USDSPZ_API +#define USDSPZ_API_TEMPLATE_CLASS(...) +#define USDSPZ_API_TEMPLATE_STRUCT(...) +#define USDSPZ_LOCAL #else -# if defined(USDSPZ_EXPORTS) -# define USDSPZ_API ARCH_EXPORT -# define USDSPZ_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) -# define USDSPZ_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) -# else -# define USDSPZ_API ARCH_IMPORT -# define USDSPZ_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) -# define USDSPZ_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) -# endif -# define USDSPZ_LOCAL ARCH_HIDDEN +#if defined(USDSPZ_EXPORTS) +#define USDSPZ_API ARCH_EXPORT +#define USDSPZ_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define USDSPZ_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#else +#define USDSPZ_API ARCH_IMPORT +#define USDSPZ_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define USDSPZ_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#endif +#define USDSPZ_LOCAL ARCH_HIDDEN #endif \ No newline at end of file diff --git a/spz/src/fileFormat.cpp b/spz/src/fileFormat.cpp index daa61259..9309f944 100644 --- a/spz/src/fileFormat.cpp +++ b/spz/src/fileFormat.cpp @@ -23,7 +23,6 @@ governing permissions and limitations under the License. #include #include -#include using namespace adobe::usd; using namespace spz; @@ -169,14 +168,16 @@ UsdSpzFileFormat::WriteToString(const SdfLayer& layer, const std::string& comment) const { // Write USD as SPZ: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToString(layer, str, comment); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToString(layer, str, comment); } bool UsdSpzFileFormat::WriteToStream(const SdfSpecHandle& spec, std::ostream& out, size_t indent) const { // Write USD as SPZ: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToStream(spec, out, indent); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToStream(spec, out, indent); } PXR_NAMESPACE_CLOSE_SCOPE diff --git a/spz/src/fileFormat.h b/spz/src/fileFormat.h index f201e4bb..3a879e40 100644 --- a/spz/src/fileFormat.h +++ b/spz/src/fileFormat.h @@ -11,8 +11,8 @@ governing permissions and limitations under the License. */ #pragma once #include "api.h" -#include #include +#include #include #include #include @@ -38,7 +38,7 @@ TF_DECLARE_WEAK_AND_REF_PTRS(UsdSpzFileFormat); /// \brief SdfData specialization for working with spz files. class SpzData : public FileFormatDataBase { - public: +public: bool gsplatsWithZup = false; PXR_NS::VtFloatArray gsplatsClippingBox = { -2.0, -2.0, -2.0, 2.0, 2.0, 2.0 }; static SpzDataRefPtr InitData(const SdfFileFormat::FileFormatArguments& args); @@ -50,7 +50,7 @@ class USDSPZ_API UsdSpzFileFormat : public SdfFileFormat , public PcpDynamicFileFormatInterface { - public: +public: friend class SpzData; virtual SdfAbstractDataRefPtr InitData(const FileFormatArguments& args) const override; @@ -88,7 +88,7 @@ class USDSPZ_API UsdSpzFileFormat std::string* str, const std::string& comment = std::string()) const override; - protected: +protected: static const TfToken gsplatsWithZupToken; static const TfToken gsplatsWithClippingToken; @@ -96,7 +96,7 @@ class USDSPZ_API UsdSpzFileFormat virtual ~UsdSpzFileFormat(); UsdSpzFileFormat(); - private: +private: bool ReadFromStream(SdfLayer* layer, std::istream& input, bool metadataOnly, diff --git a/spz/src/spzExport.cpp b/spz/src/spzExport.cpp index 59eb5fbb..0fbbc2c6 100644 --- a/spz/src/spzExport.cpp +++ b/spz/src/spzExport.cpp @@ -11,44 +11,15 @@ governing permissions and limitations under the License. */ #include "spzExport.h" #include "debugCodes.h" +#include +#include #include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include -#include -#include -#include -#include -#include -#include using namespace PXR_NS; @@ -85,9 +56,7 @@ findMaxSHCoeffSize(const UsdData& usd, int nodeIndex) } void -aggregateMeshInstance(SpzTotalMesh& totalMesh, - const Mesh& mesh, - const GfMatrix4d& modelMatrix) +aggregateMeshInstance(SpzTotalMesh& totalMesh, const Mesh& mesh, const GfMatrix4d& modelMatrix) { size_t currentMeshPointsSize = mesh.points.size(); size_t offset = totalMesh.points.size(); @@ -99,7 +68,8 @@ aggregateMeshInstance(SpzTotalMesh& totalMesh, totalMesh.points[offset + i] = GfVec3f(modelMatrix.Transform(mesh.points[i])); } - const size_t numPointOpacities = std::min(currentMeshPointsSize, mesh.opacities[0].values.size()); + const size_t numPointOpacities = + std::min(currentMeshPointsSize, mesh.opacities[0].values.size()); memcpy(totalMesh.opacity.data() + offset, mesh.opacities[0].values.data(), numPointOpacities * sizeof(mesh.opacities[0].values[0])); @@ -139,17 +109,16 @@ traverseNodesAndAggregateMeshes(const UsdData& usd, { const Node& node = usd.nodes[nodeIndex]; GfMatrix4d modelMatrix = node.worldTransform * correctionTransform; - + for (int meshIndex : node.staticMeshes) { const Mesh& mesh = usd.meshes[meshIndex]; if (!mesh.asGsplats) continue; aggregateMeshInstance(totalMesh, mesh, modelMatrix); } - + for (size_t i = 0; i < node.children.size(); ++i) { - traverseNodesAndAggregateMeshes( - usd, totalMesh, correctionTransform, node.children[i]); + traverseNodesAndAggregateMeshes(usd, totalMesh, correctionTransform, node.children[i]); } } @@ -189,7 +158,8 @@ exportSpz(const UsdData& usd, spz::GaussianCloud& gaussianCloud) std::size_t numGsplatsSHCoeffs = 0; for (size_t i = 0; i < usd.rootNodes.size(); ++i) { - numGsplatsSHCoeffs = std::max(numGsplatsSHCoeffs, findMaxSHCoeffSize(usd, usd.rootNodes[i])); + numGsplatsSHCoeffs = + std::max(numGsplatsSHCoeffs, findMaxSHCoeffSize(usd, usd.rootNodes[i])); } // We only store SH coefficients up to the degree with complete bands (i.e., 0, 9, 24, or 45 @@ -201,8 +171,7 @@ exportSpz(const UsdData& usd, spz::GaussianCloud& gaussianCloud) totalMesh.shCoeffs.resize(numGsplatsSHCoeffs); for (size_t i = 0; i < usd.rootNodes.size(); ++i) { - traverseNodesAndAggregateMeshes( - usd, totalMesh, correctionTransform, usd.rootNodes[i]); + traverseNodesAndAggregateMeshes(usd, totalMesh, correctionTransform, usd.rootNodes[i]); } gaussianCloud.numPoints = totalMesh.points.size(); diff --git a/spz/src/spzExport.h b/spz/src/spzExport.h index 30fd7434..62265bdf 100644 --- a/spz/src/spzExport.h +++ b/spz/src/spzExport.h @@ -10,8 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once -#include #include +#include namespace adobe::usd { diff --git a/spz/src/spzImport.cpp b/spz/src/spzImport.cpp index cac3a641..b6717c04 100644 --- a/spz/src/spzImport.cpp +++ b/spz/src/spzImport.cpp @@ -13,27 +13,17 @@ governing permissions and limitations under the License. #include "debugCodes.h" #include #include +#include +#include #include #include #include #include #include -#include -#include #include -#include -#include -#include -#include #include -#include #include -#include -#include -#include -#include -#include -#include +#include using namespace PXR_NS; using namespace spz; @@ -125,7 +115,7 @@ importSpz(const ImportSpzOptions& options, const spz::GaussianCloud& gaussianClo // 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 + // 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; for (size_t i = 0; i < shCoeffs.values.size(); i++) { diff --git a/spz/src/spzImport.h b/spz/src/spzImport.h index e4dc8d63..5ac0dcbc 100644 --- a/spz/src/spzImport.h +++ b/spz/src/spzImport.h @@ -10,8 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #pragma once -#include #include +#include namespace adobe::usd { diff --git a/stl/src/CMakeLists.txt b/stl/src/CMakeLists.txt index bcda4d0e..38f603de 100644 --- a/stl/src/CMakeLists.txt +++ b/stl/src/CMakeLists.txt @@ -20,6 +20,7 @@ PRIVATE target_include_directories(usdStl PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}" ) @@ -37,66 +38,6 @@ PRIVATE fileformatUtils ) -target_precompile_headers(usdStl -PRIVATE - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" -) - # Installation of plugin files mimics the file structure that USD has for plugins, # so it is easy to deploy it in a pre-existing USD build, if one chooses to do so. @@ -111,17 +52,19 @@ set_target_properties(usdStl PROPERTIES RESOURCE ${CMAKE_CURRENT_BINARY_DIR}/plu set_target_properties(usdStl PROPERTIES RESOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json:plugInfo.json") +# USDSTL_DESTINATION is set in the parent scope by the add_usd_fileformat macro + if(USDSTL_ENABLE_INSTALL) install( TARGETS usdStl - RUNTIME DESTINATION plugin/usd COMPONENT Runtime - LIBRARY DESTINATION plugin/usd COMPONENT Runtime - RESOURCE DESTINATION plugin/usd/usdStl/resources COMPONENT Runtime + RUNTIME DESTINATION ${USDSTL_DESTINATION} COMPONENT Runtime + LIBRARY DESTINATION ${USDSTL_DESTINATION} COMPONENT Runtime + RESOURCE DESTINATION ${USDSTL_DESTINATION}/usdStl/resources COMPONENT Runtime ) install( FILES plugInfo.root.json - DESTINATION plugin/usd + DESTINATION ${USDSTL_DESTINATION} RENAME plugInfo.json COMPONENT Runtime ) diff --git a/stl/src/api.h b/stl/src/api.h index d29c3b17..301c37bb 100644 --- a/stl/src/api.h +++ b/stl/src/api.h @@ -14,19 +14,19 @@ governing permissions and limitations under the License. #include "pxr/base/arch/export.h" #if defined(PXR_STATIC) -# define USDSTL_API -# define USDSTL_API_TEMPLATE_CLASS(...) -# define USDSTL_API_TEMPLATE_STRUCT(...) -# define USDSTL_LOCAL +#define USDSTL_API +#define USDSTL_API_TEMPLATE_CLASS(...) +#define USDSTL_API_TEMPLATE_STRUCT(...) +#define USDSTL_LOCAL #else -# if defined(USDSTL_EXPORTS) -# define USDSTL_API ARCH_EXPORT -# define USDSTL_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) -# define USDSTL_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) -# else -# define USDSTL_API ARCH_IMPORT -# define USDSTL_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) -# define USDSTL_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) -# endif -# define USDSTL_LOCAL ARCH_HIDDEN +#if defined(USDSTL_EXPORTS) +#define USDSTL_API ARCH_EXPORT +#define USDSTL_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define USDSTL_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#else +#define USDSTL_API ARCH_IMPORT +#define USDSTL_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define USDSTL_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#endif +#define USDSTL_LOCAL ARCH_HIDDEN #endif \ No newline at end of file diff --git a/stl/src/fileFormat.cpp b/stl/src/fileFormat.cpp index b6f246ae..6549a1c3 100644 --- a/stl/src/fileFormat.cpp +++ b/stl/src/fileFormat.cpp @@ -20,7 +20,6 @@ governing permissions and limitations under the License. #include #include -#include #include PXR_NAMESPACE_OPEN_SCOPE @@ -122,14 +121,16 @@ UsdStlFileFormat::WriteToString(const SdfLayer& layer, const std::string& comment) const { // Write USD as SBSM: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToString(layer, str, comment); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToString(layer, str, comment); } bool UsdStlFileFormat::WriteToStream(const SdfSpecHandle& spec, std::ostream& out, size_t indent) const { // Write USD as SBSM: Defer to the usda file format for now. - return SdfFileFormat::FindById(UsdUsdaFileFormatTokens->Id)->WriteToStream(spec, out, indent); + return SdfFileFormat::FindById(FileFormatsUsdaFileFormatTokensId) + ->WriteToStream(spec, out, indent); } PXR_NAMESPACE_CLOSE_SCOPE diff --git a/stl/src/fileFormat.h b/stl/src/fileFormat.h index a7cb90f4..ae34f99d 100644 --- a/stl/src/fileFormat.h +++ b/stl/src/fileFormat.h @@ -11,8 +11,8 @@ governing permissions and limitations under the License. */ #pragma once #include "api.h" -#include #include +#include #include #include #include @@ -29,7 +29,7 @@ TF_DECLARE_WEAK_AND_REF_PTRS(UsdStlFileFormat); /// \brief SdfFileFormat specialization for working with stl files. class USDSTL_API UsdStlFileFormat : public SdfFileFormat { - public: +public: virtual bool CanRead(const std::string& file) const override; virtual bool Read(SdfLayer* layer, @@ -52,12 +52,12 @@ class USDSTL_API UsdStlFileFormat : public SdfFileFormat std::string* str, const std::string& comment = std::string()) const override; - protected: +protected: SDF_FILE_FORMAT_FACTORY_ACCESS; virtual ~UsdStlFileFormat(); UsdStlFileFormat(); - private: +private: }; PXR_NAMESPACE_CLOSE_SCOPE diff --git a/stl/src/stlExport.h b/stl/src/stlExport.h index b95f2aa3..38ce8926 100644 --- a/stl/src/stlExport.h +++ b/stl/src/stlExport.h @@ -25,7 +25,6 @@ struct ExportStlOptions StlFormat readStlExportFormat(const UsdData& data); - /// \ingroup usdstl /// \brief Export USD data to a stl model. bool diff --git a/stl/src/stlImport.cpp b/stl/src/stlImport.cpp index 33c7db9e..34094b59 100644 --- a/stl/src/stlImport.cpp +++ b/stl/src/stlImport.cpp @@ -13,24 +13,8 @@ governing permissions and limitations under the License. #include "stlModel.h" #include -#include #include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include using namespace PXR_NS; using namespace adobe; diff --git a/stl/src/stlImport.h b/stl/src/stlImport.h index 877d0d91..561e1f63 100644 --- a/stl/src/stlImport.h +++ b/stl/src/stlImport.h @@ -19,7 +19,6 @@ using namespace adobe::usd; namespace usdStl { - /// \ingroup usdstl /// \brief Import stl data into a USD data cache. bool diff --git a/stl/src/stlModel.cpp b/stl/src/stlModel.cpp index 497ff46d..c85865d4 100644 --- a/stl/src/stlModel.cpp +++ b/stl/src/stlModel.cpp @@ -15,6 +15,7 @@ governing permissions and limitations under the License. #include #include #include +#include using namespace PXR_NS; diff --git a/stl/src/stlModel.h b/stl/src/stlModel.h index 9c9dd713..0cd273a2 100644 --- a/stl/src/stlModel.h +++ b/stl/src/stlModel.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 +#include #include #include #include @@ -57,8 +58,7 @@ struct StlVec3f : x(_x) , y(_y) , z(_z) - { - } + {} }; struct StlFacet @@ -69,10 +69,10 @@ struct StlFacet class StlModel { - private: +private: std::vector facets; - public: +public: void AddFacet(StlFacet facet); StlFacet GetFacet(int facetIndex) const; int FacetCount() const; diff --git a/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly.jpg b/test/baseline/Darwin/fbx/Megaphone_01_Lowpoly.jpg index f53b46a8..ebda7b53 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/SimpleRobotwithUdims.jpg b/test/baseline/Darwin/fbx/SimpleRobotwithUdims.jpg index 44784720..a9db21d4 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/cube-colors.jpg b/test/baseline/Darwin/fbx/cube-colors.jpg index 846780b1..0c5cdebe 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.jpg b/test/baseline/Darwin/fbx/cube.jpg index 33dd6353..5e419500 100644 Binary files a/test/baseline/Darwin/fbx/cube.jpg and b/test/baseline/Darwin/fbx/cube.jpg differ diff --git a/test/baseline/Linux/fbx/Megaphone_01_Lowpoly.jpg b/test/baseline/Linux/fbx/Megaphone_01_Lowpoly.jpg index 8755f63e..15edf444 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 d9bd6f72..1b153f90 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 6a35ad6c..9692f928 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 76ae3340..ae07415a 100644 Binary files a/test/baseline/Linux/fbx/cube.jpg and b/test/baseline/Linux/fbx/cube.jpg differ diff --git a/test/baseline/Windows/fbx/Megaphone_01_Lowpoly.jpg b/test/baseline/Windows/fbx/Megaphone_01_Lowpoly.jpg index 8755f63e..15edf444 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 d9bd6f72..1b153f90 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 6a35ad6c..9692f928 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 76ae3340..ae07415a 100644 Binary files a/test/baseline/Windows/fbx/cube.jpg and b/test/baseline/Windows/fbx/cube.jpg differ diff --git a/test/full_versions.json b/test/full_versions.json index fa8d7d88..7e50e9bd 100644 --- a/test/full_versions.json +++ b/test/full_versions.json @@ -1,22 +1,33 @@ { - "include": [ - {"os": "windows-2022", "usd_version": "2308"}, - {"os": "windows-2022", "usd_version": "2311"}, - {"os": "windows-2022", "usd_version": "2405"}, - {"os": "windows-2022", "usd_version": "2408"}, - {"os": "windows-2022", "usd_version": "2411"}, - {"os": "macOS-13", "usd_version": "2308"}, - {"os": "macOS-13", "usd_version": "2311"}, - {"os": "macOS-13", "usd_version": "2405"}, - {"os": "macOS-13", "usd_version": "2408"}, - {"os": "macOS-13", "usd_version": "2411"}, - {"os": "macOS-14", "usd_version": "2405"}, - {"os": "macOS-14", "usd_version": "2408"}, - {"os": "macOS-14", "usd_version": "2411"}, - {"os": "ubuntu-22.04", "usd_version": "2308"}, - {"os": "ubuntu-22.04", "usd_version": "2311"}, - {"os": "ubuntu-22.04", "usd_version": "2405"}, - {"os": "ubuntu-22.04", "usd_version": "2408"}, - {"os": "ubuntu-22.04", "usd_version": "2411"} - ] + "include": [ + { "os": "windows-2022", "usd_version": "2308" }, + { "os": "windows-2022", "usd_version": "2311" }, + { "os": "windows-2022", "usd_version": "2405" }, + { "os": "windows-2022", "usd_version": "2408" }, + { "os": "windows-2022", "usd_version": "2411" }, + { "os": "windows-2022", "usd_version": "2508" }, + { "os": "windows-2022", "usd_version": "2511" }, + + { "os": "windows-2025", "usd_version": "2508" }, + { "os": "windows-2025", "usd_version": "2511" }, + + { "os": "macOS-14", "usd_version": "2408" }, + { "os": "macOS-14", "usd_version": "2411" }, + { "os": "macOS-14", "usd_version": "2508" }, + { "os": "macOS-14", "usd_version": "2511" }, + + { "os": "macOS-15", "usd_version": "2408" }, + { "os": "macOS-15", "usd_version": "2411" }, + { "os": "macOS-15", "usd_version": "2508" }, + { "os": "macOS-15", "usd_version": "2511" }, + + { "os": "macos-26", "usd_version": "2508" }, + { "os": "macos-26", "usd_version": "2511" }, + + { "os": "ubuntu-22.04", "usd_version": "2308" }, + { "os": "ubuntu-22.04", "usd_version": "2311" }, + { "os": "ubuntu-22.04", "usd_version": "2405" }, + { "os": "ubuntu-22.04", "usd_version": "2408" }, + { "os": "ubuntu-22.04", "usd_version": "2411" } + ] } diff --git a/test/pr_versions.json b/test/pr_versions.json index 3c390640..beb413d5 100644 --- a/test/pr_versions.json +++ b/test/pr_versions.json @@ -1,12 +1,21 @@ { - "include": [ - {"os": "windows-2022", "usd_version": "2308"}, - {"os": "windows-2022", "usd_version": "2411"}, - {"os": "macOS-13", "usd_version": "2308"}, - {"os": "macOS-13", "usd_version": "2411"}, - {"os": "macOS-14", "usd_version": "2408"}, - {"os": "macOS-14", "usd_version": "2411"}, - {"os": "ubuntu-22.04", "usd_version": "2308"}, - {"os": "ubuntu-22.04", "usd_version": "2411"} - ] + "include": [ + { "os": "windows-2022", "usd_version": "2508" }, + { "os": "windows-2022", "usd_version": "2511" }, + + { "os": "windows-2025", "usd_version": "2508" }, + { "os": "windows-2025", "usd_version": "2511" }, + + { "os": "macOS-14", "usd_version": "2508" }, + { "os": "macOS-14", "usd_version": "2511" }, + + { "os": "macOS-15", "usd_version": "2508" }, + { "os": "macOS-15", "usd_version": "2511" }, + + { "os": "macos-26", "usd_version": "2508" }, + { "os": "macos-26", "usd_version": "2511" }, + + { "os": "ubuntu-22.04", "usd_version": "2508" }, + { "os": "ubuntu-22.04", "usd_version": "2511" } + ] } diff --git a/test/test.py b/test/test.py index d3ffde98..77dc2b73 100644 --- a/test/test.py +++ b/test/test.py @@ -80,7 +80,7 @@ def render(file, outputfile): def run_usdchecker(file, results_file): """ - run the usdchecdker on the file and save the results + run the usdchecker on the file and save the results Parameters: file (str): File path to the asset to be checked. results_file (str): File path where the results should be saved. diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 9c209245..6d0572bd 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -13,18 +13,32 @@ endif() add_library(fileformatUtils SHARED) target_compile_definitions(fileformatUtils PRIVATE USDFFUTILS_EXPORTS) +if(DEFINED USD_FILEFORMATS_DEFAULT_WRITE_USDPREVIEWSURFACE) + target_compile_definitions(fileformatUtils PRIVATE + USD_FILEFORMATS_DEFAULT_WRITE_USDPREVIEWSURFACE=$) +endif() +if(DEFINED USD_FILEFORMATS_DEFAULT_WRITE_ASM) + target_compile_definitions(fileformatUtils PRIVATE + USD_FILEFORMATS_DEFAULT_WRITE_ASM=$) +endif() +if(DEFINED USD_FILEFORMATS_DEFAULT_WRITE_OPENPBR) + target_compile_definitions(fileformatUtils PRIVATE + USD_FILEFORMATS_DEFAULT_WRITE_OPENPBR=$) +endif() usd_plugin_compile_config(fileformatUtils) -set(HEADERS +set(_UTILS_HEADERS "assetresolver.h" "common.h" "debugCodes.h" + "featureFlags.h" "dictencoder.h" "geometry.h" "transforms.h" "images.h" "layerRead.h" "layerReadMaterial.h" + "layerReadMaterialUtils.h" "layerWriteShared.h" "layerWriteMaterial.h" "layerWriteOpenPBR.h" @@ -37,15 +51,17 @@ set(HEADERS "usdData.h" ) -set(SOURCES +set(_UTILS_SOURCES "assetresolver.cpp" "common.cpp" "dictencoder.cpp" + "featureFlags.cpp" "geometry.cpp" "transforms.cpp" "images.cpp" "layerRead.cpp" "layerReadMaterial.cpp" + "layerReadMaterialUtils.cpp" "layerWriteShared.cpp" "layerWriteMaterial.cpp" "layerWriteOpenPBR.cpp" @@ -59,24 +75,25 @@ set(SOURCES ) if (USD_FILEFORMATS_ENABLE_PLY OR USD_FILEFORMATS_ENABLE_SPZ) - list(APPEND HEADERS + list(APPEND _UTILS_HEADERS "gsplatHelper.h" ) - list(APPEND SOURCES + list(APPEND _UTILS_SOURCES "gsplatHelper.cpp" ) endif() # Prepend paths -list(TRANSFORM HEADERS PREPEND "include/fileformatutils/") -list(TRANSFORM SOURCES PREPEND "src/") +list(TRANSFORM _UTILS_HEADERS PREPEND "include/fileformatutils/") +list(TRANSFORM _UTILS_SOURCES PREPEND "src/") + # Add sources to target target_sources(fileformatUtils PRIVATE "README.md" - ${HEADERS} - ${SOURCES} + ${_UTILS_HEADERS} + ${_UTILS_SOURCES} ) target_include_directories(fileformatUtils diff --git a/utils/include/fileformatutils/api.h b/utils/include/fileformatutils/api.h index 61749dfd..2dfbcabb 100644 --- a/utils/include/fileformatutils/api.h +++ b/utils/include/fileformatutils/api.h @@ -14,19 +14,19 @@ governing permissions and limitations under the License. #include "pxr/base/arch/export.h" #if defined(PXR_STATIC) -# define USDFFUTILS_API -# define USDFFUTILS_API_TEMPLATE_CLASS(...) -# define USDFFUTILS_API_TEMPLATE_STRUCT(...) -# define USDFFUTILS_LOCAL +#define USDFFUTILS_API +#define USDFFUTILS_API_TEMPLATE_CLASS(...) +#define USDFFUTILS_API_TEMPLATE_STRUCT(...) +#define USDFFUTILS_LOCAL #else -# if defined(USDFFUTILS_EXPORTS) -# define USDFFUTILS_API ARCH_EXPORT -# define USDFFUTILS_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) -# define USDFFUTILS_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) -# else -# define USDFFUTILS_API ARCH_IMPORT -# define USDFFUTILS_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) -# define USDFFUTILS_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) -# endif -# define USDFFUTILS_LOCAL ARCH_HIDDEN +#if defined(USDFFUTILS_EXPORTS) +#define USDFFUTILS_API ARCH_EXPORT +#define USDFFUTILS_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define USDFFUTILS_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#else +#define USDFFUTILS_API ARCH_IMPORT +#define USDFFUTILS_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define USDFFUTILS_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#endif +#define USDFFUTILS_LOCAL ARCH_HIDDEN #endif diff --git a/utils/include/fileformatutils/assetresolver.h b/utils/include/fileformatutils/assetresolver.h index b20b0372..6cf66fcb 100644 --- a/utils/include/fileformatutils/assetresolver.h +++ b/utils/include/fileformatutils/assetresolver.h @@ -12,8 +12,8 @@ governing permissions and limitations under the License. #pragma once #include "pxr/usd/ar/packageResolver.h" #include "usdData.h" -#include #include +#include namespace adobe::usd { @@ -27,9 +27,11 @@ struct AssetMap }; // Singleton class for managing asset caching. -class AssetCacheSingleton { +class AssetCacheSingleton +{ public: - static AssetCacheSingleton& getInstance() { + static AssetCacheSingleton& getInstance() + { static AssetCacheSingleton instance; // Instantiated once, when first accessed. return instance; } @@ -45,10 +47,11 @@ class AssetCacheSingleton { void populateCache(const std::string& resolvedPackagePath, std::vector&& images); // acquire the asset map for a specific package - AssetMap* acquireAssetMap(const std::string& resolvedPackagePath, - const std::string& resolvedPackagedPath, - std::stringstream& ss, - std::function&)> readCache); + AssetMap* acquireAssetMap( + const std::string& resolvedPackagePath, + const std::string& resolvedPackagedPath, + std::stringstream& ss, + std::function&)> readCache); private: AssetCacheSingleton() = default; diff --git a/utils/include/fileformatutils/common.h b/utils/include/fileformatutils/common.h index b7cf04f9..7acd5568 100644 --- a/utils/include/fileformatutils/common.h +++ b/utils/include/fileformatutils/common.h @@ -21,6 +21,14 @@ governing permissions and limitations under the License. #include +#if PXR_VERSION >= 2508 +#include +#define FileFormatsUsdaFileFormatTokensId SdfUsdaFileFormatTokens->Id +#else +#include +#define FileFormatsUsdaFileFormatTokensId UsdUsdaFileFormatTokens->Id +#endif + /// We defined these tokens to skip linking to usd imaging, which is heavy. // XXX Split this list into categories for easier maintenance // clang-format off @@ -54,15 +62,31 @@ governing permissions and limitations under the License. (sRGB) \ (st) \ (in) \ + (in1) \ + (in2) \ + (bg) \ + (fg) \ + (mix) \ (file) \ (scale) \ (bias) \ (fallback) \ (rotation) \ (translation) \ + (index) \ + (rotate) \ + (offset) \ (normals) \ (tangents) \ (varname) \ + (texcoord) \ + (uaddressmode) \ + (vaddressmode) \ + ((defaultValue, "default")) \ + (outx) \ + (outy) \ + (outz) \ + (outw) \ (UsdUVTexture) \ (UsdPrimvarReader_float2) \ (UsdTransform2d) \ @@ -79,12 +103,15 @@ governing permissions and limitations under the License. (transmission) \ (min) \ (max) \ - (originalColorSpace) + (originalColorSpace) \ + (AmbientOcclusionAsColor) \ + (AmbientOcclusionBaseColor) // clang-format on /// Tokens for MaterialX nodes // clang-format off #define MATERIAL_X_TOKENS \ + (mtlx) \ (OpenPBR) \ (srgb_texture) \ (ND_image_vector4) \ @@ -97,14 +124,23 @@ governing permissions and limitations under the License. (ND_multiply_color3) \ (ND_multiply_vector2) \ (ND_multiply_float) \ + (ND_mix_color3) \ (ND_add_vector3) \ (ND_add_color3) \ (ND_add_vector2) \ (ND_add_float) \ + (ND_subtract_float) \ (ND_place2d_vector2) \ (ND_separate4_vector4) \ (ND_convert_float_color3) \ + (ND_convert_color3_vector3) \ (ND_normalmap) \ + (ND_UsdUVTexture_23) \ + (ND_displacement_float) \ + (ND_geompropvalue_vector2) \ + (geomprop) \ + (periodic) \ + (clamp) \ (ND_open_pbr_surface_surfaceshader) // clang-format on @@ -232,10 +268,17 @@ governing permissions and limitations under the License. (baseWeight) \ (coatDarkening) \ (coatRoughnessAnisotropy) \ + (coatNormal) \ (coatTangent) \ (emissionLuminance) \ (fuzzWeight) \ + (fuzzColor) \ + (fuzzRoughness) \ + (specularRoughness) \ (specularWeight) \ + (specularRoughnessAnisotropy) \ + (subsurfaceColor) \ + (subsurfaceRadius) \ (subsurfaceRadiusScale) \ (subsurfaceScatterAnisotropy) \ (subsurfaceWeight) \ @@ -244,6 +287,9 @@ governing permissions and limitations under the License. (thinFilmThickness) \ (thinFilmWeight) \ (thinWalled) \ + (transmissionWeight) \ + (transmissionColor) \ + (transmissionDepth) \ (transmissionDispersionAbbeNumber) \ (transmissionDispersionScale) \ (transmissionScatter) \ @@ -292,6 +338,7 @@ governing permissions and limitations under the License. // 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); @@ -304,19 +351,19 @@ TF_DECLARE_PUBLIC_TOKENS(AdobeNgpTokens, USDFFUTILS_API, ADOBE_NGP_TOKENS); TF_DECLARE_PUBLIC_TOKENS(AdobeGsplatBaseTokens, USDFFUTILS_API, ADOBE_GSPLAT_BASE_TOKENS); PXR_NAMESPACE_CLOSE_SCOPE -#define VOID_GUARD(x, ...) \ - { \ - if ((x) == false) { \ - TF_RUNTIME_ERROR(__VA_ARGS__); \ - return; \ - } \ +#define VOID_GUARD(x, ...) \ + { \ + if ((x) == false) { \ + TF_RUNTIME_ERROR(__VA_ARGS__); \ + return; \ + } \ } -#define GUARD(x, ...) \ - { \ - if ((x) == false) { \ - TF_RUNTIME_ERROR(__VA_ARGS__); \ - return false; \ - } \ +#define GUARD(x, ...) \ + { \ + if ((x) == false) { \ + TF_RUNTIME_ERROR(__VA_ARGS__); \ + return false; \ + } \ } namespace adobe::usd { @@ -349,31 +396,31 @@ argComposeFloatArray(const PXR_NS::PcpDynamicFileFormatContext& context, const PXR_NS::TfToken& token, const std::string& debugTag); -void USDFFUTILS_API +bool USDFFUTILS_API argReadString(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, std::string& target, const std::string& debugTag); -void USDFFUTILS_API +bool USDFFUTILS_API argReadString(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, PXR_NS::TfToken& target, const std::string& debugTag); -void USDFFUTILS_API +bool USDFFUTILS_API argReadBool(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, bool& target, const std::string& debugTag); -void USDFFUTILS_API +bool USDFFUTILS_API argReadFloat(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, float& target, const std::string& debugTag); -void USDFFUTILS_API +bool USDFFUTILS_API argReadFloatArray(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, PXR_NS::VtFloatArray& target, @@ -419,7 +466,47 @@ split(const std::string& str, char delimiter); bool USDFFUTILS_API createDirectory(const std::filesystem::path& directoryPath); +/** + * Writes out a block of data at a given path + * + * @param assetsPath The filepath to write the data + * @param data The file data. This buffer must be at least size bytes + * @param size The size of the raw data buffer + * + * @return Whether the image was successfully written + */ +bool USDFFUTILS_API +writeDataToDisk(const std::filesystem::path& filepath, const void* data, size_t size); + std::string USDFFUTILS_API getLayerFilePath(const std::string& layerIdentifier); +std::filesystem::path USDFFUTILS_API +convertStringToPath(const std::string& str); + +#if __cplusplus >= 202002L +std::filesystem::path USDFFUTILS_API +convertStringToPath(const std::u8string& str); +#endif + +std::string USDFFUTILS_API +convertPathToString(const std::filesystem::path& path); + +/// converts u8 literal to a std::string +#if __cplusplus >= 202002L +// C++20: u8"..." → const char8_t*, need reinterpret_cast +inline std::string USDFFUTILS_API +u8_literal(const char8_t* s) +{ + return std::string(reinterpret_cast(s)); +} +#else +// C++17: u8"..." → const char*, no cast needed +inline std::string USDFFUTILS_API +u8_literal(const char* s) +{ + return std::string(s); +} +#endif + } diff --git a/utils/include/fileformatutils/dictencoder.h b/utils/include/fileformatutils/dictencoder.h index cf85f504..f710aee0 100644 --- a/utils/include/fileformatutils/dictencoder.h +++ b/utils/include/fileformatutils/dictencoder.h @@ -16,7 +16,9 @@ governing permissions and limitations under the License. namespace adobe::usd { -USDFFUTILS_API void writeDict(const PXR_NS::VtDictionary& dict, std::ostream& output); -USDFFUTILS_API PXR_NS::VtDictionary readDict(std::istream& input); +USDFFUTILS_API void +writeDict(const PXR_NS::VtDictionary& dict, std::ostream& output); +USDFFUTILS_API PXR_NS::VtDictionary +readDict(std::istream& input); } \ No newline at end of file diff --git a/utils/include/fileformatutils/featureFlags.h b/utils/include/fileformatutils/featureFlags.h new file mode 100644 index 00000000..633c024f --- /dev/null +++ b/utils/include/fileformatutils/featureFlags.h @@ -0,0 +1,108 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +#pragma once +#include "api.h" + +#include + +/// Runtime feature flags for USD file format plugins. +/// +/// These use OpenUSD's TfEnvSetting to allow toggling behavior at runtime via +/// environment variables, without requiring a separate branch or recompilation. +/// +/// There are two categories of flags: +/// +/// 1. Material model configuration -- controls which material representations +/// are written by default. These are not experimental; they are stable +/// configuration knobs. +/// +/// 2. Experimental feature flags -- gates for in-development code paths that +/// are not yet ready for production use. +/// +/// === Adding a new feature flag === +/// +/// 1. Declare the flag in this header (inside PXR_NAMESPACE): +/// +/// extern PXR_NS::TfEnvSetting USD_FILEFORMATS_MY_FLAG; +/// +/// 2. Define the flag in featureFlags.cpp: +/// +/// TF_DEFINE_ENV_SETTING(USD_FILEFORMATS_MY_FLAG, false, +/// "Description of what this flag controls"); +/// +/// 3. Use the flag in plugin code: +/// +/// #include +/// +/// if (adobe::usd::isFeatureEnabled(USD_FILEFORMATS_MY_FLAG)) { +/// // alternative code path +/// } +/// +/// === Activating at runtime === +/// +/// export USD_FILEFORMATS_MY_FLAG=1 +/// +/// Or via PIXAR_TF_ENV_SETTING_FILE (see TfEnvSetting docs). + +PXR_NAMESPACE_OPEN_SCOPE + +// --------------------------------------------------------------------------- +// Material model configuration +// +// These flags control which material representations are written by default. +// They are fully independent -- any combination is valid (0=off, 1=on). +// File format arguments (e.g. "writeOpenPBR=true") still override these +// defaults on a per-file basis. +// --------------------------------------------------------------------------- + +/// When true, UsdPreviewSurface material networks are written. +extern PXR_NS::TfEnvSetting USD_FILEFORMATS_WRITE_USDPREVIEWSURFACE; + +/// When true, AdobeStandardMaterial (ASM) material networks are written. +extern PXR_NS::TfEnvSetting USD_FILEFORMATS_WRITE_ASM; + +/// When true, OpenPBR / MaterialX material networks are written. +extern PXR_NS::TfEnvSetting USD_FILEFORMATS_WRITE_OPENPBR; + +// --------------------------------------------------------------------------- +// Experimental feature flags +// +// These gate in-development code paths. Each flag should be specific enough +// that its name reflects what it affects. +// --------------------------------------------------------------------------- + +/// Gates experimental OpenPBR processing code paths (e.g. new writer/reader +/// logic that is not yet production-ready). This is separate from the material +/// model selection above -- USD_FILEFORMATS_WRITE_OPENPBR controls *whether* +/// OpenPBR is written, while this flag controls *how* it is processed. +extern PXR_NS::TfEnvSetting USD_FILEFORMATS_EXPERIMENTAL_OPENPBR_PROCESSING; + +PXR_NAMESPACE_CLOSE_SCOPE + +namespace adobe::usd { + +/// Query whether a specific feature flag is enabled. +/// Wraps TfGetEnvSetting for a consistent, readable call site. +template +inline T +isFeatureEnabled(PXR_NS::TfEnvSetting& setting) +{ + return PXR_NS::TfGetEnvSetting(setting); +} + +/// Apply material model defaults from environment variables to the three +/// write-material booleans. Call this in the default constructor of any struct +/// that carries writeUsdPreviewSurface / writeASM / writeOpenPBR fields. +USDFFUTILS_API void +applyMaterialModelDefaults(bool& writeUsdPreviewSurface, bool& writeASM, bool& writeOpenPBR); + +} diff --git a/utils/include/fileformatutils/geometry.h b/utils/include/fileformatutils/geometry.h index 164dcbbc..457f5ddb 100644 --- a/utils/include/fileformatutils/geometry.h +++ b/utils/include/fileformatutils/geometry.h @@ -81,6 +81,12 @@ checkAndPrintMeshIssues(const UsdData& usdData); USDFFUTILS_API void createTriangulationIndices(Mesh& mesh); +/// \ingroup utils_geometry +/// \brief Compute smooth vertex normals for a mesh by averaging face normals at shared vertices. +/// The generated normals are vertex-interpolated. +USDFFUTILS_API void +computeSmoothNormals(Mesh& mesh); + /// \ingroup utils_geometry /// \brief Triangulate an existing mesh with all its primvars and subsets. // Note, the triangulation is done with a simple fan triangulation and hence diff --git a/utils/include/fileformatutils/images.h b/utils/include/fileformatutils/images.h index a3b344c3..f7398e78 100644 --- a/utils/include/fileformatutils/images.h +++ b/utils/include/fileformatutils/images.h @@ -33,7 +33,7 @@ namespace adobe::usd { /// class USDFFUTILS_API Image { - public: +public: int width; int height; int channels; diff --git a/utils/include/fileformatutils/layerRead.h b/utils/include/fileformatutils/layerRead.h index 9eb115b8..34717608 100644 --- a/utils/include/fileformatutils/layerRead.h +++ b/utils/include/fileformatutils/layerRead.h @@ -51,6 +51,7 @@ struct USDFFUTILS_API ReadLayerContext std::vector> subsetMaterialBindings; PXR_NS::UsdGeomXformCache xformCache; std::string debugTag; + bool warnAboutMissingAssets = true; }; /// \ingroup utils_layer diff --git a/utils/include/fileformatutils/layerReadMaterial.h b/utils/include/fileformatutils/layerReadMaterial.h index e29e868f..eccdeb45 100644 --- a/utils/include/fileformatutils/layerReadMaterial.h +++ b/utils/include/fileformatutils/layerReadMaterial.h @@ -19,6 +19,6 @@ namespace adobe::usd { /// Read Material at UsdMaterial prim USDFFUTILS_API bool -readMaterial(ReadLayerContext& ctx, const PXR_NS::UsdPrim& prim, int parent); +readMaterial(ReadLayerContext& ctx, const PXR_NS::UsdPrim& prim); } \ No newline at end of file diff --git a/utils/include/fileformatutils/layerReadMaterialUtils.h b/utils/include/fileformatutils/layerReadMaterialUtils.h new file mode 100644 index 00000000..7ae4ed67 --- /dev/null +++ b/utils/include/fileformatutils/layerReadMaterialUtils.h @@ -0,0 +1,186 @@ +/* +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 +#include +#include + +#include + +namespace adobe::usd { + +/// Reading UsdShade material networks can get complicated, so we've created some machinery here to +/// make it easier to setup the parsing of these networks. The type of network topology we expect +/// is something like this: +/// +/// +-------------+ +/// | | +/// Constant --> | | +/// | | +/// +-----------+ +---------+ | | +/// | TexCoords | --> | TexRead | --> | Surface | +/// +-----------+ +---------+ | Shader | --> Material +/// | | +/// +-----------+ +---------+ | | +/// | TexCoords | --> | TexRead | --> | | +/// +-----------+ +---------+ | | +/// | | +/// +-------------+ +/// +/// We have a central surface shader (UsdPreviewSurface, ASM or OpenPBR) and for its inputs we +/// expect either: +/// 1. No input +/// 2. A constant value (can use UsdShade connections) +/// 3. A small linear chain of shading nodes that express the reading of a texture. +/// +/// The no input (1.) and constant (2.) cases are pretty straight forward, but the texture reading +/// case (3.) can get pretty complicated, even when we assume a linear chain of nodes (no branching +/// or multiple connected inputs per node). +/// +/// The complication arises from UsdPreviewSurface and ASM using one set of nodes to express the +/// texture reading (Usd* shading nodes), while OpenPBR uses a different set of nodes (MaterialX). +/// We can have more than the depicted minimal case of two nodes in the chain. We can also have +/// optional nodes that might or might not be present. +/// +/// +/// So to make this more managable we've split the parsing of the texture chains into two concerns: +/// 1. The types of the nodes, the order in which they are expected and whether they're optional +/// 2. Extracting and converting the settings on a node and following to the next in the chain +/// +/// The 2. part is done via handler functions (ShaderHandler below), which deal with a single +/// type (or class of nodes). There needs to be one such function for each type of node we might +/// encounter. That handler is given the UsdShadeOutput that is used downstream in the network. +/// +/// \example Shader handler function for the UsdPrimvarReader node +/// ```cpp +/// bool +/// handleUsdPrimvarReader(InputContext& ctx, const UsdShadeOutput& shaderOutput) +/// { +/// UsdShadeShader shader(shaderOutput.GetPrim()); +/// +/// std::string texCoordPrimvarStr; +/// getShaderInputValue(shader, AdobeTokens->varname, texCoordPrimvarStr); +/// +/// // Rest of node processing to fill in ctx.input +/// +/// return true; +/// } +/// ``` +/// +/// The 1. part is done via a ShaderHandlerMappings vector, which is an ordered list of expected +/// shader types with their respective handler functions and whether that shader type is optional or +/// not. +/// +/// \example Shader handler mapping for Usd* type nodes on UsdPreviewSurface or ASM +/// ```cpp +/// ShaderHandlerMappings handlers = { +/// { {TfToken("UsdPrimvarReader_float2")}, handleUsdPrimvarReader }, +/// { {TfToken("UsdTransform2d")}, handleUsdTransform2d, kOptional }, +/// { {TfToken("UsdUVTexture")}, handleUsdUVTexture } +/// }; +/// ``` +/// +/// With these two pieces of information (and a bit more) an InputContext can be constructed, which +/// can then be given to readSurfaceInput() to either read a constant value or extract a textured +/// input by following a shader chain. +/// +/// \example Invoke the readSurfaceInput() function to parse an input on a surface shader +/// ```cpp +/// // Read diffuse color input and populate the material.diffuseColor Input struct +/// InputContext ctx{readLayerContext, TfToken("diffuseColor"), handlers, material.diffuseColor}; +/// readSurfaceInput(ctx, surfaceShader); +/// ``` +/// +/// The shader handler implementations can use followConnectedInput() to follow one of their inputs +/// (e.g. texture coordinate input) up the chain. + +/// Each node handler gets called with the InputContext and the output attribute on shader node +using ShaderHandler = std::function; + +/// Constant to use when declaring a shader handler as optional. This is clearer than a raw "true". +constexpr bool kOptional = true; + +/// The handler mapping struct expresses that all nodes listed in nodeNames should be handled by +/// this handler function. If isOptional is true, this type of nodes and its handler can be skipped. +/// Non-optional nodes must not be skipped. +struct USDFFUTILS_API ShaderHandlerMapping +{ + PXR_NS::TfTokenVector nodeNames; + ShaderHandler handler; + bool isOptional = false; +}; + +/// We have a linear order of expected ShaderHandlerMapping. Some mappings are optional and can be +/// skipped, but for each input we expect to travers a linear sequence of nodes in order. +using ShaderHandlerMappings = std::vector; + +/// Small context struct to simplify the node handler interface +/// * readLayerContext is needed when reading texture images +/// * surfaceInputName is the name of the original surface input we're tracing (mostly for errors) +/// * handlerMappings is used to decide which code to call when encountering a certain node type +/// * input is a reference to the Input struct we're filling in for this chain of nodes +/// * handlerIndex is the position in the handlerMappings from where the search should start +/// This index is monotonically increasing as we traverse the linear chain of nodes. +/// Optional handlers are checked, but might be skipped, and each manadatory handler is check and +/// if the right shader type is not found triggers an error. +struct USDFFUTILS_API InputContext +{ + ReadLayerContext& readLayerContext; + const PXR_NS::TfToken& surfaceInputName; + const ShaderHandlerMappings& handlerMappings; + Input& input; + uint32_t handlerIndex = 0; +}; + +/// Given an InputContext check if the surface has that input, handle the case of a constant value +/// or if connected, trigger the upstream processing. +USDFFUTILS_API bool +readSurfaceInput(InputContext& ctx, const PXR_NS::UsdShadeShader& surface); + +/// Given an InputContext, a shader and an input name, follow the connection on that input and +/// trigger the shader handling upstream. Returns false if either nothing was connected or if the +/// upstream handling failed. +/// This function is meant to be used by individual shader handlers to follow the input chain. +USDFFUTILS_API bool +followConnectedInput(InputContext& ctx, + const PXR_NS::UsdShadeShader& shader, + const PXR_NS::TfToken& inputName); + +/// Reads an image specified by the assetPath and stores it in the ReadLayerContext. +/// Returns the image index in the internal store. If the image has been found before, it returns +/// the previous index. +USDFFUTILS_API int +readImage(ReadLayerContext& ctx, const PXR_NS::SdfAssetPath& assetPath); + +/// Checks if the shader has the named input and if so extracts the associated default value. +/// Returns true if a value was extracted. Note, that this will follow UsdShade connections to +/// constant values. +template +bool +getShaderInputValue(const PXR_NS::UsdShadeShader& shader, const PXR_NS::TfToken& name, T& value) +{ + PXR_NS::UsdShadeInput input = shader.GetInput(name); + if (input) { + PXR_NS::UsdShadeAttributeVector valueAttrs = input.GetValueProducingAttributes(); + if (!valueAttrs.empty()) { + const PXR_NS::UsdAttribute& attr = valueAttrs.front(); + if (PXR_NS::UsdShadeInput::IsInput(attr)) { + return attr.Get(&value); + } + } + } + return false; +} + +} diff --git a/utils/include/fileformatutils/layerWriteOpenPBR.h b/utils/include/fileformatutils/layerWriteOpenPBR.h index 3bd86586..9b3eec11 100644 --- a/utils/include/fileformatutils/layerWriteOpenPBR.h +++ b/utils/include/fileformatutils/layerWriteOpenPBR.h @@ -13,6 +13,7 @@ governing permissions and limitations under the License. #include "api.h" #include "layerWriteShared.h" #include "sdfMaterialUtils.h" +#include "usdData.h" namespace adobe::usd { @@ -22,4 +23,16 @@ writeOpenPBR(WriteSdfContext& ctx, const OpenPbrMaterial& material, MaterialInputs& materialInputs); +// Ideally this function woulde be private, however because the sbsar format is using a pretty +// different implementation than those that use the above entry point, and we still want to share +// the implemenation of this function, we need to make it public. In the future if the sbsar plugin +// goes through a refactor, we should consider making this private again. +USDFFUTILS_API PXR_NS::SdfPath +createMaterialXTextureReader(PXR_NS::SdfAbstractData* sdfData, + const PXR_NS::SdfPath& parentPath, + const PXR_NS::TfToken& name, + const Input& input, + const PXR_NS::SdfPath& uvResultPath, + const PXR_NS::SdfPath& textureConnection); + } diff --git a/utils/include/fileformatutils/layerWriteShared.h b/utils/include/fileformatutils/layerWriteShared.h index c26e4fad..177be6f8 100644 --- a/utils/include/fileformatutils/layerWriteShared.h +++ b/utils/include/fileformatutils/layerWriteShared.h @@ -21,18 +21,22 @@ namespace adobe::usd { struct WriteLayerOptions { - WriteLayerOptions() {} + WriteLayerOptions() + { + applyMaterialModelDefaults(writeUsdPreviewSurface, writeASM, writeOpenPBR); + } WriteLayerOptions(const PXR_NS::FileFormatDataBase& fileFormatData) : writeUsdPreviewSurface(fileFormatData.writeUsdPreviewSurface) , writeASM(fileFormatData.writeASM) , writeOpenPBR(fileFormatData.writeOpenPBR) + , preserveExtraMaterialInfo(fileFormatData.preserveExtraMaterialInfo) , assetsPath(fileFormatData.assetsPath) - { - } + {} bool writeUsdPreviewSurface = true; bool writeASM = true; bool writeOpenPBR = false; + bool preserveExtraMaterialInfo = true; bool pruneJoints = false; bool animationTracks = false; bool createRenderSettingsPrim = false; @@ -135,7 +139,7 @@ struct USDFFUTILS_API OpenPbrMaterial 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 + /// displacement input. But this can be expressed in MaterialX via displacement shader and /// directly in other material models. Input displacement; @@ -186,9 +190,29 @@ struct USDFFUTILS_API OpenPbrMaterial /// /// 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 +/// transfers 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. +USDFFUTILS_API OpenPbrMaterial mapMaterialStructToOpenPbrMaterialStruct(const Material& material); +/// @brief Converts an OpenPbrMaterial struct into a Material struct +/// +/// This implements the inverse of mapMaterialStructToOpenPbrMaterialStruct() +USDFFUTILS_API Material +mapOpenPbrMaterialStructToMaterialStruct(const OpenPbrMaterial& material); + +/// @brief Create custom attributes to carry extra non-OpenPBR fields +/// +/// This covers: normalScale, useSpecularWorkflow, opacityThreshold, clearcoatModelsTransmissionTint +/// and isUnlit +USDFFUTILS_API void +createExtraConstantAttribute(PXR_NS::SdfAbstractData* sdfData, + const OpenPbrMaterial& material, + const PXR_NS::SdfPath& surfaceShaderPath); + +/// OpenPBR emission values are in nits, but ASM value are not scaled to the surface area. As an +/// approximate conversion we apply a factor to get the output into a usable range. +static constexpr float kAsmToOpenPbrEmissionFactor = 1000.0f; +static constexpr float kOpenPbrToAsmEmissionFactor = 1.0f / kAsmToOpenPbrEmissionFactor; + } diff --git a/utils/include/fileformatutils/materials.h b/utils/include/fileformatutils/materials.h index e4f98c3d..50d3bd4c 100644 --- a/utils/include/fileformatutils/materials.h +++ b/utils/include/fileformatutils/materials.h @@ -36,7 +36,7 @@ namespace adobe::usd { /// * actually generating image data is optional. class USDFFUTILS_API InputTranslator { - public: +public: /// @param[in] exportImages: Whether to actually generate image data. /// @param[in] inputImages: Input images InputTranslator(bool exportImages, @@ -141,7 +141,7 @@ class USDFFUTILS_API InputTranslator ImageFormat format, bool intermediate = false); - private: +private: std::string mDebugTag; bool mExportImages; std::unordered_map mCache; diff --git a/utils/include/fileformatutils/neuralAssetsHelper.h b/utils/include/fileformatutils/neuralAssetsHelper.h index ebe15669..1e47933f 100644 --- a/utils/include/fileformatutils/neuralAssetsHelper.h +++ b/utils/include/fileformatutils/neuralAssetsHelper.h @@ -12,8 +12,8 @@ governing permissions and limitations under the License. #pragma once #include "api.h" -#include #include +#include #include namespace adobe::usd { @@ -24,10 +24,12 @@ USDFFUTILS_API void float32ToFloat16(const float* inputData, std::uint16_t* outputData, std::size_t numElements); template -T maxOfFloatArray(const T* inputData, std::size_t numElements); +T +maxOfFloatArray(const T* inputData, std::size_t numElements); template -T infNormOfFloatArray(const T* inputData, std::size_t numElements); +T +infNormOfFloatArray(const T* inputData, std::size_t numElements); USDFFUTILS_API void unpackMLPWeight(const float* in, float* out, const std::size_t d1, const std::size_t d2); @@ -36,7 +38,9 @@ USDFFUTILS_API void packMLPWeight(const float* in, float* out, const std::size_t d1, const std::size_t d2); USDFFUTILS_API bool -decompress(const std::uint8_t* inputData, std::size_t inLen, std::vector& decompressedData); +decompress(const std::uint8_t* inputData, + std::size_t inLen, + std::vector& decompressedData); USDFFUTILS_API bool compress(const std::uint8_t* inputData, std::size_t inLen, std::vector& outputData); diff --git a/utils/include/fileformatutils/resolver.h b/utils/include/fileformatutils/resolver.h index e6c77dad..e65fc505 100644 --- a/utils/include/fileformatutils/resolver.h +++ b/utils/include/fileformatutils/resolver.h @@ -11,8 +11,8 @@ governing permissions and limitations under the License. */ #pragma once #include "api.h" -#include "pxr/usd/ar/packageResolver.h" #include "usdData.h" +#include namespace adobe::usd { @@ -28,7 +28,7 @@ namespace adobe::usd { /// class USDFFUTILS_API Resolver : public PXR_NS::ArPackageResolver { - public: +public: Resolver(const std::string& name); ~Resolver(); @@ -50,11 +50,11 @@ class USDFFUTILS_API Resolver : public PXR_NS::ArPackageResolver static void populateCache(const std::string& resolvedPackagePath, std::vector&& images); - protected: +protected: virtual void readCache(const std::string& resolvedPackagePath, std::vector& images) = 0; - private: +private: // Name of resolver std::string mName; }; diff --git a/utils/include/fileformatutils/sdfMaterialUtils.h b/utils/include/fileformatutils/sdfMaterialUtils.h index 49ac6ee6..91cfa02f 100644 --- a/utils/include/fileformatutils/sdfMaterialUtils.h +++ b/utils/include/fileformatutils/sdfMaterialUtils.h @@ -81,8 +81,7 @@ struct KeyVtValuePair KeyVtValuePair(const std::string& key, const PXR_NS::VtValue& value) : first(key) , second(value) - { - } + {} // Convenient constructor to create the key from char* and the VtValue from an arbitrarily typed // value @@ -90,8 +89,7 @@ struct KeyVtValuePair KeyVtValuePair(const char* key, const T& value) : first(key) , second(value) - { - } + {} // Convenient constructor to create the key from a TfToken and the VtValue from an arbitrarily // typed value @@ -99,8 +97,7 @@ struct KeyVtValuePair KeyVtValuePair(const PXR_NS::TfToken& key, const T& value) : first(key.GetString()) , second(value) - { - } + {} }; struct InputTypePair @@ -147,6 +144,7 @@ addMaterialInputTexture(PXR_NS::SdfAbstractData* sdfData, const PXR_NS::SdfPath& materialPath, const PXR_NS::TfToken& name, const std::string& texturePath, + bool isColorTexture, MaterialInputs& materialInputs); /// Create shader prim spec with inputs and one output @@ -203,7 +201,7 @@ struct ShaderInfo // shader definition registry (Sdr) module. Unfortunate, the ASM terminal nodes are not found there. class ShaderRegistry { - public: +public: static ShaderRegistry& getInstance() { static ShaderRegistry m_instance; @@ -235,7 +233,7 @@ class ShaderRegistry return m_openPbrInputRemapping; } - private: +private: ShaderRegistry(); ~ShaderRegistry() = default; diff --git a/utils/include/fileformatutils/sdfUtils.h b/utils/include/fileformatutils/sdfUtils.h index fff6c129..026c30f3 100644 --- a/utils/include/fileformatutils/sdfUtils.h +++ b/utils/include/fileformatutils/sdfUtils.h @@ -12,6 +12,7 @@ governing permissions and limitations under the License. #pragma once #include "api.h" +#include "featureFlags.h" #include #include @@ -172,29 +173,44 @@ setAttributeMetadata(PXR_NS::SdfAbstractData* data, /// \ingroup utils_layer /// Set the default value of an attribute +/// +/// typeName is used to ensure that the value has the type that matches the type name of the +/// property. A coding error will be issued in case of a mismatch and the default value will not be +/// set. USDFFUTILS_API void setAttributeDefaultValue(PXR_NS::SdfAbstractData* data, const PXR_NS::SdfPath& propertyPath, - const PXR_NS::VtValue& value); + const PXR_NS::VtValue& value, + const PXR_NS::SdfValueTypeName& typeName); /// \ingroup utils_layer /// Set the default value of an attribute +/// +/// typeName is used to ensure that the value has the type that matches the type name of the +/// property. A coding error will be issued in case of a mismatch and the default value will not be +/// set. USDFFUTILS_API void setAttributeDefaultValue(PXR_NS::SdfAbstractData* data, const PXR_NS::SdfPath& propertyPath, - const PXR_NS::SdfAbstractDataConstValue& value); + const PXR_NS::SdfAbstractDataConstValue& value, + const PXR_NS::SdfValueTypeName& typeName); /// \ingroup utils_layer /// Set the default value of an attribute +/// +/// typeName is used to ensure that the value has the type that matches the type name of the +/// property. A coding error will be issued in case of a mismatch and the default value will not be +/// set. template void setAttributeDefaultValue(PXR_NS::SdfAbstractData* data, const PXR_NS::SdfPath& propertyPath, - const T& value) + const T& value, + const PXR_NS::SdfValueTypeName& typeName) { const PXR_NS::SdfAbstractDataConstTypedValue inValue(&value); const PXR_NS::SdfAbstractDataConstValue& untypedInValue = inValue; - setAttributeDefaultValue(data, propertyPath, untypedInValue); + setAttributeDefaultValue(data, propertyPath, untypedInValue, typeName); } /// Set the time sampled values for an animated attribute @@ -312,7 +328,7 @@ PXR_NAMESPACE_OPEN_SCOPE /// \brief SdfData specialization. class USDFFUTILS_API FileFormatDataBase : public SdfData { - public: +public: FileFormatDataBase() { // It's very important to create the pseudo root spec right away as there are codepaths that @@ -322,11 +338,13 @@ class USDFFUTILS_API FileFormatDataBase : public SdfData // 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); + adobe::usd::applyMaterialModelDefaults(writeUsdPreviewSurface, writeASM, writeOpenPBR); }; bool writeUsdPreviewSurface = true; bool writeASM = true; bool writeOpenPBR = false; + bool preserveExtraMaterialInfo = true; std::string assetsPath; /// Parse common settings from the file format arguments diff --git a/utils/include/fileformatutils/test.h b/utils/include/fileformatutils/test.h index 74fad2c8..6747f398 100644 --- a/utils/include/fileformatutils/test.h +++ b/utils/include/fileformatutils/test.h @@ -26,6 +26,8 @@ governing permissions and limitations under the License. #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)(( \ @@ -36,22 +38,21 @@ PXR_NAMESPACE_OPEN_SCOPE TF_DECLARE_PUBLIC_TOKENS(TestTokens, USDFFUTILS_API, TEST_TOKENS); PXR_NAMESPACE_CLOSE_SCOPE -#define ASSERT_PRIM(...) assertPrim(__VA_ARGS__) -#define ASSERT_NODE(...) assertNode(__VA_ARGS__) -#define ASSERT_MESH(...) assertMesh(__VA_ARGS__) -#define ASSERT_POINTS(...) assertPoints(__VA_ARGS__) -#define ASSERT_MATERIAL(...) assertMaterial(__VA_ARGS__) -#define ASSERT_ANIMATION(...) assertAnimation(__VA_ARGS__) -#define ASSERT_CAMERA(...) assertCamera(__VA_ARGS__) -#define ASSERT_LIGHT(...) assertLight(__VA_ARGS__) -#define ASSERT_DISPLAY_NAME(...) assertDisplayName(__VA_ARGS__) -#define ASSERT_VISIBILITY(...) assertVisibility(__VA_ARGS__) +#define ASSERT_PRIM(...) ASSERT_TRUE(assertPrim(__VA_ARGS__)) +#define ASSERT_NODE(...) ASSERT_TRUE(assertNode(__VA_ARGS__)) +#define ASSERT_MESH(...) ASSERT_TRUE(assertMesh(__VA_ARGS__)) +#define ASSERT_POINTS(...) ASSERT_TRUE(assertPoints(__VA_ARGS__)) +#define ASSERT_MATERIAL(...) ASSERT_TRUE(assertMaterial(__VA_ARGS__)) +#define ASSERT_ANIMATION(...) ASSERT_TRUE(assertAnimation(__VA_ARGS__)) +#define ASSERT_CAMERA(...) ASSERT_TRUE(assertCamera(__VA_ARGS__)) +#define ASSERT_LIGHT(...) ASSERT_TRUE(assertLight(__VA_ARGS__)) +#define ASSERT_DISPLAY_NAME(...) ASSERT_TRUE(assertDisplayName(__VA_ARGS__)) +#define ASSERT_VISIBILITY(...) ASSERT_TRUE(assertVisibility(__VA_ARGS__)) #ifdef DO_RENDER -# define ASSERT_RENDER(...) assertRender(__VA_ARGS__) +#define ASSERT_RENDER(filename, imageFilename) ASSERT_TRUE(assertRender(filename, imageFilename)) #else -# define ASSERT_RENDER(...) \ - { \ - } +#define ASSERT_RENDER(...) \ + {} #endif // XXX This duplication of structs is highly suspicious @@ -165,23 +166,23 @@ struct USDFFUTILS_API LightData // ImageAsset texture }; -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertPrim(PXR_NS::UsdStageRefPtr stage, const std::string& path); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertNode(PXR_NS::UsdStageRefPtr stage, const std::string& path); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertMesh(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MeshData& data); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertPoints(PXR_NS::UsdStageRefPtr stage, const std::string& path, const PointsData& data); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertMaterial(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MaterialData& data); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertAnimation(PXR_NS::UsdStageRefPtr stage, const std::string& path, const AnimationData& data); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertCamera(PXR_NS::UsdStageRefPtr stage, const std::string& path, const CameraData& data); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightData& data); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertDisplayName(PXR_NS::UsdStageRefPtr stage, const std::string& primPath, const std::string& displayName); @@ -195,15 +196,24 @@ assertDisplayName(PXR_NS::UsdStageRefPtr stage, * @param expectedActualVisibility If the prim is expected to be visible or invisible, when the * effective visibility is computed with UsdGeomImageable::ComputeVisibility() */ -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertVisibility(PXR_NS::UsdStageRefPtr stage, const std::string& path, bool expectedVisibilityAttr, bool expectedActualVisibility); -USDFFUTILS_API void +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult assertRender(const std::string& filename, const std::string& imageFilename); +/// Compares a USD layer against a baseline USDA file. +/// If generateBaseline is true, exports the layer to the baseline path instead of comparing. +/// If dumpOnFailure is true, writes the actual output next to the baseline when comparison fails. +[[nodiscard]] USDFFUTILS_API ::testing::AssertionResult +assertUsda(const PXR_NS::SdfLayerHandle& sdfLayer, + const std::string& baselinePath, + bool generateBaseline = false, + bool dumpOnFailure = false); + template bool extractUsdAttribute(PXR_NS::UsdPrim prim, @@ -220,7 +230,7 @@ extractUsdAttribute(PXR_NS::UsdPrim prim, // Class to catch messages from the USD library class UsdDiagnosticDelegate : public PXR_NS::TfDiagnosticMgr::Delegate { - public: +public: UsdDiagnosticDelegate() { PXR_NS::TfDiagnosticMgr::GetInstance().AddDelegate(this); } ~UsdDiagnosticDelegate() override @@ -256,7 +266,7 @@ class UsdDiagnosticDelegate : public PXR_NS::TfDiagnosticMgr::Delegate 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 8beaa4df..55615aaf 100644 --- a/utils/include/fileformatutils/usdData.h +++ b/utils/include/fileformatutils/usdData.h @@ -122,7 +122,7 @@ struct USDFFUTILS_API Subset { PXR_NS::VtIntArray faces; // indices to a subset of geom faces PXR_NS::VtIntArray indices; // subset of geom indices - int material; + int material = -1; }; /// \ingroup utils_geometry @@ -163,6 +163,7 @@ struct USDFFUTILS_API Mesh PXR_NS::VtFloatArray weights; int material = -1; std::vector subsets; + PXR_NS::VtIntArray patchIds; bool doubleSided = false; bool instanceable = false; bool asPoints = false; @@ -296,10 +297,11 @@ enum USDFFUTILS_API ImageFormat struct USDFFUTILS_API ImageAsset { + // name acts like a display name, whereas uri is the string used in brackets in unresolved USD + // packages. (example: if USD has @asset.fbx[image.png]@, uri would be image.png) std::string name; - // Images are referenced differently than nodes, so they do not have display names - std::string uri; + ImageFormat format = ImageFormatUnknown; std::vector image; }; @@ -340,6 +342,17 @@ 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); +// In order to decode tangent space normals from a normal map that is stored as [0,1] colors, the +// `n = 2*c - 1` formula is applied. As scale and bias on the texture node, this manifests as these +// constants. +// +// Note that there are two common encoding convention. The one used by USD and MaterialX is the +// OpenGL convention. The DirectX convention has the green/y coordinate flipped. +constexpr PXR_NS::GfVec4f kOpenGLNormalTexScale = PXR_NS::GfVec4f(2.0f, 2.0f, 2.0f, 1.0f); +constexpr PXR_NS::GfVec4f kOpenGLNormalTexBias = PXR_NS::GfVec4f(-1.0f, -1.0f, -1.0f, 0.0f); +constexpr PXR_NS::GfVec4f kDirectXNormalTexScale = PXR_NS::GfVec4f(2.0f, -2.0f, 2.0f, 1.0f); +constexpr PXR_NS::GfVec4f kDirectXNormalTexBias = PXR_NS::GfVec4f(-1.0f, 1.0f, -1.0f, 0.0f); + /// \ingroup utils_materials /// \brief Material Input data struct USDFFUTILS_API Input @@ -552,7 +565,7 @@ class USDFFUTILS_API UniqueNameEnforcer { std::unordered_map namesMap; - public: +public: void enforceUniqueness(std::string& name); }; diff --git a/utils/src/assetresolver.cpp b/utils/src/assetresolver.cpp index 1a4189b0..f905c03f 100644 --- a/utils/src/assetresolver.cpp +++ b/utils/src/assetresolver.cpp @@ -23,13 +23,13 @@ namespace adobe::usd { // Ideally, when available, use that one instead of defining our own. class ImageArAsset : public ArAsset { - public: +public: explicit ImageArAsset(const std::vector&& data) - : _data(data){}; + : _data(data) {}; const std::vector& getData() const { return _data; } virtual size_t GetSize() const override { return _data.size(); } - private: +private: std::vector _data; virtual std::shared_ptr GetBuffer() const override @@ -49,19 +49,21 @@ class ImageArAsset : public ArAsset } }; -void AssetCacheSingleton::garbageCollectCacheExcluding(const std::string& excludedPath) { +void +AssetCacheSingleton::garbageCollectCacheExcluding(const std::string& excludedPath) +{ using namespace std::chrono_literals; - + auto currentTime = std::chrono::steady_clock::now(); std::lock_guard lock(mAssetCacheMutex); // Garbage collect entries in the cache older than 60 seconds for (auto it = mAssetCache.begin(); it != mAssetCache.end();) { std::chrono::seconds timePassed = - std::chrono::duration_cast(currentTime - it->second.creationTime); + std::chrono::duration_cast(currentTime - it->second.creationTime); if (timePassed > 60s && it->first != excludedPath) { TF_DEBUG_MSG( - UTIL_PACKAGE_RESOLVER, "Removing cached items for package '%s'\n", it->first.c_str()); + UTIL_PACKAGE_RESOLVER, "Removing cached items for package '%s'\n", it->first.c_str()); it = mAssetCache.erase(it); } else { ++it; @@ -69,12 +71,17 @@ void AssetCacheSingleton::garbageCollectCacheExcluding(const std::string& exclud } } -void AssetCacheSingleton::clearCache(const std::string& resolvedPackagePath) { +void +AssetCacheSingleton::clearCache(const std::string& resolvedPackagePath) +{ std::lock_guard lock(mAssetCacheMutex); mAssetCache.erase(resolvedPackagePath); } -void AssetCacheSingleton::populateCache(const std::string& resolvedPackagePath, std::vector&& images) { +void +AssetCacheSingleton::populateCache(const std::string& resolvedPackagePath, + std::vector&& images) +{ std::lock_guard lock(mAssetCacheMutex); auto it = mAssetCache.find(resolvedPackagePath); @@ -88,17 +95,23 @@ void AssetCacheSingleton::populateCache(const std::string& resolvedPackagePath, } } -AssetMap* AssetCacheSingleton::acquireAssetMap(const std::string& resolvedPackagePath, - const std::string& resolvedPackagedPath, - std::stringstream& ss, - std::function&)> readCache) { - AssetMap* assetMap = nullptr; +AssetMap* +AssetCacheSingleton::acquireAssetMap( + const std::string& resolvedPackagePath, + const std::string& resolvedPackagedPath, + std::stringstream& ss, + std::function&)> readCache) +{ + AssetMap* assetMap = nullptr; std::lock_guard lock(mAssetCacheMutex); - + auto it = mAssetCache.find(resolvedPackagePath); if (it != mAssetCache.end()) { - TF_DEBUG_MSG( - UTIL_PACKAGE_RESOLVER, "%s: %p::%s Cached file", resolvedPackagedPath.c_str(), this, ss.str().c_str()); + TF_DEBUG_MSG(UTIL_PACKAGE_RESOLVER, + "%s: %p::%s Cached file", + resolvedPackagedPath.c_str(), + this, + ss.str().c_str()); assetMap = &it->second; } else { TF_DEBUG_MSG(UTIL_PACKAGE_RESOLVER, @@ -116,5 +129,4 @@ AssetMap* AssetCacheSingleton::acquireAssetMap(const std::string& resolvedPackag return assetMap; } - -} // namespace adobe::usd \ No newline at end of file +} // namespace adobe::usd diff --git a/utils/src/common.cpp b/utils/src/common.cpp index 1f15faa4..c3220b28 100644 --- a/utils/src/common.cpp +++ b/utils/src/common.cpp @@ -20,6 +20,7 @@ governing permissions and limitations under the License. #include #include #include +#include #include #include #include @@ -128,7 +129,7 @@ argComposeFloatArray(const PcpDynamicFileFormatContext& context, } } -void +bool argReadString(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, std::string& target, @@ -141,21 +142,26 @@ argReadString(const PXR_NS::SdfFileFormat::FileFormatArguments& args, debugTag.c_str(), arg.c_str(), it->second.c_str()); + return true; } + return false; } -void +bool argReadString(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, PXR_NS::TfToken& target, const std::string& debugTag) { std::string targetStr; - argReadString(args, arg, targetStr, debugTag); - target = PXR_NS::TfToken(targetStr); + if (argReadString(args, arg, targetStr, debugTag)) { + target = PXR_NS::TfToken(targetStr); + return true; + } + return false; } -void +bool argReadBool(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, bool& target, @@ -168,10 +174,12 @@ argReadBool(const PXR_NS::SdfFileFormat::FileFormatArguments& args, debugTag.c_str(), arg.c_str(), target ? "true" : "false"); + return true; } + return false; } -void +bool argReadFloat(const PXR_NS::SdfFileFormat::FileFormatArguments& args, const std::string& arg, float& target, @@ -184,10 +192,12 @@ argReadFloat(const PXR_NS::SdfFileFormat::FileFormatArguments& args, debugTag.c_str(), arg.c_str(), it->second.c_str()); + return true; } + return false; } -void +bool argReadFloatArray(const SdfFileFormat::FileFormatArguments& args, const std::string& arg, VtFloatArray& target, @@ -209,7 +219,9 @@ argReadFloatArray(const SdfFileFormat::FileFormatArguments& args, debugTag.c_str(), arg.c_str(), it->second.c_str()); + return true; } + return false; } void @@ -278,6 +290,19 @@ createDirectory(const std::filesystem::path& directoryPath) return true; } +bool +writeDataToDisk(const std::filesystem::path& filepath, const void* data, size_t size) +{ + std::ofstream file(filepath, std::ios::out | std::ios::binary); + if (!file.is_open()) { + TF_WARN("Could not open file %s for writing.", filepath.c_str()); + return false; + } + file.write(reinterpret_cast(data), size); + file.close(); + return true; +} + // Retrieves the file path associated with a given layer identifier. // Parses the layer identifier to extract the outer and inner paths, // and returns the inner path if available; otherwise, returns the outer path. @@ -291,4 +316,33 @@ getLayerFilePath(const std::string& layerIdentifier) return inner.empty() ? outer : inner; } -} \ No newline at end of file +std::filesystem::path +convertStringToPath(const std::string& str) +{ +#if __cplusplus >= 202002L + return std::filesystem::path(str); +#else + return std::filesystem::u8path(str); +#endif +} + +#if __cplusplus >= 202002L +std::filesystem::path +convertStringToPath(const std::u8string& str) +{ + return std::filesystem::path(reinterpret_cast(str.c_str())); +} +#endif + +// convert a path to a string in a c++ version dependent way +std::string +convertPathToString(const std::filesystem::path& path) +{ +#if __cplusplus >= 202002L + return std::string(reinterpret_cast(path.u8string().c_str())); +#else + return path.u8string(); +#endif +} + +} // namespace adobe::usd diff --git a/utils/src/featureFlags.cpp b/utils/src/featureFlags.cpp new file mode 100644 index 00000000..fc3ca289 --- /dev/null +++ b/utils/src/featureFlags.cpp @@ -0,0 +1,57 @@ +/* +Copyright 2026 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 + +#ifndef USD_FILEFORMATS_DEFAULT_WRITE_USDPREVIEWSURFACE +#define USD_FILEFORMATS_DEFAULT_WRITE_USDPREVIEWSURFACE true +#endif + +#ifndef USD_FILEFORMATS_DEFAULT_WRITE_ASM +#define USD_FILEFORMATS_DEFAULT_WRITE_ASM true +#endif + +#ifndef USD_FILEFORMATS_DEFAULT_WRITE_OPENPBR +#define USD_FILEFORMATS_DEFAULT_WRITE_OPENPBR false +#endif + +PXR_NAMESPACE_OPEN_SCOPE + +TF_DEFINE_ENV_SETTING(USD_FILEFORMATS_WRITE_USDPREVIEWSURFACE, + (bool)(USD_FILEFORMATS_DEFAULT_WRITE_USDPREVIEWSURFACE), + "Write UsdPreviewSurface material networks by default"); + +TF_DEFINE_ENV_SETTING(USD_FILEFORMATS_WRITE_ASM, + (bool)(USD_FILEFORMATS_DEFAULT_WRITE_ASM), + "Write AdobeStandardMaterial networks by default"); + +TF_DEFINE_ENV_SETTING(USD_FILEFORMATS_WRITE_OPENPBR, + (bool)(USD_FILEFORMATS_DEFAULT_WRITE_OPENPBR), + "Write OpenPBR / MaterialX material networks by default"); + +TF_DEFINE_ENV_SETTING(USD_FILEFORMATS_EXPERIMENTAL_OPENPBR_PROCESSING, + false, + "Enable experimental OpenPBR processing code paths"); + +PXR_NAMESPACE_CLOSE_SCOPE + +namespace adobe::usd { + +void +applyMaterialModelDefaults(bool& writeUsdPreviewSurface, bool& writeASM, bool& writeOpenPBR) +{ + writeUsdPreviewSurface = + PXR_NS::TfGetEnvSetting(PXR_NS::USD_FILEFORMATS_WRITE_USDPREVIEWSURFACE); + writeASM = PXR_NS::TfGetEnvSetting(PXR_NS::USD_FILEFORMATS_WRITE_ASM); + writeOpenPBR = PXR_NS::TfGetEnvSetting(PXR_NS::USD_FILEFORMATS_WRITE_OPENPBR); +} + +} diff --git a/utils/src/geometry.cpp b/utils/src/geometry.cpp index 683b4011..2939585f 100644 --- a/utils/src/geometry.cpp +++ b/utils/src/geometry.cpp @@ -90,15 +90,15 @@ checkFiniteFloats(const VtArray& array, std::vector& invalidIndices) // in local scope to use as a reusable staging buffer for the message. // It also expects a string path, which is used in the message. // It also expects a bool foundError, which it sets to true if an Error is encountered. -#define LOG_ISSUE(level, format, ...) \ - if (issues != nullptr) { \ - int n = snprintf(tempBuffer, tempBufferSize, format, ##__VA_ARGS__); \ - if (n > 0 && n < tempBufferSize) { \ - issues->push_back(Issue{ level, path, tempBuffer }); \ - } \ - } \ - if (level == Issue::Level::Error) { \ - foundError = true; \ +#define LOG_ISSUE(level, format, ...) \ + if (issues != nullptr) { \ + int n = snprintf(tempBuffer, tempBufferSize, format, ##__VA_ARGS__); \ + if (n > 0 && n < tempBufferSize) { \ + issues->push_back(Issue{ level, path, tempBuffer }); \ + } \ + } \ + if (level == Issue::Level::Error) { \ + foundError = true; \ } bool @@ -755,8 +755,8 @@ computeSmoothNormals(Mesh& mesh) continue; } if ((size_t)(faceVertexIndex + numFaceVertices) >= totalNumFaceVertices) { - TF_WARN("Invalid mesh topology: offset {} into indices for face {} is larger than " - "total indices {}", + TF_WARN("Invalid mesh topology: offset %d into indices for face %zu is larger than " + "total indices %zu", faceVertexIndex + numFaceVertices, faceIdx, totalNumFaceVertices); diff --git a/utils/src/images.cpp b/utils/src/images.cpp index a1d75be7..b2d620d0 100644 --- a/utils/src/images.cpp +++ b/utils/src/images.cpp @@ -28,8 +28,7 @@ Image::Image() : width(0U) , height(0U) , channels(0U) -{ -} +{} Image::~Image() {} @@ -590,7 +589,8 @@ extractFilePathFromAssetPath(const std::string& assetPath) std::string usage = getSbsarUsageFromParameters(parameters); if (!usage.empty()) { // graphs/CardBoard/images -> CardBoard - std::string graphName = std::filesystem::path(subpath).parent_path().filename().u8string(); + std::string graphName = + convertPathToString(std::filesystem::path(subpath).parent_path().filename()); subpath = graphName + "_" + usage; } diff --git a/utils/src/layerRead.cpp b/utils/src/layerRead.cpp index 76a9ad11..3f89680a 100644 --- a/utils/src/layerRead.cpp +++ b/utils/src/layerRead.cpp @@ -144,8 +144,7 @@ readScope(ReadLayerContext& ctx, const UsdPrim& prim, int parent) node.path = prim.GetPath().GetString(); node.markedInvisible = isMarkedInvisible(ctx, prim); readTransform(ctx, prim, node, parent); - UsdPrimSiblingRange children = - prim.GetFilteredChildren(UsdTraverseInstanceProxies(UsdPrimAllPrimsPredicate)); + UsdPrimSiblingRange children = prim.GetFilteredChildren(UsdTraverseInstanceProxies()); for (const UsdPrim& p : children) { readPrim(ctx, p, nodeIndex); } @@ -161,8 +160,7 @@ readUnknown(ReadLayerContext& ctx, const UsdPrim& prim, int parent) prim.GetTypeName().GetText(), prim.GetName().GetText()); - UsdPrimSiblingRange children = - prim.GetFilteredChildren(UsdTraverseInstanceProxies(UsdPrimAllPrimsPredicate)); + UsdPrimSiblingRange children = prim.GetFilteredChildren(UsdTraverseInstanceProxies()); bool skipAddingNode = false; if (prim.GetPrimTypeInfo() == UsdPrimTypeInfo::GetEmptyPrimType()) { @@ -330,8 +328,7 @@ readXform(ReadLayerContext& ctx, const UsdPrim& prim, int parent) readXformInternal(ctx, pair.second, prim, parent); } - UsdPrimSiblingRange children = - prim.GetFilteredChildren(UsdTraverseInstanceProxies(UsdPrimAllPrimsPredicate)); + UsdPrimSiblingRange children = prim.GetFilteredChildren(UsdTraverseInstanceProxies()); for (const UsdPrim& p : children) { readPrim(ctx, p, nodeIndex); } @@ -452,14 +449,14 @@ readMeshOrPointsData(ReadLayerContext& ctx, Mesh& mesh, int meshIndex, const Usd } } } - + // 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; @@ -543,8 +540,7 @@ readMeshOrPointsData(ReadLayerContext& ctx, Mesh& mesh, int meshIndex, const Usd if (prim.IsA()) { UsdShadeMaterialBindingAPI::BindingsCache bindingsCache; UsdShadeMaterialBindingAPI::CollectionQueryCache collQueryCache; - UsdPrimSiblingRange children = - prim.GetFilteredChildren(UsdTraverseInstanceProxies(UsdPrimAllPrimsPredicate)); + UsdPrimSiblingRange children = prim.GetFilteredChildren(UsdTraverseInstanceProxies()); for (const UsdPrim& child : children) { if (child.IsA()) { ctx.subsetMaterialBindings.back().push_back(""); @@ -623,8 +619,10 @@ readMeshOrPointsData(ReadLayerContext& ctx, Mesh& mesh, int meshIndex, const Usd int shIndex = 0; while (true) { Primvar shCoeffs; - if (!readPrimvar( - primvarsAPI, TfToken(std::string("fRest") + std::to_string(shIndex)), shCoeffs) || !shCoeffs.values.size()) + if (!readPrimvar(primvarsAPI, + TfToken(std::string("fRest") + std::to_string(shIndex)), + shCoeffs) || + !shCoeffs.values.size()) break; auto [pointSHCoeffSetIndex, pointSHCoeffSet] = ctx.usd->addPointSHCoeffSet(meshIndex); @@ -808,7 +806,6 @@ readSkelRoot(ReadLayerContext& ctx, const UsdPrim& prim, int parent) } // Process animation data - int boneCount = skeleton.restTransforms.size(); const UsdSkelAnimQuery& skelAnimQuery = skelQuery.GetAnimQuery(); if (skelAnimQuery.IsValid()) { std::vector times; @@ -905,8 +902,7 @@ readPointInstancer(ReadLayerContext& ctx, const UsdPrim& prim, int parent) protoInstanceAttr.Get(&protoIndices, time); const int meshesBeforePrototypesAdded = ctx.usd->meshes.size(); - UsdPrimSiblingRange children = - prim.GetFilteredChildren(UsdTraverseInstanceProxies(UsdPrimAllPrimsPredicate)); + UsdPrimSiblingRange children = prim.GetFilteredChildren(UsdTraverseInstanceProxies()); for (const UsdPrim& p : children) { readPrim(ctx, p, nodeIndex); } @@ -1247,7 +1243,10 @@ readPrim(ReadLayerContext& ctx, const UsdPrim& prim, int parent) else if (prim.IsA()) f = readSkelRoot; else if (prim.IsA()) - f = readMaterial; + // The readMaterial function doesn't use the parent index + f = [](ReadLayerContext& ctx, const UsdPrim& prim, int /*parent*/) { + return readMaterial(ctx, prim); + }; else if (prim.IsA()) f = readCamera; else if (prim.IsA()) @@ -1278,7 +1277,7 @@ resolveMaterialBindings(ReadLayerContext& ctx) int index = it->second; ctx.usd->meshes[i].material = index; TF_DEBUG_MSG(FILE_FORMAT_UTIL, - "%s: mesh[%d].material = %d: %s\n", + "%s: mesh[%zu].material = %d: %s\n", ctx.debugTag.c_str(), i, index, @@ -1306,7 +1305,7 @@ resolveMaterialBindings(ReadLayerContext& ctx) int index = it->second; ctx.usd->meshes[i].subsets[j].material = index; TF_DEBUG_MSG(FILE_FORMAT_UTIL, - "%s: mesh[%d].subset[%d].material = %d: %s\n", + "%s: mesh[%zu].subset[%zu].material = %d: %s\n", ctx.debugTag.c_str(), i, j, @@ -1419,7 +1418,7 @@ splitAnimationTracks(UsdData& usd) node.animations.resize(usd.animationTracks.size()); // For each track, filter all timepoints that are within range - for (int animationTrackIndex = 0; animationTrackIndex < usd.animationTracks.size(); + for (size_t animationTrackIndex = 0; animationTrackIndex < usd.animationTracks.size(); animationTrackIndex++) { AnimationTrack& track = usd.animationTracks[animationTrackIndex]; float mainMinTime = track.minTime + track.offsetToJoinedTimeline; @@ -1427,7 +1426,7 @@ splitAnimationTracks(UsdData& usd) auto filterTimeValues = [&track, mainMinTime, mainMaxTime](const auto& srcTimeValues, auto& dstTimeValues) { - int t = 0; + size_t t = 0; for (const float time : srcTimeValues.times) { if (srcTimeValues.values.size() <= t) { diff --git a/utils/src/layerReadMaterial.cpp b/utils/src/layerReadMaterial.cpp index 065755ab..288efd38 100644 --- a/utils/src/layerReadMaterial.cpp +++ b/utils/src/layerReadMaterial.cpp @@ -10,471 +10,800 @@ 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 -#include -#include - -using namespace PXR_NS; +PXR_NAMESPACE_USING_DIRECTIVE 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) +// The following helpers read special inputs as attributes. These are used to read values that are +// not native shader inputs on some of the surface shaders. + +// Natively supported on ASM, but not OpenPBR +// Note used by UsdPreviewSurface networks +float +_readNormalScale(const UsdShadeShader& surface) { - // 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); + float value = 1.0f; + surface.GetPrim().GetAttribute(AsmTokens->normalScale).Get(&value); + return value; } +// Natively supported on UsdPreviewSurface, but not ASM and OpenPBR bool -readImage(ReadLayerContext& ctx, const SdfAssetPath& assetPath, int& index) +_readUseSpecularWorkflow(const UsdShadeShader& surface) { - 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; - } + bool value = false; + surface.GetPrim().GetAttribute(UsdPreviewSurfaceTokens->useSpecularWorkflow).Get(&value); + return value; +} + +// Natively supported on UsdPreviewSurface, but not ASM and OpenPBR +float +_readOpacityThreshold(const UsdShadeShader& surface) +{ + float value = 0.0f; + surface.GetPrim().GetAttribute(UsdPreviewSurfaceTokens->opacityThreshold).Get(&value); + return value; +} + +// Custom attribute not natively supported by any surface +// Note used by UsdPreviewSurface networks +bool +_readClearcoatModelsTransmissionTint(const UsdShadeShader& surface) +{ + bool value = false; + surface.GetPrim().GetAttribute(AdobeTokens->clearcoatModelsTransmissionTint).Get(&value); + return value; +} - // 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()); +// Custom attribute not natively supported by any surface +// Note used by UsdPreviewSurface networks +bool +_readUnlit(const UsdShadeShader& surface) +{ + bool value = false; + surface.GetPrim().GetAttribute(AdobeTokens->unlit).Get(&value); + return value; +} + +// ------------------------------------------------------------------------------------------------- +// UsdPreviewSurface & ASM network node handlers +// +// Note, an ASM network uses the same nodes as UsdPreviewSurface that ship with USD. We expect that +// each input on the surface can either use a constant value or a simple linear chain of nodes to +// read a texture value: +// +// TexCoords (UsdPrimvarReader_float2) +// | +// V +// TexCoordXform (UsdTransform2d) [OPTIONAL] +// | +// V +// TexRead (UsdUVTexture) (* include scale & bias and channel selection) +// | +// V +// UsdPreviewSurface (UsdPreviewSurface) or ASM Surface (AdobeStandardMaterial_4_0) +// +// * Note that these surfaces will automatically apply the normal map transform for normal inputs. +// +// ------------------------------------------------------------------------------------------------- + +bool +handleUsdPrimvarReader(InputContext& ctx, const UsdShadeOutput& shaderOutput) +{ + UsdShadeShader shader(shaderOutput.GetPrim()); + + 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 { - ctx.imageNames[name] = 1; + getShaderInputValue(shader, AdobeTokens->varname, texCoordPrimvar); } - - 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); + int uvIndex = getSTPrimvarTokenIndex(texCoordPrimvar); + if (uvIndex >= 0) { + ctx.input.uvIndex = uvIndex; } 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()); + TF_WARN("Texture reader %s is reading primvar %s. Only 'st' or 'st1'..'stN' is supported " + "(Input %s)", + shader.GetPrim().GetPath().GetText(), + texCoordPrimvar.GetText(), + ctx.surfaceInputName.GetText()); } - 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) +bool +handleUsdTransform2d(InputContext& ctx, const UsdShadeOutput& shaderOutput) { - if (mult == 1.0f) { - return; - } + UsdShadeShader shader(shaderOutput.GetPrim()); - 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; - } + getShaderInputValue(shader, AdobeTokens->rotation, ctx.input.uvRotation); + getShaderInputValue(shader, AdobeTokens->scale, ctx.input.uvScale); + getShaderInputValue(shader, AdobeTokens->translation, ctx.input.uvTranslation); + + return followConnectedInput(ctx, shader, AdobeTokens->in); } -template +// Handle texture-related shader inputs such as file paths and wrapping modes. bool -getShaderInputValue(const UsdShadeShader& shader, const TfToken& name, T& value) +handleUsdUVTexture(InputContext& ctx, const UsdShadeOutput& shaderOutput) { - 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; - } - } + UsdShadeShader shader(shaderOutput.GetPrim()); + + SdfAssetPath assetPath; + if (getShaderInputValue(shader, AdobeTokens->file, assetPath)) { + ctx.input.image = readImage(ctx.readLayerContext, assetPath); } - return false; + getShaderInputValue(shader, AdobeTokens->wrapS, ctx.input.wrapS); + getShaderInputValue(shader, AdobeTokens->wrapT, ctx.input.wrapT); + getShaderInputValue(shader, AdobeTokens->minFilter, ctx.input.minFilter); + getShaderInputValue(shader, AdobeTokens->magFilter, ctx.input.magFilter); + getShaderInputValue(shader, AdobeTokens->scale, ctx.input.scale); + getShaderInputValue(shader, AdobeTokens->bias, ctx.input.bias); + getShaderInputValue(shader, AdobeTokens->sourceColorSpace, ctx.input.colorspace); + + // Default to 0th UVs unless overridden in handlePrimvarReader + ctx.input.uvIndex = 0; + + // The name of the output on the texture reader determines which channel(s) of the texture we + // read. + ctx.input.channel = shaderOutput.GetBaseName(); + + return followConnectedInput(ctx, shader, AdobeTokens->st); } -// 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. +// These handlers are used both for UsdPreviewSurface networks and also ASM networks +const ShaderHandlerMappings usdPreviewSurfaceHandlers = { + { { AdobeTokens->UsdUVTexture }, handleUsdUVTexture }, + { { AdobeTokens->UsdTransform2d }, handleUsdTransform2d, kOptional }, + { { AdobeTokens->UsdPrimvarReader_float2 }, handleUsdPrimvarReader } +}; + +// ------------------------------------------------------------------------------------------------- + bool -fetchPrimaryConnectedAttribute(const UsdShadeInput& shadeInput, - UsdAttribute& outAttribute, - bool expectShader) +readUsdPreviewSurfaceMaterial(ReadLayerContext& ctx, + OpenPbrMaterial& material, + const UsdShadeShader& surface) { - 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()) { + TfToken shaderId; + surface.GetShaderId(&shaderId); + if (shaderId != AdobeTokens->UsdPreviewSurface) { 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; + + int useSpecularWorkflow = 0; + getShaderInputValue(surface, UsdPreviewSurfaceTokens->useSpecularWorkflow, useSpecularWorkflow); + material.useSpecularWorkflow = useSpecularWorkflow != 0; + getShaderInputValue( + surface, UsdPreviewSurfaceTokens->opacityThreshold, material.opacityThreshold); + + bool success = true; + auto input = [&ctx, &surface, &success](const TfToken& inputName, Input& input) { + InputContext inputContext = { ctx, inputName, usdPreviewSurfaceHandlers, input }; + success &= readSurfaceInput(inputContext, surface); + }; + input(UsdPreviewSurfaceTokens->diffuseColor, material.base_color); + input(UsdPreviewSurfaceTokens->emissiveColor, material.emission_color); + input(UsdPreviewSurfaceTokens->specularColor, material.specular_color); + input(UsdPreviewSurfaceTokens->normal, material.geometry_normal); + input(UsdPreviewSurfaceTokens->metallic, material.base_metalness); + input(UsdPreviewSurfaceTokens->roughness, material.specular_roughness); + input(UsdPreviewSurfaceTokens->clearcoat, material.coat_weight); + input(UsdPreviewSurfaceTokens->clearcoatRoughness, material.coat_roughness); + input(UsdPreviewSurfaceTokens->opacity, material.geometry_opacity); + input(UsdPreviewSurfaceTokens->displacement, material.displacement); + input(UsdPreviewSurfaceTokens->occlusion, material.occlusion); + input(UsdPreviewSurfaceTokens->ior, material.specular_ior); + + return success; } -// Handle texture-related shader inputs such as file paths and wrapping modes. -void -handleTextureShader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) +bool +readASMMaterial(ReadLayerContext& ctx, OpenPbrMaterial& material, const UsdShadeShader& surface) { - SdfAssetPath assetPath; - if (getShaderInputValue(shader, AdobeTokens->file, assetPath)) { - readImage(ctx, assetPath, input.image); + TfToken shaderId; + surface.GetShaderId(&shaderId); + if (shaderId != AdobeTokens->adobeStandardMaterial) { + return false; } - 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; + // Note, we currently only support fixed values for normalScale. No texture support. + bool scatter = false; + getShaderInputValue(surface, AsmTokens->scatter, scatter); + getShaderInputValue(surface, AsmTokens->normalScale, material.normalScale); + + bool success = true; + auto input = [&ctx, &surface, &success](const TfToken& inputName, Input& input) { + InputContext inputContext = { ctx, inputName, usdPreviewSurfaceHandlers, input }; + success &= readSurfaceInput(inputContext, surface); + }; + input(AsmTokens->baseColor, material.base_color); + input(AsmTokens->roughness, material.specular_roughness); + input(AsmTokens->metallic, material.base_metalness); + input(AsmTokens->opacity, material.geometry_opacity); + input(AsmTokens->specularLevel, material.specular_weight); + input(AsmTokens->specularEdgeColor, material.specular_color); + input(AsmTokens->normal, material.geometry_normal); + input(AsmTokens->height, material.displacement); + input(AsmTokens->anisotropyLevel, material.specular_roughness_anisotropy); + input(AsmTokens->anisotropyAngle, material.anisotropyAngle); + input(AsmTokens->emissiveIntensity, material.emission_luminance); + input(AsmTokens->emissive, material.emission_color); + input(AsmTokens->sheenOpacity, material.fuzz_weight); + input(AsmTokens->sheenColor, material.fuzz_color); + input(AsmTokens->sheenRoughness, material.fuzz_roughness); + input(AsmTokens->translucency, material.transmission_weight); + input(AsmTokens->IOR, material.specular_ior); + input(AsmTokens->absorptionColor, material.transmission_color); + input(AsmTokens->absorptionDistance, material.transmission_depth); + if (scatter) { + material.subsurface_weight = Input{ VtValue(1.0f) }; + input(AsmTokens->scatteringColor, material.subsurface_color); + input(AsmTokens->scatteringDistance, material.subsurface_radius); + input(AsmTokens->scatteringDistanceScale, material.subsurface_radius_scale); + } + input(AsmTokens->coatOpacity, material.coat_weight); + input(AsmTokens->coatColor, material.coat_color); + input(AsmTokens->coatRoughness, material.coat_roughness); + input(AsmTokens->coatIOR, material.coat_ior); + input(AsmTokens->coatNormal, material.geometry_coat_normal); + + // Non-OpenPBR inputs + input(AsmTokens->coatSpecularLevel, material.coatSpecularLevel); + input(AsmTokens->ambientOcclusion, material.occlusion); + input(AsmTokens->volumeThickness, material.volumeThickness); + + material.useSpecularWorkflow = _readUseSpecularWorkflow(surface); + material.opacityThreshold = _readOpacityThreshold(surface); + material.clearcoatModelsTransmissionTint = _readClearcoatModelsTransmissionTint(surface); + material.isUnlit = _readUnlit(surface); + + return success; } -UsdShadeShader -handleTransformShader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) +// ------------------------------------------------------------------------------------------------- +// OpenPBR/MaterialX network node handlers +// +// Note: that we use a MaterialX network with its nodes and have an OpenPBR surface node. +// We only support a specific node subset and topology that we parse here. We expect that each input +// on the surface can either use a constant value or a simple linear chain of nodes to read a +// texture value: +// +// TexCoords (ND_texcoord_vector2) +// | +// V +// TexCoordXform (ND_place2d_vector2) [OPTIONAL] +// | +// V +// TexRead (ND_image_vector4, ND_image_color3, ND_image_vector3, ND_UsdUVTexture_23) +// | +// V +// Convert (ND_convert_color3_vector3) (* only with ND_UsdUVTexture_23 and ND_normalmap) [OPTIONAL*] +// | +// V +// Normalmap (ND_normalmap) (* only with geometry_normal and geometry_coat_normal) [OPTIONAL*] +// | +// V +// ChannelSelect (ND_separate4_vector4) (* only float input w/ ND_image_vector4) [OPTIONAL*] +// | +// V +// Scale (ND_multiply_float, ND_multiply_color3, ND_multiply_vector3) [OPTIONAL] +// | +// V +// Bias (ND_add_float, ND_add_color3, ND_add_vector3) [OPTIONAL] +// | +// V +// AmbientOcclusionBaseColor (ND_mix_color3) [OPTIONAL] +// | +// V +// OpenPBR Surface (ND_open_pbr_surface_surfaceshader) +// +// ------------------------------------------------------------------------------------------------- + +bool +handleTexcoordVector2(InputContext& ctx, const UsdShadeOutput& shaderOutput) { + UsdShadeShader shader(shaderOutput.GetPrim()); - UsdShadeShader nextShader; - getShaderInputValue(shader, AdobeTokens->rotation, input.uvRotation); - getShaderInputValue(shader, AdobeTokens->scale, input.uvScale); - getShaderInputValue(shader, AdobeTokens->translation, input.uvTranslation); + TfToken shaderId; + shader.GetShaderId(&shaderId); + if (shaderId == MtlXTokens->ND_texcoord_vector2) { + // Note, we have no information on how to map this index to a name of a texcoord primvar + ctx.input.uvIndex = 0; + getShaderInputValue(shader, AdobeTokens->index, ctx.input.uvIndex); - UsdShadeInput stInputCoordReader = shader.GetInput(AdobeTokens->in); - UsdAttribute stSourcesInner; - if (fetchPrimaryConnectedAttribute(stInputCoordReader, stSourcesInner, true)) { - nextShader = UsdShadeShader(stSourcesInner.GetPrim()); + return true; + } else if (shaderId == MtlXTokens->ND_geompropvalue_vector2) { + // try to map the geomprop to a uv index + int index = 0; + TfToken geomprop; + std::string geompropStr; + if (getShaderInputValue(shader, MtlXTokens->geomprop, geompropStr)) { + index = getSTPrimvarTokenIndex(TfToken(geompropStr)); + } else if (getShaderInputValue(shader, MtlXTokens->geomprop, geomprop)) { + index = getSTPrimvarTokenIndex(geomprop); + } + ctx.input.uvIndex = index; + return true; } - return nextShader; + return false; } -void -handlePrimvarReader(ReadLayerContext& ctx, const UsdShadeShader& shader, Input& input) +bool +handlePlace2dVector2(InputContext& ctx, const UsdShadeOutput& shaderOutput) { - 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()); - } + UsdShadeShader shader(shaderOutput.GetPrim()); + + getShaderInputValue(shader, AdobeTokens->rotate, ctx.input.uvRotation); + GfVec2f scale(1.0f); + getShaderInputValue(shader, AdobeTokens->scale, scale); + // For the place2d node, the scale is not a multiplier, but the overall scale and so we need to + // invert the value + ctx.input.uvScale[0] = scale[0] != 0.0f ? 1.0f / scale[0] : 0.0f; + ctx.input.uvScale[1] = scale[1] != 0.0f ? 1.0f / scale[1] : 0.0f; + getShaderInputValue(shader, AdobeTokens->offset, ctx.input.uvTranslation); + + return followConnectedInput(ctx, shader, AdobeTokens->texcoord); } -void -readInput(ReadLayerContext& ctx, const UsdShadeShader& surface, const TfToken& name, Input& input) +bool +handleImage(InputContext& ctx, const UsdShadeOutput& shaderOutput) { - UsdShadeInput shadeInput = surface.GetInput(name); - if (!shadeInput) { - return; + UsdShadeShader shader(shaderOutput.GetPrim()); + + SdfAssetPath assetPath; + if (getShaderInputValue(shader, AdobeTokens->file, assetPath)) { + ctx.input.image = readImage(ctx.readLayerContext, assetPath); + + UsdShadeInput fileInput = shader.GetInput(AdobeTokens->file); + if (fileInput) { + TfToken mtlxColorSpace = fileInput.GetAttr().GetColorSpace(); + ctx.input.colorspace = + mtlxColorSpace == MtlXTokens->srgb_texture ? AdobeTokens->sRGB : AdobeTokens->raw; + } } - UsdAttribute attr; - if (fetchPrimaryConnectedAttribute(shadeInput, attr, false)) { - UsdShadeSourceInfoVector sources = shadeInput.GetConnectedSources(); + TfToken shaderId; + shader.GetShaderId(&shaderId); + if (shaderId == MtlXTokens->ND_UsdUVTexture_23) { + // The name of the output on the texture reader determines which channel(s) of the texture + // we read. + ctx.input.channel = shaderOutput.GetBaseName(); - // 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 if (shaderId == MtlXTokens->ND_image_color3 || + shaderId == MtlXTokens->ND_image_vector3) { + ctx.input.channel = AdobeTokens->rgb; + } else { + // Note, if a single float channel is read via a ND_image_vector4 node, which is then + // followed by a ND_separate4_vector4 node to extract one out of 4 RGBA channels, then the + // handler for that node will set input.channel to the appropriate value + } + + if (shaderId == MtlXTokens->ND_UsdUVTexture_23) { + GfVec4f defaultValue; + if (getShaderInputValue(shader, AdobeTokens->fallback, defaultValue)) { + if (ctx.input.channel == AdobeTokens->r) { + ctx.input.value = defaultValue[0]; + } else if (ctx.input.channel == AdobeTokens->g) { + ctx.input.value = defaultValue[1]; + } else if (ctx.input.channel == AdobeTokens->b) { + ctx.input.value = defaultValue[2]; + } else if (ctx.input.channel == AdobeTokens->a) { + ctx.input.value = defaultValue[3]; + } else if (ctx.input.channel == AdobeTokens->rgb) { + ctx.input.value = GfVec3f(defaultValue[0], defaultValue[1], defaultValue[2]); + } else { + TF_WARN("ND_UsdUVTexture_23 node at %s has a default value, but the channel is not " + "valid: '%s' (input %s)", + shader.GetPath().GetText(), + ctx.input.channel.GetText(), + ctx.surfaceInputName.GetText()); } - } 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 if (shaderId == MtlXTokens->ND_image_color3 || + shaderId == MtlXTokens->ND_image_vector3) { + GfVec3f defaultValue; + if (getShaderInputValue(shader, AdobeTokens->defaultValue, defaultValue)) { + ctx.input.value = defaultValue; + } + } else if (shaderId == MtlXTokens->ND_image_vector4) { + GfVec4f defaultValue; + if (getShaderInputValue(shader, AdobeTokens->defaultValue, defaultValue)) { + // The ND_image_vector4 node is used to read a texture as RGBA and then extract a single + // channel. So the default value has to extract the same thing. + // Note, that the channel should have been set by the handleSeparate4Vector4() + // function. + if (ctx.input.channel == AdobeTokens->r) { + ctx.input.value = defaultValue[0]; + } else if (ctx.input.channel == AdobeTokens->g) { + ctx.input.value = defaultValue[1]; + } else if (ctx.input.channel == AdobeTokens->b) { + ctx.input.value = defaultValue[2]; + } else if (ctx.input.channel == AdobeTokens->a) { + ctx.input.value = defaultValue[3]; } else { - TF_WARN( - "Unsupported shader type %s for input %s", shaderId.GetText(), name.GetText()); + TF_WARN("ND_image_vector4 node at %s has a default value, but the channel is not " + "valid: '%s' (input %s)", + shader.GetPath().GetText(), + ctx.input.channel.GetText(), + ctx.surfaceInputName.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()); + } + + auto fromMaterialXAddressMode = [](const std::string& addressMode) -> TfToken { + if (addressMode.empty()) { + return TfToken(); + } else if (addressMode == "periodic") { + // "periodic" maps to "repeat", which is also the default. So we suppress it + return TfToken(); + } else if (addressMode == "clamp") { + return AdobeTokens->clamp; + } else if (addressMode == "mirror") { + return AdobeTokens->mirror; + } else if (addressMode == "constant") { + return AdobeTokens->black; + } else { + TF_WARN("Unknown addressmode '%s'", addressMode.c_str()); + return TfToken(); } + }; + + if (shaderId == MtlXTokens->ND_UsdUVTexture_23) { + // The inputs on the node are called wrapS and wrapT, but are using string values like the + // uaddressmode and vaddressmode on other MaterialX texture nodes. + std::string wrapS, wrapT; + getShaderInputValue(shader, AdobeTokens->wrapS, wrapS); + getShaderInputValue(shader, AdobeTokens->wrapT, wrapT); + ctx.input.wrapS = fromMaterialXAddressMode(wrapS); + ctx.input.wrapT = fromMaterialXAddressMode(wrapT); + getShaderInputValue(shader, AdobeTokens->scale, ctx.input.scale); + getShaderInputValue(shader, AdobeTokens->bias, ctx.input.bias); + + if (ctx.surfaceInputName == OpenPbrTokens->geometry_normal || + ctx.surfaceInputName == OpenPbrTokens->geometry_coat_normal) { + // In MaterialX, the ND_normalmap node, which is downstream of the ND_UsdUVTexture_23 + // will decode the normal from the raw texture value, assuming the OpenGL convention, + // using a scale and bias of 2 (kOpenGLNormalTexScale) and -1 (kOpenGLNormalTexBias). + // That means it would be redundant to have this on the ND_UsdUVTexture_23 node. + // + // We still want to have these values in our Input struct, especially if we're trying to + // differentiate it from a DirectX encoded normalmap and/or a normal strength + // multiplier. So we apply the affine transform to the usually neutral scale and bias + // value to recover the expected decoding values. + // + // Note that this mirrors the process in the OpenPBR writing code. + ctx.input.scale = GfCompMult(kOpenGLNormalTexScale, ctx.input.scale); + ctx.input.bias = + GfCompMult(kOpenGLNormalTexScale, ctx.input.bias) + kOpenGLNormalTexBias; + } + + return followConnectedInput(ctx, shader, AdobeTokens->st); + } else { + std::string uaddressmode, vaddressmode; + getShaderInputValue(shader, AdobeTokens->uaddressmode, uaddressmode); + getShaderInputValue(shader, AdobeTokens->vaddressmode, vaddressmode); + ctx.input.wrapS = fromMaterialXAddressMode(uaddressmode); + ctx.input.wrapT = fromMaterialXAddressMode(vaddressmode); + + return followConnectedInput(ctx, shader, AdobeTokens->texcoord); } } bool -readUsdPreviewSurfaceMaterial(ReadLayerContext& ctx, - Material& material, - const UsdShadeShader& surface) +handleConvertColorToVector(InputContext& ctx, const UsdShadeOutput& shaderOutput) { - 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); + UsdShadeShader shader(shaderOutput.GetPrim()); - return true; + return followConnectedInput(ctx, shader, AdobeTokens->in); } bool -_readClearcoatModelsTransmissionTint(const UsdShadeShader& surface) +handleNormalMap(InputContext& ctx, const UsdShadeOutput& shaderOutput) { - 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; + UsdShadeShader shader(shaderOutput.GetPrim()); + + return followConnectedInput(ctx, shader, AdobeTokens->in); } bool -_readUnlit(const UsdShadeShader& surface) +handleAmbientOcclusionBaseColorMap(InputContext& ctx, const UsdShadeOutput& shaderOutput) { - 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; + UsdShadeShader shader(shaderOutput.GetPrim()); + + // create a new input context that reads the 'bg' input of the ND_mix_color3 node + InputContext inputContext = { + ctx.readLayerContext, AdobeTokens->bg, ctx.handlerMappings, ctx.input, ctx.handlerIndex + }; + return readSurfaceInput(inputContext, shader); } bool -readASMMaterial(ReadLayerContext& ctx, Material& material, const UsdShadeShader& surface) +handleAmbientOcclusionMap(InputContext& ctx, const UsdShadeOutput& shaderOutput) { - TfToken infoIdToken; - surface.GetShaderId(&infoIdToken); - if (infoIdToken != AdobeTokens->adobeStandardMaterial) { - return false; - } + UsdShadeShader shader(shaderOutput.GetPrim()); - material.clearcoatModelsTransmissionTint = _readClearcoatModelsTransmissionTint(surface); - material.isUnlit = _readUnlit(surface); + // create a new input context that reads the 'fg' input of the ND_mix_color3 node + InputContext inputContext = { + ctx.readLayerContext, AdobeTokens->fg, ctx.handlerMappings, ctx.input, ctx.handlerIndex + }; + return readSurfaceInput(inputContext, shader); +} - // 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; +bool +handleConvertFloatToColor(InputContext& ctx, const UsdShadeOutput& shaderOutput) +{ + UsdShadeShader shader(shaderOutput.GetPrim()); - auto getConstShaderInput = [&](const TfToken& inputName, auto& var) { - VtValue val; - if (getShaderInputValue(surface, inputName, val)) { - if (val.IsHolding>()) { - var = val.UncheckedGet>(); - } + // create a new input context that reads the 'in' input of the ND_convert_float_color3 node + InputContext inputContext = { + ctx.readLayerContext, AdobeTokens->in, ctx.handlerMappings, ctx.input, ctx.handlerIndex + }; + return readSurfaceInput(inputContext, shader); +} + +bool +handleSeparate4Vector4(InputContext& ctx, const UsdShadeOutput& shaderOutput) +{ + UsdShadeShader shader(shaderOutput.GetPrim()); + + auto outputNameToChannelToken = [](const TfToken& outputName) -> TfToken { + if (outputName == AdobeTokens->outx) { + return AdobeTokens->r; + } else if (outputName == AdobeTokens->outy) { + return AdobeTokens->g; + } else if (outputName == AdobeTokens->outz) { + return AdobeTokens->b; + } else if (outputName == AdobeTokens->outw) { + return AdobeTokens->a; } + + TF_WARN("Unknown output name '%s'", outputName.GetText()); + + return AdobeTokens->r; }; - 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); + // Note, this node goes together with a ND_image_vector4. The handleImage() function will + // not set the channel in that case, so that the channel choice here takes effect. + ctx.input.channel = outputNameToChannelToken(shaderOutput.GetBaseName()); + + return followConnectedInput(ctx, shader, AdobeTokens->in); +} + +bool +handleMultiply(InputContext& ctx, const UsdShadeOutput& shaderOutput) +{ + UsdShadeShader shader(shaderOutput.GetPrim()); + + TfToken shaderId; + shader.GetShaderId(&shaderId); + if (shaderId == MtlXTokens->ND_multiply_float) { + float scale = ctx.input.scale[0]; + getShaderInputValue(shader, AdobeTokens->in1, scale); + ctx.input.scale[0] = scale; + } else if (shaderId == MtlXTokens->ND_multiply_color3 || + shaderId == MtlXTokens->ND_multiply_vector3) { + GfVec3f scale(ctx.input.scale[0], ctx.input.scale[1], ctx.input.scale[2]); + getShaderInputValue(shader, AdobeTokens->in1, scale); + ctx.input.scale[0] = scale[0]; + ctx.input.scale[1] = scale[1]; + ctx.input.scale[2] = scale[2]; + } else { + TF_CODING_ERROR("handleMultiply called for unexpected node of type %s", shaderId.GetText()); + return false; } - 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 + + return followConnectedInput(ctx, shader, AdobeTokens->in2); +} + +bool +handleAdd(InputContext& ctx, const UsdShadeOutput& shaderOutput) +{ + UsdShadeShader shader(shaderOutput.GetPrim()); + + TfToken shaderId; + shader.GetShaderId(&shaderId); + if (shaderId == MtlXTokens->ND_add_float) { + float bias = ctx.input.bias[0]; + getShaderInputValue(shader, AdobeTokens->in1, bias); + ctx.input.bias[0] = bias; + } else if (shaderId == MtlXTokens->ND_add_color3 || shaderId == MtlXTokens->ND_add_vector3) { + GfVec3f bias(ctx.input.bias[0], ctx.input.bias[1], ctx.input.bias[2]); + getShaderInputValue(shader, AdobeTokens->in1, bias); + ctx.input.bias[0] = bias[0]; + ctx.input.bias[1] = bias[1]; + ctx.input.bias[2] = bias[2]; + } else { + TF_CODING_ERROR("handleAdd called for unexpected node of type %s", shaderId.GetText()); + return false; } - 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); + + return followConnectedInput(ctx, shader, AdobeTokens->in2); +} + +// Note, we currently can't express that a normal input should have a ND_normalmap, but must not +// have a ND_separate4_vector4 node. Or that a float input needs a ND_separate4_vector4 followed by +// a ND_image_vector4 or just a ND_ND_UsdUVTexture_23 node. These could be modeled by different +// handler mappings and then switching depending on the surface input. + +// clang-format off +const ShaderHandlerMappings materialXHandlers = { + { { MtlXTokens->ND_normalmap }, + handleNormalMap, + kOptional }, + { { MtlXTokens->ND_mix_color3 }, + handleAmbientOcclusionBaseColorMap, + kOptional }, + { { MtlXTokens->ND_convert_color3_vector3 }, + handleConvertColorToVector, + kOptional }, + { { MtlXTokens->ND_add_float, MtlXTokens->ND_add_color3, MtlXTokens->ND_add_vector3 }, + handleAdd, + kOptional }, + { { MtlXTokens->ND_multiply_float, MtlXTokens->ND_multiply_color3, MtlXTokens->ND_multiply_vector3 }, + handleMultiply, + kOptional }, + { { MtlXTokens->ND_separate4_vector4 }, + handleSeparate4Vector4, + kOptional }, + { { MtlXTokens->ND_UsdUVTexture_23, MtlXTokens->ND_image_vector4, MtlXTokens->ND_image_color3, + MtlXTokens->ND_image_vector3 }, + handleImage }, + { { MtlXTokens->ND_place2d_vector2 }, + handlePlace2dVector2, kOptional }, + { { MtlXTokens->ND_texcoord_vector2, MtlXTokens->ND_geompropvalue_vector2 }, + handleTexcoordVector2 } +}; + +// This is a custom handler sequence to handle ambient occlusion input connections to base color +const ShaderHandlerMappings materialXAmbientOcclusionHandlers = { + { { MtlXTokens->ND_mix_color3 }, + handleAmbientOcclusionMap }, + { { MtlXTokens->ND_convert_float_color3 }, + handleConvertFloatToColor }, + { { MtlXTokens->ND_UsdUVTexture_23, MtlXTokens->ND_image_vector4, MtlXTokens->ND_image_color3, + MtlXTokens->ND_image_vector3 }, + handleImage }, + { { MtlXTokens->ND_place2d_vector2 }, + handlePlace2dVector2, kOptional }, + { { MtlXTokens->ND_texcoord_vector2, MtlXTokens->ND_geompropvalue_vector2 }, + handleTexcoordVector2 } +}; +// clang-format on + +// ------------------------------------------------------------------------------------------------- + +bool +readOpenPbrMaterial(ReadLayerContext& ctx, OpenPbrMaterial& material, const UsdShadeShader& surface) +{ + TfToken shaderId; + surface.GetShaderId(&shaderId); + if (shaderId != MtlXTokens->ND_open_pbr_surface_surfaceshader) { + return false; } - 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 success = true; + auto input = [&ctx, &surface, &success]( + const TfToken& inputName, Input& input, const ShaderHandlerMappings& handlers) { + InputContext inputContext = { ctx, inputName, handlers, input }; + bool result = readSurfaceInput(inputContext, surface); + if (!result) { + TF_WARN("Failed to read input %s on OpenPBR surface %s", + inputName.GetText(), + surface.GetPath().GetText()); + success = false; + } + }; + +#define INPUT(x) input(OpenPbrTokens->x, material.x, materialXHandlers); + 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 + + // handle collecting the ambient occlusion input by following the base_color input but using a + // differnt set of handlers to control the shade path traversal + input(OpenPbrTokens->base_color, material.occlusion, materialXAmbientOcclusionHandlers); + + // Non-OpenPBR inputs + input(UsdPreviewSurfaceTokens->displacement, material.displacement, materialXHandlers); + input(AsmTokens->anisotropyAngle, material.anisotropyAngle, materialXHandlers); + input(AsmTokens->coatSpecularLevel, material.coatSpecularLevel, materialXHandlers); + input(AsmTokens->volumeThickness, material.volumeThickness, materialXHandlers); + material.normalScale = _readNormalScale(surface); + material.useSpecularWorkflow = _readUseSpecularWorkflow(surface); + material.opacityThreshold = _readOpacityThreshold(surface); + material.clearcoatModelsTransmissionTint = _readClearcoatModelsTransmissionTint(surface); + material.isUnlit = _readUnlit(surface); + + return success; } +// ------------------------------------------------------------------------------------------------- + bool -readMaterial(ReadLayerContext& ctx, const UsdPrim& prim, int parent) +readMaterial(ReadLayerContext& ctx, const UsdPrim& prim) { - auto [materialIndex, material] = ctx.usd->addMaterial(); - ctx.materials[prim.GetPath().GetString()] = materialIndex; + OpenPbrMaterial material; 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) { + // We have a priority order of surface types: + // 1. OpenPBR/MaterialX (mtlx) + // 2. ASM (adobe) + // 3. UsdPreviewSurface (the universal fallback) + UsdShadeShader surface = + usdMaterial.ComputeSurfaceSource({ MtlXTokens->mtlx, AdobeTokens->adobe }); + if (!surface) { + TF_WARN("No surface shader for material %s", prim.GetPath().GetText()); + return false; + } + + bool success = readOpenPbrMaterial(ctx, material, surface); + if (!success) { 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); + // Question: when the reading fails, should the material be removed from the UsdData? + // Currently we keep a partially parsed Material in there. + auto [materialIndex, outputMaterial] = ctx.usd->addMaterial(); + ctx.materials[prim.GetPath().GetString()] = materialIndex; + outputMaterial = mapOpenPbrMaterialStructToMaterialStruct(material); + + printMaterial("layer::read", prim.GetPath(), outputMaterial, ctx.debugTag); return success; } diff --git a/utils/src/layerReadMaterialUtils.cpp b/utils/src/layerReadMaterialUtils.cpp new file mode 100644 index 00000000..81d6c426 --- /dev/null +++ b/utils/src/layerReadMaterialUtils.cpp @@ -0,0 +1,261 @@ +/* +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 + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace adobe::usd { + +// 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.GetAttr().GetPath().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.GetAttr().GetPath().GetText()); + } + outAttribute = attrs.front(); + if (expectShader) { + if (UsdShadeInput::IsInput(outAttribute)) { + TF_WARN("Input %s is connected to an attribute that is not a shader output.", + shadeInput.GetAttr().GetPath().GetText()); + return false; + } + } + return true; +} + +// Given an InputContext and a shader output, find the right handler based on the type of the shader +// node and call that handler. Return false if the output was not valid, a handler could not be +// found or the upstream handling failed. +bool +_handleShader(InputContext& ctx, const UsdShadeOutput& shaderOutput) +{ + UsdShadeShader shader(shaderOutput.GetPrim()); + if (!shader) { + TF_WARN("Prim %s is not a shader prim (input %s)", + shader.GetPath().GetText(), + ctx.surfaceInputName.GetText()); + return false; + } + + TfToken shaderId; + shader.GetShaderId(&shaderId); + + TF_DEBUG_MSG( + FILE_FORMAT_UTIL, "Handle shader %s at %s", shaderId.GetText(), shader.GetPath().GetText()); + + if (ctx.handlerIndex >= ctx.handlerMappings.size()) { + TF_CODING_ERROR("handlerIndex out of range"); + return false; + } + + const uint32_t numHandlers = ctx.handlerMappings.size(); + for (; ctx.handlerIndex < numHandlers; ++ctx.handlerIndex) { + const ShaderHandlerMapping& handlerMapping = ctx.handlerMappings[ctx.handlerIndex]; + for (const TfToken& handlerId : handlerMapping.nodeNames) { + if (handlerId == shaderId) { + // Advance the index, now that we've found this handler + ctx.handlerIndex++; + return handlerMapping.handler(ctx, shaderOutput); + } + } + + // We can't continue over a non-optional node + if (!handlerMapping.isOptional) { + TF_WARN( + "Expected shader of type %s (or equivalent) but got %s for prim at %s (input %s)", + handlerMapping.nodeNames[0].GetText(), + shaderId.GetText(), + shader.GetPath().GetText(), + ctx.surfaceInputName.GetText()); + return false; + } + } + + TF_WARN("Unexpected shader of type %s for prim at %s (input %s)", + shaderId.GetText(), + shader.GetPath().GetText(), + ctx.surfaceInputName.GetText()); + + return false; +} + +bool +readSurfaceInput(InputContext& ctx, const UsdShadeShader& surface) +{ + UsdShadeInput shadeInput = surface.GetInput(ctx.surfaceInputName); + if (!shadeInput) { + return true; + } + + // fetchPrimaryConnectedAttribute will return the current shadeInput as an attribute if there is + // no connection, but the attribute exists + UsdAttribute attr; + if (_fetchPrimaryConnectedAttribute(shadeInput, attr, false)) { + if (UsdShadeInput::IsInput(attr)) { + // Attempt to retrieve the constant value from the attribute. Not having a value is the + // same as the input attribute not existing + attr.Get(&ctx.input.value); + } else { + return _handleShader(ctx, UsdShadeOutput(attr)); + } + } + + return true; +} + +bool +followConnectedInput(InputContext& ctx, const UsdShadeShader& shader, const TfToken& inputName) +{ + UsdShadeInput input = shader.GetInput(inputName); + if (!input) { + TF_WARN("No input %s on node %s (input %s)", + inputName.GetText(), + shader.GetPath().GetText(), + ctx.surfaceInputName.GetText()); + return false; + } + + UsdAttribute attr; + if (_fetchPrimaryConnectedAttribute(input, attr, true)) { + return _handleShader(ctx, UsdShadeOutput(attr)); + } else { + TF_WARN("Expected valid shader input on %s for node %s (input %s)", + inputName.GetText(), + shader.GetPath().GetText(), + ctx.surfaceInputName.GetText()); + return false; + } +} + +// 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& filePath, + std::string& name, + std::string& extension, + bool warnAboutUnresolvedAssets) +{ + // Make sure we have a resolved path, either coming from SdfAssetPath value or by running it + // throught the resolver. + resolvedAssetPath = path.GetResolvedPath(); + if (resolvedAssetPath.empty()) { + resolvedAssetPath = ArGetResolver().Resolve(path.GetAssetPath()); + if (resolvedAssetPath.empty()) { + // As a fallback we continue with the unresolved path + resolvedAssetPath = path.GetAssetPath(); + if (warnAboutUnresolvedAssets) { + TF_WARN("Continuing with unresolved path '%s'", resolvedAssetPath.c_str()); + } + } + } + + // 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 + 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); +} + +int +readImage(ReadLayerContext& ctx, const SdfAssetPath& assetPath) +{ + std::string resolvedAssetPath, filePath, name, extension; + _populatePathPartsFromAssetPath( + assetPath, resolvedAssetPath, filePath, name, extension, ctx.warnAboutMissingAssets); + + // Check in the cache if we've processed this image before + if (const auto& it = ctx.images.find(resolvedAssetPath); it != ctx.images.end()) { + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Image (cached): %s\n", + ctx.debugTag.c_str(), + resolvedAssetPath.c_str()); + return it->second; + } + + // 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); + transcodeImageAssetToMemory(resolvedAssetPath, image.uri, image.image); + } else { + auto asset = ArGetResolver().OpenAsset(ArResolvedPath(resolvedAssetPath)); + if (asset) { + image.image.resize(asset->GetSize()); + memcpy(image.image.data(), asset->GetBuffer().get(), asset->GetSize()); + } else { + if (ctx.warnAboutMissingAssets) { + TF_WARN("%s: Unable to open asset: %s\n", + ctx.debugTag.c_str(), + resolvedAssetPath.c_str()); + } + } + } + + image.name = name; + image.uri = filePath; + image.format = getFormat(extension); + ctx.images[resolvedAssetPath] = imageIndex; + + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "%s: Image (new): index: %d uri: %s\n", + ctx.debugTag.c_str(), + imageIndex, + resolvedAssetPath.c_str()); + + return imageIndex; +} + +} diff --git a/utils/src/layerWriteMaterial.cpp b/utils/src/layerWriteMaterial.cpp index f89bbfc3..8db7f31b 100644 --- a/utils/src/layerWriteMaterial.cpp +++ b/utils/src/layerWriteMaterial.cpp @@ -64,7 +64,7 @@ _createStReader(SdfAbstractData* sdfData, const SdfPath& parentPath, int uvIndex getSTTexCoordReaderToken(uvIndex), AdobeTokens->UsdPrimvarReader_float2, "result", - { { "varname", getSTPrimvarAttrToken(uvIndex) } }, + { { "varname", getSTPrimvarAttrToken(uvIndex).GetString() } }, {}); } @@ -99,6 +99,7 @@ _createTextureReader(SdfAbstractData* sdfData, const TfToken& name, const Input& input, const SdfPath& stResultPath, + const std::string& texturePath, const SdfPath& textureConnection) { // Note, we're setting the texture path directly on this texture reader, which means the @@ -109,7 +110,8 @@ _createTextureReader(SdfAbstractData* sdfData, // attribute on the material and connect all corresponding texture readers to that attribute // value. - // Only emit scale and bias if they are not the default values + // Only emit scale and bias if they are not the default values. Empty values for + // scale/bias will be ignored VtValue scale, bias; if (input.scale != kDefaultTexScale) { scale = input.scale; @@ -117,6 +119,7 @@ _createTextureReader(SdfAbstractData* sdfData, if (input.bias != kDefaultTexBias) { bias = input.bias; } + InputValues inputValues = { { "fallback", _createFallbackValue(input.value) }, { "sourceColorSpace", _checkToken(input.colorspace) }, { "wrapS", _checkToken(input.wrapS) }, @@ -124,7 +127,9 @@ _createTextureReader(SdfAbstractData* sdfData, { "minFilter", _checkToken(input.minFilter) }, { "magFilter", _checkToken(input.magFilter) }, { "scale", scale }, - { "bias", bias } }; + { "bias", bias }, + { "file", VtValue(SdfAssetPath(texturePath)) } }; + InputConnections inputConnections = { { "st", stResultPath }, { "file", textureConnection } }; return createShader(sdfData, @@ -169,8 +174,21 @@ _setupInput(WriteSdfContext& ctx, } else { std::string texturePath = createTexturePath(ctx.srcAssetFilename, ctx.usdData->images[input.image].uri); - SdfPath textureConnection = addMaterialInputTexture( - ctx.sdfData, materialPath, materialInputName, texturePath, materialInputs); + // For UsdPreviewSurface we can detect color inputs with the type directly. For the ASM + // shader we need to both check the type to be Float3 and also to have "Color" or + // "emissive" in the name to differentiate from the normal inputs, which are also + // Float3. + const std::string& inputName = materialInputName.GetString(); + bool isColorTexture = inputType == SdfValueTypeNames->Color3f || + (inputType == SdfValueTypeNames->Float3 && + (inputName.find("Color") != std::string::npos || + inputName.find("emissive") != std::string::npos)); + SdfPath textureConnection = addMaterialInputTexture(ctx.sdfData, + materialPath, + materialInputName, + texturePath, + isColorTexture, + materialInputs); // Create the ST reader on demand when we create the first textured input SdfPath stReaderResultPath; @@ -188,7 +206,7 @@ _setupInput(WriteSdfContext& ctx, ctx.sdfData, parentPath, name.GetString(), input, stReaderResultPath); SdfPath texResultPath = _createTextureReader( - ctx.sdfData, parentPath, name, input, stResultPath, textureConnection); + ctx.sdfData, parentPath, name, input, stResultPath, texturePath, textureConnection); inputConnections.emplace_back(name.GetString(), texResultPath); } @@ -203,6 +221,43 @@ _setupInput(WriteSdfContext& ctx, } } +Input +_createEmissiveColorInput(const OpenPbrMaterial& material) +{ + if (material.emission_luminance.isEmpty() || material.emission_color.isZeroInput()) { + return {}; + } + + // Note, we do not handle the case where emission_luminance is driven by a texture + float luminanceValue = 0.0f; + if (material.emission_luminance.image >= 0) { + TF_WARN("emission_luminance driven by a texture. Can't be folded into emissiveColor."); + luminanceValue = 1.0f; + } else if (material.emission_luminance.value.IsHolding()) { + luminanceValue = material.emission_luminance.value.UncheckedGet(); + // Multiply by a factor to convert OpenPBR's emission_luminance. + // Without this, emission might be very blown out. + luminanceValue *= kOpenPbrToAsmEmissionFactor; + } + + Input emissiveColor = material.emission_color; + if (emissiveColor.isEmpty()) { + emissiveColor.value = GfVec3f(luminanceValue, luminanceValue, luminanceValue); + } else if (emissiveColor.image == -1) { + // Fold the luminance multiplier into the constant color value + GfVec3f colorValue = GfVec3f(1.0f); + if (emissiveColor.value.IsHolding()) { + colorValue = emissiveColor.value.UncheckedGet(); + } + emissiveColor.value = luminanceValue * colorValue; + } else { + // Fold the luminance multiplier into the texture scale + emissiveColor.scale *= luminanceValue; + } + + return emissiveColor; +} + void writeUsdPreviewSurface(WriteSdfContext& ctx, const SdfPath& materialPath, @@ -224,7 +279,7 @@ writeUsdPreviewSurface(WriteSdfContext& ctx, const InputToMaterialInputTypeMap& remapping = ShaderRegistry::getInstance().getUsdPreviewSurfaceInputRemapping(); auto writeInput = [&](const TfToken& name, const Input& input) { - if (!input.isEmpty()) + if (!input.isEmpty()) { _setupInput(ctx, materialPath, parentPath, @@ -235,11 +290,11 @@ writeUsdPreviewSurface(WriteSdfContext& ctx, inputConnections, remapping, materialInputs); + } }; 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); + writeInput(UsdPreviewSurfaceTokens->emissiveColor, _createEmissiveColorInput(material)); if (material.useSpecularWorkflow) { writeInput(UsdPreviewSurfaceTokens->useSpecularWorkflow, Input{ VtValue(1) }); } @@ -287,6 +342,28 @@ writeUsdPreviewSurface(WriteSdfContext& ctx, } } +Input +_createEmissiveIntensityInput(const Input& emission_luminance) +{ + if (emission_luminance.isEmpty()) { + return {}; + } + + // Note, we do not handle the case where emission_luminance is driven by a texture + float luminanceValue = 0.0f; + if (emission_luminance.image >= 0) { + TF_WARN("emission_luminance driven by a texture. Can't be folded into emissiveColor."); + luminanceValue = 1.0f; + } else if (emission_luminance.value.IsHolding()) { + luminanceValue = emission_luminance.value.UncheckedGet(); + // Multiply by a factor to convert OpenPBR's emission_luminance. + // Without this, emission might be very blown out. + luminanceValue *= kOpenPbrToAsmEmissionFactor; + } + + return Input{ VtValue(luminanceValue) }; +} + void writeAsmMaterial(WriteSdfContext& ctx, const SdfPath& materialPath, @@ -307,21 +384,20 @@ writeAsmMaterial(WriteSdfContext& ctx, const InputToMaterialInputTypeMap& remapping = ShaderRegistry::getInstance().getAsmInputRemapping(); auto writeInput = [&](const TfToken& name, const Input& input) { - _setupInput(ctx, - materialPath, - parentPath, - name, - input, - stReaderResultPathMap, - inputValues, - inputConnections, - remapping, - materialInputs); + if (!input.isEmpty()) { + _setupInput(ctx, + materialPath, + parentPath, + name, + input, + stReaderResultPathMap, + inputValues, + inputConnections, + remapping, + materialInputs); + } }; - // Currently unused inputs - // Input useSpecularWorkflow; - writeInput(AsmTokens->baseColor, material.base_color); writeInput(AsmTokens->roughness, material.specular_roughness); writeInput(AsmTokens->metallic, material.base_metalness); @@ -332,14 +408,17 @@ writeAsmMaterial(WriteSdfContext& ctx, if (material.normalScale != 1.0f) { writeInput(AsmTokens->normalScale, Input{ VtValue(material.normalScale) }); } + // combineNormalAndHeight = false (flag) (no source info) + writeInput(AsmTokens->height, material.displacement); // heightScale (no source info) // heightLevel (no source info) 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->emissiveIntensity, + _createEmissiveIntensityInput(material.emission_luminance)); writeInput(AsmTokens->emissive, material.emission_color); writeInput(AsmTokens->sheenOpacity, material.fuzz_weight); writeInput(AsmTokens->sheenColor, material.fuzz_color); @@ -369,19 +448,12 @@ writeAsmMaterial(WriteSdfContext& ctx, writeInput(AsmTokens->coatSpecularLevel, material.coatSpecularLevel); writeInput(AsmTokens->coatNormal, material.geometry_coat_normal); // coatNormalScale (the scale is part of the coatNormal `scale` or `value`) + 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, @@ -394,22 +466,6 @@ writeAsmMaterial(WriteSdfContext& ctx, ctx.sdfData, materialPath, "adobe:surface", SdfValueTypeNames->Token, outputPath); SdfPath surfaceShaderPath = parentPath.AppendChild(AdobeTokens->ASM); - if (material.isUnlit) { - // 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); - } - - if (material.clearcoatModelsTransmissionTint) { - // Author a custom attribute to leave an indicator where the clearcoat came from - SdfPath p = createAttributeSpec(ctx.sdfData, - surfaceShaderPath, - AdobeTokens->clearcoatModelsTransmissionTint, - SdfValueTypeNames->Bool); - setAttributeMetadata(ctx.sdfData, p, SdfFieldKeys->Custom, VtValue(true)); - setAttributeDefaultValue(ctx.sdfData, p, true); - } + createExtraConstantAttribute(ctx.sdfData, material, surfaceShaderPath); } } diff --git a/utils/src/layerWriteOpenPBR.cpp b/utils/src/layerWriteOpenPBR.cpp index 21eaf098..5d22d5f7 100644 --- a/utils/src/layerWriteOpenPBR.cpp +++ b/utils/src/layerWriteOpenPBR.cpp @@ -27,17 +27,15 @@ const std::string stPrimvarNameAttrName = "stPrimvarName"; SdfPath _createMaterialXUvReader(SdfAbstractData* sdfData, const SdfPath& parentPath, int uvIndex) { - // XXX The MaterialX texcoord reader function has an index to specify which set of UV - // coordinates to read, but it does not have the ability to specify a primvar by name. So we - // currently default to the first set, but there is something to be figured out about how to - // connect a named primvar to a UV coordinate index in MaterialX. - // Maybe ND_geompropvalue_vector2 with geomprop="st" will do the trick. Note, that the shared - // stPrimvarNameAttrName input attribute is of type Token, but `geomprop` is of type String + // Map the index to a unique name for the texture uv coordinate reader and map the index to + // one of st, st1, st2, ... for use as the geomprop value. return createShader(sdfData, parentPath, getSTTexCoordReaderToken(uvIndex), - MtlXTokens->ND_texcoord_vector2, - "out"); + MtlXTokens->ND_geompropvalue_vector2, + "out", + { { "geomprop", getSTPrimvarAttrToken(uvIndex).GetString() } }, + {}); } // If a texture coordinate transform is needed for the given input a transform will be created and @@ -70,7 +68,6 @@ _createMaterialXUvTransform(SdfAbstractData* sdfData, "out", { { "scale", scale }, { "rotate", input.uvRotation }, { "offset", input.uvTranslation } }, { { "texcoord", uvReaderResultPath } }); - ; } std::string @@ -93,156 +90,124 @@ _toMaterialXAddressMode(const TfToken& wrapMode) } SdfPath -_createScaleAndBiasNodes(SdfAbstractData* sdfData, - const SdfPath& parentPath, - const std::string& baseName, - const SdfPath& textureInput, - int numChannels, - bool isColor, - const GfVec4f& scale4, - const GfVec4f& bias4) +createMaterialXTextureReader(SdfAbstractData* sdfData, + const SdfPath& parentPath, + const TfToken& name, + const Input& input, + const SdfPath& uvResultPath, + const SdfPath& textureConnection) { - TfToken scaleShaderType, biasShaderType; - VtValue scale, bias; - if (numChannels == 1) { - float s = scale4[0]; - if (s != 1.0f) { - scale = s; - scaleShaderType = MtlXTokens->ND_multiply_float; - } - float b = bias4[0]; - if (b != 0.0f) { - bias = b; - biasShaderType = MtlXTokens->ND_add_float; - } - } else if (numChannels == 3) { - GfVec3f s = GfVec3f(scale4[0], scale4[1], scale4[2]); - if (s != GfVec3f(1.0f)) { - scale = s; - scaleShaderType = - isColor ? MtlXTokens->ND_multiply_color3 : MtlXTokens->ND_multiply_vector3; + // Normal and tangent map textures need a bit of special processing in MaterialX + const bool isNormalMap = + name == OpenPbrTokens->geometry_normal || name == OpenPbrTokens->geometry_coat_normal; + const bool isTangentMap = + name == OpenPbrTokens->geometry_tangent || name == OpenPbrTokens->geometry_coat_tangent; + + // Most texture inputs are for color and single float inputs on the OpenPBR surface shader + // and these inputs need to support channel selection from packed RGBA texture and also + // scale & bias support to adjust the read texel values. That is why we're using a + // ND_UsdUVTexture_23 shader, which was designed to emulate the UsdUVTexture node that we've + // been using before. + TfToken outputName = input.channel; + static const GfVec4f defaultFallback(0.0f, 0.0f, 0.0f, 1.0f); + GfVec4f fallback = defaultFallback; + if (outputName == AdobeTokens->r) { + if (input.value.IsHolding()) { + fallback[0] = input.value.UncheckedGet(); } - GfVec3f b = GfVec3f(bias4[0], bias4[1], bias4[2]); - if (b != GfVec3f(0.0f)) { - bias = b; - biasShaderType = isColor ? MtlXTokens->ND_add_color3 : MtlXTokens->ND_add_vector3; + } else if (outputName == AdobeTokens->g) { + if (input.value.IsHolding()) { + fallback[1] = input.value.UncheckedGet(); } - } - - SdfPath textureOutput = textureInput; - if (!scale.IsEmpty()) { - textureOutput = createShader(sdfData, - parentPath, - TfToken(baseName + "_scale"), - scaleShaderType, - "out", - { { "in1", scale } }, - { { "in2", textureOutput } }); - } - if (!bias.IsEmpty()) { - textureOutput = createShader(sdfData, - parentPath, - TfToken(baseName + "_bias"), - biasShaderType, - "out", - { { "in1", bias } }, - { { "in2", textureOutput } }); - } - - return textureOutput; -} - -SdfPath -_createMaterialXTextureReader(SdfAbstractData* sdfData, - const SdfPath& parentPath, - const TfToken& name, - const Input& input, - const SdfPath& uvResultPath, - const SdfPath& textureConnection, - bool isNormalMap, - bool convertToColor) -{ - int numChannels = input.numChannels(); - TfToken shaderType; - VtValue defaultValue; - if (numChannels == 1) { - // If we want to extract a single channel we read the RGBA version of the texture in linear - // color space. - shaderType = MtlXTokens->ND_image_vector4; + } else if (outputName == AdobeTokens->b) { if (input.value.IsHolding()) { - // We're always using a RGBA texture reader (ND_image_vector4), so the fallback value - // has to match, even if we only care about a single channel. - float f = input.value.UncheckedGet(); - defaultValue = GfVec4f(f); + fallback[2] = input.value.UncheckedGet(); } - } else if (numChannels == 3) { - // We differentiate between two types of texture readers depending on the type of input on - // the surface shader. A mismatch in types will lead to errors. - if (name == OpenPbrTokens->geometry_normal || name == OpenPbrTokens->geometry_coat_normal || - name == OpenPbrTokens->geometry_tangent) { - shaderType = MtlXTokens->ND_image_vector3; - } else { - shaderType = MtlXTokens->ND_image_color3; + } else if (outputName == AdobeTokens->a) { + if (input.value.IsHolding()) { + fallback[3] = input.value.UncheckedGet(); } + } else if (outputName == AdobeTokens->rgb) { if (input.value.IsHolding()) { - defaultValue = input.value; + const GfVec3f& vec3 = input.value.UncheckedGet(); + fallback = GfVec4f(vec3[0], vec3[1], vec3[2], 1.0f); } } else { - TF_CODING_ERROR( - "Unsupported texture type for %d channels on input %s", numChannels, name.GetText()); + TF_CODING_ERROR("Unsupported texture type for channel %s on input %s", + outputName.GetText(), + name.GetText()); return SdfPath(); } // In MaterialX, each input attribute on a node can have an associated color space. We - // explicitly mark the "file" input with a color space if we know that we got a sRGB texture. - // Note, this will become the "colorSpace" metadata on the input attribute. + // explicitly mark the "file" input with a color space if we know that we got a sRGB + // texture. Note, this will become the "colorSpace" metadata on the input attribute. InputColorSpaces inputColorSpaces; if (input.colorspace == AdobeTokens->sRGB) { inputColorSpaces["file"] = MtlXTokens->srgb_texture; } - InputValues inputValues = { { "default", defaultValue }, - { "uaddressmode", _toMaterialXAddressMode(input.wrapS) }, - { "vaddressmode", _toMaterialXAddressMode(input.wrapT) } }; - InputConnections inputConnections = { { "texcoord", uvResultPath }, - { "file", textureConnection } }; - - // Note, we're setting the texture path directly on this texture reader, which means the - // path is duplicated on each texture reader of the same texture for each of the different - // sub networks. This is currently needed since some software is not correctly following - // connections to resolve input values. - // Once that has improved in the ecosystem we could author the asset path once as an - // attribute on the material and connect all corresponding texture readers to that attribute - // value. + // The inputs on the node are called wrapS and wrapT, but are using string values like the + // uaddressmode and vaddressmode on other MaterialX texture nodes. + InputValues inputValues = { { "wrapS", _toMaterialXAddressMode(input.wrapS) }, + { "wrapT", _toMaterialXAddressMode(input.wrapT) } }; + if (fallback != defaultFallback) { + inputValues.emplace_back("fallback", fallback); + } + + GfVec4f scale = input.scale; + GfVec4f bias = input.bias; + if (isNormalMap) { + // In MaterialX, the ND_normalmap node, which is downstream of the ND_UsdUVTexture_23 will + // decode the normal from the raw texture value, assuming the OpenGL convention, using a + // scale and bias of 2 (kOpenGLNormalTexScale) and -1 (kOpenGLNormalTexBias). That means it + // would be redundant to have this on the ND_UsdUVTexture_23 node. + // + // We have these decoding scale and bias values in our Input struct, especially if we're + // trying to differentiate it from a DirectX encoded normalmap and/or a normal strength + // multiplier. So we apply the inverse affine transform using the OpenGL decoding values, + // which yields a scale of 1 and a bias of 0, if it was indeed the OpenGL convention. In the + // case of something else it will yield a transformation to something that can be decoding + // with the OpenGL convention. Thus we can represent DirectX encoding and multipliers. + // + // Note that this mirrors the process in the OpenPBR reading code. + scale = GfCompDiv(scale, kOpenGLNormalTexScale); + bias = GfCompDiv(bias - kOpenGLNormalTexBias, kOpenGLNormalTexScale); + } + if (scale != kDefaultTexScale) { + inputValues.emplace_back("scale", scale); + } + if (bias != kDefaultTexBias) { + inputValues.emplace_back("bias", bias); + } + + InputConnections inputConnections = { { "st", uvResultPath }, { "file", textureConnection } }; + SdfPath textureOutput = createShader(sdfData, parentPath, name, - shaderType, - "out", + MtlXTokens->ND_UsdUVTexture_23, + outputName.GetString(), inputValues, inputConnections, inputColorSpaces); - // Extract the single channel from the 4 channel reader - if (numChannels == 1) { - std::string out = input.channel == AdobeTokens->r ? "outx" - : input.channel == AdobeTokens->g ? "outy" - : input.channel == AdobeTokens->b ? "outz" - : "outw"; + if (isNormalMap || isTangentMap) { + // The rgb output of the ND_UsdUVTexture_23 is of type color3, but the ND_normalmap node + // for normal maps and the tangent map input on the surface require vector3. So we inject a + // simple type conversion node for correctness. textureOutput = createShader(sdfData, parentPath, - TfToken(name.GetString() + "_to_float"), - MtlXTokens->ND_separate4_vector4, - out, + TfToken(name.GetString() + "_as_vector"), + MtlXTokens->ND_convert_color3_vector3, + "out", {}, { { "in", textureOutput } }); } if (isNormalMap) { - // The texture reader for a normal map reads a texture map in tangent space, which needs to - // be transformed into world space. Route normal map through a normal map node - // Note, we skip the usual scale and bias of 2 and -1 for the normal map data and send the - // data directly into the normalmap node. + // The texture reader for a normal map reads a texture map in tangent space, which needs + // to be transformed into world space. Route normal map through a normal map node. textureOutput = createShader(sdfData, parentPath, TfToken(name.GetString() + "_to_world_space"), @@ -250,28 +215,6 @@ _createMaterialXTextureReader(SdfAbstractData* sdfData, "out", {}, { { "in", textureOutput } }); - } else { - if (!input.hasDefaultScaleAndBias()) { - bool isColor = shaderType == MtlXTokens->ND_image_color3; - textureOutput = _createScaleAndBiasNodes(sdfData, - parentPath, - name.GetString(), - textureOutput, - numChannels, - isColor, - input.scale, - input.bias); - } - } - - if (convertToColor && numChannels == 1) { - textureOutput = createShader(sdfData, - parentPath, - TfToken(name.GetString() + "_to_color"), - MtlXTokens->ND_convert_float_color3, - "out", - {}, - { { "in", textureOutput } }); } return textureOutput; @@ -313,8 +256,13 @@ _setupOpenPbrInput(WriteSdfContext& ctx, std::string texturePath = createTexturePath(ctx.srcAssetFilename, ctx.usdData->images[input.image].uri); - SdfPath textureConnection = addMaterialInputTexture( - ctx.sdfData, materialPath, materialInputName, texturePath, materialInputs); + bool isColorTexture = inputType == SdfValueTypeNames->Color3f; + SdfPath textureConnection = addMaterialInputTexture(ctx.sdfData, + materialPath, + materialInputName, + texturePath, + isColorTexture, + materialInputs); // Create the ST reader on demand when we create the first textured input SdfPath uvReaderResultPath; @@ -332,18 +280,8 @@ _setupOpenPbrInput(WriteSdfContext& ctx, SdfPath stResultPath = _createMaterialXUvTransform( ctx.sdfData, parentPath, name.GetString(), input, uvReaderResultPath); - bool isNormalMap = - name == OpenPbrTokens->geometry_normal || name == OpenPbrTokens->geometry_coat_normal; - // geometry_opacity expects a color, but our input opacity is a float input - bool convertToColor = name == OpenPbrTokens->geometry_opacity; - SdfPath texResultPath = _createMaterialXTextureReader(ctx.sdfData, - parentPath, - name, - input, - stResultPath, - textureConnection, - isNormalMap, - convertToColor); + SdfPath texResultPath = createMaterialXTextureReader( + ctx.sdfData, parentPath, name, input, stResultPath, textureConnection); inputConnections.emplace_back(name.GetString(), texResultPath); } @@ -398,8 +336,8 @@ writeOpenPBR(WriteSdfContext& ctx, materialInputs); }; -#define INPUT(x) writeInput(OpenPbrTokens->x, material.x); - INPUT(base_weight); +#define INPUT(x) writeInput(OpenPbrTokens->x, material.x) + INPUT(base_weight); // has no UsdPreviewSurface or ASM equivalent INPUT(base_color); INPUT(base_diffuse_roughness); INPUT(base_metalness); @@ -440,8 +378,82 @@ writeOpenPBR(WriteSdfContext& ctx, INPUT(geometry_coat_normal); INPUT(geometry_tangent); INPUT(geometry_coat_tangent); + + // We handle ambient occlusion for OpenPBR with a custom shader graph created below (if + // necessary) + writeInput(UsdPreviewSurfaceTokens->occlusion, material.occlusion); #undef INPUT + // Non-OpenPBR inputs + // When in transcoding mode (preserveExtraMaterialInfo=true) we write these fields to not loose + // any information when reading the USD data again and exporting to a format that supports these + // inputs. + // When not transcoding we do not write them, since a MaterialX / OpenPBR renderer will not use + // these fields and might even fail to validate. + if (ctx.options->preserveExtraMaterialInfo) { + // TODO turn into proper displacement setup + writeInput(UsdPreviewSurfaceTokens->displacement, material.displacement); + writeInput(AsmTokens->anisotropyAngle, material.anisotropyAngle); + writeInput(AsmTokens->coatSpecularLevel, material.coatSpecularLevel); + writeInput(AsmTokens->volumeThickness, material.volumeThickness); + } + + // first check if we have connections for both base_color and occlusion + auto baseColorIt = + std::find_if(inputConnections.begin(), inputConnections.end(), [&](const auto& p) { + return p.first == OpenPbrTokens->base_color.GetString(); + }); + auto occlusionIt = + std::find_if(inputConnections.begin(), inputConnections.end(), [&](const auto& p) { + return p.first == UsdPreviewSurfaceTokens->occlusion.GetString(); + }); + + // If there are connections to base_color and occlusion to be added to the OpenPBR shader, we + // generate an OpenPBR subgraph that feeds the base_color and occlusion + // inputs to an ND_mix_color3 shader node and then connect the ND_mix_color3 output to the + // OpenPBR base_color input. The occlusion input is first converted from a float to a color3 + // with the ND_convert_float_color3 shader node. + if (baseColorIt != inputConnections.end() && occlusionIt != inputConnections.end()) { + // capture the input paths for the base_color and occlusion connections + SdfPath baseColorConnection = baseColorIt->second; + SdfPath occlusionConnection = occlusionIt->second; + + // Remove both the base_color and occlusion connections. We'll create a new base_color + // connection below where an ND_mix_color3 node will be created to combine the base_color + // and occlusion sources which will then be input source for the OpenPBR base_color input. + inputConnections.erase(baseColorIt); + occlusionIt = + std::find_if(inputConnections.begin(), inputConnections.end(), [&](const auto& p) { + return p.first == UsdPreviewSurfaceTokens->occlusion.GetString(); + }); + if (occlusionIt != inputConnections.end()) + inputConnections.erase(occlusionIt); + + // convert ambientOcclusion float to color3 + SdfPath occlusionColorOutput = createShader(ctx.sdfData, + parentPath, + AdobeTokens->AmbientOcclusionAsColor, + MtlXTokens->ND_convert_float_color3, + "out", + {}, + { { "in", occlusionConnection } }); + + // Provide base_color and occlusion color as inputs to the ND_mix_color3 node. + // Note: We use a fixed value of 0.0 for the "mix" input which means that the "bg" input is + // connected to the base color source and "fg" is connected to the ambient occlusion source. + SdfPath ambientOcclusionBaseColor = + createShader(ctx.sdfData, + parentPath, + AdobeTokens->AmbientOcclusionBaseColor, + MtlXTokens->ND_mix_color3, + "out", + { { "mix", 0.0f } }, + { { "bg", baseColorConnection }, { "fg", occlusionColorOutput } }); + + // add the connection from the ND_mix_color3 output to the OpenPBR base_color input + inputConnections.emplace_back("base_color", ambientOcclusionBaseColor); + } + // Create OpenPBR surface shader SdfPath outputPath = createShader(ctx.sdfData, parentPath, @@ -453,6 +465,9 @@ writeOpenPBR(WriteSdfContext& ctx, createShaderOutput( ctx.sdfData, materialPath, "mtlx:surface", SdfValueTypeNames->Token, outputPath); + SdfPath surfaceShaderPath = parentPath.AppendChild(MtlXTokens->OpenPBR); + createExtraConstantAttribute(ctx.sdfData, material, surfaceShaderPath); + // TODO: create displacement setup } } diff --git a/utils/src/layerWriteSdfData.cpp b/utils/src/layerWriteSdfData.cpp index 881f2d66..9751541d 100644 --- a/utils/src/layerWriteSdfData.cpp +++ b/utils/src/layerWriteSdfData.cpp @@ -32,6 +32,7 @@ governing permissions and limitations under the License. #include #include +#include #include using namespace PXR_NS; @@ -56,6 +57,8 @@ TF_DEFINE_PRIVATE_TOKENS(_tokens, // Render settings ((render, "Render")) ((primarySetting, "PrimarySetting")) + // Geometry subsets + ((subsetFamilyMaterialBindFamilyType, "subsetFamily:materialBind:familyType")) ); // clang-format on @@ -139,7 +142,7 @@ _writeCamera(SdfAbstractData* sdfData, const SdfPath& parentPath, const Camera& auto createAttr = [&](const TfToken& name, const SdfValueTypeName& type, const auto& value) { SdfPath p = createAttributeSpec(sdfData, primPath, name, type); - setAttributeDefaultValue(sdfData, p, value); + setAttributeDefaultValue(sdfData, p, value, type); }; if (camera.markedInvisible) { @@ -178,7 +181,7 @@ _writeNgp(SdfAbstractData* sdfData, const SdfPath& parentPath, const NgpData& ng type, uniform ? PXR_NS::SdfVariabilityUniform : PXR_NS::SdfVariabilityVarying); - setAttributeDefaultValue(sdfData, p, value); + setAttributeDefaultValue(sdfData, p, value, type); }; createAttr(ngpPrimPath, @@ -261,7 +264,7 @@ _writeLight(SdfAbstractData* sdfData, const SdfPath& parentPath, const Light& li SdfPath lightPath = SdfPath(); auto createAttr = [&](const TfToken& name, const SdfValueTypeName& type, const auto& value) { SdfPath p = createAttributeSpec(sdfData, lightPath, name, type); - setAttributeDefaultValue(sdfData, p, value); + setAttributeDefaultValue(sdfData, p, value, type); }; switch (light.type) { @@ -415,7 +418,7 @@ _writeXformAttributes(SdfAbstractData* sdfData, const SdfPath& primPath, const N if (node.markedInvisible) { SdfPath p = createAttributeSpec( sdfData, primPath, UsdGeomTokens->visibility, SdfValueTypeNames->Token); - setAttributeDefaultValue(sdfData, p, UsdGeomTokens->invisible); + setAttributeDefaultValue(sdfData, p, UsdGeomTokens->invisible, SdfValueTypeNames->Token); } VtArray xformOpOrder; @@ -427,7 +430,7 @@ _writeXformAttributes(SdfAbstractData* sdfData, const SdfPath& primPath, const N xformOpOrder.push_back(_tokens->xformOpTranslate); if (hasTranslation) { - setAttributeDefaultValue(sdfData, p, node.translation); + setAttributeDefaultValue(sdfData, p, node.translation, SdfValueTypeNames->Double3); } // XXX currently the translations is stored as GfVec3f, but needs to be authored as GfVec3d _writeTimeSamples(sdfData, p, nodeAnimation.translations); @@ -439,7 +442,7 @@ _writeXformAttributes(SdfAbstractData* sdfData, const SdfPath& primPath, const N xformOpOrder.push_back(_tokens->xformOpOrient); if (hasRotation) { - setAttributeDefaultValue(sdfData, p, node.rotation); + setAttributeDefaultValue(sdfData, p, node.rotation, SdfValueTypeNames->Quatf); } _writeTimeSamples(sdfData, p, nodeAnimation.rotations); } @@ -450,14 +453,14 @@ _writeXformAttributes(SdfAbstractData* sdfData, const SdfPath& primPath, const N xformOpOrder.push_back(_tokens->xformOpScale); if (hasScale) { - setAttributeDefaultValue(sdfData, p, node.scale); + setAttributeDefaultValue(sdfData, p, node.scale, SdfValueTypeNames->Float3); } _writeTimeSamples(sdfData, p, nodeAnimation.scales); } if (node.hasTransform && node.transform != GfMatrix4d().SetIdentity()) { SdfPath p = createAttributeSpec( sdfData, primPath, _tokens->xformOpTransform, SdfValueTypeNames->Matrix4d); - setAttributeDefaultValue(sdfData, p, node.transform); + setAttributeDefaultValue(sdfData, p, node.transform, SdfValueTypeNames->Matrix4d); xformOpOrder.push_back(_tokens->xformOpTransform); } @@ -467,7 +470,7 @@ _writeXformAttributes(SdfAbstractData* sdfData, const SdfPath& primPath, const N UsdGeomTokens->xformOpOrder, SdfValueTypeNames->TokenArray, SdfVariabilityUniform); - setAttributeDefaultValue(sdfData, p, xformOpOrder); + setAttributeDefaultValue(sdfData, p, xformOpOrder, SdfValueTypeNames->TokenArray); } } @@ -494,18 +497,18 @@ _createGeomSubset(SdfAbstractData* sdfData, UsdGeomTokens->elementType, SdfValueTypeNames->Token, SdfVariabilityUniform); - setAttributeDefaultValue(sdfData, p, UsdGeomTokens->face); + setAttributeDefaultValue(sdfData, p, UsdGeomTokens->face, SdfValueTypeNames->Token); // Face indices p = createAttributeSpec(sdfData, subsetPath, UsdGeomTokens->indices, SdfValueTypeNames->IntArray); - setAttributeDefaultValue(sdfData, p, subset.faces); + setAttributeDefaultValue(sdfData, p, subset.faces, SdfValueTypeNames->IntArray); // family type = materialBind p = createAttributeSpec(sdfData, subsetPath, UsdGeomTokens->familyName, SdfValueTypeNames->Token, SdfVariabilityUniform); - setAttributeDefaultValue(sdfData, p, UsdShadeTokens->materialBind); + setAttributeDefaultValue(sdfData, p, UsdShadeTokens->materialBind, SdfValueTypeNames->Token); return subsetPath; } @@ -526,14 +529,15 @@ _writePrimvar(SdfAbstractData* sdfData, setAttributeMetadata( sdfData, primvarAttrPath, UsdGeomTokens->interpolation, VtValue(primvar.interpolation)); - setAttributeDefaultValue(sdfData, primvarAttrPath, primvar.values); + setAttributeDefaultValue(sdfData, primvarAttrPath, primvar.values, typeName); if (!primvar.indices.empty()) { // The indices are stored in a sibling attribute TfToken indicesAttrName("primvars:" + primvarName + ":indices"); SdfPath primvarIndicesAttrPath = createAttributeSpec(sdfData, primPath, indicesAttrName, SdfValueTypeNames->IntArray); - setAttributeDefaultValue(sdfData, primvarIndicesAttrPath, primvar.indices); + setAttributeDefaultValue( + sdfData, primvarIndicesAttrPath, primvar.indices, SdfValueTypeNames->IntArray); } return primvarAttrPath; @@ -554,7 +558,8 @@ _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); + _writePrimvar( + sdfData, primPath, "bitangents", SdfValueTypeNames->Float3Array, mesh.bitangents); } auto indexedName = [](const std::string& baseName, int index) -> std::string { @@ -600,7 +605,8 @@ _writePrimvars(SdfAbstractData* sdfData, const SdfPath& primPath, const Mesh& me UsdGeomTokens->extent, SdfValueTypeNames->Float3Array, PXR_NS::SdfVariabilityVarying); - setAttributeDefaultValue(sdfData, extentAttrSpec, mesh.clippingBox.values); + setAttributeDefaultValue( + sdfData, extentAttrSpec, mesh.clippingBox.values, SdfValueTypeNames->Float3Array); } } } @@ -617,7 +623,7 @@ _writePoints(SdfAbstractData* sdfData, const SdfPath& parentPath, const Mesh& me auto createAttr = [&](const TfToken& name, const SdfValueTypeName& type, const auto& value) { SdfPath p = createAttributeSpec(sdfData, primPath, name, type); - setAttributeDefaultValue(sdfData, p, value); + setAttributeDefaultValue(sdfData, p, value, type); return p; }; @@ -671,7 +677,7 @@ _writeMesh(SdfAbstractData* sdfData, SdfVariability variability = uniform ? PXR_NS::SdfVariabilityUniform : PXR_NS::SdfVariabilityVarying; SdfPath p = createAttributeSpec(sdfData, primPath, name, type, variability); - setAttributeDefaultValue(sdfData, p, value); + setAttributeDefaultValue(sdfData, p, value, type); }; if (mesh.markedInvisible) { @@ -726,6 +732,20 @@ _writeMesh(SdfAbstractData* sdfData, // Subsets if (mesh.subsets.size()) { + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "write mesh subsets: %zu subsets for mesh %s\n", + mesh.subsets.size(), + meshName.c_str()); + + // Set the subsetFamily:materialBind:familyType for this mesh to declare the subsets a + // partition. Note that the "materialBind" part has to match the familyName on the subsets + // themselves, which is also "materialBind". See the schema documentation for more info: + // https://github.com/PixarAnimationStudios/OpenUSD/blob/v25.11/pxr/usd/usdGeom/schema.usda#L1354 + createAttr(_tokens->subsetFamilyMaterialBindFamilyType, + SdfValueTypeNames->Token, + UsdGeomTokens->partition, + true); + for (size_t i = 0; i < mesh.subsets.size(); i++) { const Subset& subset = mesh.subsets[i]; TfToken subsetName = TfToken(meshName + "_sub" + std::to_string(i)); @@ -830,7 +850,7 @@ _writeNurb(SdfAbstractData* sdfData, const SdfPath& parentPath, NurbData& nurb) auto createAttr = [&](const TfToken& name, const SdfValueTypeName& type, const auto& value) { SdfPath p = createAttributeSpec(sdfData, primPath, name, type); - setAttributeDefaultValue(sdfData, p, value); + setAttributeDefaultValue(sdfData, p, value, type); }; createAttr(UsdGeomTokens->uOrder, SdfValueTypeNames->Int, nurb.uOrder); @@ -904,7 +924,7 @@ _writeCurve(WriteSdfContext& ctx, const SdfPath& parentPath, const Curve& curve) SdfVariability variability = uniform ? PXR_NS::SdfVariabilityUniform : PXR_NS::SdfVariabilityVarying; SdfPath p = createAttributeSpec(ctx.sdfData, primPath, name, type, variability); - setAttributeDefaultValue(ctx.sdfData, p, value); + setAttributeDefaultValue(ctx.sdfData, p, value, type); return p; }; @@ -1024,7 +1044,6 @@ _writeNode(WriteSdfContext& ctx, const SdfPath& primPath, const Node& node) // Instanced meshes second. They need a name resolution to make sure they are unique UniqueNameEnforcer enforcer; - int i = 0; for (int meshIndex : node.staticMeshes) { const Mesh& mesh = ctx.usdData->meshes[meshIndex]; if (mesh.instanceable) { @@ -1144,7 +1163,7 @@ _writeSkeleton(SdfAbstractData* sdfData, const SdfPath& parentPath, const Skelet SdfVariability variability = uniform ? PXR_NS::SdfVariabilityUniform : PXR_NS::SdfVariabilityVarying; SdfPath p = createAttributeSpec(sdfData, primPath, name, type, variability); - setAttributeDefaultValue(sdfData, p, value); + setAttributeDefaultValue(sdfData, p, value, type); }; createAttr(UsdSkelTokens->joints, SdfValueTypeNames->TokenArray, skeleton.joints, true); @@ -1199,7 +1218,7 @@ _writeSkeletonAnimation(SdfAbstractData* sdfData, UsdSkelTokens->joints, SdfValueTypeNames->TokenArray, SdfVariabilityUniform); - setAttributeDefaultValue(sdfData, p, skeleton.animatedJoints); + setAttributeDefaultValue(sdfData, p, skeleton.animatedJoints, SdfValueTypeNames->TokenArray); SdfPath rotAttrPath = createAttributeSpec( sdfData, primPath, UsdSkelTokens->rotations, SdfValueTypeNames->QuatfArray); @@ -1257,17 +1276,6 @@ _writeMaterial(WriteSdfContext& ctx, const SdfPath& parentPath, const OpenPbrMat return materialPath; } -void -_writeImage(const std::string& assetsPath, const ImageAsset& image) -{ - std::string filename = assetsPath + "/" + image.uri; - std::ofstream file(filename, std::ios::out | std::ios::binary); - if (!file.is_open()) - return; - file.write(reinterpret_cast(image.image.data()), image.image.size()); - file.close(); -} - // Processes animation tracks, putting animation track data in the metadata void _writeAnimationTracks(const WriteLayerOptions& options, UsdData& data) @@ -1278,7 +1286,7 @@ _writeAnimationTracks(const WriteLayerOptions& options, UsdData& data) // Ignore all animation tracks beyond the first one if we aren't importing multiple tracks int firstTrackWithTimepoints = -1; - for (int trackIndex = 0; trackIndex < data.animationTracks.size(); trackIndex++) { + for (size_t trackIndex = 0; trackIndex < data.animationTracks.size(); trackIndex++) { AnimationTrack& track = data.animationTracks[trackIndex]; if (!track.hasTimepoints) { continue; @@ -1293,9 +1301,8 @@ _writeAnimationTracks(const WriteLayerOptions& options, UsdData& data) } // Calculate offsetToJoinedTimeline for each track, ignoring empty tracks - float offsetToJoinedTimeline = 0.0f; int prevTrackIndex = -1; - for (int trackIndex = 0; trackIndex < data.animationTracks.size(); trackIndex++) { + for (size_t trackIndex = 0; trackIndex < data.animationTracks.size(); trackIndex++) { AnimationTrack& track = data.animationTracks[trackIndex]; if (!track.hasTimepoints) { continue; @@ -1426,6 +1433,8 @@ _writeLayerSdfData(const WriteLayerOptions& options, const std::string& sourceFileType, const std::string& debugTag) { + TfStopwatch phaseSW; + // Get the raw pointer for efficiency while we hold the ref pointer SdfAbstractData* sdfData = get_pointer(sdfDataPtr); @@ -1454,6 +1463,7 @@ _writeLayerSdfData(const WriteLayerOptions& options, _writeMetadata(sdfData, usdData, rootNodePath, sourceFileType, renderSettingsPath); + phaseSW.Start(); if (!usdData.materials.empty()) { ctx.materialMap.resize(usdData.materials.size()); TfToken materialsPrimName("Materials"); @@ -1468,10 +1478,17 @@ _writeLayerSdfData(const WriteLayerOptions& options, printMaterial("layer::write", materialPath, material, ctx.debugTag); } } + phaseSW.Stop(); + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "_writeLayerSdfData materials time: %ld ms (%zu materials)\n", + static_cast(phaseSW.GetMilliseconds()), + usdData.materials.size()); + phaseSW.Reset(); // This map is filled with paths to prototypes as we process instanceable meshes ctx.meshPrototypeMap.resize(usdData.meshes.size()); + phaseSW.Start(); if (!usdData.nodes.empty()) { ctx.nodeMap.resize(usdData.nodes.size()); @@ -1485,6 +1502,13 @@ _writeLayerSdfData(const WriteLayerOptions& options, _writeNonParentedNodes(ctx, rootNodePath, usdData.nodes); } } + phaseSW.Stop(); + TF_DEBUG_MSG(FILE_FORMAT_UTIL, + "_writeLayerSdfData nodes time: %ld ms (%zu nodes, %zu meshes)\n", + static_cast(phaseSW.GetMilliseconds()), + usdData.nodes.size(), + usdData.meshes.size()); + phaseSW.Reset(); // Write skeletons after nodes, as we sometimes want skeletons to be parented to the nodes if (!usdData.skeletons.empty()) { @@ -1515,14 +1539,11 @@ _writeLayerSdfData(const WriteLayerOptions& options, SdfPath skeletonPath = _writeSkeleton(sdfData, skelRootPath, skeleton); ctx.skeletonMap[i++] = skeletonPath; - int meshChildIndex = 0; for (int meshIndex : skeleton.meshSkinningTargets) { const Mesh& mesh = ctx.usdData->meshes[meshIndex]; // Note, skinned meshes are never emitted as instanced _writePointsOrMesh(ctx, skelRootPath, mesh, skeletonPath); - - meshChildIndex++; } if (!skeleton.skeletonAnimations.empty()) { @@ -1539,7 +1560,11 @@ _writeLayerSdfData(const WriteLayerOptions& options, if (!options.assetsPath.empty() && usdData.images.size()) { TfMakeDirs(options.assetsPath, -1, true); for (const ImageAsset& image : usdData.images) { - _writeImage(options.assetsPath, image); + std::filesystem::path filepath = std::filesystem::path(options.assetsPath) / image.uri; + if (!writeDataToDisk(filepath, image.image.data(), image.image.size())) { + TF_WARN( + "Could not write image %s to %s", image.uri.c_str(), options.assetsPath.c_str()); + } } } diff --git a/utils/src/layerWriteShared.cpp b/utils/src/layerWriteShared.cpp index 84c24051..c0d3bac3 100644 --- a/utils/src/layerWriteShared.cpp +++ b/utils/src/layerWriteShared.cpp @@ -13,7 +13,7 @@ governing permissions and limitations under the License. #include #include -#include +#include using namespace PXR_NS; @@ -115,6 +115,17 @@ createTexturePath(const std::string& srcAssetFilename, const std::string& imageU return srcAssetFilename.empty() ? imageUri : srcAssetFilename + "[" + imageUri + "]"; } +void +convertAsmEmissionToOpenPbrEmission(OpenPbrMaterial& pbrMaterial, const Material& material) +{ + if (material.emissiveColor.isEmpty() || material.emissiveColor.isZeroInput()) + return; + // NOTE: we are ignoring the cases where coatOpacity and sheenOpacity might not be 0 (see ASM to + // OpenPBR conversion doc for details) + pbrMaterial.emission_luminance = Input{ VtValue(1000.0f) }; + pbrMaterial.emission_color = material.emissiveColor; +} + OpenPbrMaterial mapMaterialStructToOpenPbrMaterialStruct(const Material& material) { @@ -166,6 +177,7 @@ mapMaterialStructToOpenPbrMaterialStruct(const Material& material) result.subsurface_color = material.scatteringColor; result.subsurface_radius = material.scatteringDistance; result.subsurface_radius_scale = material.scatteringDistanceScale; + // subsurface_scatter_anisotropy (no source info) // fuzz result.fuzz_weight = Input{ fuzz ? VtValue(1.0f) : VtValue() }; @@ -186,7 +198,10 @@ mapMaterialStructToOpenPbrMaterialStruct(const Material& material) // thin_film_ior (no source info) // emission - result.emission_luminance = Input{ emission ? VtValue(1.0f) : VtValue() }; + // Note, OpenPBR's emission_luminance is in different units than ASM's emissiveIntensity and + // hence we use a factor to convert + result.emission_luminance = + Input{ emission ? VtValue(kAsmToOpenPbrEmissionFactor) : VtValue() }; result.emission_color = material.emissiveColor; // geometry @@ -223,4 +238,92 @@ mapMaterialStructToOpenPbrMaterialStruct(const Material& material) return result; } +Material +mapOpenPbrMaterialStructToMaterialStruct(const OpenPbrMaterial& material) +{ + Material outputMaterial; + + outputMaterial.name = material.name; + outputMaterial.displayName = material.displayName; + + // OpenPBR inputs + outputMaterial.diffuseColor = material.base_color; + outputMaterial.metallic = material.base_metalness; + outputMaterial.specularLevel = material.specular_weight; + outputMaterial.specularColor = material.specular_color; + outputMaterial.roughness = material.specular_roughness; + outputMaterial.ior = material.specular_ior; + outputMaterial.anisotropyLevel = material.specular_roughness_anisotropy; + outputMaterial.transmission = material.transmission_weight; + outputMaterial.absorptionColor = material.transmission_color; + outputMaterial.absorptionDistance = material.transmission_depth; + outputMaterial.scatteringColor = material.subsurface_color; + outputMaterial.scatteringDistance = material.subsurface_radius; + outputMaterial.sheenColor = material.fuzz_color; + outputMaterial.sheenRoughness = material.fuzz_roughness; + outputMaterial.clearcoat = material.coat_weight; + outputMaterial.clearcoatColor = material.coat_color; + outputMaterial.clearcoatRoughness = material.coat_roughness; + outputMaterial.clearcoatIor = material.coat_ior; + outputMaterial.emissiveColor = material.emission_color; + outputMaterial.opacity = material.geometry_opacity; + outputMaterial.normal = material.geometry_normal; + outputMaterial.clearcoatNormal = material.geometry_coat_normal; + + // Non-OpenPBR inputs + outputMaterial.displacement = material.displacement; + outputMaterial.occlusion = material.occlusion; + outputMaterial.anisotropyAngle = material.anisotropyAngle; + outputMaterial.clearcoatSpecular = material.coatSpecularLevel; + outputMaterial.volumeThickness = material.volumeThickness; + if (material.normalScale != 1.0f) { + outputMaterial.normalScale = Input{ VtValue(material.normalScale) }; + } + if (material.useSpecularWorkflow) { + outputMaterial.useSpecularWorkflow = Input{ VtValue(1) }; + } + if (material.opacityThreshold != 0.0f) { + outputMaterial.opacityThreshold = Input{ VtValue(material.opacityThreshold) }; + } + outputMaterial.clearcoatModelsTransmissionTint = material.clearcoatModelsTransmissionTint; + outputMaterial.isUnlit = material.isUnlit; + + return outputMaterial; +} + +void +createExtraConstantAttribute(PXR_NS::SdfAbstractData* sdfData, + const OpenPbrMaterial& material, + const SdfPath& surfaceShaderPath) +{ + auto createCustomAttr = + [&](const TfToken& attrName, const SdfValueTypeName& typeName, auto defaultValue) { + SdfPath p = createAttributeSpec(sdfData, surfaceShaderPath, attrName, typeName); + setAttributeMetadata(sdfData, p, SdfFieldKeys->Custom, VtValue(true)); + setAttributeDefaultValue(sdfData, p, defaultValue, typeName); + }; + + // The custom attributes below are not part of OpenPBR, but are needed for accurate round + // tripping + if (material.normalScale != 1.0f) { + createCustomAttr(AsmTokens->normalScale, SdfValueTypeNames->Float, material.normalScale); + } + if (material.useSpecularWorkflow) { + createCustomAttr( + UsdPreviewSurfaceTokens->useSpecularWorkflow, SdfValueTypeNames->Bool, VtValue(true)); + } + if (material.opacityThreshold != 0.0f) { + createCustomAttr(UsdPreviewSurfaceTokens->opacityThreshold, + SdfValueTypeNames->Float, + material.opacityThreshold); + } + if (material.isUnlit) { + createCustomAttr(AdobeTokens->unlit, SdfValueTypeNames->Bool, VtValue(true)); + } + if (material.clearcoatModelsTransmissionTint) { + createCustomAttr( + AdobeTokens->clearcoatModelsTransmissionTint, SdfValueTypeNames->Bool, VtValue(true)); + } +} + } diff --git a/utils/src/resolver.cpp b/utils/src/resolver.cpp index 03d91a21..585adc5d 100644 --- a/utils/src/resolver.cpp +++ b/utils/src/resolver.cpp @@ -9,11 +9,11 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#include #include #include #include #include +#include #include #include #include @@ -69,11 +69,13 @@ Resolver::OpenAsset(const std::string& resolvedPackagePath, const std::string& r ss << threadId; AssetMap* assetMap = AssetCacheSingleton::getInstance().acquireAssetMap( - resolvedPackagePath, resolvedPackagedPath, ss, - [this](const std::string& path, std::vector& images) { - readCache(path, images); // Assuming 'readCache' is a member function of the 'Resolver' class - } - ); + resolvedPackagePath, + resolvedPackagedPath, + ss, + [this](const std::string& path, std::vector& images) { + readCache(path, + images); // Assuming 'readCache' is a member function of the 'Resolver' class + }); if (assetMap) { TF_DEBUG_MSG(UTIL_PACKAGE_RESOLVER, " : %s \n", resolvedPackagedPath.c_str()); auto it = assetMap->assets.find(resolvedPackagedPath); @@ -86,13 +88,11 @@ Resolver::OpenAsset(const std::string& resolvedPackagePath, const std::string& r void Resolver::BeginCacheScope(VtValue* data) -{ -} +{} void Resolver::EndCacheScope(VtValue* data) -{ -} +{} void Resolver::clearCache(const std::string& resolvedPackagePath) diff --git a/utils/src/sdfMaterialUtils.cpp b/utils/src/sdfMaterialUtils.cpp index 7eb8ee98..9f520df7 100644 --- a/utils/src/sdfMaterialUtils.cpp +++ b/utils/src/sdfMaterialUtils.cpp @@ -22,7 +22,7 @@ _setShaderType(SdfAbstractData* data, const SdfPath& shaderPath, const TfToken& { SdfPath p = createAttributeSpec( data, shaderPath, UsdShadeTokens->infoId, SdfValueTypeNames->Token, SdfVariabilityUniform); - setAttributeDefaultValue(data, p, shaderType); + setAttributeDefaultValue(data, p, shaderType, SdfValueTypeNames->Token); } SdfPath @@ -119,7 +119,7 @@ addMaterialInputValue(SdfAbstractData* sdfData, if (inserted) { TfToken inputToken("inputs:" + name.GetString()); SdfPath path = _createShaderAttr(sdfData, materialPath, inputToken, type); - setAttributeDefaultValue(sdfData, path, value); + setAttributeDefaultValue(sdfData, path, value, type); it->second = path; return path; } @@ -131,12 +131,23 @@ addMaterialInputTexture(SdfAbstractData* sdfData, const SdfPath& materialPath, const TfToken& name, const std::string& texturePath, + bool isColorTexture, MaterialInputs& materialInputs) { VtValue value = VtValue(SdfAssetPath(texturePath)); TfToken texturePathInputName(name.GetString() + "Texture"); - return addMaterialInputValue( + SdfPath inputAttrPath = addMaterialInputValue( sdfData, materialPath, texturePathInputName, SdfValueTypeNames->Asset, value, materialInputs); + // We set the color space on the attribute that will carry the texture asset path + // This is an important clue for the OpenPBR/MaterialX shading network, as the texture + // reading nodes there do not have a field for the color space. That space is specified + // on the attribute that holds the asset path. + // XXX: In the future we should switch these tokens to GfColorSpaceNames, which + // specifies a larger set of color spaces. For now we stick to "srgb_texture" and "raw", which + // are MaterialX supported and recognized color space names. + const TfToken& colorSpace = isColorTexture ? MtlXTokens->srgb_texture : AdobeTokens->raw; + setAttributeMetadata(sdfData, inputAttrPath, SdfFieldKeys->ColorSpace, VtValue(colorSpace)); + return inputAttrPath; } SdfPath @@ -194,7 +205,7 @@ createShader(SdfAbstractData* data, TfToken inputToken("inputs:" + inputName); SdfValueTypeName inputType = shaderInfo.getInputType(inputToken); SdfPath p = _createShaderAttr(data, shaderPath, inputToken, inputType); - setAttributeDefaultValue(data, p, inputValue); + setAttributeDefaultValue(data, p, inputValue, inputType); // Set the colorSpace metadata if we a specific value for this input const auto it = inputColorSpaces.find(inputName); @@ -247,25 +258,25 @@ ShaderRegistry::ShaderRegistry() // Initialize shaderInfos // clang-format off m_shaderInfos = { - { AdobeTokens->UsdUVTexture, {{ - { TfToken("inputs:file"), SdfValueTypeNames->Asset }, - { TfToken("inputs:st"), SdfValueTypeNames->Float2 }, - { TfToken("inputs:wrapS"), SdfValueTypeNames->Token }, - { TfToken("inputs:wrapT"), SdfValueTypeNames->Token }, - { TfToken("inputs:minFilter"), SdfValueTypeNames->Token }, - { TfToken("inputs:magFilter"), SdfValueTypeNames->Token }, - { TfToken("inputs:fallback"), SdfValueTypeNames->Float4 }, - { TfToken("inputs:scale"), SdfValueTypeNames->Float4 }, - { TfToken("inputs:bias"), SdfValueTypeNames->Float4 }, - { TfToken("inputs:sourceColorSpace"), SdfValueTypeNames->Token } - }, { - { TfToken("outputs:r"), SdfValueTypeNames->Float }, - { TfToken("outputs:g"), SdfValueTypeNames->Float }, - { TfToken("outputs:b"), SdfValueTypeNames->Float }, - { TfToken("outputs:a"), SdfValueTypeNames->Float }, - { TfToken("outputs:rgb"), SdfValueTypeNames->Float3 } - }}}, - { AdobeTokens->UsdTransform2d, {{ + { AdobeTokens->UsdUVTexture, {{ + { TfToken("inputs:file"), SdfValueTypeNames->Asset }, + { TfToken("inputs:st"), SdfValueTypeNames->Float2 }, + { TfToken("inputs:wrapS"), SdfValueTypeNames->Token }, + { TfToken("inputs:wrapT"), SdfValueTypeNames->Token }, + { TfToken("inputs:minFilter"), SdfValueTypeNames->Token }, + { TfToken("inputs:magFilter"), SdfValueTypeNames->Token }, + { TfToken("inputs:fallback"), SdfValueTypeNames->Float4 }, + { TfToken("inputs:scale"), SdfValueTypeNames->Float4 }, + { TfToken("inputs:bias"), SdfValueTypeNames->Float4 }, + { TfToken("inputs:sourceColorSpace"), SdfValueTypeNames->Token } + }, { + { TfToken("outputs:r"), SdfValueTypeNames->Float }, + { TfToken("outputs:g"), SdfValueTypeNames->Float }, + { TfToken("outputs:b"), SdfValueTypeNames->Float }, + { TfToken("outputs:a"), SdfValueTypeNames->Float }, + { TfToken("outputs:rgb"), SdfValueTypeNames->Float3 } + }}}, + { AdobeTokens->UsdTransform2d, {{ { TfToken("inputs:in"), SdfValueTypeNames->Float2 }, { TfToken("inputs:rotation"), SdfValueTypeNames->Float }, { TfToken("inputs:scale"), SdfValueTypeNames->Float2 }, @@ -345,6 +356,11 @@ ShaderRegistry::ShaderRegistry() }, { { TfToken("outputs:out"), SdfValueTypeNames->Color3f } }}}, + { MtlXTokens->ND_convert_color3_vector3, {{ + { TfToken("inputs:in"), SdfValueTypeNames->Color3f } + }, { + { TfToken("outputs:out"), SdfValueTypeNames->Float3 } + }}}, { MtlXTokens->ND_multiply_float, {{ { TfToken("inputs:in1"), SdfValueTypeNames->Float }, { TfToken("inputs:in2"), SdfValueTypeNames->Float } @@ -363,12 +379,25 @@ ShaderRegistry::ShaderRegistry() }, { { TfToken("outputs:out"), SdfValueTypeNames->Float3 } }}}, + { MtlXTokens->ND_mix_color3, {{ + { TfToken("inputs:fg"), SdfValueTypeNames->Color3f }, + { TfToken("inputs:bg"), SdfValueTypeNames->Color3f }, + { TfToken("inputs:mix"), SdfValueTypeNames->Float } + }, { + { TfToken("outputs:out"), SdfValueTypeNames->Color3f } + }}}, { MtlXTokens->ND_add_float, {{ { TfToken("inputs:in1"), SdfValueTypeNames->Float }, { TfToken("inputs:in2"), SdfValueTypeNames->Float } }, { { TfToken("outputs:out"), SdfValueTypeNames->Float } }}}, + { MtlXTokens->ND_subtract_float, {{ + { TfToken("inputs:in1"), SdfValueTypeNames->Float }, + { TfToken("inputs:in2"), SdfValueTypeNames->Float } + }, { + { TfToken("outputs:out"), SdfValueTypeNames->Float } + }}}, { MtlXTokens->ND_add_color3, {{ { TfToken("inputs:in1"), SdfValueTypeNames->Color3f }, { TfToken("inputs:in2"), SdfValueTypeNames->Color3f } @@ -381,6 +410,21 @@ ShaderRegistry::ShaderRegistry() }, { { TfToken("outputs:out"), SdfValueTypeNames->Float3 } }}}, + { MtlXTokens->ND_UsdUVTexture_23, {{ + { TfToken("inputs:file"), SdfValueTypeNames->Asset }, + { TfToken("inputs:st"), SdfValueTypeNames->Float2 }, + { TfToken("inputs:wrapS"), SdfValueTypeNames->String }, + { TfToken("inputs:wrapT"), SdfValueTypeNames->String }, + { TfToken("inputs:fallback"), SdfValueTypeNames->Float4 }, + { TfToken("inputs:scale"), SdfValueTypeNames->Float4 }, + { TfToken("inputs:bias"), SdfValueTypeNames->Float4 } + }, { + { TfToken("outputs:r"), SdfValueTypeNames->Float }, + { TfToken("outputs:g"), SdfValueTypeNames->Float }, + { TfToken("outputs:b"), SdfValueTypeNames->Float }, + { TfToken("outputs:a"), SdfValueTypeNames->Float }, + { TfToken("outputs:rgb"), SdfValueTypeNames->Color3f } + }}}, { MtlXTokens->ND_image_vector4, {{ { TfToken("inputs:texcoord"), SdfValueTypeNames->Float2 }, { TfToken("inputs:file"), SdfValueTypeNames->Asset }, @@ -418,10 +462,26 @@ ShaderRegistry::ShaderRegistry() { TfToken("outputs:out"), SdfValueTypeNames->Float } }}}, { MtlXTokens->ND_normalmap, {{ - { TfToken("inputs:in"), SdfValueTypeNames->Float3 } + { TfToken("inputs:in"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:scale"), SdfValueTypeNames->Float }, + { TfToken("inputs:normal"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:tangent"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:bitangent"), SdfValueTypeNames->Float3 } }, { { TfToken("outputs:out"), SdfValueTypeNames->Float3 } }}}, + { MtlXTokens->ND_displacement_float, {{ + { TfToken("inputs:displacement"), SdfValueTypeNames->Float }, + { TfToken("inputs:scale"), SdfValueTypeNames->Float } + }, { + { TfToken("outputs:out"), SdfValueTypeNames->Token } + }}}, + { MtlXTokens->ND_geompropvalue_vector2, {{ + { TfToken("inputs:geomprop"), SdfValueTypeNames->String }, + { TfToken("inputs:default"), SdfValueTypeNames->Float2 } + }, { + { TfToken("outputs:out"), SdfValueTypeNames->Float2 } + }}}, { MtlXTokens->ND_open_pbr_surface_surfaceshader, {{ { TfToken("inputs:base_weight"), SdfValueTypeNames->Float }, { TfToken("inputs:base_color"), SdfValueTypeNames->Color3f }, @@ -464,21 +524,25 @@ ShaderRegistry::ShaderRegistry() { TfToken("inputs:geometry_coat_normal"), SdfValueTypeNames->Float3 }, { TfToken("inputs:geometry_tangent"), SdfValueTypeNames->Float3 }, { TfToken("inputs:geometry_coat_tangent"), SdfValueTypeNames->Float3 }, + // XXX Non-OpenPBR inputs we support for round-tripping purposes + // XXX turn displacement into actual MaterialX displacement + { TfToken("inputs:displacement"), SdfValueTypeNames->Float }, + { TfToken("inputs:occlusion"), SdfValueTypeNames->Float }, + { TfToken("inputs:anisotropyAngle"), SdfValueTypeNames->Float }, + { TfToken("inputs:coatSpecularLevel"), SdfValueTypeNames->Float }, + { TfToken("inputs:volumeThickness"), SdfValueTypeNames->Float }, }, { { TfToken("outputs:out"), SdfValueTypeNames->Token } }}}, // Adobe Standard Material surface node { AdobeTokens->adobeStandardMaterial, {{ - { TfToken("inputs:baseColor"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:baseColor"), SdfValueTypeNames->Color3f }, { TfToken("inputs:roughness"), SdfValueTypeNames->Float }, { TfToken("inputs:metallic"), SdfValueTypeNames->Float }, { TfToken("inputs:opacity"), SdfValueTypeNames->Float }, - // XXX ASM doesn't actually have an opacityThreshold, which is a UsdPreviewSurface concept - // But we use it to carry the information about the threshold for transcoding uses - { TfToken("inputs:opacityThreshold"), SdfValueTypeNames->Float }, { TfToken("inputs:specularLevel"), SdfValueTypeNames->Float }, - { TfToken("inputs:specularEdgeColor"), SdfValueTypeNames->Float3 }, - { TfToken("inputs:normal"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:specularEdgeColor"), SdfValueTypeNames->Color3f }, + { TfToken("inputs:normal"), SdfValueTypeNames->Normal3f }, { TfToken("inputs:normalScale"), SdfValueTypeNames->Float }, { TfToken("inputs:combineNormalAndHeight"), SdfValueTypeNames->Bool }, { TfToken("inputs:height"), SdfValueTypeNames->Float }, @@ -487,27 +551,27 @@ ShaderRegistry::ShaderRegistry() { TfToken("inputs:anisotropyLevel"), SdfValueTypeNames->Float }, { TfToken("inputs:anisotropyAngle"), SdfValueTypeNames->Float }, { TfToken("inputs:emissiveIntensity"), SdfValueTypeNames->Float }, - { TfToken("inputs:emissive"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:emissive"), SdfValueTypeNames->Color3f }, { TfToken("inputs:sheenOpacity"), SdfValueTypeNames->Float }, - { TfToken("inputs:sheenColor"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:sheenColor"), SdfValueTypeNames->Color3f }, { TfToken("inputs:sheenRoughness"), SdfValueTypeNames->Float }, { TfToken("inputs:translucency"), SdfValueTypeNames->Float }, { TfToken("inputs:IOR"), SdfValueTypeNames->Float }, { TfToken("inputs:dispersion"), SdfValueTypeNames->Float }, - { TfToken("inputs:absorptionColor"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:absorptionColor"), SdfValueTypeNames->Color3f }, { TfToken("inputs:absorptionDistance"), SdfValueTypeNames->Float }, { TfToken("inputs:scatter"), SdfValueTypeNames->Bool }, - { TfToken("inputs:scatteringColor"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:scatteringColor"), SdfValueTypeNames->Color3f }, { TfToken("inputs:scatteringDistance"), SdfValueTypeNames->Float }, { TfToken("inputs:scatteringDistanceScale"), SdfValueTypeNames->Float3 }, { TfToken("inputs:scatteringRedShift"), SdfValueTypeNames->Float }, { TfToken("inputs:scatteringRayleigh"), SdfValueTypeNames->Float }, { TfToken("inputs:coatOpacity"), SdfValueTypeNames->Float }, - { TfToken("inputs:coatColor"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:coatColor"), SdfValueTypeNames->Color3f }, { TfToken("inputs:coatRoughness"), SdfValueTypeNames->Float }, { TfToken("inputs:coatIOR"), SdfValueTypeNames->Float }, { TfToken("inputs:coatSpecularLevel"), SdfValueTypeNames->Float }, - { TfToken("inputs:coatNormal"), SdfValueTypeNames->Float3 }, + { TfToken("inputs:coatNormal"), SdfValueTypeNames->Normal3f }, { TfToken("inputs:coatNormalScale"), SdfValueTypeNames->Float }, { TfToken("inputs:ambientOcclusion"), SdfValueTypeNames->Float }, { TfToken("inputs:volumeThickness"), SdfValueTypeNames->Float }, @@ -565,45 +629,36 @@ ShaderRegistry::ShaderRegistry() // Initialize asmInputRemapping // XXX This is incomplete m_asmInputRemapping = { - { AsmTokens->absorptionColor, { AsmTokens->absorptionColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->absorptionColor, { AsmTokens->absorptionColor, SdfValueTypeNames->Color3f } }, { 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->baseColor, { AsmTokens->baseColor, SdfValueTypeNames->Color3f } }, + { AsmTokens->coatColor, { AsmTokens->coatColor, SdfValueTypeNames->Color3f } }, { AsmTokens->coatIOR, { AsmTokens->coatIOR, SdfValueTypeNames->Float } }, - { AsmTokens->coatNormal, { AsmTokens->coatNormal, SdfValueTypeNames->Float3 } }, + { AsmTokens->coatNormal, { AsmTokens->coatNormal, SdfValueTypeNames->Normal3f } }, { 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->emissive, { AsmTokens->emissive, SdfValueTypeNames->Color3f } }, { 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->normal, { AsmTokens->normal, SdfValueTypeNames->Normal3f } }, { 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->scatteringColor, { AsmTokens->scatteringColor, SdfValueTypeNames->Color3f } }, { AsmTokens->scatteringDistance, { AsmTokens->scatteringDistance, SdfValueTypeNames->Float } }, { AsmTokens->scatteringDistanceScale, { AsmTokens->scatteringDistanceScale, SdfValueTypeNames->Float3 } }, - { AsmTokens->sheenColor, { AsmTokens->sheenColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->sheenColor, { AsmTokens->sheenColor, SdfValueTypeNames->Color3f } }, { AsmTokens->sheenOpacity, { AsmTokens->sheenOpacity, SdfValueTypeNames->Float } }, { AsmTokens->sheenRoughness, { AsmTokens->sheenRoughness, SdfValueTypeNames->Float } }, - { AsmTokens->specularEdgeColor, { AsmTokens->specularEdgeColor, SdfValueTypeNames->Float3 } }, + { AsmTokens->specularEdgeColor, { AsmTokens->specularEdgeColor, SdfValueTypeNames->Color3f } }, { AsmTokens->specularLevel, { AsmTokens->specularLevel, SdfValueTypeNames->Float } }, { AsmTokens->translucency, { AsmTokens->translucency, SdfValueTypeNames->Float } }, { AsmTokens->volumeThickness, { AsmTokens->volumeThickness, SdfValueTypeNames->Float } }, @@ -617,24 +672,24 @@ ShaderRegistry::ShaderRegistry() { 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_roughness, { OpenPbrMaterialInputTokens->specularRoughness, 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->specular_roughness_anisotropy, { OpenPbrMaterialInputTokens->specularRoughnessAnisotropy, SdfValueTypeNames->Float } }, + { OpenPbrTokens->transmission_weight, { OpenPbrMaterialInputTokens->transmissionWeight , SdfValueTypeNames->Float } }, + { OpenPbrTokens->transmission_color, { OpenPbrMaterialInputTokens->transmissionColor , SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->transmission_depth, { OpenPbrMaterialInputTokens->transmissionDepth , 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_color, { OpenPbrMaterialInputTokens->subsurfaceColor, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->subsurface_radius, { OpenPbrMaterialInputTokens->subsurfaceRadius, 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->fuzz_color, { OpenPbrMaterialInputTokens->fuzzColor, SdfValueTypeNames->Color3f } }, + { OpenPbrTokens->fuzz_roughness, { OpenPbrMaterialInputTokens->fuzzRoughness, SdfValueTypeNames->Float } }, { OpenPbrTokens->coat_weight, { AsmTokens->coatOpacity, SdfValueTypeNames->Float } }, { OpenPbrTokens->coat_color, { AsmTokens->coatColor, SdfValueTypeNames->Color3f } }, { OpenPbrTokens->coat_roughness, { AsmTokens->coatRoughness, SdfValueTypeNames->Float } }, @@ -652,6 +707,12 @@ ShaderRegistry::ShaderRegistry() { OpenPbrTokens->geometry_coat_normal, { AsmTokens->coatNormal, SdfValueTypeNames->Float3 } }, { OpenPbrTokens->geometry_tangent, { OpenPbrMaterialInputTokens->tangent, SdfValueTypeNames->Float3 } }, { OpenPbrTokens->geometry_coat_tangent, { OpenPbrMaterialInputTokens->coatTangent, SdfValueTypeNames->Float3 } }, + // Non-OpenPBR inputs + { UsdPreviewSurfaceTokens->displacement, { AsmTokens->height, SdfValueTypeNames->Float } }, + { UsdPreviewSurfaceTokens->occlusion, { AsmTokens->ambientOcclusion, SdfValueTypeNames->Float } }, + { AsmTokens->anisotropyAngle, { AsmTokens->anisotropyAngle, SdfValueTypeNames->Float } }, + { AsmTokens->coatSpecularLevel, {AsmTokens->coatSpecularLevel, SdfValueTypeNames->Float } }, + { AsmTokens->volumeThickness, { AsmTokens->volumeThickness, SdfValueTypeNames->Float } }, }; // clang-format on } diff --git a/utils/src/sdfUtils.cpp b/utils/src/sdfUtils.cpp index a97727cd..6adf7a36 100644 --- a/utils/src/sdfUtils.cpp +++ b/utils/src/sdfUtils.cpp @@ -68,6 +68,52 @@ _prependListOp(SdfAbstractData* data, const SdfPath& specPath, const TfToken& fi data->Set(specPath, field, SdfAbstractDataConstTypedValue(&listOp)); } +bool +_checkValueMatchesTypeName(const VtValue& value, + const SdfValueTypeName& typeName, + const SdfPath& path) +{ + // XXX This should be replaced with SdfValueTypeName::CanRepresent(), which was added on + // 09/16/25, when we upgrade to a more modern version of USD. + if (value.GetType() == typeName.GetType()) { + return true; + } + + TF_CODING_ERROR("Input value '%s' of type '%s' is incompatible with attribute '%s' of " + "type '%s' (%s)", + TfStringify(value).c_str(), + value.GetType().GetTypeName().c_str(), + path.GetName().c_str(), + typeName.GetAsToken().GetText(), + path.GetText()); + + return false; +} + +bool +_checkValueMatchesTypeName(const SdfAbstractDataConstValue& value, + const SdfValueTypeName& typeName, + const SdfPath& path) +{ + // XXX This should be replaced with SdfValueTypeName::CanRepresent(), which was added on + // 09/16/25, when we upgrade to a more modern version of USD. + if (TfSafeTypeCompare(typeName.GetType().GetTypeid(), value.valueType)) { + return true; + } + + TfType valueType = TfType::FindByTypeid(value.valueType); + // Note that we can't use TfStringify to print the value here, since it is not compatible with + // SdfAbstractDataConstValue. + TF_CODING_ERROR("Input value of type '%s' is incompatible with attribute '%s' of " + "type '%s' (%s)", + valueType.GetTypeName().c_str(), + path.GetName().c_str(), + typeName.GetAsToken().GetText(), + path.GetText()); + + return false; +} + } // end anonymous namespace namespace adobe::usd { @@ -187,9 +233,14 @@ createAttributeSpec(SdfAbstractData* data, const SdfValueTypeName& typeName, SdfVariability variability) { + assert(primPath.IsPrimOrPrimVariantSelectionPath()); SdfPath propertyPath = primPath.AppendProperty(attrName); - data->CreateSpec(propertyPath, SdfSpecTypeAttribute); + + bool hasAttr = data->HasSpec(propertyPath); + if (!hasAttr) { + data->CreateSpec(propertyPath, SdfSpecTypeAttribute); + } TfToken typeNameToken = typeName.GetAsToken(); data->Set(propertyPath, SdfFieldKeys->TypeName, SdfAbstractDataConstTypedValue(&typeNameToken)); @@ -198,7 +249,9 @@ createAttributeSpec(SdfAbstractData* data, propertyPath, SdfFieldKeys->Variability, SdfAbstractDataConstTypedValue(&variability)); } - _appendChild(data, primPath, SdfChildrenKeys->PropertyChildren, attrName); + if (!hasAttr) { + _appendChild(data, primPath, SdfChildrenKeys->PropertyChildren, attrName); + } return propertyPath; } @@ -215,18 +268,35 @@ setAttributeMetadata(SdfAbstractData* data, } void -setAttributeDefaultValue(SdfAbstractData* data, const SdfPath& propertyPath, const VtValue& value) +setAttributeDefaultValue(SdfAbstractData* data, + const SdfPath& propertyPath, + const VtValue& value, + const PXR_NS::SdfValueTypeName& typeName) { assert(propertyPath.IsPropertyPath()); + assert(typeName); + // We can always write an empty value + if (!value.IsEmpty() && !_checkValueMatchesTypeName(value, typeName, propertyPath)) { + return; + } data->Set(propertyPath, SdfFieldKeys->Default, value); } void setAttributeDefaultValue(SdfAbstractData* data, const SdfPath& propertyPath, - const SdfAbstractDataConstValue& value) + const SdfAbstractDataConstValue& value, + const PXR_NS::SdfValueTypeName& typeName) { assert(propertyPath.IsPropertyPath()); + assert(typeName); + // The type check is only valid if the incoming value is not of type void. + // We can always write an empty value. + if (!TfSafeTypeCompare(value.valueType, typeid(void))) { + if (!_checkValueMatchesTypeName(value, typeName, propertyPath)) { + return; + } + } data->Set(propertyPath, SdfFieldKeys->Default, value); } @@ -350,6 +420,7 @@ FileFormatDataBase::parseFromFileFormatArgs(const SdfLayer::FileFormatArguments& argReadBool(args, "writeUsdPreviewSurface", writeUsdPreviewSurface, debugTag); argReadBool(args, "writeASM", writeASM, debugTag); argReadBool(args, "writeOpenPBR", writeOpenPBR, debugTag); + argReadBool(args, "preserveExtraMaterialInfo", preserveExtraMaterialInfo, debugTag); argReadString(args, "assetsPath", assetsPath, debugTag); } diff --git a/utils/src/test.cpp b/utils/src/test.cpp index 64b282c8..38f995ae 100644 --- a/utils/src/test.cpp +++ b/utils/src/test.cpp @@ -11,8 +11,9 @@ governing permissions and limitations under the License. */ #include +#include #include - +#include #include #include #include @@ -36,14 +37,254 @@ PXR_NAMESPACE_CLOSE_SCOPE using namespace PXR_NS; -int +const float LIGHT_INTENSITY_EPSILON = 0.1f; + +/** + * ASSERT_EQ_VAL(actual, expected, msg, ...) + * + * Checks that `actual` and `expected` are equal. + * - For ints, floats, vectors uses `fuzzyEqual` for comparison. + * - For other types, uses `operator==`. + * - If an extra argument is provided, it's forwarded to `fuzzyEqual`. + * + * Returns: + * - success: execution continues. + * - failure: returns ::testing::AssertionFailure() with a detailed message. + * + * Example usage: + * ASSERT_EQ_VAL(value, expectedValue, "Value mismatch"); + * ASSERT_EQ_VAL(floatValue, expectedFloatValue, "Float value mismatch", 0.001f); + * ASSERT_EQ_VAL(value, expectedValue, "Value mistach", &failingIndex); + */ + +inline std::string +ToString(const VtValue& v) +{ + std::ostringstream ss; + VtStreamOut(v, ss); + return ss.str(); +} + +#define ASSERT_EQ_VAL(actual, expected, msg, ...) \ + do { \ + auto _res = AssertEqValHelper( \ + (actual), (expected), std::string(msg), __FILE__, __LINE__, ##__VA_ARGS__); \ + if (!_res.first) { \ + ::testing::AssertionResult ar = ::testing::AssertionFailure(); \ + ar << _res.second; \ + return ar; \ + } \ + } while (0) + +/** + * ASSERT_GE_VAL(actual, expected, msg) + * + * Checks that `actual` is greater than or equal to `expected`. + * If the check fails, returns a ::testing::AssertionFailure() + * with a detailed message including: + * + * Returns: + * - success: execution continues. + * - failure: returns ::testing::AssertionFailure() with a detailed message. + * + * Example usage: + * ASSERT_GE_VAL(value, 10, "Value should be at least 10"); + */ +#define ASSERT_GE_VAL(actual, expected, msg) \ + do { \ + if (!((actual) >= (expected))) { \ + std::ostringstream oss; \ + oss << msg << ": " << actual << " is not >= " << expected << "\n At " << __FILE__ \ + << ":" << __LINE__; \ + return ::testing::AssertionFailure() << oss.str(); \ + } \ + } while (0) + +/** + * ASSERT_CHECK(expr, ...) + * + * Macro wraps AssertCheckHelper. + * Evaluates the expression and returns a GoogleTest AssertionResult + * with an optional message. + * + * Returns: + * - success: execution continues. + * - failure: returns ::testing::AssertionFailure() with a detailed message. + * + * Example usage: + * ASSERT_CHECK(9 != 8, "Expected valid pointer"); + * ASSERT_CHECK(assertVec(vecA, vecB), "Vector mismatch"); + * ASSERT_CHECK(assertVec(vecA, vecB)); + * ASSERT_CHECK(prim); + * ASSERT_CHECK(prim, "Expected valid prim at path /World/MyPrim"); + */ +#define ASSERT_CHECK(expr, ...) \ + do { \ + auto _res = AssertCheckHelper((expr), __FILE__, __LINE__, ##__VA_ARGS__); \ + if (!_res.first) { \ + ::testing::AssertionResult ar = ::testing::AssertionFailure(); \ + ar << _res.second; \ + return ar; \ + } \ + } while (0) + +/** + * AssertCheckHelper(T&& expr, Msg&&... msg) + * Assertion helper that evaluates `expr` and returns a GoogleTest + * `AssertionSuccess()` or `AssertionFailure()`. Supports: + * 1. bool + * 2. ::testing::AssertionResult + * 3. Expressions returning bool (like x > y) + * 4. USD objects/handles (e.g. UsdPrim) + * + * Behavior: + * - If `expr` is true, returns `AssertionSuccess()`. + * - If `expr` is false, returns `AssertionFailure()` with a detailed message. + * - If `expr` is an `AssertionResult`, its `.message()` is included automatically. + * - Optional `msg` arguments are streamed into the failure message. + * - File and line information is appended automatically. + * + * Returns: + * - `::testing::AssertionSuccess()` if the expression is true. + * - `::testing::AssertionFailure()` with optional message and location if false. + */ +template +std::pair +AssertCheckHelper(T&& expr, const char* file, int line, Msg&&... msg) +{ + using DecayedT = std::decay_t; + bool ok = false; + + // Handle void expressions + if constexpr (std::is_void_v) { + std::ostringstream oss; + ((oss << msg), ...); + oss << " (ASSERT_CHECK called with a void expression!)" + << " At " << file << ":" << line; + return { false, oss.str() }; + } + // Raw pointers + else if constexpr (std::is_pointer_v) { + ok = (expr != nullptr); + } + // USD primitives + else if constexpr (std::is_same_v) { + ok = expr.IsValid(); + } + // (e.g expressions like x > y, smart pointers, ::testing::AssertionResult, etc) + else { + ok = static_cast(expr); + } + + if (ok) + return { true, "" }; + + std::ostringstream oss; + + if constexpr (std::is_same_v) { + oss << expr.message() << " " << std::endl; + } + + // Append any user-provided messages + ((oss << msg), ...); + oss << " At " << file << ":" << line; + + return { false, oss.str() }; +} + +struct ArchSystemResult +{ + int exitCode; + std::string output; // includes both stdout and stderr +}; + +/** + * utf8ToWstring + * + * Converts a UTF-8 encoded std::string to a wide-character std::wstring. + * + * On Windows, many APIs dealing with file paths, environment variables, + * or process creation (e.g., `_wfopen`, `_wstat`, `_wpopen`) expect + * `wchar_t*` (UTF-16) strings. If a file path or argument contains non-ASCII + * chars then passing a UTF-8 `std::string` will lead to errors. + * + * */ +inline std::wstring +utf8ToWstring(const std::string& str) +{ +#if defined(_WIN32) + if (str.empty()) + return L""; + int size_needed = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), NULL, 0); + std::wstring wstrTo(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), &wstrTo[0], size_needed); + return wstrTo; +#else + return std::wstring(str.begin(), str.end()); +#endif +} + +ArchSystemResult archSystem(const std::string& command) +/** + * Executes a system command and captures both its output and exit code. + * + * This function runs a shell command using the native platform mechanism: + * - On Windows, it uses `cmd /c` with `_popen()` to execute the command. + * - On macOS and Linux, it uses `popen()` directly. + * + * In both cases, standard error (stderr) is redirected to standard output (stdout), + * so the captured output includes all text printed by the command. + * + * @param command The system command to execute (e.g., "ls -l" or "dir"). + * + * @return An `ArchSystemResult` struct containing: + * - `exitCode`: The exit code returned by the process (or -1 if execution failed). + * - `output`: The combined standard output and standard error of the command. + * + * @note The command runs synchronously — the function will block until it finishes. + * @note On Windows, the exit code is obtained from `_pclose()`, and on Unix-like systems from + * `pclose()`. + * + */ { + ArchSystemResult result; + result.exitCode = -1; + std::array buffer{}; + std::string output; + #if defined(_WIN32) - return _wsystem(ArchWindowsUtf8ToUtf16(command).c_str()); + // convert command to wide string for UTF-8 support + std::wstring wCommand = utf8ToWstring(command + " 2>&1"); + std::unique_ptr pipe(_wpopen(wCommand.c_str(), L"r"), _pclose); + if (!pipe) { + result.output = "Failed to open pipe"; + return result; + } + + while (fgets(buffer.data(), (int)buffer.size(), pipe.get()) != nullptr) + output += buffer.data(); + + FILE* rawPipe = pipe.release(); + result.exitCode = _pclose(rawPipe); + #else - return system(command.c_str()); + std::string fullCmd = command + " 2>&1"; + std::unique_ptr pipe(popen(fullCmd.c_str(), "r"), pclose); + if (!pipe) { + result.output = "Failed to open pipe"; + return result; + } + + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) + output += buffer.data(); + + FILE* rawPipe = pipe.release(); + result.exitCode = pclose(rawPipe); #endif + + result.output = output; + return result; } template @@ -72,7 +313,7 @@ readPrimvar(UsdGeomPrimvarsAPI& api, const TfToken& name, Primvar& primvar) * fuzzyEqual for integer types is just the == operator */ template -constexpr typename std::enable_if::value, bool>::type +[[nodiscard]] constexpr typename std::enable_if::value, bool>::type fuzzyEqual(IntLike a, IntLike b) { return a == b; @@ -83,8 +324,9 @@ fuzzyEqual(IntLike a, IntLike b) * 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) +[[nodiscard]] constexpr + typename std::enable_if::value, bool>::type + fuzzyEqual(FloatLike a, FloatLike b, FloatLike epsilon = 1e-6) { return std::abs(a - b) < epsilon; } @@ -100,10 +342,10 @@ fuzzyEqual(FloatLike a, FloatLike b, FloatLike epsilon = 1e-6) * - Vec has a static dimension field */ template -typename std::enable_if::value && - std::is_arithmetic()[0])>::type>::value, - bool>::type +[[nodiscard]] typename std::enable_if::value && + std::is_arithmetic()[0])>::type>::value, + bool>::type fuzzyEqual(const Vec& a, const Vec& b, size_t* failingIndex = nullptr) { for (size_t i = 0; i < Vec::dimension; ++i) { @@ -117,34 +359,85 @@ fuzzyEqual(const Vec& a, const Vec& b, size_t* failingIndex = nullptr) return true; } -#define ASSERT_ARRAY(...) assertArray(__VA_ARGS__) +// Trait to detect GfVec types automatically +template +struct is_gfvec : std::false_type +{}; + +template +struct is_gfvec()[0])>> + : std::true_type +{}; + +/** + * AssertEqValHelper + * + * Compares two values `actual` and `expected` and returns a ::testing::AssertionResult. + * Different comparison methods are used based on the value types: + * 1. Integer types (int): + * - Calls `fuzzyEqual for ints. + * 2. Floating-point types (float): + * - Calls `fuzzyEqual` for aproximate comparison. + * - If an additional argument is passed, it's used as the epsilon for comparison.' + * 3. Vector-like or container types (std::vector): + * - Calls 'fuzzyEqual' for element-wise comparison. + * - If an additional argument is passed, it's used as a failing index pointer. + * Returns: + * - ::testing::AssertionSuccess() if values are equal according to the comparison rules. + * - ::testing::AssertionFailure() with a detailed message, failure location, and + * actual and expected values. + */ +template +std::pair +AssertEqValHelper(const T& actual, + const U& expected, + const std::string& msg, + const char* file, + int line, + Args&&... args) +{ + bool equal = false; + + if constexpr ((std::is_arithmetic_v && std::is_arithmetic_v) || + (is_gfvec::value && is_gfvec::value)) { + equal = fuzzyEqual(actual, expected, std::forward(args)...); + } else { + equal = (actual == expected); + } + + if (equal) { + return { true, "" }; // Success + } + + std::ostringstream oss; + oss << msg << " mismatch: " << actual << " vs " << expected << "\n" + << " At " << file << ":" << line; + + return { false, oss.str() }; +} + template -void -assertArray(VtArray& actual, +[[nodiscard]] ::testing::AssertionResult +assertArray(const pxr::VtArray& actual, const ArrayData& expected, const std::string& name) // test a subset of the array { - 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; + ASSERT_EQ_VAL(actual.size(), expected.size, name + " element count"); + ASSERT_GE_VAL(actual.size(), + expected.values.size(), + "There are fewer " + name + " than elements to be checked."); size_t i; for (i = 0; i < expected.values.size(); i++) { - if (!fuzzyEqual(actual[i], expected.values[i])) { - arraysMatch = false; - break; - } + ASSERT_EQ_VAL( + actual[i], expected.values[i], name + " element at index " + std::to_string(i)); } - ASSERT_TRUE(arraysMatch) << "Variable: " << name << ". Elements at [" << i - << "] differ. Actual = " << actual[i] - << ", Expected = " << expected.values[i]; + + return ::testing::AssertionSuccess(); } -#define ASSERT_VEC(...) assertVec(__VA_ARGS__) -template -void -assertVec(const GfVec& actual, - const GfVec& expected, - std::string msg = "") +template +[[nodiscard]] ::testing::AssertionResult +assertVec(const GfVec& actual, const GfVec& expected, std::string msg = "") { if (msg != "") { // Add a space after the message if it's not empty @@ -152,15 +445,14 @@ assertVec(const GfVec& actual, } // If the vectors are not equal, idx will be set to the index where they differ - size_t idx; + size_t idx = 0; + + ASSERT_EQ_VAL(actual, expected, msg + " Vector dimension", &idx); - ASSERT_TRUE(fuzzyEqual(actual, expected, &idx)) - << msg << "Elements at [" << idx << "] differ. Actual = " << actual[idx] - << ", Expected = " << expected[idx]; + return ::testing::AssertionSuccess(); } -#define ASSERT_QUATF(...) assertQuatf(__VA_ARGS__) -void +[[nodiscard]] ::testing::AssertionResult assertQuatf(const PXR_NS::GfQuatf& actual, const PXR_NS::GfQuatf& expected, std::string msg = "") // test a quaternion of floats @@ -170,36 +462,45 @@ assertQuatf(const PXR_NS::GfQuatf& actual, msg += ": "; } - ASSERT_TRUE(fuzzyEqual(actual.GetReal(), expected.GetReal())) - << msg << "Real elements differ. Actual = " << actual.GetReal() - << ", Expected = " << expected.GetReal(); + ASSERT_EQ_VAL(actual.GetReal(), expected.GetReal(), msg + "Real elements"); + ASSERT_CHECK(assertVec( + actual.GetImaginary(), expected.GetImaginary(), msg + "GfQuatf imaginary component")); - ASSERT_VEC(actual.GetImaginary(), expected.GetImaginary(), msg + "GfQuatf imaginary component"); + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertPrim(PXR_NS::UsdStageRefPtr stage, const std::string& path) { + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); - ASSERT_TRUE(prim); + ASSERT_CHECK(prim); + + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertNode(UsdStageRefPtr stage, const std::string& path) { + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); - ASSERT_TRUE(prim); + ASSERT_CHECK(prim); UsdGeomXform xform(prim); - ASSERT_TRUE(xform); + ASSERT_CHECK(xform); + + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertMesh(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MeshData& data) { + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); - ASSERT_TRUE(prim); + ASSERT_CHECK(prim); + UsdGeomMesh geomMesh(prim); - ASSERT_TRUE(geomMesh); + ASSERT_CHECK(geomMesh); + UsdGeomPrimvarsAPI primvarsAPI(geomMesh); VtIntArray faceVertexCounts; @@ -225,36 +526,46 @@ assertMesh(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MeshData readPrimvar(primvarsAPI, UsdGeomTokens->primvarsDisplayColor, displayColor); readPrimvar(primvarsAPI, UsdGeomTokens->primvarsDisplayOpacity, displayOpacity); - ASSERT_ARRAY(faceVertexCounts, data.faceVertexCounts, "faceVertexCounts"); - ASSERT_ARRAY(faceVertexIndices, data.faceVertexIndices, "faceVertexIndices"); - ASSERT_ARRAY(points, data.points, "points"); - ASSERT_ARRAY(normals.values, data.normals.values, "normals.values"); - ASSERT_ARRAY(normals.indices, data.normals.indices, "normals.indices"); - ASSERT_ARRAY(uvs.values, data.uvs.values, "uvs.values"); - ASSERT_ARRAY(uvs.indices, data.uvs.indices, "uvs.indices"); - ASSERT_ARRAY(displayColor.values, data.displayColor.values, "displayColor.values"); - ASSERT_ARRAY(displayColor.indices, data.displayColor.indices, "displayColor.indices"); - ASSERT_ARRAY(displayOpacity.values, data.displayOpacity.values, "displayOpacity.values"); - ASSERT_ARRAY(displayOpacity.indices, data.displayOpacity.indices, "displayOpacity.indices"); + ASSERT_CHECK(assertArray(faceVertexCounts, data.faceVertexCounts, "faceVertexCounts")); + ASSERT_CHECK(assertArray(faceVertexIndices, data.faceVertexIndices, "faceVertexIndices")); + ASSERT_CHECK(assertArray(points, data.points, "points")); + ASSERT_CHECK(assertArray(normals.values, data.normals.values, "normals.values")); + ASSERT_CHECK(assertArray(normals.indices, data.normals.indices, "normals.indices")); + ASSERT_CHECK(assertArray(uvs.values, data.uvs.values, "uvs.values")); + ASSERT_CHECK(assertArray(uvs.indices, data.uvs.indices, "uvs.indices")); + ASSERT_CHECK(assertArray(displayColor.values, data.displayColor.values, "displayColor.values")); + ASSERT_CHECK( + assertArray(displayColor.indices, data.displayColor.indices, "displayColor.indices")); + ASSERT_CHECK( + assertArray(displayOpacity.values, data.displayOpacity.values, "displayOpacity.values")); + ASSERT_CHECK( + assertArray(displayOpacity.indices, data.displayOpacity.indices, "displayOpacity.indices")); + if (normals.indices.size()) { - ASSERT_EQ(normals.interpolation, data.normals.interpolation); + ASSERT_EQ_VAL(normals.interpolation, data.normals.interpolation, "Normals interpolation"); } - if (uvs.indices.size()) { - ASSERT_EQ(uvs.interpolation, data.uvs.interpolation); + if (uvs.indices.size() && uvs.interpolation != data.uvs.interpolation) { + ASSERT_EQ_VAL(uvs.interpolation, data.uvs.interpolation, "Normals interpolation"); } if (displayColor.indices.size()) { - ASSERT_EQ(displayColor.interpolation, data.displayColor.interpolation); + ASSERT_EQ_VAL(displayColor.interpolation, + data.displayColor.interpolation, + "DisplayColor interpolation"); } if (displayOpacity.indices.size()) { - ASSERT_EQ(displayOpacity.interpolation, data.displayOpacity.interpolation); + ASSERT_EQ_VAL(displayOpacity.interpolation, + data.displayOpacity.interpolation, + "DisplayOpacity interpolation"); } + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertAnimation(PXR_NS::UsdStageRefPtr stage, const std::string& path, const AnimationData& data) { + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); - ASSERT_TRUE(prim); + ASSERT_CHECK(prim); // We only test the animation samples given in the data. This means that the data can have fewer // samples than the actual USD, so that the entire animation doesn't have to be copied into the @@ -262,69 +573,72 @@ assertAnimation(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Ani for (auto pair = data.orient.begin(); pair != data.orient.end(); pair++) { float time = pair->first; - GfQuatf orientValue; extractUsdAttribute( prim, TfToken("xformOp:orient"), &orientValue, UsdTimeCode(time)); - ASSERT_QUATF(orientValue, - data.orient.at(time), - std::string("xformOp:orient[") + std::to_string(time) + "]"); + ASSERT_CHECK(assertQuatf(orientValue, + data.orient.at(time), + std::string("xformOp:orient[") + std::to_string(time) + "]")); } for (auto pair = data.scale.begin(); pair != data.scale.end(); pair++) { float time = pair->first; - GfVec3f scaleValue; extractUsdAttribute( prim, TfToken("xformOp:scale"), &scaleValue, UsdTimeCode(time)); - ASSERT_VEC(scaleValue, - data.scale.at(time), - std::string("xformOp:scale[") + std::to_string(time) + "]"); + ASSERT_CHECK(assertVec(scaleValue, + data.scale.at(time), + std::string("xformOp:scale[") + std::to_string(time) + "]")); } for (auto pair = data.translate.begin(); pair != data.translate.end(); pair++) { float time = pair->first; - GfVec3d translateValue; extractUsdAttribute( prim, TfToken("xformOp:translate"), &translateValue, UsdTimeCode(time)); - ASSERT_VEC(translateValue, - data.translate.at(time), - std::string("xformOp:translate[") + std::to_string(time) + "]"); + ASSERT_CHECK(assertVec(translateValue, + data.translate.at(time), + std::string("xformOp:translate[") + std::to_string(time) + "]")); } + + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertCamera(PXR_NS::UsdStageRefPtr stage, const std::string& path, const CameraData& cameraData) { const bool WARN_IF_ATTRIBUTE_NOT_FOUND = false; + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); - ASSERT_TRUE(prim); + ASSERT_CHECK(prim); // The transformations for cameras in our USD assets tend to be stored by a parent node. We // first verify that the camera's transform is correct by extracting it from the prim's parent UsdPrim parent = prim.GetParent(); - ASSERT_TRUE(parent); + ASSERT_CHECK(parent); GfVec3d translation; GfQuatf rotation; GfVec3f scale; if (extractUsdAttribute(parent, TfToken("xformOp:translate"), &translation)) { - ASSERT_VEC( - translation, cameraData.translate, path + "'s parent translation does not match\n"); + ASSERT_CHECK(assertVec( + translation, cameraData.translate, path + "'s parent translation does not match")); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No translation attribute found for %s\n", path.c_str()); } + if (extractUsdAttribute(parent, TfToken("xformOp:orient"), &rotation)) { - ASSERT_QUATF(rotation, cameraData.orient, path + "'s parent rotation does not match\n"); + ASSERT_CHECK( + assertQuatf(rotation, cameraData.orient, path + "'s parent rotation does not match")); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No rotation attribute found for %s\n", path.c_str()); } + if (extractUsdAttribute(parent, TfToken("xformOp:scale"), &scale)) { - ASSERT_VEC(scale, cameraData.scale, path + "'s parent scale does not match\n"); + ASSERT_CHECK(assertVec(scale, cameraData.scale, path + "'s parent scale does not match")); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No scale attribute found for %s\n", path.c_str()); } @@ -332,75 +646,74 @@ assertCamera(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Camera // Next, we check the camera data itself UsdGeomCamera camera(prim); - ASSERT_TRUE(camera) << path << " could not be cast to camera\n"; + ASSERT_CHECK(camera, "camera at path: " + path); GfVec2f clippingRange; if (camera.GetClippingRangeAttr().Get(&clippingRange)) { - ASSERT_VEC( - clippingRange, cameraData.clippingRange, path + "'s clipping range does not match\n"); + ASSERT_CHECK(assertVec( + clippingRange, cameraData.clippingRange, path + "'s clipping range does not match")); } 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_EQ_VAL(focalLength, cameraData.focalLength, path + " focal length"); } 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_EQ_VAL(focusDistance, cameraData.focusDistance, path + "'s focus distance"); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No focus distance attribute found for %s\n", path.c_str()); } float fStop; if (camera.GetFStopAttr().Get(&fStop)) { - ASSERT_FLOAT_EQ(fStop, cameraData.fStop) << path << " fStop does not match\n"; + ASSERT_EQ_VAL(fStop, cameraData.fStop, path + " fStop does not match"); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No fStop attribute found for %s\n", path.c_str()); } float horizontalAperture; if (camera.GetHorizontalApertureAttr().Get(&horizontalAperture)) { - ASSERT_FLOAT_EQ(horizontalAperture, cameraData.horizontalAperture) - << path << " horizontal aperture does not match\n"; + ASSERT_EQ_VAL( + horizontalAperture, cameraData.horizontalAperture, path + "'s horizontal aperture"); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No horizontal aperture attribute found for %s\n", path.c_str()); } TfToken projection; if (camera.GetProjectionAttr().Get(&projection)) { - ASSERT_EQ(projection, TfToken(cameraData.projection)) - << path << " projection does not match\n"; + ASSERT_EQ_VAL(projection, TfToken(cameraData.projection), path + "'s projection"); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No projection attribute found for %s\n", path.c_str()); } float verticalAperture; if (camera.GetVerticalApertureAttr().Get(&verticalAperture)) { - ASSERT_FLOAT_EQ(verticalAperture, cameraData.verticalAperture) - << path << " vertical aperture does not match\n"; + ASSERT_EQ_VAL(verticalAperture, cameraData.verticalAperture, path + "'s vertical aperture"); } else if (WARN_IF_ATTRIBUTE_NOT_FOUND) { TF_WARN("No vertical aperture attribute found for %s\n", path.c_str()); } + + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightData& lightData) { + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); - ASSERT_TRUE(prim); + ASSERT_CHECK(prim); // The transformations for lights in our USD assets tend to be stored by a parent node. We // first verify that the light's transform is correct by extracting it from the prim's parent UsdPrim parent = prim.GetParent(); - ASSERT_TRUE(parent); + ASSERT_CHECK(parent); GfVec3d translation; GfQuatf rotation; @@ -408,133 +721,137 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa // The transformations for lights in our USD assets tend to be stored by a parent node if (lightData.translation) { - ASSERT_TRUE( - extractUsdAttribute(parent, TfToken("xformOp:translate"), &translation)) - << "Expected translation attribute not found for " << path << "\n"; - ASSERT_VEC(translation, - lightData.translation.value(), - path + "'s parent translation does not match\n"); + ASSERT_CHECK( + extractUsdAttribute(parent, TfToken("xformOp:translate"), &translation), + "Expected translate attribute at " + path); + ASSERT_CHECK( + assertVec(translation, lightData.translation.value(), path + " parent translation")); } + if (lightData.rotation) { - ASSERT_TRUE(extractUsdAttribute(parent, TfToken("xformOp:orient"), &rotation)) - << "Expected orient attribute not found for " << path << "\n"; - ASSERT_QUATF( - rotation, lightData.rotation.value(), path + "'s parent rotation does not match\n"); + ASSERT_CHECK(extractUsdAttribute(parent, TfToken("xformOp:orient"), &rotation), + "Expected orient attribute at " + path); + ASSERT_CHECK(assertQuatf(rotation, lightData.rotation.value(), path + " parent rotation")); } + if (lightData.scale) { - ASSERT_TRUE(extractUsdAttribute(parent, TfToken("xformOp:scale"), &scale)) - << "Expected scale attribute not found for " << path << "\n"; - ASSERT_VEC(scale, lightData.scale.value(), path + "'s parent scale does not match\n"); + ASSERT_CHECK(extractUsdAttribute(parent, TfToken("xformOp:scale"), &scale), + "Expected scale attribute not found for " + path); + ASSERT_CHECK(assertVec(scale, lightData.scale.value(), path + " parent scale")); } // Next, we check the light data itself if (prim.IsA()) { UsdLuxSphereLight sphereLight(prim); - ASSERT_TRUE(sphereLight) << path << " could not be cast to sphere light\n"; + ASSERT_CHECK(sphereLight, "UsdLuxSphereLight for path " + path); if (lightData.color) { PXR_NS::GfVec3f color; - ASSERT_TRUE(sphereLight.GetColorAttr().Get(&color)) - << path << " is missing expected color attribute\n"; - ASSERT_VEC(color, lightData.color.value(), path + " color does not match\n"); + ASSERT_CHECK(sphereLight.GetColorAttr().Get(&color), + "Expected color attribute at " + path); + ASSERT_CHECK(assertVec(color, lightData.color.value(), path + " color")); } if (lightData.intensity) { float intensity; - ASSERT_TRUE(sphereLight.GetIntensityAttr().Get(&intensity)) - << path << " is missing expected intensity attribute\n"; - ASSERT_FLOAT_EQ(intensity, lightData.intensity.value()) - << path << " intensity does not match\n"; + ASSERT_CHECK(sphereLight.GetIntensityAttr().Get(&intensity), + "Expected intensity attribute at " + path); + ASSERT_EQ_VAL( + intensity, lightData.intensity.value(), path + " intensity", LIGHT_INTENSITY_EPSILON); } if (lightData.radius) { float radius; - ASSERT_TRUE(sphereLight.GetRadiusAttr().Get(&radius)) - << path << " is missing expected radius attribute\n"; - ASSERT_FLOAT_EQ(radius, lightData.radius.value()) << path << " radius does not match\n"; + ASSERT_CHECK(sphereLight.GetRadiusAttr().Get(&radius), + "Expected radius attribute at " + path); + ASSERT_EQ_VAL(radius, lightData.radius.value(), path + "'s radius"); } } else if (prim.IsA()) { UsdLuxDistantLight distantLight(prim); - ASSERT_TRUE(distantLight) << path << " could not be cast to distant light\n"; + ASSERT_CHECK(distantLight, "Distant light for path " + path); if (lightData.color) { PXR_NS::GfVec3f color; - ASSERT_TRUE(distantLight.GetColorAttr().Get(&color)) - << path << " is missing expected color attribute\n"; - ASSERT_VEC(color, lightData.color.value(), path + " color does not match\n"); + ASSERT_CHECK(distantLight.GetColorAttr().Get(&color), + "Expected color attribute at " + path); + ASSERT_CHECK( + assertVec(color, lightData.color.value(), path + " color does not match\n")); } if (lightData.intensity) { float intensity; - ASSERT_TRUE(distantLight.GetIntensityAttr().Get(&intensity)) - << path << " is missing expected intensity attribute\n"; - ASSERT_FLOAT_EQ(intensity, lightData.intensity.value()) - << path << " intensity does not match\n"; + ASSERT_CHECK(distantLight.GetIntensityAttr().Get(&intensity), + "Intensity attribute at " + path); + ASSERT_EQ_VAL(intensity, + lightData.intensity.value(), + path + "'s intensity", + LIGHT_INTENSITY_EPSILON); } // Distant lights don't have a radius } else if (prim.IsA()) { UsdLuxDiskLight diskLight(prim); - ASSERT_TRUE(diskLight) << path << " could not be cast to disk light\n"; + ASSERT_CHECK(diskLight, "Disk light for path " + path); if (lightData.color) { - PXR_NS::GfVec3f color; - ASSERT_TRUE(diskLight.GetColorAttr().Get(&color)) - << path << " is missing expected color attribute\n"; - ASSERT_VEC(color, lightData.color.value(), path + " color does not match\n"); + GfVec3f color; + ASSERT_CHECK(diskLight.GetColorAttr().Get(&color), + "Expected color attribute at " + path); + ASSERT_CHECK(assertVec(color, lightData.color.value(), path + " color")); } if (lightData.intensity) { float intensity; - ASSERT_TRUE(diskLight.GetIntensityAttr().Get(&intensity)) - << path << " is missing expected intensity attribute\n"; - ASSERT_FLOAT_EQ(intensity, lightData.intensity.value()) - << path << " intensity does not match\n"; + ASSERT_CHECK(diskLight.GetIntensityAttr().Get(&intensity), + "Expected intensity attribute at " + path); + ASSERT_EQ_VAL(intensity, + lightData.intensity.value(), + path + "'s light intensity", + LIGHT_INTENSITY_EPSILON); } if (lightData.radius) { float radius; - ASSERT_TRUE(diskLight.GetRadiusAttr().Get(&radius)) - << path << " is missing expected radius attribute\n"; - ASSERT_FLOAT_EQ(radius, lightData.radius.value()) << path << " radius does not match\n"; + ASSERT_CHECK(diskLight.GetRadiusAttr().Get(&radius), + "Expected radius attribute at " + path); + ASSERT_EQ_VAL(radius, lightData.radius.value(), path + "'s radius"); } } else if (prim.IsA()) { - ASSERT_TRUE(false) << "Rectangle lights are not supported yet on import\n"; + return ::testing::AssertionFailure() + << "lights are not supported yet on import; Rectangle lights at " << path; /* Uncomment this once we support import of rectangle lights - UsdLuxRectLight rectLight(prim); ASSERT_TRUE(rectLight); - ASSERT_VEC(rectLight.color, lightData.color); ASSERT_FLOAT_EQ(rectLight.intensity, lightData.intensity); + ADD_FAILURE() << "Rectangle lights not supported yet: " << path << "\n"; + return false; // Rectangle specific attributes ASSERT_FLOAT_EQ(rectLight.length[0], lightData.length[0]); ASSERT_FLOAT_EQ(rectLight.length[1], lightData.length[1]); */ } else if (prim.IsA()) { - ASSERT_TRUE(false) << "Dome lights are not supported yet on import\n"; + return ::testing::AssertionFailure() + << "Dome lights are not supported yet on import; Dome lights at " << path; /* Uncomment this once we support import of dome lights - UsdLuxDomeLight domeLight(prim); ASSERT_TRUE(domeLight); - ASSERT_VEC(domeLight.color, lightData.color); ASSERT_FLOAT_EQ(domeLight.intensity, lightData.intensity); - - // Add texture test once we support this on import + // Add texture test once we support this on import */ } else { - ASSERT_TRUE(false) << "Expected a supported light, but encountered a prim of type \"" - << prim.GetTypeName().GetString() << "\" at \"" << path << "\"\n"; + return ::testing::AssertionFailure() + << "Encountered prim of type " << prim.GetTypeName().GetString() << " at " << path; } // Spotlights use shaping APIs @@ -545,145 +862,152 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa if (lightData.coneAngle) { float coneAngle; - ASSERT_TRUE(shapingAPI.GetShapingConeAngleAttr().Get(&coneAngle)) - << path << " is missing expected angle attribute\n"; - ASSERT_FLOAT_EQ(coneAngle, lightData.coneAngle.value()) - << path << " cone angle does not match\n"; + ASSERT_CHECK(shapingAPI.GetShapingConeAngleAttr().Get(&coneAngle), + "Cone angle attribute at " + path); + ASSERT_EQ_VAL(coneAngle, lightData.coneAngle.value(), path + "'s cone angle"); } if (lightData.coneFalloff) { float coneFalloff; - ASSERT_TRUE(shapingAPI.GetShapingConeSoftnessAttr().Get(&coneFalloff)) - << path << " is missing expected softness attribute\n"; - ASSERT_FLOAT_EQ(coneFalloff, lightData.coneFalloff.value()) - << path << " cone falloff does not match\n"; + ASSERT_CHECK(shapingAPI.GetShapingConeSoftnessAttr().Get(&coneFalloff), + "Cone falloff attribute at " + path); + ASSERT_EQ_VAL(coneFalloff, lightData.coneFalloff.value(), path + "'s cone falloff"); } } + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertDisplayName(PXR_NS::UsdStageRefPtr stage, const std::string& primPath, const std::string& displayName) { + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(primPath)); - ASSERT_TRUE(prim) << primPath << " not found when verifying prim had proper display name\n"; - ASSERT_EQ(prim.GetDisplayName(), displayName) - << primPath << " has incorrect display name; expected \"" << displayName << "\" but got \"" - << prim.GetDisplayName() << "\"\n "; + ASSERT_CHECK(prim, "Proper dislay name for prim at " + primPath); + ASSERT_EQ_VAL(prim.GetDisplayName(), displayName, primPath + " display name"); + + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertVisibility(PXR_NS::UsdStageRefPtr stage, const std::string& path, bool expectedVisibilityAttr, bool expectedActualVisibility) { + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); UsdGeomImageable imageable(prim); - ASSERT_TRUE(imageable) << "Test setup error: " << path << " is not an imageable prim"; + ASSERT_CHECK(imageable, "imageable prim at path: " + path + " "); // Visibility attribute should always be present, even if it's not written explicitly - ASSERT_TRUE(imageable.GetVisibilityAttr().HasValue()) - << "Unexpected error: " << path << " missing visibility attribute"; + ASSERT_CHECK(imageable.GetVisibilityAttr().HasValue(), + "Visibility attribute on prim at path: " + path); // Check visibility attribute TfToken visibility; imageable.GetVisibilityAttr().Get(&visibility); - ASSERT_EQ(expectedVisibilityAttr, visibility == UsdGeomTokens->inherited) - << path << " has visibility attribute " - << (expectedVisibilityAttr ? "inherited" : "invisible") << " that is expected to be " - << visibility.GetString(); + ASSERT_EQ_VAL(expectedVisibilityAttr, + visibility == UsdGeomTokens->inherited, + path + "'s visibility attribute"); - // Check actual visibility + // Check computed (actual) visibility visibility = imageable.ComputeVisibility(); - ASSERT_EQ(expectedActualVisibility, visibility == UsdGeomTokens->inherited) - << path << " is computed as " << visibility.GetString() << " but is expected to be " - << (expectedVisibilityAttr ? "visible" : "invisible"); + ASSERT_EQ_VAL(expectedActualVisibility, + visibility == UsdGeomTokens->inherited, + path + "'s computed visibility"); + + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertPoints(PXR_NS::UsdStageRefPtr stage, const std::string& path, const PointsData& data) { + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(SdfPath(path)); - ASSERT_TRUE(prim); + ASSERT_CHECK(prim); UsdGeomPoints geomPoints(prim); - ASSERT_TRUE(geomPoints); + ASSERT_CHECK(geomPoints); VtVec3fArray points; geomPoints.GetPointsAttr().Get(&points, 0); - ASSERT_EQ(points.size(), data.pointsCount); -} + ASSERT_EQ_VAL(points.size(), data.pointsCount, "points count"); -#define ASSERT_INPUT(...) assertInput(__VA_ARGS__) -#define ASSERT_INPUT_FIELD(...) assertInputField(__VA_ARGS__) -#define ASSERT_INPUT_PATH(...) assertInputPath(__VA_ARGS__) + return ::testing::AssertionSuccess(); +} template -void -assertInputField(const UsdShadeShader& shader, const std::string& name, const T& value) +[[nodiscard]] ::testing::AssertionResult +assertInputField(const pxr::UsdShadeShader& shader, const std::string& name, const T& value) { - auto attr = shader.GetInput(TfToken(name)); + auto attr = shader.GetInput(pxr::TfToken(name)); if (attr) { auto valueAttrs = attr.GetValueProducingAttributes(); if (valueAttrs.size()) { T actual; valueAttrs.front().Get(&actual); - ASSERT_EQ(actual, value) - << "Input field " << name << " for shader " << shader.GetPath().GetString() - << " doesn't match (" << typeid(value).name() << ")"; - return; + ASSERT_EQ_VAL( + actual, value, "Input field " + name + " for shader " + shader.GetPath().GetString()); } } + return ::testing::AssertionSuccess(); // TODO check if attr missing } // Cannot reuse assertInputField for this since SdfAssetPath equality fails for ::resolvedPath, // so here we match only the ::assetPath. -void -assertInputPath(const UsdShadeShader& shader, const std::string& name, const std::string& value) +[[nodiscard]] ::testing::AssertionResult +assertInputPath(const pxr::UsdShadeShader& shader, + const std::string& name, + const std::string& value) { - auto attr = shader.GetInput(TfToken(name)); + auto attr = shader.GetInput(pxr::TfToken(name)); + if (attr) { auto valueAttrs = attr.GetValueProducingAttributes(); if (valueAttrs.size()) { - SdfAssetPath actualAssetPath; + pxr::SdfAssetPath actualAssetPath; valueAttrs.front().Get(&actualAssetPath); - const std::string actual = TfNormPath(actualAssetPath.GetAssetPath()); - ASSERT_EQ(actual, value); - return; + const std::string actual = pxr::TfNormPath(actualAssetPath.GetAssetPath()); + ASSERT_EQ_VAL( + actual, value, "Input path " + name + " for shader " + shader.GetPath().GetString()); } } + return ::testing::AssertionSuccess(); // TODO check if attr missing } -void +::testing::AssertionResult assertMaterial(PXR_NS::UsdStageRefPtr stage, const std::string& path, const MaterialData& data) { const std::string currentDir = TfAbsPath("."); const SdfPath materialPath = SdfPath(path); + + ASSERT_CHECK(stage); UsdPrim prim = stage->GetPrimAtPath(materialPath); - ASSERT_TRUE(prim); + ASSERT_CHECK(prim); + UsdShadeMaterial material(prim); - ASSERT_TRUE(material); + ASSERT_CHECK(material); SdfPathVector connections; UsdShadeShader shader; UsdAttribute surface = material.GetSurfaceAttr(); surface.GetConnections(&connections); - ASSERT_TRUE(connections.size() > 0); + ASSERT_CHECK(connections.size() > 0); const SdfPath shaderPath = connections[0].GetPrimPath(); shader = UsdShadeShader(stage->GetPrimAtPath(shaderPath)); TfToken shaderId; shader.GetShaderId(&shaderId); - ASSERT_TRUE(shaderId == TfToken("UsdPreviewSurface")); + ASSERT_CHECK(shaderId == TfToken("UsdPreviewSurface")); auto assertInput = [&](const TfToken& name, const InputData& data) { UsdShadeInput shadeInput = shader.GetInput(name); if (!shadeInput) - return; + return ::testing::AssertionSuccess(); if (shadeInput.HasConnectedSource()) { UsdShadeInput::SourceInfoVector sources = shadeInput.GetConnectedSources(); for (UsdShadeConnectionSourceInfo source : sources) { @@ -691,26 +1015,31 @@ assertMaterial(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Mate const UsdShadeShader textureShader = UsdShadeShader(stage->GetPrimAtPath(sourcePath)); if (sourcePath == materialPath) { - ASSERT_INPUT_FIELD(textureShader, name, data.value); + ASSERT_CHECK(assertInputField(textureShader, name, data.value)); } else { const SdfPath textureShaderPath = textureShader.GetPath(); TfToken uvTextureId; textureShader.GetShaderId(&uvTextureId); - ASSERT_EQ(uvTextureId.GetString(), std::string("UsdUVTexture")); - + ASSERT_EQ_VAL(uvTextureId.GetString(), + std::string("UsdUVTexture"), + "Shader at path " + textureShaderPath.GetString() + + " is not a UsdUVTexture"); const std::string assetPath = TfNormPath(currentDir + "/" + data.file); - ASSERT_INPUT_PATH(textureShader, "file", assetPath); + ASSERT_CHECK(assertInputPath(textureShader, "file", assetPath)); // TODO? ASSERT_IMAGE(ctx, assetPath, input.image); - ASSERT_INPUT_FIELD(textureShader, "wrapS", data.wrapS); - ASSERT_INPUT_FIELD(textureShader, "wrapT", data.wrapT); - ASSERT_INPUT_FIELD(textureShader, "scale", data.scale); - ASSERT_INPUT_FIELD(textureShader, "bias", data.bias); - ASSERT_INPUT_FIELD(textureShader, "fallback", data.value); - ASSERT_EQ(data.channel, source.sourceName); + ASSERT_CHECK(assertInputField(textureShader, "wrapS", data.wrapS)); + ASSERT_CHECK(assertInputField(textureShader, "wrapT", data.wrapT)); + ASSERT_CHECK(assertInputField(textureShader, "scale", data.scale)); + ASSERT_CHECK(assertInputField(textureShader, "bias", data.bias)); + ASSERT_CHECK(assertInputField(textureShader, "fallback", data.value)); + ASSERT_EQ_VAL(data.channel, + source.sourceName, + "Source name for shader at path " + + textureShaderPath.GetString()); UsdShadeInput stInput = textureShader.GetInput(TestTokens->st); if (!stInput) - return; + return ::testing::AssertionSuccess(); if (stInput.HasConnectedSource()) { UsdShadeInput::SourceInfoVector sources = stInput.GetConnectedSources(); for (UsdShadeConnectionSourceInfo source : sources) { @@ -721,16 +1050,18 @@ assertMaterial(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Mate stShader.GetShaderId(&shaderId); if (!data.uvRotation.IsEmpty() || !data.uvScale.IsEmpty() || !data.uvTranslation.IsEmpty()) { - ASSERT_TRUE(shaderId == TestTokens->UsdTransform2d); - ASSERT_INPUT_FIELD(stShader, "rotation", data.uvRotation); - ASSERT_INPUT_FIELD(stShader, "scale", data.uvScale); - ASSERT_INPUT_FIELD(stShader, "translation", data.uvTranslation); + ASSERT_CHECK(shaderId == TestTokens->UsdTransform2d); + ASSERT_CHECK( + assertInputField(stShader, "rotation", data.uvRotation)); + ASSERT_CHECK(assertInputField(stShader, "scale", data.uvScale)); + ASSERT_CHECK( + assertInputField(stShader, "translation", data.uvTranslation)); } else { std::string shaderName = stShader.GetPrim().GetName().GetString(); if (shaderName == "texCoordReader") { - ASSERT_TRUE(shaderId == TestTokens->UsdPrimvarReader_float2); + ASSERT_CHECK(shaderId == TestTokens->UsdPrimvarReader_float2); } else { - ASSERT_TRUE(shaderId == TestTokens->UsdTransform2d); + ASSERT_CHECK(shaderId == TestTokens->UsdTransform2d); } } } @@ -740,31 +1071,190 @@ assertMaterial(PXR_NS::UsdStageRefPtr stage, const std::string& path, const Mate } else { VtValue actualValue; shadeInput.Get(&actualValue); - ASSERT_TRUE(actualValue == data.value); + ASSERT_CHECK(actualValue == data.value, + "Actual value = " + TfStringify(actualValue) + + ", expected value = " + TfStringify(data.value)); } + return ::testing::AssertionSuccess(); }; - ASSERT_INPUT(TestTokens->useSpecularWorkflow, data.useSpecularWorkflow); - ASSERT_INPUT(TestTokens->diffuseColor, data.diffuseColor); - ASSERT_INPUT(TestTokens->emissiveColor, data.emissiveColor); - ASSERT_INPUT(TestTokens->specularColor, data.specularColor); - ASSERT_INPUT(TestTokens->normal, data.normal); - ASSERT_INPUT(TestTokens->metallic, data.metallic); - ASSERT_INPUT(TestTokens->roughness, data.roughness); - ASSERT_INPUT(TestTokens->clearcoat, data.clearcoat); - ASSERT_INPUT(TestTokens->clearcoatRoughness, data.clearcoatRoughness); - ASSERT_INPUT(TestTokens->opacity, data.opacity); - ASSERT_INPUT(TestTokens->opacityThreshold, data.opacityThreshold); - ASSERT_INPUT(TestTokens->displacement, data.displacement); - ASSERT_INPUT(TestTokens->occlusion, data.occlusion); - ASSERT_INPUT(TestTokens->ior, data.ior); + + ASSERT_CHECK(assertInput(TestTokens->useSpecularWorkflow, data.useSpecularWorkflow)); + ASSERT_CHECK(assertInput(TestTokens->diffuseColor, data.diffuseColor)); + ASSERT_CHECK(assertInput(TestTokens->emissiveColor, data.emissiveColor)); + ASSERT_CHECK(assertInput(TestTokens->specularColor, data.specularColor)); + ASSERT_CHECK(assertInput(TestTokens->normal, data.normal)); + ASSERT_CHECK(assertInput(TestTokens->metallic, data.metallic)); + ASSERT_CHECK(assertInput(TestTokens->roughness, data.roughness)); + ASSERT_CHECK(assertInput(TestTokens->clearcoat, data.clearcoat)); + ASSERT_CHECK(assertInput(TestTokens->clearcoatRoughness, data.clearcoatRoughness)); + ASSERT_CHECK(assertInput(TestTokens->opacity, data.opacity)); + ASSERT_CHECK(assertInput(TestTokens->opacityThreshold, data.opacityThreshold)); + ASSERT_CHECK(assertInput(TestTokens->displacement, data.displacement)); + ASSERT_CHECK(assertInput(TestTokens->occlusion, data.occlusion)); + ASSERT_CHECK(assertInput(TestTokens->ior, data.ior)); + + return ::testing::AssertionSuccess(); } -void +::testing::AssertionResult assertRender(const std::string& filename, const std::string& imageFilename) { const std::string imageParentPath = TfGetPathName(imageFilename); TfMakeDirs(imageParentPath, -1, true); - 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 + + // Build the platform-appropriate command +#if defined(_WIN32) + // On Windows, set env var via `set` and `&&` + const std::string command = + "set HYDRA_ENABLE_HGIGL=0 && usdrecord \"" + filename + "\" \"" + imageFilename + "\""; +#else + // On Unix-like systems, use inline env var syntax + const std::string command = + "HYDRA_ENABLE_HGIGL=0 usdrecord \"" + filename + "\" \"" + imageFilename + "\""; +#endif + + ArchSystemResult result = archSystem(command); + ASSERT_EQ_VAL(result.exitCode, 0, "usdrecord exit code:\n" + result.output); + + return ::testing::AssertionSuccess(); +} + +::testing::AssertionResult +assertUsda(const SdfLayerHandle& sdfLayer, + const std::string& baselinePath, + bool generateBaseline, + bool dumpOnFailure) +{ + ASSERT_CHECK(sdfLayer, "SdfLayer is invalid"); + + if (generateBaseline) { + std::cout << "Updating USDA baseline " << baselinePath << std::endl; + sdfLayer->Export(baselinePath); + } + + SdfLayerRefPtr baselineLayer = SdfLayer::FindOrOpen(baselinePath); + ASSERT_CHECK(baselineLayer, "Failed to load baseline layer from " + baselinePath); + + std::string layerStr; + sdfLayer->ExportToString(&layerStr); + std::string baselineStr; + baselineLayer->ExportToString(&baselineStr); + + // Skip the header (up to and including the doc string) which contains version/date info + auto skipHeader = [](const std::string& str) -> size_t { + // Find the end of the doc string: look for closing ''' + size_t docStart = str.find("doc = '''"); + if (docStart != std::string::npos) { + // Find the closing ''' (skip past the opening ''') + size_t docEnd = str.find("'''", docStart + 9); + if (docEnd != std::string::npos) { + // Skip to next newline after closing ''' + docEnd = str.find('\n', docEnd + 3); + if (docEnd != std::string::npos) { + return docEnd + 1; + } + } + } + return 0; // If we can't find the pattern, compare from start + }; + + // Normalize asset paths to ignore build-specific directories + auto normalizeAssetPaths = [](const std::string& str) -> std::string { + std::string result = str; + size_t pos = 0; + + // Find patterns like: asset inputs:xxx = @/path/to/file.glb[texture.png]@ ( + // But exclude patterns with dots in the input name like: asset inputs:file.connect + while ((pos = result.find("asset inputs:", pos)) != std::string::npos) { + // Find the equals sign + size_t equalsPos = result.find(" = ", pos); + if (equalsPos == std::string::npos) + break; + + // Check if there's a dot between "inputs:" and " = " (would indicate .connect, etc) + std::string inputName = + result.substr(pos + 13, equalsPos - (pos + 13)); // "inputs:" is 13 chars + if (inputName.find('.') != std::string::npos) { + pos = equalsPos + 3; + continue; // Skip this one, it has a dot (like file.connect) + } + + // Find the opening @ + size_t atStartPos = result.find("@", equalsPos); + if (atStartPos == std::string::npos || atStartPos > equalsPos + 10) { + pos = equalsPos + 3; + continue; + } + + // Find the closing @ + size_t atEndPos = result.find("@", atStartPos + 1); + if (atEndPos == std::string::npos) + break; + + // Extract the full path between the @ symbols + std::string fullPath = result.substr(atStartPos + 1, atEndPos - atStartPos - 1); + + // Keep only the filename part (after last / or \) + size_t lastSlash = fullPath.find_last_of("/\\"); + std::string normalizedPath = + (lastSlash != std::string::npos) ? fullPath.substr(lastSlash + 1) : fullPath; + + // Replace the full path with just the filename + result.replace(atStartPos + 1, atEndPos - atStartPos - 1, normalizedPath); + + pos = atStartPos + normalizedPath.length() + 2; + } + + return result; + }; + + size_t layerStart = skipHeader(layerStr); + size_t baselineStart = skipHeader(baselineStr); + + std::string layerContent = normalizeAssetPaths(layerStr.substr(layerStart)); + std::string baselineContent = normalizeAssetPaths(baselineStr.substr(baselineStart)); + + if (layerContent != baselineContent) { + if (dumpOnFailure) { + std::cout << "Layer content has length: " << layerContent.size() + << "\nBaseline content has length: " << baselineContent.size() + << " (compared without header)" << std::endl; + + const std::string searchStr = "baseline_"; + const std::string replaceStr = "output_"; + std::string outputPath = baselinePath; + size_t pos = outputPath.find(searchStr); + if (pos != std::string::npos) { + outputPath.replace(pos, searchStr.length(), replaceStr); + } else { + return ::testing::AssertionFailure() + << "Expected '" << searchStr << "' in baseline path '" << baselinePath + << "'"; + } + std::fstream out(outputPath, std::ios::out); + out << layerContent; + out.close(); + std::cout << "Output dumped to " << outputPath << " (without header)" << std::endl; + + // Very poor person's diff operation. Can we do better without bringing + // in a diff library? + for (size_t i = 0; i < layerContent.size(); ++i) { + if (i >= baselineContent.size()) { + std::cout << "Size difference. Output has more characters than baseline" + << std::endl; + break; + } + if (layerContent[i] != baselineContent[i]) { + std::cout << "Mismatch at char " << i << " (after header)" << std::endl; + std::cout << "Remainder in output:\n" << &layerContent[i] << std::endl; + std::cout << "Remainder in baseline:\n" << &baselineContent[i] << std::endl; + break; + } + } + } + + return ::testing::AssertionFailure() << "Output of layer " << sdfLayer->GetIdentifier() + << " does not match baseline " << baselinePath; + } + return ::testing::AssertionSuccess(); +} diff --git a/utils/src/transforms.cpp b/utils/src/transforms.cpp index f300c972..504a657a 100644 --- a/utils/src/transforms.cpp +++ b/utils/src/transforms.cpp @@ -9,9 +9,9 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#include #include #include +#include #include using namespace PXR_NS; diff --git a/utils/src/usdData.cpp b/utils/src/usdData.cpp index 10f71b44..462f1f2d 100644 --- a/utils/src/usdData.cpp +++ b/utils/src/usdData.cpp @@ -181,7 +181,7 @@ printMaterial(const std::string& header, { TF_DEBUG_MSG( FILE_FORMAT_UTIL, - "%s: %s material { %s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n", + "%s: %s material { %s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n", debugTag.c_str(), header.c_str(), path.GetAsString().c_str(), @@ -735,9 +735,9 @@ UniqueNameEnforcer::enforceUniqueness(std::string& name) void removeBrackets(std::string& name) { - name.erase(std::remove_if(name.begin(), name.end(), - [](char c) { return c == '[' || c == ']'; }), - name.end()); + name.erase( + std::remove_if(name.begin(), name.end(), [](char c) { return c == '[' || c == ']'; }), + name.end()); } void diff --git a/utils/tests/CMakeLists.txt b/utils/tests/CMakeLists.txt index 2b7d87d2..1acc77a7 100644 --- a/utils/tests/CMakeLists.txt +++ b/utils/tests/CMakeLists.txt @@ -19,3 +19,4 @@ target_link_libraries(utilsTests 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 index 12fdfa08..fdc4bc86 100644 --- a/utils/tests/data/baseline_writeASM.usda +++ b/utils/tests/data/baseline_writeASM.usda @@ -9,9 +9,11 @@ def Xform "Scene" { def "Materials" { - def Material "GeneralTestMaterial" + def Material "GeneralTestMaterial" ( + displayName = "General Test Material" + ) { - float3 inputs:absorptionColor = (0.25, 0.5, 1) + color3f inputs:absorptionColor = (0.25, 0.5, 1) float inputs:absorptionDistance = 111 float inputs:ambientOcclusion = 0.01 ( customData = { @@ -37,8 +39,8 @@ def Xform "Scene" } } ) - float3 inputs:baseColor = (1, 2, 3) - float3 inputs:coatColor = (1, 1, 0) + color3f inputs:baseColor = (1, 0.5, 0.25) + color3f inputs:coatColor = (1, 1, 0) float inputs:coatIOR = 1.33 ( customData = { dictionary range = { @@ -47,7 +49,7 @@ def Xform "Scene" } } ) - float3 inputs:coatNormal = (0.66, 0, 0.66) + normal3f inputs:coatNormal = (0.66, 0, 0.66) float inputs:coatOpacity = 0.55 ( customData = { dictionary range = { @@ -56,7 +58,7 @@ def Xform "Scene" } } ) - float inputs:coatRoughness = 0.66 ( + float inputs:coatRoughness = 0.44 ( customData = { dictionary range = { double max = 1 @@ -72,7 +74,7 @@ def Xform "Scene" } } ) - float3 inputs:emissive = (1, 2, 3) + color3f inputs:emissive = (1, 0.5, 0.25) float inputs:emissiveIntensity = 1 float inputs:height = 1.23 ( customData = { @@ -98,8 +100,8 @@ def Xform "Scene" } } ) - float3 inputs:normal = (0.33, 0.33, 0.33) - float inputs:normalScale = 0.666 + normal3f inputs:normal = (0.5, 0.5, 0.5) + float inputs:normalScale = 0.5 float inputs:opacity = 0.8 ( customData = { dictionary range = { @@ -108,7 +110,7 @@ def Xform "Scene" } } ) - float inputs:opacityThreshold = 0.75 ( + float inputs:roughness = 0.66 ( customData = { dictionary range = { double max = 1 @@ -116,17 +118,9 @@ def Xform "Scene" } } ) - float inputs:roughness = 0.44 ( - customData = { - dictionary range = { - double max = 1 - double min = 0 - } - } - ) - float3 inputs:scatteringColor = (1, 0.5, 1) + color3f inputs:scatteringColor = (1, 0.5, 1) float inputs:scatteringDistance = 222 - float3 inputs:sheenColor = (0, 1, 1) + color3f inputs:sheenColor = (0, 1, 1) float inputs:sheenOpacity = 1 ( customData = { dictionary range = { @@ -135,7 +129,7 @@ def Xform "Scene" } } ) - float inputs:sheenRoughness = 0.99 ( + float inputs:sheenRoughness = 0.5 ( customData = { dictionary range = { double max = 1 @@ -143,7 +137,7 @@ def Xform "Scene" } } ) - float3 inputs:specularEdgeColor = (1, 0, 1) + color3f inputs:specularEdgeColor = (1, 0, 1) float inputs:specularLevel = 0.5 ( customData = { dictionary range = { @@ -174,53 +168,74 @@ def Xform "Scene" { def Shader "ASM" { + custom bool clearcoatModelsTransmissionTint = 1 uniform token info:id = "AdobeStandardMaterial_4_0" - float3 inputs:absorptionColor.connect = + color3f 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 = + color3f inputs:baseColor.connect = + color3f inputs:coatColor.connect = float inputs:coatIOR.connect = - float3 inputs:coatNormal.connect = + normal3f inputs:coatNormal.connect = float inputs:coatOpacity.connect = float inputs:coatRoughness.connect = float inputs:coatSpecularLevel.connect = - float3 inputs:emissive.connect = + color3f inputs:emissive.connect = float inputs:emissiveIntensity.connect = float inputs:height.connect = float inputs:IOR.connect = float inputs:metallic.connect = - float3 inputs:normal.connect = + normal3f 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 = + color3f inputs:scatteringColor.connect = float inputs:scatteringDistance.connect = - float3 inputs:sheenColor.connect = + color3f inputs:sheenColor.connect = float inputs:sheenOpacity.connect = float inputs:sheenRoughness.connect = - float3 inputs:specularEdgeColor.connect = + color3f inputs:specularEdgeColor.connect = float inputs:specularLevel.connect = float inputs:translucency.connect = float inputs:volumeThickness.connect = + custom float normalScale = 0.5 + custom float opacityThreshold = 0.75 token outputs:surface + custom bool unlit = 1 + custom bool useSpecularWorkflow = 1 } } } - def Material "TextureTestMaterial" + def Material "TextureTestMaterial" ( + displayName = "Texture Test Material" + ) { - asset inputs:baseColorTexture = @textures/color.png@ - asset inputs:coatNormalTexture = @textures/normal.png@ - asset inputs:coatOpacityTexture = @textures/color.png@ + asset inputs:ambientOcclusionTexture = @textures/occlusion.png@ ( + colorSpace = "raw" + ) + asset inputs:baseColorTexture = @textures/color.png@ ( + colorSpace = "srgb_texture" + ) + asset inputs:coatNormalTexture = @textures/normal.png@ ( + colorSpace = "raw" + ) + asset inputs:coatOpacityTexture = @textures/color.png@ ( + colorSpace = "raw" + ) float inputs:emissiveIntensity = 1 - asset inputs:emissiveTexture = @textures/color.png@ - asset inputs:normalTexture = @textures/normal.png@ - asset inputs:roughnessTexture = @textures/greyscale.png@ + asset inputs:emissiveTexture = @textures/color.png@ ( + colorSpace = "srgb_texture" + ) + asset inputs:normalTexture = @textures/normal.png@ ( + colorSpace = "raw" + ) + asset inputs:roughnessTexture = @textures/greyscale.png@ ( + colorSpace = "raw" + ) token outputs:adobe:surface.connect = def NodeGraph "ASM" @@ -235,25 +250,44 @@ def Xform "Scene" def Shader "baseColor" { uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/color.png@ asset inputs:file.connect = token inputs:sourceColorSpace = "sRGB" float2 inputs:st.connect = float3 outputs:rgb } + def Shader "roughness_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 "roughness" { uniform token info:id = "UsdUVTexture" + float4 inputs:bias = (0.1, 0, 0, 0) + asset inputs:file = @textures/greyscale.png@ asset inputs:file.connect = + float4 inputs:scale = (0.55, 1, 1, 1) token inputs:sourceColorSpace = "raw" - float2 inputs:st.connect = + float2 inputs:st.connect = + token inputs:wrapS = "black" + token inputs:wrapT = "black" float outputs:r } def Shader "normal" { uniform token info:id = "UsdUVTexture" + float4 inputs:bias = (-0.75, -0.75, -0.75, 0) + asset inputs:file = @textures/normal.png@ asset inputs:file.connect = + float4 inputs:scale = (1.5, 1.5, 1.5, 0.75) token inputs:sourceColorSpace = "raw" float2 inputs:st.connect = float3 outputs:rgb @@ -273,6 +307,7 @@ def Xform "Scene" { uniform token info:id = "UsdUVTexture" float4 inputs:bias = (0.1, 0.2, 0.3, 0) + asset inputs:file = @textures/color.png@ asset inputs:file.connect = float4 inputs:scale = (1, 2, 0.5, 1) token inputs:sourceColorSpace = "sRGB" @@ -285,7 +320,9 @@ def Xform "Scene" def Shader "coatOpacity" { uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/color.png@ asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" float2 inputs:st.connect = float outputs:g } @@ -293,28 +330,44 @@ def Xform "Scene" def Shader "coatNormal" { uniform token info:id = "UsdUVTexture" + float4 inputs:bias = (-1, 1, -1, 0) + asset inputs:file = @textures/normal.png@ asset inputs:file.connect = + float4 inputs:scale = (2, -2, 2, 1) token inputs:sourceColorSpace = "raw" float2 inputs:st.connect = float3 outputs:rgb } + def Shader "ambientOcclusion" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/occlusion.png@ + asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" + float2 inputs:st.connect = + float outputs:r + } + def Shader "ASM" { uniform token info:id = "AdobeStandardMaterial_4_0" - float3 inputs:baseColor.connect = - float3 inputs:coatNormal.connect = + float inputs:ambientOcclusion.connect = + color3f inputs:baseColor.connect = + normal3f inputs:coatNormal.connect = float inputs:coatOpacity.connect = - float3 inputs:emissive.connect = + color3f inputs:emissive.connect = float inputs:emissiveIntensity.connect = - float3 inputs:normal.connect = + normal3f inputs:normal.connect = float inputs:roughness.connect = token outputs:surface } } } - def Material "TransmissionTestMaterial" + def Material "TransmissionTestMaterial" ( + displayName = "Transmission Test Material" + ) { float inputs:translucency = 0.543 ( customData = { diff --git a/utils/tests/data/baseline_writeOpenPBR.usda b/utils/tests/data/baseline_writeOpenPBR.usda index 527eb4dc..b7ae7078 100644 --- a/utils/tests/data/baseline_writeOpenPBR.usda +++ b/utils/tests/data/baseline_writeOpenPBR.usda @@ -9,11 +9,11 @@ def Xform "Scene" { def "Materials" { - def Material "GeneralTestMaterial" + def Material "GeneralTestMaterial" ( + displayName = "General Test Material" + ) { - color3f inputs:absorptionColor = (0.25, 0.5, 1) - float inputs:absorptionDistance = 111 - float inputs:anisotropyLevel = 0.321 ( + float inputs:ambientOcclusion = 0.01 ( customData = { dictionary range = { double max = 1 @@ -21,7 +21,15 @@ def Xform "Scene" } } ) - color3f inputs:baseColor = (1, 2, 3) + float inputs:anisotropyAngle = 0.777 ( + customData = { + dictionary range = { + double max = 1 + double min = 0 + } + } + ) + color3f inputs:baseColor = (1, 0.5, 0.25) color3f inputs:coatColor = (1, 1, 0) float inputs:coatIOR = 1.33 ( customData = { @@ -40,7 +48,7 @@ def Xform "Scene" } } ) - float inputs:coatRoughness = 0.66 ( + float inputs:coatRoughness = 0.44 ( customData = { dictionary range = { double max = 1 @@ -48,18 +56,20 @@ def Xform "Scene" } } ) - float inputs:emissionLuminance = 1 - color3f inputs:emissive = (1, 2, 3) - float inputs:fuzzWeight = 1 - float inputs:IOR = 1.55 ( + float inputs:coatSpecularLevel = 0.88 ( customData = { dictionary range = { - double max = 3 - double min = 1 + double max = 1 + double min = 0 } } ) - float inputs:metallic = 0.22 ( + float inputs:emissionLuminance = 1000 + color3f inputs:emissive = (1, 0.5, 0.25) + color3f inputs:fuzzColor = (0, 1, 1) + float inputs:fuzzRoughness = 0.5 + float inputs:fuzzWeight = 1 + float inputs:height = 1.23 ( customData = { dictionary range = { double max = 1 @@ -67,16 +77,15 @@ def Xform "Scene" } } ) - float3 inputs:normal = (0.33, 0.33, 0.33) - float inputs:opacity = 0.8 ( + float inputs:IOR = 1.55 ( customData = { dictionary range = { - double max = 1 - double min = 0 + double max = 3 + double min = 1 } } ) - float inputs:roughness = 0.44 ( + float inputs:metallic = 0.22 ( customData = { dictionary range = { double max = 1 @@ -84,10 +93,8 @@ def Xform "Scene" } } ) - color3f inputs:scatteringColor = (1, 0.5, 1) - float inputs:scatteringDistance = 222 - color3f inputs:sheenColor = (0, 1, 1) - float inputs:sheenRoughness = 0.99 ( + float3 inputs:normal = (0.5, 0.5, 0.5) + float inputs:opacity = 0.8 ( customData = { dictionary range = { double max = 1 @@ -96,9 +103,16 @@ def Xform "Scene" } ) color3f inputs:specularEdgeColor = (1, 0, 1) + float inputs:specularRoughness = 0.66 + float inputs:specularRoughnessAnisotropy = 0.321 float inputs:specularWeight = 0.5 + color3f inputs:subsurfaceColor = (1, 0.5, 1) + float inputs:subsurfaceRadius = 222 float inputs:subsurfaceWeight = 1 - float inputs:translucency = 0.123 ( + color3f inputs:transmissionColor = (0.25, 0.5, 1) + float inputs:transmissionDepth = 111 + float inputs:transmissionWeight = 0.123 + float inputs:volumeThickness = 0.987 ( customData = { dictionary range = { double max = 1 @@ -110,103 +124,144 @@ def Xform "Scene" def NodeGraph "OpenPBR" { + def Shader "AmbientOcclusionAsColor" + { + uniform token info:id = "ND_convert_float_color3" + float inputs:in.connect = + color3f outputs:out + } + + def Shader "AmbientOcclusionBaseColor" + { + uniform token info:id = "ND_mix_color3" + color3f inputs:bg.connect = + color3f inputs:fg.connect = + float inputs:mix = 0 + color3f outputs:out + } + def Shader "OpenPBR" { + custom bool clearcoatModelsTransmissionTint = 1 uniform token info:id = "ND_open_pbr_surface_surfaceshader" - color3f inputs:base_color.connect = + float inputs:anisotropyAngle.connect = + 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 = + float inputs:coatSpecularLevel.connect = + float inputs:displacement.connect = color3f inputs:emission_color.connect = float inputs:emission_luminance.connect = - color3f inputs:fuzz_color.connect = - float inputs:fuzz_roughness.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_roughness.connect = + float inputs:specular_roughness_anisotropy.connect = float inputs:specular_weight.connect = - color3f inputs:subsurface_color.connect = - float inputs:subsurface_radius.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 = + color3f inputs:transmission_color.connect = + float inputs:transmission_depth.connect = + float inputs:transmission_weight.connect = + float inputs:volumeThickness.connect = + custom float normalScale = 0.5 + custom float opacityThreshold = 0.75 token outputs:out + custom bool unlit = 1 + custom bool useSpecularWorkflow = 1 } } } - def Material "TextureTestMaterial" + def Material "TextureTestMaterial" ( + displayName = "Texture Test Material" + ) { - 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@ + asset inputs:ambientOcclusionTexture = @textures/occlusion.png@ ( + colorSpace = "raw" + ) + asset inputs:baseColorTexture = @textures/color.png@ ( + colorSpace = "srgb_texture" + ) + asset inputs:coatNormalTexture = @textures/normal.png@ ( + colorSpace = "raw" + ) + asset inputs:coatOpacityTexture = @textures/color.png@ ( + colorSpace = "raw" + ) + float inputs:emissionLuminance = 1000 + asset inputs:emissiveTexture = @textures/color.png@ ( + colorSpace = "srgb_texture" + ) + asset inputs:normalTexture = @textures/normal.png@ ( + colorSpace = "raw" + ) + asset inputs:specularRoughnessTexture = @textures/greyscale.png@ ( + colorSpace = "raw" + ) token outputs:mtlx:surface.connect = def NodeGraph "OpenPBR" { def Shader "texCoordReader" { - uniform token info:id = "ND_texcoord_vector2" + uniform token info:id = "ND_geompropvalue_vector2" + string inputs:geomprop = "st" float2 outputs:out } def Shader "base_color" { - uniform token info:id = "ND_image_color3" + uniform token info:id = "ND_UsdUVTexture_23" 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 + float2 inputs:st.connect = + string inputs:wrapS = "periodic" + string inputs:wrapT = "periodic" + color3f outputs:rgb } - def Shader "specular_roughness" + def Shader "specular_roughness_uv_transform" { - uniform token info:id = "ND_image_vector4" - asset inputs:file.connect = + 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 = - string inputs:uaddressmode = "periodic" - string inputs:vaddressmode = "periodic" - float4 outputs:out + float2 outputs:out } - def Shader "specular_roughness_to_float" + def Shader "specular_roughness" { - uniform token info:id = "ND_separate4_vector4" - float4 inputs:in.connect = - float outputs:outx + uniform token info:id = "ND_UsdUVTexture_23" + float4 inputs:bias = (0.1, 0, 0, 0) + asset inputs:file.connect = + float4 inputs:scale = (0.55, 1, 1, 1) + float2 inputs:st.connect = + string inputs:wrapS = "constant" + string inputs:wrapT = "constant" + float outputs:r } def Shader "coat_weight" { - uniform token info:id = "ND_image_vector4" + uniform token info:id = "ND_UsdUVTexture_23" 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 + float2 inputs:st.connect = + string inputs:wrapS = "periodic" + string inputs:wrapT = "periodic" + float outputs:g } def Shader "emission_color_uv_transform" @@ -221,92 +276,117 @@ def Xform "Scene" def Shader "emission_color" { - uniform token info:id = "ND_image_color3" + uniform token info:id = "ND_UsdUVTexture_23" + float4 inputs:bias = (0.1, 0.2, 0.3, 0) 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 + float4 inputs:scale = (1, 2, 0.5, 1) + float2 inputs:st.connect = + string inputs:wrapS = "clamp" + string inputs:wrapT = "mirror" + color3f outputs:rgb } - 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" + def Shader "geometry_normal" { - uniform token info:id = "ND_add_color3" - color3f inputs:in1 = (0.1, 0.2, 0.3) - color3f inputs:in2.connect = - color3f outputs:out + uniform token info:id = "ND_UsdUVTexture_23" + float4 inputs:bias = (0.125, 0.125, 0.125, 0) + asset inputs:file.connect = + float4 inputs:scale = (0.75, 0.75, 0.75, 0.75) + float2 inputs:st.connect = + string inputs:wrapS = "periodic" + string inputs:wrapT = "periodic" + color3f outputs:rgb } - def Shader "geometry_normal" + def Shader "geometry_normal_as_vector" { - uniform token info:id = "ND_image_vector3" - asset inputs:file.connect = - float2 inputs:texcoord.connect = - string inputs:uaddressmode = "periodic" - string inputs:vaddressmode = "periodic" + uniform token info:id = "ND_convert_color3_vector3" + color3f inputs:in.connect = float3 outputs:out } def Shader "geometry_normal_to_world_space" { uniform token info:id = "ND_normalmap" - float3 inputs:in.connect = + float3 inputs:in.connect = float3 outputs:out } def Shader "geometry_coat_normal" { - uniform token info:id = "ND_image_vector3" + uniform token info:id = "ND_UsdUVTexture_23" + float4 inputs:bias = (0, 1, 0, 0) asset inputs:file.connect = - float2 inputs:texcoord.connect = - string inputs:uaddressmode = "periodic" - string inputs:vaddressmode = "periodic" + float4 inputs:scale = (1, -1, 1, 1) + float2 inputs:st.connect = + string inputs:wrapS = "periodic" + string inputs:wrapT = "periodic" + color3f outputs:rgb + } + + def Shader "geometry_coat_normal_as_vector" + { + uniform token info:id = "ND_convert_color3_vector3" + color3f inputs:in.connect = float3 outputs:out } def Shader "geometry_coat_normal_to_world_space" { uniform token info:id = "ND_normalmap" - float3 inputs:in.connect = + float3 inputs:in.connect = float3 outputs:out } + def Shader "occlusion" + { + uniform token info:id = "ND_UsdUVTexture_23" + asset inputs:file.connect = + float2 inputs:st.connect = + string inputs:wrapS = "periodic" + string inputs:wrapT = "periodic" + float outputs:r + } + + def Shader "AmbientOcclusionAsColor" + { + uniform token info:id = "ND_convert_float_color3" + float inputs:in.connect = + color3f outputs:out + } + + def Shader "AmbientOcclusionBaseColor" + { + uniform token info:id = "ND_mix_color3" + color3f inputs:bg.connect = + color3f inputs:fg.connect = + float inputs:mix = 0 + color3f 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 = + 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 = + float inputs:specular_roughness.connect = token outputs:out } } } - def Material "TransmissionTestMaterial" + def Material "TransmissionTestMaterial" ( + displayName = "Transmission Test Material" + ) { - float inputs:translucency = 0.543 ( - customData = { - dictionary range = { - double max = 1 - double min = 0 - } - } - ) + float inputs:transmissionWeight = 0.543 token outputs:mtlx:surface.connect = def NodeGraph "OpenPBR" @@ -314,10 +394,11 @@ def Xform "Scene" def Shader "OpenPBR" { uniform token info:id = "ND_open_pbr_surface_surfaceshader" - float inputs:transmission_weight.connect = + float inputs:transmission_weight.connect = token outputs:out } } } } } + diff --git a/utils/tests/data/baseline_writeUsdPreviewSurface.usda b/utils/tests/data/baseline_writeUsdPreviewSurface.usda index 1028fe3a..72a16e05 100644 --- a/utils/tests/data/baseline_writeUsdPreviewSurface.usda +++ b/utils/tests/data/baseline_writeUsdPreviewSurface.usda @@ -9,7 +9,9 @@ def Xform "Scene" { def "Materials" { - def Material "GeneralTestMaterial" + def Material "GeneralTestMaterial" ( + displayName = "General Test Material" + ) { float inputs:ambientOcclusion = 0.01 ( customData = { @@ -19,7 +21,7 @@ def Xform "Scene" } } ) - color3f inputs:baseColor = (1, 2, 3) + color3f inputs:baseColor = (1, 0.5, 0.25) float inputs:coatOpacity = 0.55 ( customData = { dictionary range = { @@ -28,7 +30,7 @@ def Xform "Scene" } } ) - float inputs:coatRoughness = 0.66 ( + float inputs:coatRoughness = 0.44 ( customData = { dictionary range = { double max = 1 @@ -36,7 +38,7 @@ def Xform "Scene" } } ) - color3f inputs:emissive = (1, 2, 3) + color3f inputs:emissive = (1, 0.5, 0.25) float inputs:height = 1.23 ( customData = { dictionary range = { @@ -61,7 +63,7 @@ def Xform "Scene" } } ) - normal3f inputs:normal = (0.33, 0.33, 0.33) + normal3f inputs:normal = (0.5, 0.5, 0.5) float inputs:opacity = 0.8 ( customData = { dictionary range = { @@ -78,7 +80,7 @@ def Xform "Scene" } } ) - float inputs:roughness = 0.44 ( + float inputs:roughness = 0.66 ( customData = { dictionary range = { double max = 1 @@ -123,13 +125,28 @@ def Xform "Scene" } } - def Material "TextureTestMaterial" + def Material "TextureTestMaterial" ( + displayName = "Texture Test Material" + ) { - 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@ + asset inputs:ambientOcclusionTexture = @textures/occlusion.png@ ( + colorSpace = "raw" + ) + asset inputs:baseColorTexture = @textures/color.png@ ( + colorSpace = "srgb_texture" + ) + asset inputs:coatOpacityTexture = @textures/color.png@ ( + colorSpace = "raw" + ) + asset inputs:emissiveTexture = @textures/color.png@ ( + colorSpace = "srgb_texture" + ) + asset inputs:normalTexture = @textures/normal.png@ ( + colorSpace = "raw" + ) + asset inputs:roughnessTexture = @textures/greyscale.png@ ( + colorSpace = "raw" + ) token outputs:displacement.connect = token outputs:surface.connect = @@ -145,6 +162,7 @@ def Xform "Scene" def Shader "diffuseColor" { uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/color.png@ asset inputs:file.connect = token inputs:sourceColorSpace = "sRGB" float2 inputs:st.connect = @@ -165,6 +183,7 @@ def Xform "Scene" { uniform token info:id = "UsdUVTexture" float4 inputs:bias = (0.1, 0.2, 0.3, 0) + asset inputs:file = @textures/color.png@ asset inputs:file.connect = float4 inputs:scale = (1, 2, 0.5, 1) token inputs:sourceColorSpace = "sRGB" @@ -174,19 +193,36 @@ def Xform "Scene" float3 outputs:rgb } + def Shader "roughness_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 "roughness" { uniform token info:id = "UsdUVTexture" + float4 inputs:bias = (0.1, 0, 0, 0) + asset inputs:file = @textures/greyscale.png@ asset inputs:file.connect = + float4 inputs:scale = (0.55, 1, 1, 1) token inputs:sourceColorSpace = "raw" - float2 inputs:st.connect = + float2 inputs:st.connect = + token inputs:wrapS = "black" + token inputs:wrapT = "black" float outputs:r } def Shader "clearcoat" { uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/color.png@ asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" float2 inputs:st.connect = float outputs:g } @@ -194,12 +230,25 @@ def Xform "Scene" def Shader "normal" { uniform token info:id = "UsdUVTexture" + float4 inputs:bias = (-0.75, -0.75, -0.75, 0) + asset inputs:file = @textures/normal.png@ asset inputs:file.connect = + float4 inputs:scale = (1.5, 1.5, 1.5, 0.75) token inputs:sourceColorSpace = "raw" float2 inputs:st.connect = float3 outputs:rgb } + def Shader "occlusion" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/occlusion.png@ + asset inputs:file.connect = + token inputs:sourceColorSpace = "raw" + float2 inputs:st.connect = + float outputs:r + } + def Shader "UsdPreviewSurface" { uniform token info:id = "UsdPreviewSurface" @@ -207,6 +256,7 @@ def Xform "Scene" color3f inputs:diffuseColor.connect = color3f inputs:emissiveColor.connect = normal3f inputs:normal.connect = + float inputs:occlusion.connect = float inputs:roughness.connect = token outputs:displacement token outputs:surface @@ -214,7 +264,9 @@ def Xform "Scene" } } - def Material "TransmissionTestMaterial" + def Material "TransmissionTestMaterial" ( + displayName = "Transmission Test Material" + ) { float inputs:opacity = 0.45700002 ( customData = { diff --git a/utils/tests/data/test_invalidNetworks.usda b/utils/tests/data/test_invalidNetworks.usda new file mode 100644 index 00000000..146fb5b4 --- /dev/null +++ b/utils/tests/data/test_invalidNetworks.usda @@ -0,0 +1,452 @@ +#usda 1.0 +( + defaultPrim = "Scene" +) + +def Xform "Scene" +{ + def "Materials" + { + # Please note: only one defect per network, since a single issues can mask the detection + # of other issues + + # ------------------------------------------------------------------------------------------ + # UsdPreviewSurface & ASM networks + # ------------------------------------------------------------------------------------------ + + def Material "MissingTexCoordReader1" + { + token outputs:surface.connect = + + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = <../diffuseColor.outputs:rgb> + color3f inputs:emissiveColor.connect = <../emissiveColor.outputs:rgb> + token outputs:surface + } + + def Shader "diffuseColor" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/color.png@ + # XXX No inputs:st + # float2 inputs:st.connect = ... + float3 outputs:rgb + } + } + + def Material "MissingTexCoordReader2" + { + token outputs:surface.connect = + + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = <../diffuseColor.outputs:rgb> + token outputs:surface + } + + def Shader "diffuseColor" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/color.png@ + # XXX Invalid inputs:st + float2 inputs:st.connect = + float3 outputs:rgb + } + } + + def Material "InvalidSurfaceInput1" + { + token outputs:surface.connect = + + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = <../texCoordReader.outputs:result> + token outputs:surface + } + + # XXX connecting a color3f input directly to a float2 output on a UsdPrimvarReader_float2 + # node is invalid + def Shader "texCoordReader" + { + uniform token info:id = "UsdPrimvarReader_float2" + string inputs:varname = "st" + float2 outputs:result + } + } + + def Material "InvalidSurfaceInput2" + { + token outputs:surface.connect = + + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = <../stTransform.outputs:result> + token outputs:surface + } + + # XXX connecting a color3f input directly to a float2 output on a UsdTransform2d node + # is invalid + def Shader "stTransform" + { + uniform token info:id = "UsdTransform2d" + string inputs:varname = "st" + float2 outputs:result + } + } + + def Material "MultipleTexCoordTransform" + { + token outputs:surface.connect = + + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = <../diffuseColor.outputs:rgb> + token outputs:surface + } + + def Shader "diffuseColor" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/color.png@ + float2 inputs:st.connect = <../diffuseColor_stTransform.outputs:result> + float3 outputs:rgb + } + + def Shader "diffuseColor_stTransform" + { + uniform token info:id = "UsdTransform2d" + float2 inputs:in.connect = <../diffuseColor_stTransform2.outputs:result> + float2 outputs:result + } + + # XXX a second transform is not supported + def Shader "diffuseColor_stTransform2" + { + uniform token info:id = "UsdTransform2d" + float2 inputs:in.connect = <../texCoordReader.outputs:result> + float2 outputs:result + } + + def Shader "texCoordReader" + { + uniform token info:id = "UsdPrimvarReader_float2" + string inputs:varname = "st" + float2 outputs:result + } + } + + def Material "TexCoordLoop" + { + token outputs:surface.connect = + + def Shader "UsdPreviewSurface" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor.connect = <../diffuseColor.outputs:rgb> + token outputs:surface + } + + def Shader "diffuseColor" + { + uniform token info:id = "UsdUVTexture" + asset inputs:file = @textures/color.png@ + float2 inputs:st.connect = <../diffuseColor_stTransform.outputs:result> + float3 outputs:rgb + } + + def Shader "diffuseColor_stTransform" + { + uniform token info:id = "UsdTransform2d" + # XXX This loop should not throw the code for a loop + float2 inputs:in.connect = <../diffuseColor_stTransform.outputs:result> + float2 outputs:result + } + } + + # ------------------------------------------------------------------------------------------ + # OpenPBR/MaterialX networks + # ------------------------------------------------------------------------------------------ + + def Material "MissingTexCoordReader3" + { + token outputs:mtlx:surface.connect = + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = <../base_color.outputs:out> + token outputs:out + } + + def Shader "base_color" + { + uniform token info:id = "ND_image_color3" + asset inputs:file = @textures/color.png@ + # XXX No inputs:st + # float2 inputs:texcoord.connect = ... + color3f outputs:out + } + } + + def Material "MissingTexCoordReader4" + { + token outputs:mtlx:surface.connect = + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = <../base_color.outputs:out> + token outputs:out + } + + def Shader "base_color" + { + uniform token info:id = "ND_image_color3" + asset inputs:file = @textures/color.png@ + float2 inputs:texcoord.connect = <../base_color_uv_transform.outputs:out> + color3f outputs:out + } + + def Shader "base_color_uv_transform" + { + uniform token info:id = "ND_place2d_vector2" + float2 inputs:offset = (0.12, 3.45) + # XXX No inputs:st + # float2 inputs:texcoord.connect = ... + float2 outputs:out + } + } + + def Material "InvalidSurfaceInput3" + { + token outputs:mtlx:surface.connect = + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = <../subtract_color.outputs:out> + token outputs:out + } + + def Shader "subtract_color" + { + # XXX ND_subtract_color3 is an unsupported node + uniform token info:id = "ND_subtract_color3" + color3f inputs:in1 = (1, 2, 0.5) + color3f inputs:in2 = (.5, 2, 1) + color3f outputs:out + } + } + + def Material "IncorrectScaleInputs" + { + token outputs:mtlx:surface.connect = + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = <../base_color_scale.outputs:out> + token outputs:out + } + + def Shader "base_color_scale" + { + uniform token info:id = "ND_multiply_color3" + # XXX The continuing connection has to be on `in2` and not `in1` + color3f inputs:in2 = (1, 2, 0.5) + color3f inputs:in1.connect = <../base_color_bias.outputs:out> + color3f outputs:out + } + + def Shader "base_color" + { + uniform token info:id = "ND_image_color3" + asset inputs:file = @textures/color.png@ + float2 inputs:texcoord.connect = <../texCoordReader.outputs:out> + color3f outputs:out + } + + def Shader "texCoordReader" + { + uniform token info:id = "ND_texcoord_vector2" + float2 outputs:out + } + } + + def Material "WrongScaleAndBiasOrder" + { + token outputs:mtlx:surface.connect = + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = <../base_color_scale.outputs:out> + token outputs:out + } + + # XXX The order is wrong, from the direction of the OpenPBR surface, the bias node needs + # to come first and then the scale node. Having just the scale node is fine, but having + # it be followed by a bias node is not correct. + def Shader "base_color_scale" + { + uniform token info:id = "ND_multiply_color3" + color3f inputs:in1 = (1, 2, 0.5) + color3f inputs:in2.connect = <../base_color_bias.outputs:out> + color3f outputs:out + } + + def Shader "base_color_bias" + { + uniform token info:id = "ND_add_color3" + color3f inputs:in1 = (0.1, 0.2, 0.3) + color3f inputs:in2.connect = <../base_color.outputs:out> + color3f outputs:out + } + + def Shader "base_color" + { + uniform token info:id = "ND_image_color3" + asset inputs:file = @textures/color.png@ + float2 inputs:texcoord.connect = <../texCoordReader.outputs:out> + color3f outputs:out + } + + def Shader "texCoordReader" + { + uniform token info:id = "ND_texcoord_vector2" + float2 outputs:out + } + } + + # Same as WrongScaleAndBiasOrder, but for a float input + def Material "WrongScaleAndBiasOrder2" + { + token outputs:mtlx:surface.connect = + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + float inputs:specular_roughness.connect = <../specular_roughness_scale.outputs:out> + token outputs:out + } + + # XXX The order is wrong, from the direction of the OpenPBR surface, the bias node needs + # to come first and then the scale node. Having just the scale node is fine, but having + # it be followed by a bias node is not correct. + def Shader "specular_roughness_scale" + { + uniform token info:id = "ND_multiply_float" + float inputs:in1 = 2.0 + float inputs:in2.connect = <../specular_roughness_bias.outputs:out> + float outputs:out + } + + def Shader "specular_roughness_bias" + { + uniform token info:id = "ND_add_float" + float inputs:in1 = -1.0 + float inputs:in2.connect = <../specular_roughness_to_float.outputs:outx> + float outputs:out + } + + def Shader "specular_roughness_to_float" + { + uniform token info:id = "ND_separate4_vector4" + float4 inputs:in.connect = <../specular_roughness.outputs:out> + float outputs:outy + } + + def Shader "specular_roughness" + { + uniform token info:id = "ND_image_vector4" + asset inputs:file = @textures/roughness.png@ + float2 inputs:texcoord.connect = <../texCoordReader.outputs:out> + float4 outputs:out + } + + def Shader "texCoordReader" + { + uniform token info:id = "ND_texcoord_vector2" + float2 outputs:out + } + } + + def Material "MultipleTexCoordTransform2" + { + token outputs:mtlx:surface.connect = + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = <../base_color.outputs:out> + token outputs:out + } + + def Shader "base_color" + { + uniform token info:id = "ND_image_color3" + asset inputs:file = @textures/color.png@ + float2 inputs:texcoord.connect = <../base_color_uv_transform.outputs:out> + color3f outputs:out + } + + def Shader "base_color_uv_transform" + { + uniform token info:id = "ND_place2d_vector2" + float2 inputs:texcoord.connect = <../base_color_uv_transform2.outputs:out> + float2 outputs:out + } + + # XXX a second transform is not supported + def Shader "base_color_uv_transform2" + { + uniform token info:id = "ND_place2d_vector2" + float2 inputs:texcoord.connect = <../texCoordReader.outputs:out> + float2 outputs:out + } + + def Shader "texCoordReader" + { + uniform token info:id = "ND_texcoord_vector2" + float2 outputs:out + } + } + + def Material "TexCoordLoop2" + { + token outputs:mtlx:surface.connect = + + def Shader "OpenPBR" + { + uniform token info:id = "ND_open_pbr_surface_surfaceshader" + color3f inputs:base_color.connect = <../base_color.outputs:out> + token outputs:out + } + + def Shader "base_color" + { + uniform token info:id = "ND_image_color3" + asset inputs:file = @textures/color.png@ + float2 inputs:texcoord.connect = <../base_color_uv_transform.outputs:out> + color3f outputs:out + } + + def Shader "base_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) + # XXX This loop should not throw the code for a loop + float2 inputs:texcoord.connect = <../base_color_uv_transform.outputs:out> + float2 outputs:out + } + } + } +} diff --git a/utils/tests/tests.cpp b/utils/tests/tests.cpp index 3a090ffd..602530c1 100644 --- a/utils/tests/tests.cpp +++ b/utils/tests/tests.cpp @@ -12,6 +12,8 @@ governing permissions and limitations under the License. #include #include +#include +#include #include #include @@ -21,22 +23,20 @@ governing permissions and limitations under the License. #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 + +// Set to true to (re-)generate USDA baselines, false to compare against them +constexpr bool UPDATE_USDA_BASELINES = false; + +// Set to true to dump the output next to the baselines if a comparison fails +constexpr bool DUMP_FAILED_USDA_OUTPUT = false; + +// Macro to compare against or generate a USDA baseline +#define ASSERT_USDA(usdaLayer, baselinePath) \ + ASSERT_TRUE(assertUsda(usdaLayer, baselinePath, UPDATE_USDA_BASELINES, DUMP_FAILED_USDA_OUTPUT)) + +// if running locally from VSCode, replace the empty assetDir with the commented out line below +std::string assetDir = ""; +// std::string assetDir = std::filesystem::current_path().string() + "/utils/tests/"; PXR_NAMESPACE_USING_DIRECTIVE @@ -45,80 +45,38 @@ using namespace adobe::usd; // This class is here to expose the protected SdfFileFormat::_SetLayerData function to this test class TestFileFormat : public SdfFileFormat { - public: +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 +Material& fillGeneralTestMaterial(UsdData& data) { Material& m = data.addMaterial().second; m.name = "GeneralTestMaterial"; + m.displayName = "General Test Material"; + // 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.diffuseColor = Input{ VtValue(GfVec3f(1.0f, 0.5f, 0.25f)) }; + m.emissiveColor = Input{ VtValue(GfVec3f(1.0f, 0.5f, 0.25f)) }; 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.normal = Input{ VtValue(GfVec3f(0.5f, 0.5f, 0.5f)) }; + m.normalScale = Input{ VtValue(0.5f) }; m.metallic = Input{ VtValue(0.22f) }; - m.roughness = Input{ VtValue(0.44f) }; + m.roughness = Input{ VtValue(0.66f) }; m.clearcoat = Input{ VtValue(0.55f) }; m.clearcoatColor = Input{ VtValue(GfVec3f(1.0f, 1.0f, 0.0f)) }; - m.clearcoatRoughness = Input{ VtValue(0.66f) }; + m.clearcoatRoughness = Input{ VtValue(0.44f) }; 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.sheenRoughness = Input{ VtValue(0.5f) }; m.anisotropyLevel = Input{ VtValue(0.321f) }; m.anisotropyAngle = Input{ VtValue(0.777f) }; m.opacity = Input{ VtValue(0.8f) }; @@ -132,6 +90,9 @@ fillGeneralTestMaterial(UsdData& data) 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)) }; + m.clearcoatModelsTransmissionTint = true; + m.isUnlit = true; + return m; } void @@ -139,20 +100,26 @@ fillTextureTestMaterial(UsdData& data) { // Add some images to use auto [colorId, colorImage] = data.addImage(); - colorImage.name = "color.png"; + colorImage.name = "color"; colorImage.uri = "textures/color.png"; colorImage.format = ImageFormat::ImageFormatPng; auto [normalId, normalImage] = data.addImage(); - normalImage.name = "normal.png"; + normalImage.name = "normal"; normalImage.uri = "textures/normal.png"; normalImage.format = ImageFormat::ImageFormatPng; auto [greyscaleId, greyscaleImage] = data.addImage(); - greyscaleImage.name = "greyscale.png"; + greyscaleImage.name = "greyscale"; greyscaleImage.uri = "textures/greyscale.png"; greyscaleImage.format = ImageFormat::ImageFormatPng; + auto [occlusionId, occlusionImage] = data.addImage(); + occlusionImage.name = "occlusion"; + occlusionImage.uri = "textures/occlusion.png"; + occlusionImage.format = ImageFormat::ImageFormatPng; Material& m = data.addMaterial().second; m.name = "TextureTestMaterial"; + m.displayName = "Texture Test Material"; + // Set different inputs to specific texture setups // Color textures @@ -166,7 +133,7 @@ fillTextureTestMaterial(UsdData& data) // 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.scale = GfVec4f(1.0f, 2.0f, 0.5f, 1.0f); input.bias = GfVec4f(0.1f, 0.2f, 0.3f, 0.0f); input.uvRotation = 15.0f; input.uvScale = GfVec2f(1.5f, 0.75f); @@ -180,8 +147,16 @@ fillTextureTestMaterial(UsdData& data) input.image = normalId; input.channel = AdobeTokens->rgb; input.colorspace = AdobeTokens->raw; + // GLTF files sometimes scale the normals + const float nmScale = 0.75f; + input.scale = GfVec4f(2.0f, 2.0f, 2.0f, 1.0f) * nmScale; + input.bias = GfVec4f(-1.0f, -1.0f, -1.0f, 0.0f) * nmScale; m.normal = input; m.clearcoatNormal = input; + // Put DirectX convention decoding values on the clearcoat normals + m.clearcoatNormal.scale = GfVec4f(2.0f, -2.0f, 2.0f, 1.0f); + m.clearcoatNormal.bias = GfVec4f(-1.0f, 1.0f, -1.0f, 0.0f); + // XXX test normal map scale the way GLTF can create } // Greyscale maps @@ -190,14 +165,34 @@ fillTextureTestMaterial(UsdData& data) input.image = greyscaleId; input.channel = AdobeTokens->r; input.colorspace = AdobeTokens->raw; + + // Wrap mode, scale & bias, UV transform for the single channel case + input.wrapS = AdobeTokens->black; + input.wrapT = AdobeTokens->black; + input.scale = GfVec4f(0.55f, 1.0f, 1.0f, 1.0f); + input.bias = GfVec4f(0.1f, 0.0f, 0.0f, 0.0f); + input.uvRotation = 15.0f; + input.uvScale = GfVec2f(1.5f, 0.75f); + input.uvTranslation = GfVec2f(0.12f, 3.45f); m.roughness = input; } + // Occlusion maps + { + Input input; + input.image = occlusionId; + input.channel = AdobeTokens->r; + input.colorspace = AdobeTokens->raw; + + m.occlusion = input; + } + // Single channel from RGB map { Input input; input.image = colorId; input.channel = AdobeTokens->g; + input.colorspace = AdobeTokens->raw; m.clearcoat = input; } } @@ -207,12 +202,150 @@ fillTransmissionMaterial(UsdData& data) { Material& m = data.addMaterial().second; m.name = "TransmissionTestMaterial"; + m.displayName = "Transmission Test Material"; - // Set transmission, but not opacity. For UsdPreviewSurface this should be mapped as an inverse + // Set transmission, but NOT opacity. For UsdPreviewSurface this should be mapped as an inverse // to opacity m.transmission = Input{ VtValue(0.543f) }; } +void +fillTransmissionMaterialAsUsdPreviewSurfaceBaseline(UsdData& data) +{ + Material& m = data.addMaterial().second; + m.name = "TransmissionTestMaterial"; + m.displayName = "Transmission Test Material"; + + // Set opacity to the inverse of the above transmission, since this is how this should be read + // from a UsdPreviewSurface representation + m.opacity = Input{ VtValue(1.0f - 0.543f) }; +} + +void +compareInputs(const std::string& inputName, + const Input& input, + const Input& baseline, + const UsdData& data, + const UsdData& baselineData) +{ + EXPECT_EQ(input.value, baseline.value) << inputName; + // Comparing the `image` member is a bit involved, since it is an index into the owning UsdData + // Either both image indices are invalid or both are valid + EXPECT_EQ(input.image == -1, baseline.image == -1) << inputName; + if (input.image != -1 && baseline.image != -1) { + ASSERT_TRUE(input.image < data.images.size()) << inputName; + ASSERT_TRUE(baseline.image < baselineData.images.size()) << inputName; + const ImageAsset& image = data.images[input.image]; + const ImageAsset& baselineImage = baselineData.images[baseline.image]; + EXPECT_EQ(image.name, baselineImage.name) << inputName; + EXPECT_EQ(image.uri, baselineImage.uri) << inputName; + EXPECT_EQ(image.format, baselineImage.format) << inputName; + } + EXPECT_EQ(input.uvIndex, baseline.uvIndex) << inputName; + EXPECT_EQ(input.channel, baseline.channel) << inputName; + EXPECT_EQ(input.wrapS, baseline.wrapS) << inputName; + EXPECT_EQ(input.wrapT, baseline.wrapT) << inputName; + EXPECT_EQ(input.minFilter, baseline.minFilter) << inputName; + EXPECT_EQ(input.magFilter, baseline.magFilter) << inputName; + EXPECT_EQ(input.colorspace, baseline.colorspace) << inputName; + EXPECT_EQ(input.scale, baseline.scale) << inputName; + EXPECT_EQ(input.bias, baseline.bias) << inputName; + EXPECT_EQ(input.uvRotation, baseline.uvRotation) << inputName; + EXPECT_EQ(input.uvScale, baseline.uvScale) << inputName; + EXPECT_EQ(input.uvTranslation, baseline.uvTranslation) << inputName; +} + +void +compareMaterials(const Material& material, + const Material& baseline, + const UsdData& data, + const UsdData& baselineData) +{ + EXPECT_EQ(material.name, baseline.name); + EXPECT_EQ(material.displayName, baseline.displayName); + + EXPECT_EQ(material.clearcoatModelsTransmissionTint, baseline.clearcoatModelsTransmissionTint); + EXPECT_EQ(material.isUnlit, baseline.isUnlit); + +#define COMP_INPUT(x) compareInputs(#x, material.x, baseline.x, data, baselineData); + COMP_INPUT(useSpecularWorkflow) + COMP_INPUT(diffuseColor) + COMP_INPUT(emissiveColor) + COMP_INPUT(specularLevel) + COMP_INPUT(specularColor) + COMP_INPUT(normal) + COMP_INPUT(normalScale) + COMP_INPUT(metallic) + COMP_INPUT(roughness) + COMP_INPUT(clearcoat) + COMP_INPUT(clearcoatColor) + COMP_INPUT(clearcoatRoughness) + COMP_INPUT(clearcoatIor) + COMP_INPUT(clearcoatSpecular) + COMP_INPUT(clearcoatNormal) + COMP_INPUT(sheenColor) + COMP_INPUT(sheenRoughness) + COMP_INPUT(anisotropyLevel) + COMP_INPUT(anisotropyAngle) + COMP_INPUT(opacity) + COMP_INPUT(opacityThreshold) + COMP_INPUT(displacement) + COMP_INPUT(occlusion) + COMP_INPUT(ior) + COMP_INPUT(transmission) + COMP_INPUT(volumeThickness) + COMP_INPUT(absorptionDistance) + COMP_INPUT(absorptionColor) + COMP_INPUT(scatteringDistance) + COMP_INPUT(scatteringColor) +#undef COMP_INPUT +} + +TEST(FileFormatUtilsTests, materialStructConversions) +{ + // Note, only Material -> OpenPbrMaterial -> Material needs to be preserving all information + // OpenPbrMaterial can carry additional information that would not correctly round trip + + { + UsdData data; + fillGeneralTestMaterial(data); + ASSERT_EQ(data.materials.size(), 1); + Material& baselineMaterial = data.materials[0]; + + OpenPbrMaterial openPbrMaterial = + mapMaterialStructToOpenPbrMaterialStruct(baselineMaterial); + Material outputMaterial = mapOpenPbrMaterialStructToMaterialStruct(openPbrMaterial); + + compareMaterials(outputMaterial, baselineMaterial, data, data); + } + + { + UsdData data; + fillTextureTestMaterial(data); + ASSERT_EQ(data.materials.size(), 1); + Material& baselineMaterial = data.materials[0]; + + OpenPbrMaterial openPbrMaterial = + mapMaterialStructToOpenPbrMaterialStruct(baselineMaterial); + Material outputMaterial = mapOpenPbrMaterialStructToMaterialStruct(openPbrMaterial); + + compareMaterials(outputMaterial, baselineMaterial, data, data); + } + + { + UsdData data; + fillTransmissionMaterial(data); + ASSERT_EQ(data.materials.size(), 1); + Material& baselineMaterial = data.materials[0]; + + OpenPbrMaterial openPbrMaterial = + mapMaterialStructToOpenPbrMaterialStruct(baselineMaterial); + Material outputMaterial = mapOpenPbrMaterialStructToMaterialStruct(openPbrMaterial); + + compareMaterials(outputMaterial, baselineMaterial, data, data); + } +} + TEST(FileFormatUtilsTests, writeUsdPreviewSurface) { SdfLayerRefPtr layer = SdfLayer::CreateAnonymous("Scene.usda"); @@ -234,7 +367,7 @@ TEST(FileFormatUtilsTests, writeUsdPreviewSurface) // be updated all the time layer->SetDocumentation(""); - ASSERT_USDA(layer, "data/baseline_writeUsdPreviewSurface.usda"); + ASSERT_USDA(layer, assetDir + "data/baseline_writeUsdPreviewSurface.usda"); } #ifdef USD_FILEFORMATS_ENABLE_ASM @@ -259,7 +392,7 @@ TEST(FileFormatUtilsTests, writeASM) // be updated all the time layer->SetDocumentation(""); - ASSERT_USDA(layer, "data/baseline_writeASM.usda"); + ASSERT_USDA(layer, assetDir + "data/baseline_writeASM.usda"); } #endif // USD_FILEFORMATS_ENABLE_ASM @@ -284,5 +417,146 @@ TEST(FileFormatUtilsTests, writeOpenPBR) // be updated all the time layer->SetDocumentation(""); - ASSERT_USDA(layer, "data/baseline_writeOpenPBR.usda"); -} \ No newline at end of file + ASSERT_USDA(layer, assetDir + "data/baseline_writeOpenPBR.usda"); +} + +const SdfPath generalTestMaterialPath("/Scene/Materials/GeneralTestMaterial"); +const SdfPath textureTestMaterialPath("/Scene/Materials/TextureTestMaterial"); +const SdfPath transmissionTestMaterialPath("/Scene/Materials/TransmissionTestMaterial"); + +void +defaultBaselineProcessor(Material&) +{} + +template +void +readAndCompareMaterial(const UsdStageRefPtr& stage, + const SdfPath& materialPath, + BaselineGenerator baselineGenerator, + BaselineProcessor baselineProcessor) +{ + UsdPrim materialPrim = stage->GetPrimAtPath(materialPath); + ASSERT_TRUE(materialPrim); + + UsdData usdData; + + ReadLayerOptions options; + ReadLayerContext ctx; + ctx.stage = stage; + ctx.usd = &usdData; + ctx.options = &options; + ctx.debugTag = "Test"; + ctx.warnAboutMissingAssets = false; + EXPECT_TRUE(readMaterial(ctx, materialPrim)); + + ASSERT_EQ(usdData.materials.size(), 1); + const Material& material = usdData.materials[0]; + + UsdData baselineData; + baselineGenerator(baselineData); + ASSERT_EQ(baselineData.materials.size(), 1); + Material& baselineMaterial = baselineData.materials[0]; + baselineProcessor(baselineMaterial); + + compareMaterials(material, baselineMaterial, usdData, baselineData); +} + +TEST(FileFormatUtilsTests, readUsdPreviewSurface) +{ + UsdStageRefPtr stage = UsdStage::Open(assetDir + "data/baseline_writeUsdPreviewSurface.usda"); + ASSERT_TRUE(stage); + + auto usdPreviewSurfaceBaselineProcessor = [](Material& baselineMaterial) { + // UsdPreviewSurface doesn't support a couple of inputs, so we clear them + baselineMaterial.specularLevel = {}; + baselineMaterial.normalScale = {}; + baselineMaterial.clearcoatColor = {}; + baselineMaterial.clearcoatIor = {}; + baselineMaterial.clearcoatSpecular = {}; + baselineMaterial.clearcoatNormal = {}; + baselineMaterial.sheenColor = {}; + baselineMaterial.sheenRoughness = {}; + baselineMaterial.anisotropyLevel = {}; + baselineMaterial.anisotropyAngle = {}; + baselineMaterial.transmission = {}; + baselineMaterial.volumeThickness = {}; + baselineMaterial.absorptionDistance = {}; + baselineMaterial.absorptionColor = {}; + baselineMaterial.scatteringDistance = {}; + baselineMaterial.scatteringColor = {}; + baselineMaterial.clearcoatModelsTransmissionTint = false; + baselineMaterial.isUnlit = false; + }; + readAndCompareMaterial( + stage, generalTestMaterialPath, fillGeneralTestMaterial, usdPreviewSurfaceBaselineProcessor); + + readAndCompareMaterial( + stage, textureTestMaterialPath, fillTextureTestMaterial, usdPreviewSurfaceBaselineProcessor); + + readAndCompareMaterial(stage, + transmissionTestMaterialPath, + fillTransmissionMaterialAsUsdPreviewSurfaceBaseline, + usdPreviewSurfaceBaselineProcessor); +} + +TEST(FileFormatUtilsTests, readASM) +{ + UsdStageRefPtr stage = UsdStage::Open(assetDir + "data/baseline_writeASM.usda"); + ASSERT_TRUE(stage); + + readAndCompareMaterial( + stage, generalTestMaterialPath, fillGeneralTestMaterial, defaultBaselineProcessor); + + readAndCompareMaterial( + stage, textureTestMaterialPath, fillTextureTestMaterial, defaultBaselineProcessor); + + readAndCompareMaterial( + stage, transmissionTestMaterialPath, fillTransmissionMaterial, defaultBaselineProcessor); +} + +TEST(FileFormatUtilsTests, readOpenPBR) +{ + UsdStageRefPtr stage = UsdStage::Open(assetDir + "data/baseline_writeOpenPBR.usda"); + ASSERT_TRUE(stage); + + readAndCompareMaterial( + stage, generalTestMaterialPath, fillGeneralTestMaterial, defaultBaselineProcessor); + + readAndCompareMaterial( + stage, textureTestMaterialPath, fillTextureTestMaterial, defaultBaselineProcessor); + + readAndCompareMaterial( + stage, transmissionTestMaterialPath, fillTransmissionMaterial, defaultBaselineProcessor); +} + +TEST(FileFormatUtilsTests, invalidNetworkReading) +{ + UsdStageRefPtr stage = UsdStage::Open(assetDir + "data/test_invalidNetworks.usda"); + ASSERT_TRUE(stage); + + UsdPrim materials = stage->GetPrimAtPath(SdfPath("/Scene/Materials")); + ASSERT_TRUE(materials); + + for (UsdPrim materialPrim : materials.GetChildren()) { + UsdShadeMaterial material(materialPrim); + ASSERT_TRUE(material); + + std::cout << "Reading bad network at " << materialPrim.GetPath().GetText() << std::endl; + + UsdData usdData; + + ReadLayerOptions options; + ReadLayerContext ctx; + ctx.stage = stage; + ctx.usd = &usdData; + ctx.options = &options; + ctx.debugTag = "Test"; + ctx.warnAboutMissingAssets = false; + // This material is bad and we expect a failure to read the network + EXPECT_FALSE(readMaterial(ctx, materialPrim)); + + // XXX This is worth debating, whether or not we should have a partial material in the scene + // We current continue with a partial material + ASSERT_EQ(usdData.materials.size(), 1); + } +} diff --git a/version b/version deleted file mode 100644 index 26aaba0e..00000000 --- a/version +++ /dev/null @@ -1 +0,0 @@ -1.2.0 diff --git a/version.json b/version.json new file mode 100644 index 00000000..6115de7f --- /dev/null +++ b/version.json @@ -0,0 +1,3 @@ +{ + "version": "2026.03.0" +}