├── .github └── workflows │ ├── build-usd.yml │ ├── build.yml │ ├── deploy-release-manual.yml │ ├── deploy-release-weekly.yml │ ├── deploy-release.yml │ ├── run-tests-manual.yml │ ├── run-tests-usd2403.yml │ ├── run-tests-usd2405.yml │ ├── run-tests-usd2408.yml │ ├── run-tests-usd2411.yml │ ├── run-tests-usd2502.yml │ └── run-tests.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── Finddraco.cmake ├── UsdDefaults.cmake └── gucConfig.cmake.in ├── docs ├── Ecosystem_Limitations.md └── Structure_Mapping.md ├── extern ├── CMakeLists.txt └── stb_image │ ├── LICENSE │ └── stb_image.h ├── preview_glTFSampleViewer.png ├── preview_hdStorm.png └── src ├── CMakeLists.txt ├── guc ├── CMakeLists.txt ├── license.h.in └── main.c └── libguc ├── CMakeLists.txt ├── include └── guc.h ├── plugInfo.json.in └── src ├── cgltf_util.cpp ├── cgltf_util.h ├── converter.cpp ├── converter.h ├── debugCodes.cpp ├── debugCodes.h ├── fileFormat.cpp ├── fileFormat.h ├── guc.cpp ├── image.cpp ├── image.h ├── materialx.cpp ├── materialx.h ├── mesh.cpp ├── mesh.h ├── naming.cpp ├── naming.h ├── usdpreviewsurface.cpp └── usdpreviewsurface.h /.github/workflows/build-usd.yml: -------------------------------------------------------------------------------- 1 | name: Build guc for USD 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | usd-version: 7 | required: true 8 | type: string 9 | build-config: 10 | required: true 11 | type: string 12 | extra-cmake-flags: 13 | required: false 14 | type: string 15 | upload-archive: 16 | required: false 17 | type: boolean 18 | default: true 19 | archive-name-suffix: 20 | required: false 21 | type: string 22 | 23 | jobs: 24 | build: 25 | name: Build ${{ matrix.os-family }} ${{ matrix.build-config }} 26 | 27 | strategy: 28 | matrix: 29 | include: 30 | - os-family: Linux 31 | image: ubuntu-20.04 32 | usd-download-url: "https://github.com/pablode/USD/releases/download/v${{ inputs.usd-version }}-ci-release/USD${{ inputs.usd-version }}_Linux_x64.tar.gz" 33 | usd-install-path: /home/runner/work/USD/USD/INSTALL 34 | archive-name: guc_USD${{ inputs.usd-version }}_Linux_x64 35 | 36 | - os-family: MacOS 37 | image: macos-14 38 | usd-download-url: "https://github.com/pablode/USD/releases/download/v${{ inputs.usd-version }}-ci-release/USD${{ inputs.usd-version }}_MacOS_ARM.tar.gz" 39 | usd-install-path: /Users/runner/work/USD/USD/INSTALL 40 | archive-name: guc_USD${{ inputs.usd-version }}_MacOS_ARM 41 | 42 | - os-family: Windows 43 | image: windows-2019 44 | usd-download-url: "https://github.com/pablode/USD/releases/download/v${{ inputs.usd-version }}-ci-release/USD${{ inputs.usd-version }}_Windows_x64.tar.gz" 45 | usd-install-path: C:/INSTALL 46 | archive-name: guc_USD${{ inputs.usd-version }}_Windows_x64 47 | 48 | uses: ./.github/workflows/build.yml 49 | with: 50 | image: ${{ matrix.image }} 51 | build-config: ${{ inputs.build-config }} 52 | usd-download-url: ${{ matrix.usd-download-url }} 53 | usd-install-path: ${{ matrix.usd-install-path }} 54 | upload-archive: ${{ inputs.upload-archive }} 55 | archive-name: ${{ matrix.archive-name }}${{ inputs.archive-name-suffix }} 56 | extra-cmake-flags: ${{ inputs.extra-cmake-flags }} 57 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build guc 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | image: 7 | required: true 8 | type: string 9 | build-config: 10 | required: true 11 | type: string 12 | # Must match CI installation path (see USD issue #1025) 13 | usd-download-url: 14 | required: true 15 | type: string 16 | usd-install-path: 17 | required: true 18 | type: string 19 | upload-archive: 20 | required: true 21 | type: boolean 22 | archive-name: 23 | required: true 24 | type: string 25 | extra-cmake-flags: 26 | required: false 27 | type: string 28 | 29 | defaults: 30 | run: 31 | shell: bash 32 | 33 | jobs: 34 | build: 35 | name: Build 36 | runs-on: ${{ inputs.image }} 37 | 38 | steps: 39 | - name: Check out repository 40 | uses: actions/checkout@v3 41 | with: 42 | submodules: recursive 43 | 44 | - name: Fetch USD binaries 45 | run: curl ${{ inputs.usd-download-url }} -L -v -o USD.tar.gz 46 | 47 | - name: Unpack USD binaries 48 | run: mkdir -p ${{ inputs.usd-install-path }} && tar -xvf USD.tar.gz -C ${{ inputs.usd-install-path }} 49 | 50 | - name: Create temporary folders 51 | run: mkdir BUILD INSTALL 52 | 53 | - name: Generate build system files using CMake 54 | working-directory: BUILD 55 | run: cmake .. -Dpxr_DIR=${{ inputs.usd-install-path }} -DCMAKE_BUILD_TYPE=${{ inputs.build-config }} -DGUC_BUILD_USDGLTF=ON ${{ inputs.extra-cmake-flags }} 56 | 57 | - name: Build guc 58 | working-directory: BUILD 59 | run: cmake --build . --config ${{ inputs.build-config }} -j 2 60 | 61 | - name: Install guc 62 | working-directory: BUILD 63 | run: | 64 | cmake --install . --config ${{ inputs.build-config }} --component guc --prefix "$PWD/../INSTALL" 65 | cmake --install . --config ${{ inputs.build-config }} --component libguc --prefix "$PWD/../INSTALL" 66 | cmake --install . --config ${{ inputs.build-config }} --component usdGlTF --prefix "$PWD/../INSTALL/plugin/usd" 67 | 68 | - name: Create archive 69 | if: ${{ inputs.upload-archive }} 70 | working-directory: INSTALL 71 | run: tar -zcvf ${{ inputs.archive-name }}.tar.gz * 72 | 73 | - name: Upload archive 74 | if: ${{ inputs.upload-archive }} 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: ${{ inputs.archive-name }} 78 | path: INSTALL/${{ inputs.archive-name }}.tar.gz 79 | retention-days: 1 80 | -------------------------------------------------------------------------------- /.github/workflows/deploy-release-manual.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Release (Manual) 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version-name: 7 | required: false 8 | type: string 9 | default: 'Version XXX' 10 | release-notes: 11 | required: false 12 | type: string 13 | default: 'TBD' 14 | 15 | jobs: 16 | deploy-release: 17 | name: Deploy Release 18 | uses: ./.github/workflows/deploy-release.yml 19 | with: 20 | version-name: ${{ inputs.version-name }} 21 | release-notes: ${{ inputs.release-notes }} 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy-release-weekly.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Release (Weekly) 2 | 3 | on: 4 | schedule: 5 | # Run every sunday at 16:00 UTC (6 PM CET) 6 | - cron: '0 16 * * 0' 7 | 8 | jobs: 9 | deploy-release: 10 | name: Deploy Release 11 | uses: ./.github/workflows/deploy-release.yml 12 | with: 13 | version-name: Weekly 14 | release-notes: 'Weekly build of the main branch.' 15 | tag-name: weekly 16 | publish-prerelease: true 17 | -------------------------------------------------------------------------------- /.github/workflows/deploy-release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Release 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | version-name: 7 | required: true 8 | type: string 9 | release-notes: 10 | required: false 11 | type: string 12 | default: 'TBD' 13 | tag-name: 14 | required: false 15 | type: string 16 | publish-prerelease: 17 | required: false 18 | default: false 19 | type: boolean 20 | 21 | jobs: 22 | build-2502: 23 | name: Build guc for USD v25.02 24 | uses: ./.github/workflows/build-usd.yml 25 | with: 26 | usd-version: 25.02 27 | build-config: Release 28 | 29 | build-2411: 30 | name: Build guc for USD v24.11 31 | uses: ./.github/workflows/build-usd.yml 32 | with: 33 | usd-version: 24.11 34 | build-config: Release 35 | 36 | build-2408: 37 | name: Build guc for USD v24.08 38 | uses: ./.github/workflows/build-usd.yml 39 | with: 40 | usd-version: 24.08 41 | build-config: Release 42 | 43 | build-2405: 44 | name: Build guc for USD v24.05 45 | uses: ./.github/workflows/build-usd.yml 46 | with: 47 | usd-version: 24.05 48 | build-config: Release 49 | 50 | build-2403: 51 | name: Build guc for USD v24.03 52 | uses: ./.github/workflows/build-usd.yml 53 | with: 54 | usd-version: 24.03 55 | build-config: Release 56 | 57 | deploy-release: 58 | name: Deploy Release 59 | needs: [build-2502, build-2411, build-2408, build-2405, build-2403] 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - name: Download artifacts 64 | uses: actions/download-artifact@v4 65 | with: 66 | merge-multiple: true 67 | 68 | - name: Deploy draft release 69 | uses: softprops/action-gh-release@d4e8205d7e959a9107da6396278b2f1f07af0f9b 70 | with: 71 | name: ${{ inputs.version-name }} 72 | body: ${{ inputs.release-notes }} 73 | files: | 74 | *.tar.gz 75 | fail_on_unmatched_files: true 76 | tag_name: ${{ inputs.tag-name }} 77 | prerelease: ${{ inputs.publish-prerelease }} 78 | draft: ${{ ! inputs.publish-prerelease }} 79 | make_latest: false 80 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-manual.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests (Manual) 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | enable-graphical-tests-mtlx: 7 | description: Enable MaterialX graphical tests 8 | type: boolean 9 | default: false 10 | enable-graphical-tests-preview: 11 | description: Enable preview graphical tests 12 | type: boolean 13 | default: false 14 | enable-usdchecker: 15 | description: Run usdchecker 16 | type: boolean 17 | default: true 18 | usd-version: 19 | description: The version of USD to build against 20 | type: string 21 | default: 24.05 22 | tests-revision: 23 | description: Test repo revision 24 | type: string 25 | default: 'main' 26 | test-filter: 27 | description: Filter (regular expression) 28 | type: string 29 | 30 | jobs: 31 | run-tests: 32 | name: Run Tests 33 | uses: ./.github/workflows/run-tests.yml 34 | with: 35 | enable-graphical-tests-mtlx: ${{ inputs.enable-graphical-tests-mtlx }} 36 | enable-graphical-tests-preview: ${{ inputs.enable-graphical-tests-preview }} 37 | enable-usdchecker: ${{ inputs.enable-usdchecker }} 38 | usd-version: ${{ inputs.usd-version }} 39 | tests-revision: ${{ inputs.tests-revision }} 40 | test-filter: ${{ inputs.test-filter }} 41 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-usd2403.yml: -------------------------------------------------------------------------------- 1 | name: USD v24.03 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | pull_request: 8 | paths-ignore: 9 | - '**.md' 10 | 11 | jobs: 12 | run-tests: 13 | name: Run Tests for USD v24.03 14 | uses: ./.github/workflows/run-tests.yml 15 | with: 16 | usd-version: 24.03 17 | test-filter: '-Draco|_Equal2411|_Above2411' 18 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-usd2405.yml: -------------------------------------------------------------------------------- 1 | name: USD v24.05 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | pull_request: 8 | paths-ignore: 9 | - '**.md' 10 | 11 | jobs: 12 | run-tests: 13 | name: Run Tests for USD v24.05 14 | uses: ./.github/workflows/run-tests.yml 15 | with: 16 | usd-version: 24.05 17 | test-filter: '-Draco|_Equal2411|_Above2411' 18 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-usd2408.yml: -------------------------------------------------------------------------------- 1 | name: USD v24.08 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | pull_request: 8 | paths-ignore: 9 | - '**.md' 10 | 11 | jobs: 12 | run-tests: 13 | name: Run Tests for USD v24.08 14 | uses: ./.github/workflows/run-tests.yml 15 | with: 16 | usd-version: 24.08 17 | test-filter: '-Draco|_Equal2411|_Above2411' 18 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-usd2411.yml: -------------------------------------------------------------------------------- 1 | name: USD v24.11 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | pull_request: 8 | paths-ignore: 9 | - '**.md' 10 | 11 | jobs: 12 | run-tests: 13 | name: Run Tests for USD v24.11 14 | uses: ./.github/workflows/run-tests.yml 15 | with: 16 | usd-version: 24.11 17 | test-filter: '-Draco|_Below2411|_Above2411' 18 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-usd2502.yml: -------------------------------------------------------------------------------- 1 | name: USD v25.02 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | pull_request: 8 | paths-ignore: 9 | - '**.md' 10 | 11 | jobs: 12 | run-tests: 13 | name: Run Tests for USD v25.02 14 | uses: ./.github/workflows/run-tests.yml 15 | with: 16 | usd-version: 25.02 17 | test-filter: '-Draco|_Below2411|_Equal2411' 18 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | enable-graphical-tests-mtlx: 7 | required: false 8 | type: boolean 9 | enable-graphical-tests-preview: 10 | required: false 11 | type: boolean 12 | enable-usdchecker: 13 | required: false 14 | type: boolean 15 | default: true 16 | usd-version: 17 | required: true 18 | type: string 19 | tests-revision: 20 | required: false 21 | type: string 22 | default: '05e58f260b8b28266b5f9f24d35063e2420f36de' 23 | test-filter: 24 | required: false 25 | type: string 26 | 27 | defaults: 28 | run: 29 | shell: bash 30 | 31 | jobs: 32 | build-debug: 33 | name: Build guc for USD v${{ inputs.usd-version }} (Debug) 34 | uses: ./.github/workflows/build-usd.yml 35 | with: 36 | usd-version: ${{ inputs.usd-version }} 37 | build-config: Debug 38 | extra-cmake-flags: -DCMAKE_COMPILE_WARNING_AS_ERROR=ON -DCMAKE_DISABLE_FIND_PACKAGE_OpenImageIO=1 39 | archive-name-suffix: _debug 40 | 41 | build-debug-oiio: 42 | name: Build guc for USD v${{ inputs.usd-version }} (Debug, OpenImageIO) 43 | uses: ./.github/workflows/build-usd.yml 44 | with: 45 | usd-version: ${{ inputs.usd-version }} 46 | build-config: Debug 47 | extra-cmake-flags: -DCMAKE_COMPILE_WARNING_AS_ERROR=ON 48 | upload-archive: false 49 | 50 | build-release: 51 | name: Build guc for USD v${{ inputs.usd-version }} (Release) 52 | uses: ./.github/workflows/build-usd.yml 53 | with: 54 | usd-version: ${{ inputs.usd-version }} 55 | build-config: Release 56 | extra-cmake-flags: -DCMAKE_COMPILE_WARNING_AS_ERROR=ON -DCMAKE_DISABLE_FIND_PACKAGE_OpenImageIO=1 57 | 58 | smoke-test: 59 | name: Smoke Test ${{ matrix.os-family }} (Release) 60 | runs-on: ${{ matrix.image }} 61 | needs: build-release 62 | 63 | strategy: 64 | fail-fast: false 65 | matrix: 66 | include: 67 | - os-family: Linux 68 | image: ubuntu-20.04 69 | usd-download-url: "https://github.com/pablode/USD/releases/download/v${{ inputs.usd-version }}-ci-release/USD${{ inputs.usd-version }}_Linux_x64_Python3.9.tar.gz" 70 | artifact-name: guc_USD${{ inputs.usd-version }}_Linux_x64.tar.gz 71 | executable-name: guc 72 | 73 | - os-family: Windows 74 | image: windows-2019 75 | usd-download-url: "https://github.com/pablode/USD/releases/download/v${{ inputs.usd-version }}-ci-release/USD${{ inputs.usd-version }}_Windows_x64_Python3.9.tar.gz" 76 | artifact-name: guc_USD${{ inputs.usd-version }}_Windows_x64.tar.gz 77 | executable-name: guc.exe 78 | 79 | steps: 80 | - name: Fetch USD binaries 81 | run: curl ${{ matrix.usd-download-url }} -L -v -o USD.tar.gz 82 | 83 | - name: Install USD binaries 84 | run: mkdir -p USD_INSTALL && tar -xvf USD.tar.gz -C $PWD/USD_INSTALL 85 | 86 | - name: Download guc artifacts 87 | id: download-guc 88 | uses: actions/download-artifact@v4 89 | with: 90 | merge-multiple: true 91 | 92 | - name: Install guc 93 | run: mkdir -p GUC_INSTALL && tar -xvf ${{ matrix.artifact-name }} -C $PWD/GUC_INSTALL 94 | 95 | - name: Set up environment variables (Linux) 96 | if: matrix.os-family == 'Linux' 97 | run: echo "LD_LIBRARY_PATH=$PWD/USD_INSTALL/lib" >> $GITHUB_ENV 98 | 99 | - name: Set up environment variables (Windows) 100 | if: matrix.os-family == 'Windows' 101 | run: | 102 | # We need to use real Windows paths, instead of MSYS2's auto-converted Unix paths 103 | echo "$(pwd -W)/USD_INSTALL/lib" >> $GITHUB_PATH 104 | echo "$(pwd -W)/USD_INSTALL/bin" >> $GITHUB_PATH 105 | 106 | - name: Set up Python 107 | uses: actions/setup-python@v5 108 | with: 109 | python-version: '3.9.13' 110 | 111 | - name: Fetch example glTF file 112 | run: mkdir test && curl "https://github.com/KhronosGroup/glTF-Sample-Models/raw/db9ff67c1116cfe28eb36320916bccd8c4127cc1/2.0/ToyCar/glTF-Binary/ToyCar.glb" -L -v -o test/asset.glb 113 | 114 | - name: Run test 115 | working-directory: test 116 | run: ../GUC_INSTALL/bin/${{ matrix.executable-name }} asset.glb asset.usda --emit-mtlx 117 | 118 | - name: Print output info 119 | working-directory: test 120 | run: | 121 | ls -la . 122 | cat asset.mtlx 123 | cat asset.usda 124 | 125 | conversion-tests: 126 | name: Conversion Tests (Debug) 127 | runs-on: ubuntu-20.04 128 | needs: build-debug 129 | 130 | env: 131 | USD_DOWNLOAD_URL: "https://github.com/pablode/USD/releases/download/v${{ inputs.usd-version }}-ci-release/USD${{ inputs.usd-version }}_Linux_x64_Python3.9.tar.gz" 132 | GUC_ARTIFACT_NAME: guc_USD${{ inputs.usd-version }}_Linux_x64_debug.tar.gz 133 | USD_INSTALL_PATH: /home/runner/work/USD/USD/INSTALL 134 | 135 | steps: 136 | - name: Fetch USD binaries 137 | run: curl ${{ env.USD_DOWNLOAD_URL }} -L -v -o USD.tar.gz 138 | 139 | - name: Install USD binaries 140 | run: mkdir -p ${{ env.USD_INSTALL_PATH }} && tar -xvf USD.tar.gz -C ${{ env.USD_INSTALL_PATH }} 141 | 142 | - name: Download guc artifacts 143 | id: download-guc 144 | uses: actions/download-artifact@v4 145 | with: 146 | merge-multiple: true 147 | 148 | # Install guc to the USD installation so that we can use the usdGlTF Sdf plugin 149 | - name: Install guc 150 | run: tar -xvf ${{ env.GUC_ARTIFACT_NAME }} -C ${{ env.USD_INSTALL_PATH }} 151 | 152 | - name: Install packages 153 | run: | 154 | # Use latest stable mesa because Storm requires OpenGL 4.6 155 | sudo add-apt-repository ppa:kisak/kisak-mesa 156 | sudo apt update 157 | sudo apt upgrade 158 | sudo apt-get install mesa-utils xvfb qt5-default 159 | 160 | - name: Set up environment variables 161 | run: | 162 | # Test options 163 | if [[ -z "${{ inputs.enable-graphical-tests-mtlx }}" ]] || [[ "${{ inputs.enable-graphical-tests-mtlx }}" = "false" ]]; then 164 | echo "GT_DISABLE_GRAPHICAL_MTLX=1" >> $GITHUB_ENV 165 | fi 166 | if [[ -z "${{ inputs.enable-graphical-tests-preview }}" ]] || [[ "${{ inputs.enable-graphical-tests-preview }}" = "false" ]]; then 167 | echo "GT_DISABLE_GRAPHICAL_PREVIEW=1" >> $GITHUB_ENV 168 | fi 169 | if [[ -z "${{ inputs.enable-usdchecker }}" ]] || [[ "${{ inputs.enable-usdchecker }}" = "false" ]]; then 170 | echo "GT_DISABLE_USDCHECKER=1" >> $GITHUB_ENV 171 | fi 172 | # Executables and libraries 173 | echo "${{ env.USD_INSTALL_PATH }}/bin" >> $GITHUB_PATH 174 | echo "LD_LIBRARY_PATH=${{ env.USD_INSTALL_PATH }}/lib" >> $GITHUB_ENV 175 | echo "PYTHONPATH=${{ env.USD_INSTALL_PATH }}/lib/python" >> $GITHUB_ENV 176 | # Software rendering 177 | echo "DISPLAY=:1" >> $GITHUB_ENV 178 | echo "LIBGL_ALWAYS_SOFTWARE=1" >> $GITHUB_ENV 179 | echo "GALLIUM_DRIVER=llvmpipe" >> $GITHUB_ENV 180 | echo "MESA_NO_ERROR=1" >> $GITHUB_ENV 181 | # guc debug output 182 | echo "TF_DEBUG=GUC" >> $GITHUB_ENV 183 | 184 | - name: Set up Python 185 | uses: actions/setup-python@v5 186 | with: 187 | python-version: '3.9.13' 188 | 189 | # usdrecord needs python (may change soon!) 190 | - name: Install Python packages 191 | run: pip3 install --user PySide2 PyOpenGL 192 | 193 | - name: Start virtual framebuffer 194 | run: | 195 | Xvfb :1 -screen 0 1280x960x24 & 196 | 197 | - name: Test glxinfo 198 | run: glxinfo 199 | 200 | - name: Print guc help 201 | run: guc -h || [ $? -eq 1 ] 202 | 203 | - name: Check out tests repository 204 | uses: actions/checkout@v3 205 | with: 206 | repository: pablode/guc-tests 207 | submodules: recursive 208 | ref: ${{ inputs.tests-revision }} 209 | 210 | - name: Run tests 211 | run: bash ./run_tests.sh "${{ inputs.test-filter }}" 212 | 213 | - name: Create test output archive 214 | if: success() || failure() 215 | working-directory: tests/output 216 | run: tar -zcvf ../../test-output.tar.gz * 217 | 218 | - name: Upload archive 219 | if: success() || failure() 220 | uses: actions/upload-artifact@v4 221 | with: 222 | path: test-output.tar.gz 223 | if-no-files-found: error 224 | name: test-output-${{ github.sha }} 225 | retention-days: 7 226 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/c,c++,vim,emacs,clion,cmake,intellij,macos 2 | 3 | ### Custom ### 4 | .idea 5 | 6 | ### C ### 7 | # Prerequisites 8 | *.d 9 | 10 | # Object files 11 | *.o 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Linker output 17 | *.ilk 18 | *.map 19 | *.exp 20 | 21 | # Precompiled Headers 22 | *.gch 23 | *.pch 24 | 25 | # Libraries 26 | *.lib 27 | *.a 28 | *.la 29 | *.lo 30 | 31 | # Shared objects (inc. Windows DLLs) 32 | *.dll 33 | *.so 34 | *.so.* 35 | *.dylib 36 | 37 | # Executables 38 | *.exe 39 | *.out 40 | *.app 41 | *.i*86 42 | *.x86_64 43 | *.hex 44 | 45 | # Debug files 46 | *.dSYM/ 47 | *.su 48 | *.idb 49 | *.pdb 50 | 51 | # Kernel Module Compile Results 52 | *.mod* 53 | *.cmd 54 | .tmp_versions/ 55 | modules.order 56 | Module.symvers 57 | Mkfile.old 58 | dkms.conf 59 | 60 | ### C++ ### 61 | # Prerequisites 62 | 63 | # Compiled Object files 64 | *.slo 65 | 66 | # Precompiled Headers 67 | 68 | # Compiled Dynamic libraries 69 | 70 | # Fortran module files 71 | *.mod 72 | *.smod 73 | 74 | # Compiled Static libraries 75 | *.lai 76 | 77 | # Executables 78 | 79 | ### CLion ### 80 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 81 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 82 | 83 | # User-specific stuff: 84 | .idea/**/workspace.xml 85 | .idea/**/tasks.xml 86 | .idea/dictionaries 87 | 88 | # Sensitive or high-churn files: 89 | .idea/**/dataSources/ 90 | .idea/**/dataSources.ids 91 | .idea/**/dataSources.xml 92 | .idea/**/dataSources.local.xml 93 | .idea/**/sqlDataSources.xml 94 | .idea/**/dynamic.xml 95 | .idea/**/uiDesigner.xml 96 | 97 | # Gradle: 98 | .idea/**/gradle.xml 99 | .idea/**/libraries 100 | 101 | # CMake 102 | cmake-build-debug/ 103 | 104 | # Mongo Explorer plugin: 105 | .idea/**/mongoSettings.xml 106 | 107 | ## File-based project format: 108 | *.iws 109 | 110 | ## Plugin-specific files: 111 | 112 | # IntelliJ 113 | /out/ 114 | 115 | # mpeltonen/sbt-idea plugin 116 | .idea_modules/ 117 | 118 | # JIRA plugin 119 | atlassian-ide-plugin.xml 120 | 121 | # Cursive Clojure plugin 122 | .idea/replstate.xml 123 | 124 | # Ruby plugin and RubyMine 125 | /.rakeTasks 126 | 127 | # Crashlytics plugin (for Android Studio and IntelliJ) 128 | com_crashlytics_export_strings.xml 129 | crashlytics.properties 130 | crashlytics-build.properties 131 | fabric.properties 132 | 133 | ### CLion Patch ### 134 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 135 | 136 | # *.iml 137 | # modules.xml 138 | # .idea/misc.xml 139 | # *.ipr 140 | 141 | # Sonarlint plugin 142 | .idea/sonarlint 143 | 144 | ### CMake ### 145 | CMakeCache.txt 146 | CMakeFiles 147 | CMakeScripts 148 | Testing 149 | Makefile 150 | cmake_install.cmake 151 | install_manifest.txt 152 | compile_commands.json 153 | CTestTestfile.cmake 154 | build 155 | 156 | ### Emacs ### 157 | # -*- mode: gitignore; -*- 158 | *~ 159 | \#*\# 160 | /.emacs.desktop 161 | /.emacs.desktop.lock 162 | *.elc 163 | auto-save-list 164 | tramp 165 | .\#* 166 | 167 | # Org-mode 168 | .org-id-locations 169 | *_archive 170 | 171 | # flymake-mode 172 | *_flymake.* 173 | 174 | # eshell files 175 | /eshell/history 176 | /eshell/lastdir 177 | 178 | # elpa packages 179 | /elpa/ 180 | 181 | # reftex files 182 | *.rel 183 | 184 | # AUCTeX auto folder 185 | /auto/ 186 | 187 | # cask packages 188 | .cask/ 189 | dist/ 190 | 191 | # Flycheck 192 | flycheck_*.el 193 | 194 | # server auth directory 195 | /server/ 196 | 197 | # projectiles files 198 | .projectile 199 | projectile-bookmarks.eld 200 | 201 | # directory configuration 202 | .dir-locals.el 203 | 204 | # saveplace 205 | places 206 | 207 | # url cache 208 | url/cache/ 209 | 210 | # cedet 211 | ede-projects.el 212 | 213 | # smex 214 | smex-items 215 | 216 | # company-statistics 217 | company-statistics-cache.el 218 | 219 | # anaconda-mode 220 | anaconda-mode/ 221 | 222 | ### Intellij ### 223 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 224 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 225 | 226 | # User-specific stuff: 227 | 228 | # Sensitive or high-churn files: 229 | 230 | # Gradle: 231 | 232 | # CMake 233 | 234 | # Mongo Explorer plugin: 235 | 236 | ## File-based project format: 237 | 238 | ## Plugin-specific files: 239 | 240 | # IntelliJ 241 | 242 | # mpeltonen/sbt-idea plugin 243 | 244 | # JIRA plugin 245 | 246 | # Cursive Clojure plugin 247 | 248 | # Ruby plugin and RubyMine 249 | 250 | # Crashlytics plugin (for Android Studio and IntelliJ) 251 | 252 | ### Intellij Patch ### 253 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 254 | 255 | # *.iml 256 | # modules.xml 257 | # .idea/misc.xml 258 | # *.ipr 259 | 260 | # Sonarlint plugin 261 | 262 | ### macOS ### 263 | *.DS_Store 264 | .AppleDouble 265 | .LSOverride 266 | 267 | # Icon must end with two \r 268 | Icon 269 | 270 | # Thumbnails 271 | ._* 272 | 273 | # Files that might appear in the root of a volume 274 | .DocumentRevisions-V100 275 | .fseventsd 276 | .Spotlight-V100 277 | .TemporaryItems 278 | .Trashes 279 | .VolumeIcon.icns 280 | .com.apple.timemachine.donotpresent 281 | 282 | # Directories potentially created on remote AFP share 283 | .AppleDB 284 | .AppleDesktop 285 | Network Trash Folder 286 | Temporary Items 287 | .apdisk 288 | 289 | ### Vim ### 290 | # swap 291 | .sw[a-p] 292 | .*.sw[a-p] 293 | # session 294 | Session.vim 295 | # temporary 296 | .netrwhist 297 | # auto-generated tag files 298 | tags 299 | 300 | # End of https://www.gitignore.io/api/c,c++,vim,emacs,clion,cmake,intellij,macos 301 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extern/cgltf"] 2 | path = extern/cgltf 3 | url = https://github.com/jkuhlmann/cgltf 4 | [submodule "extern/MikkTSpace"] 5 | path = extern/MikkTSpace 6 | url = https://github.com/mmikk/MikkTSpace 7 | [submodule "extern/cargs"] 8 | path = extern/cargs 9 | url = https://github.com/likle/cargs.git 10 | [submodule "extern/meshoptimizer"] 11 | path = extern/meshoptimizer 12 | url = https://github.com/zeux/meshoptimizer.git 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | ## Version 0.4 - 2024-07-15 5 | 6 | Added 7 | * Asset resolver support (thanks, [@expenses](https://github.com/expenses)!) 8 | * Command line option to print licenses 9 | * Display name attribute for most glTF objects 10 | * Dynamic _emitMtlx_ Sdf file format argument 11 | * Explicit documentation of GUC_BUILD_USDGLTF to README (thanks, [@expenses](https://github.com/expenses)!) 12 | * Help (-h / --help) command-line option 13 | * Support for [KHR_materials_unlit](https://github.com/KhronosGroup/glTF/blob/082d5a98f479c37dff18767982d1431fc6c014fd/extensions/2.0/Khronos/KHR_materials_unlit/README.md) extension 14 | * Third-party licenses to LICENSE file 15 | 16 | Changed 17 | * Default input values are not authored anymore in MaterialX networks 18 | * Default material binding is now used when MaterialX is disabled (thanks, [@lanxinger](https://github.com/lanxinger)!) 19 | * Fallback tangents are now only generated if MaterialX is enabled 20 | * Improved prim naming heuristics 21 | * MaterialX 1.38.6 is now required 22 | * Prebuilt binaries do not require a specific Python version anymore 23 | * Primvar naming does not emit suffix for index 0 anymore 24 | * Removed dependency on OpenImageIO (stb_image is used as a fallback) 25 | * Removed explicit color space transformations 26 | * Removed glTF PBR implementation options 27 | * Renamed Sdf plugin id from 'glTF' to 'gltf' to conform with other projects 28 | * Reorder command line arguments to conform with UNIX conventions 29 | 30 | Fixed 31 | * False-positive MSVC warning caused by Boost macros 32 | * Incorrect UsdPreviewSurface Sdf value types 33 | * Incorrect warning about greyscale texture alpha channel 34 | * Material name not being preserved if it starts with an underscore 35 | * Missing '.glb' extension in Sdf plugin definition 36 | * Missing UsdLux extents 37 | * Typo in Sdf plugin that could prevent file reading 38 | * UsdLuxShapingAPI not being applied 39 | 40 | ## Version 0.3 - 2023-07-26 41 | 42 | Added 43 | * Binaries for USD v23.08 with MaterialX 1.38.7 44 | * Metainfo that UsdGlTF plugin does not support writing 45 | * Support for [KHR_materials_emissive_strength](https://github.com/KhronosGroup/glTF/blob/d3382c30eca18312bd9cc0b36d6a9ae60e1f1bae/extensions/2.0/Khronos/KHR_materials_emissive_strength/README.md) extension 46 | * Support for [KHR_materials_iridescence](https://github.com/KhronosGroup/glTF/tree/d3382c30eca18312bd9cc0b36d6a9ae60e1f1bae/extensions/2.0/Khronos/KHR_materials_iridescence/README.md) extension 47 | * Support for [KHR_materials_variants](https://github.com/KhronosGroup/glTF/blob/d3382c30eca18312bd9cc0b36d6a9ae60e1f1bae/extensions/2.0/Khronos/KHR_materials_variants/README.md) extension 48 | * Support for [KHR_texture_transform](https://github.com/KhronosGroup/glTF/blob/d3382c30eca18312bd9cc0b36d6a9ae60e1f1bae/extensions/2.0/Khronos/KHR_texture_transform/README.md) extension 49 | * Warnings about unsupported optional extensions 50 | 51 | Changed 52 | * Disabled file glTF PBR implementation option on USD v23.08+ due to an internal crash 53 | * Renamed 'tangentSigns' primvar to 'bitangentSigns' 54 | * Updated [Ecosystem Limitations](docs/Ecosystem_Limitations.md) document with latest USD/MaterialX versions 55 | 56 | Fixed 57 | * Normalmap UsdUVTexture:scale[3] not matching USD complianceChecker 58 | 59 | ## Version 0.2 - 2023-01-09 60 | 61 | Added 62 | * CMake config file installation for libguc 63 | * Custom data for generated mesh attributes 64 | * Display color estimation from material properties 65 | * Documentation on [Ecosystem Limitations](docs/Ecosystem_Limitations.md) and [Structure Mapping](docs/Structure_Mapping.md) 66 | * Extent computation 67 | * GitHub Actions builds 68 | * Omittance of MaterialX multiplication nodes with factor 1 69 | * Option to export MaterialX glTF PBR as file 70 | * Sdf file format plugin 71 | * Support for glTF libraries (no scenes) 72 | * Test suite with graphical tests 73 | * Transmission as alpha-as-coverage to UsdPreviewSurfaces 74 | * UsdModelAPI metadata 75 | * USDZ export 76 | * Version information to header, executable and converted assets 77 | * Workaround for glTF assets referencing the wrong texture channel for alpha 78 | 79 | Changed 80 | * CMake 3.15 is now required 81 | * MikkTSpace is now linked statically 82 | * Refined output USD file structure 83 | * Renamed `guc_params` struct to `guc_options` 84 | * Updated USD version to 22.11 85 | * Vertex colors are now stored as dedicated primvars (not as displayColors) 86 | * Vertex opacities are not generated for opaque materials 87 | 88 | Fixed 89 | * Bug with base64 output buffer size estimation 90 | * Erroneous C11 compiler requirement 91 | * Fallback tangent generation 92 | * Flat normals and tangents being attempted to generate for line and point topologies 93 | * Flat normal generation not unindexing primvars 94 | * Handling of MaterialX C++ exceptions 95 | * Inactive scenes being visible 96 | * Incorrect MaterialX glTF PBR default values 97 | * Link errors with Boost and TBB (thanks, [@ix-dcourtois](https://github.com/ix-dcourtois)!) 98 | * Location of MaterialX and OIIO libraries (thanks, [@ix-dcourtois](https://github.com/ix-dcourtois)!) 99 | * MaterialX exception for very small or large float values 100 | * MaterialX inputs having multiple data source attributes 101 | * Missing MaterialX vertex color multiplication in case of missing base color texture 102 | * Mtlx-as-UsdShade error with USD 22.11 103 | * Multiple compiler warnings 104 | * Normal map tangent handling 105 | * Position independent code not being enabled for static library builds 106 | * Shared library symbols not being exported on Windows (thanks, [@ix-dcourtois](https://github.com/ix-dcourtois)!) 107 | * USD MaterialBindingAPI schema not being applied 108 | 109 | ## Version 0.1 - 2022-05-17 110 | 111 | Initial release 112 | 113 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | project(guc VERSION 0.5) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | set(CMAKE_CXX_EXTENSIONS OFF) 8 | 9 | set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 10 | 11 | # Find USD. 12 | find_package(pxr CONFIG REQUIRED) 13 | 14 | # Find MaterialX library provided by the USD installation. 15 | # Since we use UsdMtlx, using a custom MaterialX version leads to conflicts. 16 | find_package(MaterialX 1.38.6 REQUIRED HINTS ${pxr_DIR}) 17 | 18 | # We need to open PNG and JPEG files in order to read the number of channels 19 | # for shading node creation. OIIO can be provided by the USD installation, 20 | # otherwise we fall back to stb_image. 21 | find_package(OpenImageIO HINTS ${pxr_DIR}) 22 | 23 | # Optionally support draco. See Finddraco.cmake for more information. 24 | set(draco_ROOT ${pxr_DIR}) 25 | find_package(draco) 26 | 27 | option(GUC_BUILD_EXECUTABLE "Build the guc executable." ON) 28 | option(GUC_BUILD_USDGLTF "Build the Sdf file format plugin." OFF) 29 | 30 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") 31 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") 32 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") 33 | 34 | foreach(CONFIG_TYPE ${CMAKE_CONFIGURATION_TYPES}) 35 | string(TOUPPER ${CONFIG_TYPE} CONFIG_TYPE) 36 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}) 37 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) 38 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) 39 | endforeach() 40 | 41 | if(MSVC) 42 | # Disable "macro expansion producing 'defined' has undefined behavior" warning 43 | # introduced by indirect inclusion of Windows headers in guc and MikkTSpace. 44 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /wd5105") 45 | # Disable "not enough arguments for function-like macro invocation" warning caused 46 | # by the transitive inclusion of boost headers and non-conformant MSVC preprocessor 47 | # behaviour. See USD's msvcdefaults.cmake file for a detailed comment. 48 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4003") 49 | endif() 50 | 51 | add_subdirectory(extern) 52 | 53 | include(cmake/UsdDefaults.cmake) 54 | set(CMAKE_CXX_FLAGS "${GUC_CXX_FLAGS} ${CMAKE_CXX_FLAGS}") 55 | add_definitions(${GUC_CXX_DEFINITIONS}) 56 | 57 | add_subdirectory(src) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## guc 2 | 3 | ![USD v25.02](https://github.com/pablode/guc/actions/workflows/run-tests-usd2502.yml/badge.svg?branch=main) 4 | ![USD v24.11](https://github.com/pablode/guc/actions/workflows/run-tests-usd2411.yml/badge.svg?branch=main) 5 | 6 | guc is a glTF to [Universal Scene Description](https://github.com/PixarAnimationStudios/USD) (USD) converter. 7 | 8 | Unlike... 9 | - [gltf2usd](https://github.com/kcoley/gltf2usd), it aims to be more than a PoC 10 | - [usd_from_gltf](https://github.com/google/usd_from_gltf), it is not AR Quick Look centric 11 | - [Apple's USDZ Tools](https://developer.apple.com/augmented-reality/tools/), it is open-source and freely available 12 | 13 | guc furthermore supports near-lossless material translation via the [MaterialX](https://github.com/AcademySoftwareFoundation/MaterialX) standard. 14 | 15 | All glTF features except animation and skinning are implemented and get continuously tested in guc's [test suite](https://github.com/pablode/guc-tests). 16 | 17 |

18 | 19 | 20 |

21 |

22 | Wayfair's Iridescent Dish with Olives (CC BY) converted to USD+MaterialX with guc and rendered in hdStorm (left). 23 | The same model in Khronos's glTF Sample Viewer (right). 24 |

25 | 26 | ### Build 27 | 28 | You need USD v24.03+ (e.g. v25.02) with MaterialX support enabled. 29 | 30 | Do a recursive clone of the repository and set up a build folder: 31 | ``` 32 | git clone https://github.com/pablode/guc --recursive 33 | mkdir guc/build && cd guc/build 34 | ``` 35 | 36 | Pass following parameters in the CMake generation phase: 37 | ``` 38 | cmake .. -Wno-dev -DCMAKE_BUILD_TYPE=Release 39 | ``` 40 | 41 | Build the executable: 42 | ``` 43 | cmake --build . -j8 --config Release 44 | ``` 45 | 46 | > Note: set `BUILD_SHARED_LIBS` for shared builds, and `CMAKE_MSVC_RUNTIME_LIBRARY` to USD's MSVC ABI. 47 | 48 | ### Usage 49 | 50 | ``` 51 | guc 0.5 - glTF to USD converter 52 | 53 | Usage: guc [options] [--] 54 | 55 | Options: 56 | -m, --emit-mtlx Emit MaterialX materials in addition to UsdPreviewSurfaces 57 | -u, --mtlx-as-usdshade Convert and inline MaterialX materials into the USD layer using UsdMtlx 58 | -v, --default-material-variant= Index of the material variant that is selected by default 59 | -l, --licenses Print the license of guc and third-party libraries 60 | -h, --help Show the command help 61 | ``` 62 | 63 | Both glTF and GLB file types are valid input. USDA, USDC and USDZ formats can be written. 64 | 65 | An example asset conversion is described in the [Structure Mapping](docs/Structure_Mapping.md) document. 66 | 67 | ### Extension support 68 | 69 | Name | Status                         70 | ------------------------------------|---------- 71 | EXT_meshopt_compression | ✅ Complete 72 | KHR_draco_mesh_compression | ✅ Complete 73 | KHR_lights_punctual | ✅ Partial 1 74 | KHR_materials_clearcoat | ✅ Complete 75 | KHR_materials_emissive_strength | ✅ Complete 76 | KHR_materials_ior | ✅ Complete 77 | KHR_materials_iridescence | ✅ Complete 78 | KHR_materials_sheen | ✅ Complete 79 | KHR_materials_specular | ✅ Complete 80 | KHR_materials_transmission | ✅ Complete 81 | KHR_materials_unlit | ✅ Complete 82 | KHR_materials_variants | ✅ Complete 83 | KHR_materials_volume | ✅ Partial 2 84 | KHR_mesh_quantization | ✅ Complete 85 | KHR_texture_transform | ✅ Complete 86 | 87 | \[1\] Spotlight cone falloff is ignored. 88 | \[2\] Thickness is not supported by the MaterialX glTF PBR implementation. 89 | 90 | ### Sdf plugin 91 | 92 | The _usdGlTF_ library implements USD's Sdf file format interface. Enable the `GUC_BUILD_USDGLTF` CMake option before building and install it as follows: 93 | ``` 94 | cmake --install . --component usdGlTF --config Release --prefix /plugin/usd 95 | ``` 96 | 97 | glTF files can now be referenced as layers and opened with USD tooling. 98 | The _emitMtlx_ dynamic Sdf file format argument controls MaterialX material emission. 99 | 100 | ### License 101 | 102 | ``` 103 | 104 | Copyright 2024 Pablo Delgado Krämer 105 | 106 | Licensed under the Apache License, Version 2.0 (the "License"); 107 | you may not use this file except in compliance with the License. 108 | You may obtain a copy of the License at 109 | 110 | http://www.apache.org/licenses/LICENSE-2.0 111 | 112 | Unless required by applicable law or agreed to in writing, software 113 | distributed under the License is distributed on an "AS IS" BASIS, 114 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 115 | See the License for the specific language governing permissions and 116 | limitations under the License. 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /cmake/Finddraco.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Finds draco and populates following variables: 3 | # DRACO_FOUND (draco_FOUND) 4 | # DRACO_INCLUDE_DIR 5 | # DRACO_LIBRARIES 6 | # 7 | # The reason why we ship our custom Find* file is that the draco version 8 | # that USD uses, 1.3.6, has a broken CMake config file. 9 | # 10 | # This also caused trouble for USD, which is why it comes with a custom 11 | # Find* script similar to this one. However, the script has two issues: 12 | # 1) It specifies full file names, causing 'libdraco.1.dylib' to not 13 | # be found on macOS. 14 | # 2) It only finds the main draco lib, but not the decoder-specific 15 | # lib which we need in guc. 16 | # 17 | 18 | find_path(DRACO_INCLUDE_DIR NAMES "draco/core/draco_version.h") 19 | 20 | find_library(DRACO_LIBRARY NAMES draco PATH_SUFFIXES lib) 21 | find_library(DRACO_DEC_LIBRARY NAMES dracodec PATH_SUFFIXES lib) 22 | 23 | set(DRACO_LIBRARIES ${DRACO_LIBRARY} ${DRACO_DEC_LIBRARY}) 24 | 25 | include(FindPackageHandleStandardArgs) 26 | find_package_handle_standard_args(draco 27 | REQUIRED_VARS 28 | DRACO_INCLUDE_DIR 29 | DRACO_DEC_LIBRARY 30 | DRACO_LIBRARY 31 | DRACO_LIBRARIES 32 | ) 33 | -------------------------------------------------------------------------------- /cmake/UsdDefaults.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2016 Pixar 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 5 | # with the following modification; you may not use this file except in 6 | # compliance with the Apache License and the following modification to it: 7 | # Section 6. Trademarks. is deleted and replaced with: 8 | # 9 | # 6. Trademarks. This License does not grant permission to use the trade 10 | # names, trademarks, service marks, or product names of the Licensor 11 | # and its affiliates, except as required to comply with Section 4(c) of 12 | # the License and to reproduce the content of the NOTICE file. 13 | # 14 | # You may obtain a copy of the Apache License at 15 | # 16 | # http://www.apache.org/licenses/LICENSE-2.0 17 | # 18 | # Unless required by applicable law or agreed to in writing, software 19 | # distributed under the Apache License with the above modification is 20 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 21 | # KIND, either express or implied. See the Apache License for the specific 22 | # language governing permissions and limitations under the Apache License. 23 | # 24 | 25 | # This files includes most compiler and linker flags that USD uses. We need 26 | # them to be able to include the headers without warnings, and to fix some issues. 27 | 28 | # Helper functions to avoid duplicate code. 29 | function(_add_define definition) 30 | list(APPEND _GUC_CXX_DEFINITIONS "-D${definition}") 31 | set(_GUC_CXX_DEFINITIONS ${_GUC_CXX_DEFINITIONS} PARENT_SCOPE) 32 | endfunction() 33 | 34 | function(_disable_warning flag) 35 | if(MSVC) 36 | list(APPEND _GUC_CXX_WARNING_FLAGS "/wd${flag}") 37 | else() 38 | list(APPEND _GUC_CXX_WARNING_FLAGS "-Wno-${flag}") 39 | endif() 40 | set(_GUC_CXX_WARNING_FLAGS ${_GUC_CXX_WARNING_FLAGS} PARENT_SCOPE) 41 | endfunction() 42 | 43 | # Compiler-specific defines and warnings. 44 | if(MSVC) 45 | # Enable exception handling. 46 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /EHsc") 47 | 48 | # Standards compliant. 49 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /Zc:rvalueCast /Zc:strictStrings") 50 | 51 | # The /Zc:inline option strips out the "arch_ctor_" symbols used for 52 | # library initialization by ARCH_CONSTRUCTOR starting in Visual Studio 2019, 53 | # causing release builds to fail. Disable the option for this and later 54 | # versions. 55 | # 56 | # For more details, see: 57 | # https://developercommunity.visualstudio.com/content/problem/914943/zcinline-removes-extern-symbols-inside-anonymous-n.html 58 | if(MSVC_VERSION GREATER_EQUAL 1920) 59 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /Zc:inline-") 60 | else() 61 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /Zc:inline") 62 | endif() 63 | 64 | # Turn on all but informational warnings. 65 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /W3") 66 | 67 | # truncation from 'double' to 'float' due to matrix and vector classes in `Gf` 68 | _disable_warning("4244") 69 | _disable_warning("4305") 70 | 71 | # conversion from size_t to int. While we don't want this enabled 72 | # it's in the Python headers. So all the Python wrap code is affected. 73 | _disable_warning("4267") 74 | 75 | # no definition for inline function 76 | # this affects Glf only 77 | _disable_warning("4506") 78 | 79 | # 'typedef ': ignored on left of '' when no variable is declared 80 | # XXX: figure out why we need this 81 | _disable_warning("4091") 82 | 83 | # qualifier applied to function type has no meaning; ignored 84 | # tbb/parallel_for_each.h 85 | _disable_warning("4180") 86 | 87 | # '<<': result of 32-bit shift implicitly converted to 64 bits 88 | # tbb/enumerable_thread_specific.h 89 | _disable_warning("4334") 90 | 91 | # Disable warning C4996 regarding fopen(), strcpy(), etc. 92 | _add_define("_CRT_SECURE_NO_WARNINGS") 93 | 94 | # Disable warning C4996 regarding unchecked iterators for std::transform, 95 | # std::copy, std::equal, et al. 96 | _add_define("_SCL_SECURE_NO_WARNINGS") 97 | 98 | # Make sure WinDef.h does not define min and max macros which 99 | # will conflict with std::min() and std::max(). 100 | _add_define("NOMINMAX") 101 | 102 | # Needed to prevent YY files trying to include unistd.h 103 | # (which doesn't exist on Windows) 104 | _add_define("YY_NO_UNISTD_H") 105 | 106 | # Exclude headers from unnecessary Windows APIs to improve build 107 | # times and avoid annoying conflicts with macros defined in those 108 | # headers. 109 | _add_define("WIN32_LEAN_AND_MEAN") 110 | 111 | # These files require /bigobj compiler flag 112 | # Vt/arrayPyBuffer.cpp 113 | # Usd/crateFile.cpp 114 | # Usd/stage.cpp 115 | # Until we can set the flag on a per file basis, we'll have to enable it 116 | # for all translation units. 117 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /bigobj") 118 | 119 | # Enable PDB generation. 120 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /Zi") 121 | 122 | # Enable multiprocessor builds. 123 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /MP") 124 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} /Gm-") 125 | 126 | # Disable Boost and TBB auto-linking, as this causes errors when building 127 | # debug guc against their release versions. No direct linking is involved, anyway. 128 | _add_define("BOOST_ALL_NO_LIB") 129 | _add_define("__TBB_NO_IMPLICIT_LINKAGE") 130 | else() 131 | # Enable all warnings. 132 | set(_GUC_CXX_FLAGS "${_GUC_CXX_FLAGS} -Wall -Wformat-security") 133 | 134 | # We use hash_map, suppress deprecation warning. 135 | _disable_warning("deprecated") 136 | _disable_warning("deprecated-declarations") 137 | endif() 138 | 139 | # General defines. 140 | if(CMAKE_BUILD_TYPE STREQUAL "Debug") 141 | _add_define(BUILD_OPTLEVEL_DEV) 142 | endif() 143 | 144 | # Convert lists to strings. 145 | set(_GUC_CXX_FLAGS ${_GUC_CXX_FLAGS} ${_GUC_CXX_WARNING_FLAGS}) 146 | string(REPLACE ";" " " GUC_CXX_FLAGS "${_GUC_CXX_FLAGS}") 147 | string(REPLACE ";" " " GUC_CXX_DEFINITIONS "${_GUC_CXX_DEFINITIONS}") 148 | -------------------------------------------------------------------------------- /cmake/gucConfig.cmake.in: -------------------------------------------------------------------------------- 1 | include(CMakeFindDependencyMacro) 2 | 3 | find_package(pxr CONFIG REQUIRED) 4 | find_package(MaterialX REQUIRED HINTS ${pxr_DIR}) 5 | find_package(OpenImageIO REQUIRED HINTS ${pxr_DIR}) 6 | 7 | include("${CMAKE_CURRENT_LIST_DIR}/@CMAKE_PROJECT_NAME@Targets.cmake") 8 | check_required_components(@CMAKE_PROJECT_NAME@) 9 | -------------------------------------------------------------------------------- /docs/Ecosystem_Limitations.md: -------------------------------------------------------------------------------- 1 | # Ecosystem Limitations 2 | 3 | This document describes past and current OpenUSD/MaterialX/Hydra/Storm issues. 4 | 5 | > [!IMPORTANT] 6 | > Last updated with USD v25.02 and MaterialX 1.38.10 7 | 8 | ## MaterialX 9 | 10 | #### Limitations of the MaterialX shading model and the glTF PBR implementation 11 | As described in the [initial PR](https://github.com/AcademySoftwareFoundation/MaterialX/pull/861), certain properties of the glTF shading model do not map to MaterialX: 12 | * _thickness_: whether the material is thin-walled (thickness = 0) or volumetric (thickness > 0). Currently, only the volumetric case is implemented, and the input is ignored. (Issue [#1936](https://github.com/AcademySoftwareFoundation/MaterialX/issues/1936)) 13 | * _occlusion_: this concept does not exist in MaterialX. SSAO or ray traced ambient occlusion may be used instead. 14 | 15 | #### Hardcoded `` node handedness 16 | glTF tangents have four components, with the last component denoting the handedness of the calculated bitangent. 17 | For normal mapping, we would ideally want to use MaterialX's `` node, however this node makes a hard assumption on the handedness of the bitangent. 18 | A _bitangent_ input has been proposed (Issue [#945](https://github.com/AcademySoftwareFoundation/MaterialX/issues/945)), but as a fallback, guc injects a flattened, patched version of this node. 19 | 20 | > [!NOTE] 21 | > Resolved in MaterialX 1.39 22 | 23 | 24 | ## MaterialX in USD 25 | 26 | #### Implicit vs. explicit primvar reading 27 | 28 | MaterialX shading networks require geometric properties (normals, texture coordinates, tangents). These are stored as USD primvars, and the renderer needs to provide them accordingly. 29 | 30 | As an *implicit* mapping relies on non-standardized heuristics that do not cover multiple texture sets, vertex colors or tangents, guc always uses *explicit* reading of these primvars using `` nodes. 31 | However, this functionality may not be supported by some render delegates. 32 | 33 | #### Image color spaces are not copied from Hydra shading networks to MaterialX documents 34 | 35 | The HdMtlx USD library provides functions which aid Hydra render delegates in supporting shading networks via MaterialX. The `HdMtlxCreateMtlxDocumentFromHdNetwork` function is essential in that, as it translates Hydra shading networks to MaterialX documents. These documents are then processed by the renderer, and potentially used for code generation using a MaterialXGenShader backend. 36 | 37 | One outstanding problem with the translation process is that color spaces are not copied from the Hydra network to the MaterialX document. This means that, for example, normal maps may be assumed to be encoded in the sRGB color space, leading to incorrect shading. (Issue [#1523](https://github.com/PixarAnimationStudios/USD/issues/1523)) 38 | 39 | This issue affects a number of render delegates, including Pixar's [HdStorm](https://github.com/PixarAnimationStudios/USD/blob/0c7b9a95f155c221ff7df9270a39a52e3b23af8b/pxr/imaging/hdSt/materialXFilter.cpp#L877). 40 | 41 | > [!NOTE] 42 | > Resolved in OpenUSD 23.11 43 | 44 | 45 | ## MaterialX in HdStorm 46 | 47 | #### Greyscale images are incorrectly handled 48 | The behaviour does not match MaterialX. It has been logged as [MaterialX #2257](https://github.com/AcademySoftwareFoundation/MaterialX/issues/2257) for clarification. 49 | 50 | #### Texture-coordinate related regression in USD v24.03+ 51 | Issue [#3048](https://github.com/PixarAnimationStudios/OpenUSD/issues/3048) currently affects [multiple cases](https://github.com/pablode/guc-tests/commit/695f6dcc58c5b08b1d7b689dc1dc0c2d4305f154?diff=split&w=0) of the guc test suite. 52 | 53 | > [!NOTE] 54 | > Resolved in OpenUSD 24.11 55 | 56 | #### 'edf' shader compilation error on Metal 57 | Issue [#3049](https://github.com/PixarAnimationStudios/OpenUSD/issues/3049). 58 | 59 | > [!NOTE] 60 | > Resolved in OpenUSD 24.08 61 | 62 | #### One and two channel sRGB texture formats are unsupported 63 | Converted assets may use such textures, but HdStorm is not able to render them. ([Source code](https://github.com/PixarAnimationStudios/USD/blob/3abc46452b1271df7650e9948fef9f0ce602e3b2/pxr/imaging/hdSt/textureUtils.cpp#L341-L345)) 64 | 65 | > [!NOTE] 66 | > Resolved in OpenUSD 24.08 67 | 68 | #### Incorrect automatic sRGB image detection and decoding 69 | In addition to the HdMtlx color space issue above, a heuristic defined by the [UsdPreviewSurface spec](https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html#texture-reader) is used to determine whether an image is to be interpreted as sRGB or not. This heuristic can not be disabled and incorrectly classifies normal maps and occlusion-roughness-metalness textures commonly used with glTF. (Issue [#1878](https://github.com/PixarAnimationStudios/USD/issues/1878)) 70 | 71 | > [!NOTE] 72 | > Resolved in OpenUSD 23.11 73 | 74 | #### Hardcoded transparent geometry detection 75 | HdStorm is a rasterizer and therefore handles translucent geometry differently than solid geometry. In order to detect whether a geometry's material is translucent or not, a heuristic is used. However, it is not yet adjusted to the MaterialX glTF PBR. (Issue [#1882](https://github.com/PixarAnimationStudios/USD/issues/1882)) 76 | 77 | > [!NOTE] 78 | > Resolved in OpenUSD 24.08 79 | 80 | #### Materials with 1x1 images are not rendered correctly 81 | Issue [#2140](https://github.com/PixarAnimationStudios/USD/issues/2140). 82 | 83 | > [!NOTE] 84 | > Resolved in MaterialX 1.38.8 / OpenUSD 24.05 85 | 86 | #### EDF shader compilation error on Metal 87 | Issue [#3049](https://github.com/PixarAnimationStudios/OpenUSD/issues/3049). 88 | 89 | > [!NOTE] 90 | > Resolved in OpenUSD 24.08 91 | 92 | #### Tangent frame is not aligned to the UV space 93 | This causes normal maps to render incorrectly, for instance for the Open Chess Set. (Issue [#2255](https://github.com/PixarAnimationStudios/OpenUSD/issues/2255)) 94 | 95 | > [!NOTE] 96 | > Resolved in OpenUSD 24.03 97 | 98 | ## Apple RealityKit and SceneKit compatibility 99 | 100 | Apple's RealityKit (used for AR Quick Look) and SceneKit renderers only [support a subset](https://developer.apple.com/documentation/realitykit/validating-usd-files) of USD's features. guc makes no compatibility efforts, and converted assets may not be displayed correctly. 101 | -------------------------------------------------------------------------------- /docs/Structure_Mapping.md: -------------------------------------------------------------------------------- 1 | # Structure Mapping 2 | 3 | guc's conversion process produces self-containing components, which can then be 4 | composed as part of a larger assembly. 5 | A consumer may modify the components as needed, however artist-friendliness 6 | (layering, shading network complexity) can not be ensured due to the lack of usage annotations 7 | and the need to support the space of legal glTF constructions. 8 | The converted assets are hence intended to be used "as-is", for instance as leaf 9 | nodes in a USD-based pipeline, and not as intermediate products in a content-authoring pipeline. 10 | 11 | 12 | ## Case Study: A golden triangle 13 | 14 | In the following example, we're going to see how guc translates the golden triangle from 15 | [Chapter 11](https://github.khronos.org/glTF-Tutorials/gltfTutorial/gltfTutorial_011_SimpleMaterial.html) of Khronos's glTF tutorial. 16 | 17 | 18 | 19 | 22 | 55 | 56 |
20 | 21 | 23 | 24 | ```json 25 | { 26 | "asset" : { "version" : "2.0" }, 27 | "scene": 0, 28 | "scenes" : [ { "nodes" : [ 0 ] } ], 29 | "nodes" : [ { "mesh" : 0 } ], 30 | "meshes" : [ 31 | { 32 | "primitives" : [ { 33 | "attributes" : { 34 | "POSITION" : 1 35 | }, 36 | "indices" : 0, 37 | "material" : 0 38 | } ] 39 | } 40 | ], 41 | "materials" : [ 42 | { 43 | "pbrMetallicRoughness": { 44 | "baseColorFactor": [ 1.000, 0.766, 0.336, 1.0 ], 45 | "metallicFactor": 0.5, 46 | "roughnessFactor": 0.1 47 | } 48 | } 49 | ], 50 | "..." 51 | } 52 | ``` 53 | 54 |
57 | 58 |

59 | A render of the golden glTF triangle (left), and its corresponding JSON (right). 60 |

61 | 62 | ### Asset and Scenes 63 | 64 | Upon conversion, guc creates a root prim, `/Asset`, under which scenes and materials reside. 65 | Scenes contain Xform, mesh, camera and light prims. 66 | 67 | For the "golden triangle" example above, following high-level scene structure is generated: 68 | ``` 69 | #usda 1.0 70 | ( 71 | defaultPrim = "Asset" 72 | doc = "Converted from glTF with guc 0.2" 73 | metersPerUnit = 1 74 | upAxis = "Y" 75 | ) 76 | 77 | def Xform "Asset" ( 78 | customData = { 79 | string version = "2.0" 80 | } 81 | kind = "component" 82 | ) 83 | { 84 | def Xform "Scenes" 85 | { 86 | def Xform "scene" 87 | { 88 | # ... 89 | ``` 90 | 91 | Only the default scene is authored as visible, and if no scenes exists, the glTF file is a _library_. 92 | guc then generates an asset-level `/Nodes` prim. 93 | 94 | ### Nodes 95 | 96 | Nodes are glTF graph elements with an optional transformation, which can either contain a number of nodes, or a reference to a mesh, camera, or light. Nodes are translated to Xforms under the scene prim. Meshes, cameras and lights are "instanced" across scenes through references, but instancing of node trees does not exist. 97 | 98 | ### Meshes 99 | 100 | The glTF buffer, bufferView and accessor concepts are reduced to a single prim, 101 | in the case of the example above, following mesh prim: 102 | 103 | ``` 104 | def Mesh "submesh" ( 105 | prepend apiSchemas = ["MaterialBindingAPI"] 106 | ) 107 | { 108 | float3[] extent = [(0, 0, 0), (1, 1, 0)] 109 | int[] faceVertexCounts = [3] 110 | int[] faceVertexIndices = [0, 1, 2] 111 | rel material:binding = 112 | rel material:binding:preview = 113 | normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1)] ( 114 | customData = { 115 | dictionary guc = { 116 | bool generated = 1 117 | } 118 | } 119 | interpolation = "vertex" 120 | ) 121 | point3f[] points = [(0, 0, 0), (1, 0, 0), (0, 1, 0)] 122 | color3f[] primvars:displayColor = [(1, 0.766, 0.336)] ( 123 | customData = { 124 | dictionary guc = { 125 | bool generated = 1 126 | } 127 | } 128 | interpolation = "constant" 129 | ) 130 | uniform token subdivisionScheme = "none" 131 | } 132 | ``` 133 | 134 | Depending on material parameters and available accessors, guc creates following primvars: 135 | 136 | Name | Description 137 | --- | --- 138 | color[N] | Vertex color set N 139 | displayColor | Display color (constant or per-vertex) 140 | displayOpacity | Display opacity (constant or per-vertex) 141 | opacity[N] | Vertex opacity set N 142 | st[N] | Texture coordinate set N 143 | tangents | Three-component tangent vectors 144 | bitangentSigns | Bitangent handedness 145 | 146 | Additionally, a material binding relationship is always authored on the prim and its 147 | overrides, potentially binding a default material. 148 | 149 | ### Materials 150 | 151 | guc authors UsdPreviewSurface and MaterialX material collections on an asset-level `/Materials` prim. 152 | The exact path is motivated by the behaviour of UsdMtlx, which implicitly creates prim scopes. 153 | 154 | The generated UsdPreviewSurface material for the example above looks like this: 155 | ``` 156 | def Material "mat" 157 | { 158 | token outputs:surface.connect = 159 | 160 | def Shader "node" 161 | { 162 | uniform token info:id = "UsdPreviewSurface" 163 | float3 inputs:diffuseColor = (1, 0.766, 0.336) 164 | float inputs:metallic = 0.5 165 | float inputs:roughness = 0.1 166 | token outputs:surface 167 | } 168 | } 169 | ``` 170 | 171 | While the generated MaterialX document is approximately the following: 172 | ```xml 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | ``` 186 | 187 | 188 | ## MaterialX material translation 189 | 190 | guc makes use of MaterialX's [glTF PBR implementation](https://github.com/AcademySoftwareFoundation/MaterialX/blob/f66296b25e095aad135834aa8e59c13282adcdd7/libraries/bxdf/gltf_pbr.mtlx), with the shading logic being maintained and developed by MaterialX contributors. 191 | 192 | guc's leftover responsibility is to instantiate the `` node and to populate its inputs. 193 | 194 | For example, if [Chapter 3.9.2](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#metallic-roughness-material) 195 | of the glTF specification defines the base color to consist of a texture value, a factor and the vertex color, 196 | the following node graph is generated: 197 | ```mermaid 198 | flowchart RL; 199 | A["<!-- baseColorFactor -->
<constant>"]; 200 | B["<!-- COLOR_0 -->
<geompropvalue>"]; 201 | C["<multiply>"]; 202 | 203 | D["<!-- baseColorTexture -->
<image>"]; 204 | E["<!-- channel extraction -->"]; 205 | F["<!-- opt. colorspace transformation -->"]; 206 | G["<multiply>"]; 207 | 208 | H["<gltf_pbr>                     
      <!-- base_color input -->
</gltf_pbr>                     "]; 209 | 210 | A-->C 211 | B-->C 212 | C-->G 213 | 214 | D-->E 215 | E-->F 216 | F-->G 217 | 218 | G-->H 219 | 220 | classDef default fill:#fff,stroke:#999 221 | ``` 222 | Depending on conversion options, the graph may contain additional color space transformation nodes. 223 | 224 | ### Explicit tangents 225 | Normal mapped assets require tangents generated by the MikkTSpace algorithm. 226 | These are four-component vectors, with the last component specifiying the handedness of the bitangent. 227 | 228 | guc constructs the tangent space from two authored primvars, _tangents_ and _bitangentSigns_, which are 229 | read directly into a custom normal map node implementation, accounting for a limitation 230 | of MaterialX's `` node (see [Ecosystem Limitations](Ecosystem_Limitations.md)). 231 | 232 | 233 | ## Future improvements 234 | 235 | There are a number of improvements that can be made to the mapping process. 236 | 237 | For instance, the MaterialX graph complexity can be reduced with the introduction of helper nodes. 238 | Resulting USD assets could make use of payloads, and in general, incremental changes 239 | motivated by the [USD Asset WG Structure Guidelines](https://github.com/pablode/usd-wg-assets/blob/main/docs/asset-structure-guidelines.md) are to be expected. 240 | 241 | Lastly, user feedback is welcome in ensuring that assets conform to best practices. 242 | -------------------------------------------------------------------------------- /extern/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # cgltf 2 | add_library(cgltf INTERFACE) 3 | set_target_properties(cgltf PROPERTIES LINKER_LANGUAGE C) 4 | target_include_directories(cgltf INTERFACE cgltf) 5 | 6 | # MikkTSpace 7 | add_library(MikkTSpace STATIC MikkTSpace/mikktspace.c MikkTSpace/mikktspace.h) 8 | set_target_properties(MikkTSpace PROPERTIES LINKER_LANGUAGE C) 9 | target_include_directories(MikkTSpace 10 | PUBLIC $ 11 | PUBLIC $ 12 | ) 13 | 14 | # stb_image 15 | add_library(stb_image INTERFACE) 16 | set_target_properties(stb_image PROPERTIES LINKER_LANGUAGE C) 17 | target_include_directories(stb_image INTERFACE stb_image) 18 | 19 | # cargs 20 | add_subdirectory(cargs) 21 | 22 | # meshopt 23 | add_subdirectory(meshoptimizer) 24 | -------------------------------------------------------------------------------- /extern/stb_image/LICENSE: -------------------------------------------------------------------------------- 1 | This software is available under 2 licenses -- choose whichever you prefer. 2 | ------------------------------------------------------------------------------ 3 | ALTERNATIVE A - MIT License 4 | Copyright (c) 2017 Sean Barrett 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | ------------------------------------------------------------------------------ 21 | ALTERNATIVE B - Public Domain (www.unlicense.org) 22 | This is free and unencumbered software released into the public domain. 23 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 24 | software, either in source code form or as a compiled binary, for any purpose, 25 | commercial or non-commercial, and by any means. 26 | In jurisdictions that recognize copyright laws, the author or authors of this 27 | software dedicate any and all copyright interest in the software to the public 28 | domain. We make this dedication for the benefit of the public at large and to 29 | the detriment of our heirs and successors. We intend this dedication to be an 30 | overt act of relinquishment in perpetuity of all present and future rights to 31 | this software under copyright law. 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 36 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 37 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 38 | -------------------------------------------------------------------------------- /preview_glTFSampleViewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablode/guc/d12dc042ee5e48f9e5a947225475495691d7b452/preview_glTFSampleViewer.png -------------------------------------------------------------------------------- /preview_hdStorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablode/guc/d12dc042ee5e48f9e5a947225475495691d7b452/preview_hdStorm.png -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(libguc) 2 | 3 | if(GUC_BUILD_EXECUTABLE) 4 | add_subdirectory(guc) 5 | endif() 6 | -------------------------------------------------------------------------------- /src/guc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(guc main.c) 2 | 3 | set_target_properties( 4 | guc 5 | PROPERTIES 6 | LINKER_LANGUAGE C 7 | C_STANDARD 11 8 | C_STANDARD_REQUIRED ON 9 | C_EXTENSIONS OFF 10 | ) 11 | 12 | target_link_libraries( 13 | guc 14 | PRIVATE 15 | libguc 16 | cargs 17 | ) 18 | 19 | set_target_properties( 20 | guc 21 | PROPERTIES 22 | # Search libguc in executable dir on MacOS 23 | BUILD_RPATH_USE_ORIGIN ON 24 | ) 25 | 26 | # Generate C header file with a string that contains the license text 27 | file(READ "${PROJECT_SOURCE_DIR}/LICENSE" LICENSE_TEXT) 28 | string(REGEX REPLACE "\"" "\\\\\"" LICENSE_TEXT "${LICENSE_TEXT}") 29 | # Instead of concatenating a single string with backslashes, we concatenate 30 | # multiple strings in order to prevent MSVC from throwing C2026: "string too big". 31 | # This is why we manually escape quotation marks and do not use configure_file's 32 | # ESCAPE_QUOTES argument. 33 | string(REGEX REPLACE "\n" "\\\\n\" \\\\\\n \"" LICENSE_TEXT "${LICENSE_TEXT}") 34 | configure_file(license.h.in license.h @ONLY) 35 | 36 | target_include_directories( 37 | guc 38 | PRIVATE 39 | # Allow discovery of generated license.h in build dir 40 | ${CMAKE_CURRENT_BINARY_DIR} 41 | ) 42 | 43 | install( 44 | TARGETS guc 45 | COMPONENT guc 46 | ) 47 | -------------------------------------------------------------------------------- /src/guc/license.h.in: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2024 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | const char* license_text = "@LICENSE_TEXT@"; 20 | -------------------------------------------------------------------------------- /src/guc/main.c: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | #include 25 | 26 | #include "license.h" 27 | 28 | static struct cag_option cmd_options[] = { 29 | { 30 | .identifier = 'm', 31 | .access_letters = "m", 32 | .access_name = "emit-mtlx", 33 | .value_name = NULL, 34 | .description = "Emit MaterialX materials in addition to UsdPreviewSurfaces" 35 | }, 36 | { 37 | .identifier = 'u', 38 | .access_letters = "u", 39 | .access_name = "mtlx-as-usdshade", 40 | .value_name = NULL, 41 | .description = "Convert and inline MaterialX materials into the USD layer using UsdMtlx" 42 | }, 43 | { 44 | .identifier = 'v', 45 | .access_letters = "v", 46 | .access_name = "default-material-variant", 47 | .value_name = "", 48 | .description = "Index of the material variant that is selected by default" 49 | }, 50 | { 51 | .identifier = 'l', 52 | .access_letters = "l", 53 | .access_name = "licenses", 54 | .value_name = NULL, 55 | .description = "Print the license of guc and third-party libraries" 56 | }, 57 | { 58 | .identifier = 'h', 59 | .access_letters = "h", 60 | .access_name = "help", 61 | .value_name = NULL, 62 | .description = "Show the command help" 63 | } 64 | }; 65 | 66 | int main(int argc, char* argv[]) 67 | { 68 | struct guc_options options = { 69 | .emit_mtlx = false, 70 | .mtlx_as_usdshade = false, 71 | .default_material_variant = 0 72 | }; 73 | 74 | cag_option_context context; 75 | cag_option_init(&context, cmd_options, CAG_ARRAY_SIZE(cmd_options), argc, argv); 76 | while (cag_option_fetch(&context)) 77 | { 78 | switch (cag_option_get_identifier(&context)) 79 | { 80 | case 'm': 81 | options.emit_mtlx = true; 82 | break; 83 | case 'u': 84 | options.mtlx_as_usdshade = true; 85 | break; 86 | case 'v': { 87 | const char* value = cag_option_get_value(&context); 88 | options.default_material_variant = atoi(value); // fall back to 0 on error 89 | break; 90 | } 91 | case 'l': { 92 | printf("%s\n", license_text); 93 | return EXIT_SUCCESS; 94 | } 95 | case 'h': { 96 | printf("guc %s - glTF to USD converter\n\n", GUC_VERSION_STRING); 97 | printf("Usage: guc [options] [--] \n\n"); 98 | printf("Options:\n"); 99 | cag_option_print(cmd_options, CAG_ARRAY_SIZE(cmd_options), stdout); 100 | return EXIT_SUCCESS; 101 | } 102 | case '?': { 103 | cag_option_print_error(&context, stderr); 104 | return EXIT_FAILURE; 105 | } 106 | } 107 | } 108 | 109 | int param_index = cag_option_get_index(&context); 110 | 111 | if (param_index < argc && !strcmp(argv[param_index], "--")) 112 | { 113 | param_index++; 114 | } 115 | 116 | if (param_index >= argc) 117 | { 118 | fprintf(stderr, "Missing positional argument .\n"); 119 | return EXIT_FAILURE; 120 | } 121 | if ((param_index + 1) >= argc) 122 | { 123 | fprintf(stderr, "Missing positional argument .\n"); 124 | return EXIT_FAILURE; 125 | } 126 | if ((argc - param_index) != 2) 127 | { 128 | fprintf(stderr, "Too many positional arguments.\n"); 129 | return EXIT_FAILURE; 130 | } 131 | 132 | const char* gltf_path = argv[param_index]; 133 | const char* usd_path = argv[param_index + 1]; 134 | 135 | bool result = guc_convert(gltf_path, usd_path, &options); 136 | 137 | return result ? EXIT_SUCCESS : EXIT_FAILURE; 138 | } 139 | -------------------------------------------------------------------------------- /src/libguc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 2 | # In this CMakeLists.txt file, two targets are defined: 3 | # 1. libguc, a library with a USD-independent C-API 4 | # 2. usdGlTF, the USD SDF plugin that gets installed as a DLL only 5 | # 6 | # Both projects share most source files and dependencies. They differ in some 7 | # additionally included file, their build config, installation properties and 8 | # whether a header include dir is installed, or not. 9 | # 10 | 11 | set(LIBGUC_SHARED_SRCS 12 | src/cgltf_util.h 13 | src/cgltf_util.cpp 14 | src/usdpreviewsurface.h 15 | src/usdpreviewsurface.cpp 16 | src/materialx.h 17 | src/materialx.cpp 18 | src/mesh.h 19 | src/mesh.cpp 20 | src/naming.h 21 | src/naming.cpp 22 | src/converter.h 23 | src/converter.cpp 24 | src/debugCodes.h 25 | src/debugCodes.cpp 26 | src/image.h 27 | src/image.cpp 28 | ) 29 | 30 | set(LIBGUC_PUBLIC_HEADERS 31 | include/guc.h 32 | ) 33 | 34 | set(LIBGUC_DEFINES 35 | GUC_VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR} 36 | GUC_VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR} 37 | GUC_VERSION_PATCH=${CMAKE_PROJECT_VERSION_PATCH} 38 | GUC_VERSION_STRING="${CMAKE_PROJECT_VERSION}" 39 | ) 40 | 41 | if(TARGET usd_ms) 42 | set(LIBGUC_USD_LIBS usd_ms) 43 | else() 44 | set(LIBGUC_USD_LIBS usd usdGeom usdLux usdShade usdUtils usdMtlx) 45 | endif() 46 | 47 | set(LIBGUC_SHARED_LIBRARIES 48 | # Header-only library without link dependencies -- 49 | # we can get away with not installing the target. 50 | $ 51 | MikkTSpace 52 | MaterialXCore 53 | MaterialXFormat 54 | meshoptimizer 55 | ${LIBGUC_USD_LIBS} 56 | ) 57 | 58 | if(OpenImageIO_FOUND) 59 | list(APPEND LIBGUC_DEFINES "GUC_USE_OIIO") 60 | list(APPEND LIBGUC_SHARED_LIBRARIES OpenImageIO::OpenImageIO) 61 | else() 62 | list(APPEND LIBGUC_SHARED_LIBRARIES $) 63 | endif() 64 | 65 | if(draco_FOUND) 66 | list(APPEND LIBGUC_DEFINES "GUC_USE_DRACO") 67 | list(APPEND LIBGUC_SHARED_LIBRARIES ${DRACO_LIBRARIES}) 68 | endif() 69 | 70 | # 71 | # libguc 72 | # 73 | add_library( 74 | libguc 75 | ${LIBGUC_SHARED_SRCS} 76 | ${LIBGUC_PUBLIC_HEADERS} 77 | src/guc.cpp 78 | ) 79 | 80 | target_link_libraries(libguc PRIVATE ${LIBGUC_SHARED_LIBRARIES}) 81 | 82 | target_include_directories(libguc 83 | PUBLIC 84 | $ 85 | $ 86 | ) 87 | 88 | if(draco_FOUND) 89 | target_include_directories(libguc PRIVATE ${DRACO_INCLUDE_DIRS}) 90 | endif() 91 | 92 | set_target_properties( 93 | libguc 94 | PROPERTIES 95 | PUBLIC_HEADER "${LIBGUC_PUBLIC_HEADERS}" 96 | # We don't want 'liblibguc' on Linux/Unix 97 | PREFIX "" 98 | # fPIC for static linking because the USD libraries we use are built with it 99 | POSITION_INDEPENDENT_CODE ON 100 | ) 101 | 102 | if(WIN32 AND BUILD_SHARED_LIBS) 103 | set_target_properties(libguc PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE) 104 | endif() 105 | 106 | target_compile_definitions( 107 | libguc 108 | PUBLIC 109 | ${LIBGUC_DEFINES} 110 | PRIVATE 111 | # Workaround for https://github.com/PixarAnimationStudios/USD/issues/1471#issuecomment-799813477 112 | "$<$,$>:TBB_USE_DEBUG>" 113 | ) 114 | 115 | # Installation and cmake config file 116 | install(TARGETS libguc MikkTSpace EXPORT gucTargets COMPONENT libguc) 117 | 118 | include(CMakePackageConfigHelpers) 119 | write_basic_package_version_file( 120 | gucConfigVersion.cmake 121 | VERSION ${PACKAGE_VERSION} 122 | COMPATIBILITY AnyNewerVersion 123 | ) 124 | install(EXPORT gucTargets 125 | FILE gucTargets.cmake 126 | DESTINATION lib/cmake/guc 127 | COMPONENT libguc 128 | ) 129 | 130 | configure_file("${PROJECT_SOURCE_DIR}/cmake/gucConfig.cmake.in" gucConfig.cmake @ONLY) 131 | install( 132 | FILES "${CMAKE_CURRENT_BINARY_DIR}/gucConfig.cmake" 133 | "${CMAKE_CURRENT_BINARY_DIR}/gucConfigVersion.cmake" 134 | DESTINATION lib/cmake/guc 135 | COMPONENT libguc 136 | ) 137 | 138 | # 139 | # usdGlTF 140 | # 141 | if(GUC_BUILD_USDGLTF) 142 | add_library( 143 | usdGlTF SHARED 144 | ${LIBGUC_SHARED_SRCS} 145 | src/fileFormat.h 146 | src/fileFormat.cpp 147 | ) 148 | 149 | target_link_libraries( 150 | usdGlTF 151 | PRIVATE 152 | ${LIBGUC_SHARED_LIBRARIES} 153 | ) 154 | 155 | if(TARGET usd_ms) 156 | set(LIBGUC_SDF_LIB usd_ms) 157 | else() 158 | set(LIBGUC_SDF_LIB sdf) 159 | endif() 160 | 161 | target_link_libraries( 162 | usdGlTF 163 | PUBLIC 164 | ${LIBGUC_SDF_LIB} 165 | ) 166 | 167 | target_include_directories(usdGlTF PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") 168 | 169 | if(draco_FOUND) 170 | target_include_directories(usdGlTF PRIVATE ${DRACO_INCLUDE_DIRS}) 171 | endif() 172 | 173 | target_compile_definitions( 174 | usdGlTF 175 | PRIVATE 176 | ${LIBGUC_DEFINES} 177 | # Workaround for https://github.com/PixarAnimationStudios/USD/issues/1471#issuecomment-799813477 178 | "$<$,$>:TBB_USE_DEBUG>" 179 | ) 180 | 181 | # We don't want 'liblibguc' on Linux/Unix 182 | set_target_properties(usdGlTF PROPERTIES PREFIX "") 183 | 184 | # Is installed to "plugins/usd" dir; remove the default "lib" folder prefix. 185 | install(TARGETS usdGlTF COMPONENT usdGlTF DESTINATION ".") 186 | 187 | set(PLUGINFO_PATH "${CMAKE_CURRENT_BINARY_DIR}/plugInfo.json") 188 | set(PLUG_INFO_LIBRARY_PATH "../usdGlTF${CMAKE_SHARED_LIBRARY_SUFFIX}") 189 | configure_file(plugInfo.json.in "${PLUGINFO_PATH}" @ONLY) 190 | 191 | install( 192 | FILES "${PLUGINFO_PATH}" 193 | DESTINATION "usdGlTF/resources" 194 | COMPONENT usdGlTF 195 | ) 196 | endif() 197 | -------------------------------------------------------------------------------- /src/libguc/include/guc.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #ifdef __cplusplus 20 | extern "C" { 21 | #else 22 | #include 23 | #endif 24 | 25 | struct guc_options 26 | { 27 | // Generate and reference a MaterialX document containing an accurate translation 28 | // of the glTF materials. The document is serialized to a file if UsdShade inlining 29 | // is not active. 30 | bool emit_mtlx; 31 | 32 | // Parse the generated MaterialX document with UsdMtlx to a UsdShade representation 33 | // and inline it into the USD file. Note that information will be discarded as not 34 | // all MaterialX concepts can be encoded in UsdShade: 35 | // https://graphics.pixar.com/usd/release/api/usd_mtlx_page_front.html 36 | // Files generated without this option may be better supported by future USD 37 | // versions. 38 | bool mtlx_as_usdshade; 39 | 40 | // If the asset supports the KHR_materials_variants extension, select the material 41 | // variant at the given index by default. 42 | int default_material_variant; 43 | }; 44 | 45 | bool guc_convert(const char* gltf_path, 46 | const char* usd_path, 47 | const struct guc_options* options); 48 | 49 | #ifdef __cplusplus 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /src/libguc/plugInfo.json.in: -------------------------------------------------------------------------------- 1 | { 2 | "Plugins": [ 3 | { 4 | "Info": { 5 | "Types": { 6 | "UsdGlTFFileFormat": { 7 | "bases": [ 8 | "SdfFileFormat" 9 | ], 10 | "displayName": "glTF File Format", 11 | "extensions": [ 12 | "gltf", "glb" 13 | ], 14 | "supportsWriting": false, 15 | "formatId": "gltf", 16 | "primary": true, 17 | "target": "usd" 18 | } 19 | }, 20 | "SdfMetadata": { 21 | "emitMtlx": { 22 | "type": "bool", 23 | "default": "false", 24 | "displayGroup": "Core", 25 | "appliesTo": [ "layers" ], 26 | "documentation:": "Emit MaterialX materials in addition to UsdPreviewSurfaces." 27 | } 28 | } 29 | }, 30 | "LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", 31 | "Name": "usdGlTF", 32 | "ResourcePath": "resources", 33 | "Root": "..", 34 | "Type": "library" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/libguc/src/cgltf_util.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "cgltf_util.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #define CGLTF_IMPLEMENTATION 26 | #include 27 | 28 | #include 29 | 30 | #ifdef GUC_USE_DRACO 31 | #include 32 | #include 33 | #endif 34 | 35 | #include 36 | #include 37 | 38 | #include "debugCodes.h" 39 | 40 | using namespace PXR_NS; 41 | 42 | namespace detail 43 | { 44 | constexpr static const char* GLTF_EXT_MESHOPT_COMPRESSION_EXTENSION_NAME = "EXT_meshopt_compression"; 45 | #ifdef GUC_USE_DRACO 46 | constexpr static const char* GLTF_KHR_DRACO_MESH_COMPRESSION_EXTENSION_NAME = "KHR_draco_mesh_compression"; 47 | #endif 48 | 49 | bool extensionSupported(const char* name) 50 | { 51 | return strcmp(name, GLTF_EXT_MESHOPT_COMPRESSION_EXTENSION_NAME) == 0 || 52 | #ifdef GUC_USE_DRACO 53 | strcmp(name, GLTF_KHR_DRACO_MESH_COMPRESSION_EXTENSION_NAME) == 0 || 54 | #endif 55 | strcmp(name, "KHR_lights_punctual") == 0 || 56 | strcmp(name, "KHR_materials_clearcoat") == 0 || 57 | strcmp(name, "KHR_materials_emissive_strength") == 0 || 58 | strcmp(name, "KHR_materials_ior") == 0 || 59 | strcmp(name, "KHR_materials_iridescence") == 0 || 60 | strcmp(name, "KHR_materials_pbrSpecularGlossiness") == 0 || 61 | strcmp(name, "KHR_materials_sheen") == 0 || 62 | strcmp(name, "KHR_materials_specular") == 0 || 63 | strcmp(name, "KHR_materials_transmission") == 0 || 64 | strcmp(name, "KHR_materials_unlit") == 0 || 65 | strcmp(name, "KHR_materials_variants") == 0 || 66 | strcmp(name, "KHR_materials_volume") == 0 || 67 | strcmp(name, "KHR_mesh_quantization") == 0 || 68 | strcmp(name, "KHR_texture_transform") == 0; 69 | } 70 | 71 | struct BufferHolder 72 | { 73 | std::unordered_map> map; 74 | }; 75 | 76 | cgltf_result readFile(const cgltf_memory_options* memory_options, 77 | const cgltf_file_options* file_options, 78 | const char* path, 79 | cgltf_size* size, 80 | void** data) 81 | { 82 | TF_DEBUG(GUC).Msg("reading file %s\n", path); 83 | 84 | ArResolver& resolver = ArGetResolver(); 85 | std::string identifier = resolver.CreateIdentifier(path); 86 | TF_DEBUG(GUC).Msg("normalized path to %s\n", identifier.c_str()); 87 | 88 | ArResolvedPath resolvedPath = resolver.Resolve(identifier); 89 | if (!resolvedPath) 90 | { 91 | TF_RUNTIME_ERROR("unable to resolve %s", path); 92 | return cgltf_result_file_not_found; 93 | } 94 | 95 | std::string resolvedPathStr = resolvedPath.GetPathString(); 96 | TF_DEBUG(GUC).Msg("resolved path to %s\n", resolvedPathStr.c_str()); 97 | 98 | std::shared_ptr asset = resolver.OpenAsset(ArResolvedPath(path)); 99 | if (!asset) 100 | { 101 | TF_RUNTIME_ERROR("unable to open asset %s", resolvedPathStr.c_str()); 102 | return cgltf_result_file_not_found; 103 | } 104 | 105 | std::shared_ptr buffer = asset->GetBuffer(); 106 | if (!buffer) 107 | { 108 | TF_RUNTIME_ERROR("unable to open buffer for %s", resolvedPathStr.c_str()); 109 | return cgltf_result_io_error; 110 | } 111 | 112 | const char* bufferPtr = buffer.get(); 113 | (*size) = asset->GetSize(); 114 | (*data) = (void*) bufferPtr; 115 | 116 | auto bufferHolder = (BufferHolder*) file_options->user_data; 117 | bufferHolder->map[bufferPtr] = buffer; 118 | 119 | return cgltf_result_success; 120 | } 121 | 122 | void releaseFile(const cgltf_memory_options* memory_options, 123 | const cgltf_file_options* file_options, 124 | void* data) 125 | { 126 | auto bufferPtr = (const char*) data; 127 | auto bufferHolder = (BufferHolder*) file_options->user_data; 128 | bufferHolder->map.erase(bufferPtr); 129 | } 130 | 131 | #ifdef GUC_USE_DRACO 132 | template 133 | bool convertDracoVertexAttributes(const draco::PointAttribute* attribute, 134 | size_t vertexCount, 135 | uint8_t* result) 136 | { 137 | uint32_t elemSize = sizeof(T) * attribute->num_components(); 138 | 139 | const static uint32_t MAX_COMPONENT_COUNT = 4; 140 | 141 | T elems[MAX_COMPONENT_COUNT]; 142 | for (size_t i = 0; i < vertexCount; i++) 143 | { 144 | draco::PointIndex pointIndex(i); 145 | draco::AttributeValueIndex valueIndex = attribute->mapped_index(pointIndex); 146 | 147 | if (!attribute->ConvertValue(valueIndex, attribute->num_components(), elems)) 148 | { 149 | return false; 150 | } 151 | 152 | uint32_t elemOffset = i * elemSize; 153 | memcpy(&result[elemOffset], &elems[0], elemSize); 154 | } 155 | return true; 156 | } 157 | 158 | bool convertDracoVertexAttributes(cgltf_component_type componentType, 159 | const draco::PointAttribute* attribute, 160 | size_t vertexCount, 161 | uint8_t* result) 162 | { 163 | bool success = false; 164 | switch (componentType) 165 | { 166 | case cgltf_component_type_r_8: 167 | success = convertDracoVertexAttributes(attribute, vertexCount, result); 168 | case cgltf_component_type_r_8u: 169 | success = convertDracoVertexAttributes(attribute, vertexCount, result); 170 | case cgltf_component_type_r_16: 171 | success = convertDracoVertexAttributes(attribute, vertexCount, result); 172 | case cgltf_component_type_r_16u: 173 | success = convertDracoVertexAttributes(attribute, vertexCount, result); 174 | case cgltf_component_type_r_32u: 175 | success = convertDracoVertexAttributes(attribute, vertexCount, result); 176 | case cgltf_component_type_r_32f: 177 | success = convertDracoVertexAttributes(attribute, vertexCount, result); 178 | break; 179 | default: 180 | break; 181 | } 182 | return success; 183 | } 184 | 185 | bool decompressDraco(cgltf_data* data) 186 | { 187 | using EncodedAttributes = std::map; 188 | 189 | std::map attributeBuffersToDecompress; 190 | 191 | // 192 | // Phase 1: Collect draco buffers and attributes to decode. Multiple prims can reference the same buffer 193 | // with a disjunct or overlapping set of attributes, which is why we use the map/set construct. 194 | // We also overwrite the primitive indices data source which we fill in the second phase. 195 | // 196 | for (size_t i = 0; i < data->meshes_count; ++i) 197 | { 198 | const cgltf_mesh& mesh = data->meshes[i]; 199 | 200 | for (size_t j = 0; j < mesh.primitives_count; j++) 201 | { 202 | const cgltf_primitive& primitive = mesh.primitives[j]; 203 | if (!primitive.has_draco_mesh_compression) 204 | { 205 | continue; 206 | } 207 | 208 | const cgltf_draco_mesh_compression* draco = &primitive.draco_mesh_compression; 209 | if (attributeBuffersToDecompress.count(draco) == 0) 210 | { 211 | attributeBuffersToDecompress[draco] = {}; 212 | } 213 | 214 | EncodedAttributes& mapEnty = attributeBuffersToDecompress[draco]; 215 | 216 | // Spec: "the attributes defined in the extension must be a subset of the attributes of the primitive." 217 | for (size_t i = 0; i < draco->attributes_count; i++) 218 | { 219 | const cgltf_attribute* dracoAttr = &draco->attributes[i]; 220 | 221 | cgltf_attribute* srcAttr = nullptr; 222 | for (size_t j = 0; j < primitive.attributes_count; j++) 223 | { 224 | cgltf_attribute* attr = &primitive.attributes[i]; 225 | 226 | if (strcmp(attr->name, dracoAttr->name) == 0) 227 | { 228 | srcAttr = attr; 229 | break; 230 | } 231 | } 232 | TF_AXIOM(srcAttr); // ensured by validation 233 | 234 | auto dracoUid = int(cgltf_accessor_index(data, dracoAttr->data)); 235 | mapEnty[dracoUid] = srcAttr->data; 236 | } 237 | 238 | primitive.indices->offset = 0; 239 | primitive.indices->stride = sizeof(uint32_t); 240 | primitive.indices->buffer_view = draco->buffer_view; 241 | } 242 | } 243 | 244 | // 245 | // Phase 2: In this second phase, we decode attributes from the draco buffers. We can't allocate new 246 | // accessors, buffers and buffer views because this invalidates all of cgltf's pointer references. 247 | // Instead, we allocate the .data field of the draco buffer view, which takes precedence over the 248 | // encoded buffer, and use it as the decoded data source. 249 | // 250 | for (auto it = attributeBuffersToDecompress.begin(); it != attributeBuffersToDecompress.end(); ++it) 251 | { 252 | // Decompress into draco mesh 253 | const cgltf_draco_mesh_compression* draco = it->first; 254 | cgltf_buffer_view* bufferView = draco->buffer_view; 255 | cgltf_buffer* buffer = bufferView->buffer; 256 | 257 | const char* bufferData = &((const char*) buffer->data)[bufferView->offset]; 258 | 259 | draco::DecoderBuffer decoderBuffer; 260 | decoderBuffer.Init(bufferData, bufferView->size); 261 | 262 | auto geomType = draco::Decoder::GetEncodedGeometryType(&decoderBuffer); 263 | if (!geomType.ok() || geomType.value() != draco::TRIANGULAR_MESH) 264 | { 265 | TF_RUNTIME_ERROR("unsupported Draco geometry type"); 266 | return false; 267 | } 268 | 269 | draco::Decoder decoder; 270 | auto decodeResult = decoder.DecodeMeshFromBuffer(&decoderBuffer); 271 | if (!decodeResult.ok()) 272 | { 273 | TF_RUNTIME_ERROR("Draco failed to decode mesh from buffer"); 274 | return false; 275 | } 276 | 277 | const std::unique_ptr& mesh = decodeResult.value(); 278 | 279 | // Allocate decoded buffer 280 | const EncodedAttributes& attrsToDecode = it->second; 281 | 282 | uint32_t vertexCount = mesh->num_points(); 283 | uint32_t faceCount = mesh->num_faces(); 284 | uint32_t indexCount = faceCount * 3; 285 | 286 | uint32_t indicesSize = indexCount * sizeof(uint32_t); 287 | 288 | size_t attributesSize = 0; 289 | for (const auto& c : attrsToDecode) 290 | { 291 | cgltf_accessor* srcAccessor = c.second; 292 | 293 | attributesSize += vertexCount * srcAccessor->stride; 294 | } 295 | 296 | bufferView->data = malloc(indicesSize + attributesSize); 297 | 298 | // Write decoded data 299 | auto baseIndex = draco::FaceIndex(0); 300 | TF_VERIFY(sizeof(mesh->face(baseIndex)[0]) == 4); 301 | memcpy(bufferView->data, &mesh->face(baseIndex)[0], indicesSize); 302 | 303 | size_t attributeOffset = indicesSize; 304 | for (const auto& c : attrsToDecode) 305 | { 306 | int dracoUid = c.first; 307 | 308 | const draco::PointAttribute* dracoAttr = mesh->GetAttributeByUniqueId(dracoUid); 309 | if (!dracoAttr) 310 | { 311 | TF_RUNTIME_ERROR("invalid Draco attribute uid"); 312 | return false; 313 | } 314 | 315 | cgltf_accessor* srcAccessor = c.second; 316 | 317 | TF_VERIFY(srcAccessor->count == vertexCount); 318 | if (!convertDracoVertexAttributes(srcAccessor->component_type, 319 | dracoAttr, 320 | vertexCount, 321 | &((uint8_t*) bufferView->data)[attributeOffset])) 322 | { 323 | TF_RUNTIME_ERROR("failed to decode Draco attribute"); 324 | return false; 325 | } 326 | 327 | srcAccessor->buffer_view = draco->buffer_view; 328 | srcAccessor->offset = attributeOffset; 329 | 330 | attributeOffset += vertexCount * srcAccessor->stride; 331 | } 332 | } 333 | 334 | return true; 335 | } 336 | #endif 337 | 338 | // Based on https://github.com/jkuhlmann/cgltf/pull/129 339 | cgltf_result decompressMeshopt(cgltf_data* data) 340 | { 341 | for (size_t i = 0; i < data->buffer_views_count; ++i) 342 | { 343 | cgltf_buffer_view& bufferView = data->buffer_views[i]; 344 | 345 | if (!bufferView.has_meshopt_compression) 346 | { 347 | continue; 348 | } 349 | 350 | const cgltf_meshopt_compression& mc = bufferView.meshopt_compression; 351 | 352 | const unsigned char* source = (const unsigned char*) mc.buffer->data; 353 | if (!source) 354 | { 355 | return cgltf_result_invalid_gltf; 356 | } 357 | 358 | source += mc.offset; 359 | 360 | void* result = malloc(mc.count * mc.stride); 361 | if (!result) 362 | { 363 | return cgltf_result_out_of_memory; 364 | } 365 | 366 | int errorCode = -1; 367 | 368 | switch (mc.mode) 369 | { 370 | default: 371 | case cgltf_meshopt_compression_mode_invalid: 372 | break; 373 | 374 | case cgltf_meshopt_compression_mode_attributes: 375 | errorCode = meshopt_decodeVertexBuffer(result, mc.count, mc.stride, source, mc.size); 376 | break; 377 | 378 | case cgltf_meshopt_compression_mode_triangles: 379 | errorCode = meshopt_decodeIndexBuffer(result, mc.count, mc.stride, source, mc.size); 380 | break; 381 | 382 | case cgltf_meshopt_compression_mode_indices: 383 | errorCode = meshopt_decodeIndexSequence(result, mc.count, mc.stride, source, mc.size); 384 | break; 385 | } 386 | 387 | if (errorCode != 0) 388 | { 389 | free(result); 390 | return cgltf_result_io_error; 391 | } 392 | 393 | switch (mc.filter) 394 | { 395 | default: 396 | case cgltf_meshopt_compression_filter_none: 397 | break; 398 | 399 | case cgltf_meshopt_compression_filter_octahedral: 400 | meshopt_decodeFilterOct(result, mc.count, mc.stride); 401 | break; 402 | 403 | case cgltf_meshopt_compression_filter_quaternion: 404 | meshopt_decodeFilterQuat(result, mc.count, mc.stride); 405 | break; 406 | 407 | case cgltf_meshopt_compression_filter_exponential: 408 | meshopt_decodeFilterExp(result, mc.count, mc.stride); 409 | break; 410 | } 411 | 412 | bufferView.data = result; 413 | } 414 | 415 | return cgltf_result_success; 416 | } 417 | } 418 | 419 | namespace guc 420 | { 421 | bool load_gltf(const char* gltfPath, cgltf_data** data) 422 | { 423 | detail::BufferHolder* bufferHolder = new detail::BufferHolder; 424 | 425 | cgltf_file_options fileOptions = {}; 426 | fileOptions.read = detail::readFile; 427 | fileOptions.release = detail::releaseFile; 428 | fileOptions.user_data = bufferHolder; 429 | 430 | cgltf_options options = {}; 431 | options.file = fileOptions; 432 | 433 | cgltf_result result = cgltf_parse_file(&options, gltfPath, data); 434 | 435 | if (result != cgltf_result_success) 436 | { 437 | TF_RUNTIME_ERROR("unable to parse glTF file: %s", cgltf_error_string(result)); 438 | delete bufferHolder; 439 | return false; 440 | } 441 | 442 | result = cgltf_load_buffers(&options, *data, gltfPath); 443 | 444 | if (result != cgltf_result_success) 445 | { 446 | TF_RUNTIME_ERROR("unable to load glTF buffers: %s", cgltf_error_string(result)); 447 | free_gltf(*data); 448 | return false; 449 | } 450 | 451 | result = cgltf_validate(*data); 452 | 453 | if (result != cgltf_result_success) 454 | { 455 | TF_RUNTIME_ERROR("unable to validate glTF: %s", cgltf_error_string(result)); 456 | free_gltf(*data); 457 | return false; 458 | } 459 | 460 | bool meshoptCompressionRequired = false; 461 | #ifdef GUC_USE_DRACO 462 | bool dracoMeshCompressionRequired = false; 463 | #endif 464 | 465 | for (size_t i = 0; i < (*data)->extensions_required_count; i++) 466 | { 467 | const char* ext = (*data)->extensions_required[i]; 468 | TF_DEBUG(GUC).Msg("extension required: %s\n", ext); 469 | 470 | if (strcmp(ext, detail::GLTF_EXT_MESHOPT_COMPRESSION_EXTENSION_NAME) == 0) 471 | { 472 | meshoptCompressionRequired = true; 473 | } 474 | 475 | #ifdef GUC_USE_DRACO 476 | if (strcmp(ext, detail::GLTF_KHR_DRACO_MESH_COMPRESSION_EXTENSION_NAME) == 0) 477 | { 478 | dracoMeshCompressionRequired = true; 479 | } 480 | #endif 481 | 482 | if (detail::extensionSupported(ext)) 483 | { 484 | continue; 485 | } 486 | 487 | TF_RUNTIME_ERROR("extension %s not supported", ext); 488 | free_gltf(*data); 489 | return false; 490 | } 491 | 492 | result = detail::decompressMeshopt(*data); 493 | 494 | if (result != cgltf_result_success) 495 | { 496 | const char* errStr = "unable to decode meshoptimizer data: %s"; 497 | 498 | if (meshoptCompressionRequired) 499 | { 500 | TF_RUNTIME_ERROR(errStr, guc::cgltf_error_string(result)); 501 | free_gltf(*data); 502 | return false; 503 | } 504 | 505 | TF_WARN(errStr, guc::cgltf_error_string(result)); 506 | } 507 | 508 | #ifdef GUC_USE_DRACO 509 | if (!detail::decompressDraco(*data)) 510 | { 511 | const char* errStr = "unable to decode Draco data"; 512 | 513 | if (dracoMeshCompressionRequired) 514 | { 515 | TF_RUNTIME_ERROR("%s", errStr); 516 | free_gltf(*data); 517 | return false; 518 | } 519 | 520 | TF_WARN("%s", errStr); 521 | } 522 | #endif 523 | 524 | for (size_t i = 0; i < (*data)->extensions_used_count; i++) 525 | { 526 | const char* ext = (*data)->extensions_used[i]; 527 | TF_DEBUG(GUC).Msg("extension used: %s\n", ext); 528 | 529 | if (detail::extensionSupported(ext)) 530 | { 531 | continue; 532 | } 533 | 534 | TF_WARN("optional extension %s not suppported", ext); 535 | } 536 | 537 | return true; 538 | } 539 | 540 | void free_gltf(cgltf_data* data) 541 | { 542 | auto bufferHolder = (detail::BufferHolder*) data->file.user_data; 543 | cgltf_free(data); // releases buffers in buffer holder 544 | delete bufferHolder; 545 | } 546 | 547 | const char* cgltf_error_string(cgltf_result result) 548 | { 549 | TF_VERIFY(result != cgltf_result_success); 550 | TF_VERIFY(result != cgltf_result_invalid_options); 551 | switch (result) 552 | { 553 | case cgltf_result_legacy_gltf: 554 | return "legacy glTF not supported"; 555 | case cgltf_result_data_too_short: 556 | case cgltf_result_invalid_json: 557 | case cgltf_result_invalid_gltf: 558 | return "malformed glTF"; 559 | case cgltf_result_unknown_format: 560 | return "unknown format"; 561 | case cgltf_result_file_not_found: 562 | return "file not found"; 563 | case cgltf_result_io_error: 564 | return "io error"; 565 | case cgltf_result_out_of_memory: 566 | return "out of memory"; 567 | default: 568 | return "unknown"; 569 | } 570 | } 571 | 572 | const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* primitive, 573 | const char* name) 574 | { 575 | for (size_t j = 0; j < primitive->attributes_count; j++) 576 | { 577 | const cgltf_attribute* attribute = &primitive->attributes[j]; 578 | 579 | if (strcmp(attribute->name, name) == 0) 580 | { 581 | return attribute->data; 582 | } 583 | } 584 | 585 | return nullptr; 586 | } 587 | 588 | bool cgltf_transform_required(const cgltf_texture_transform& transform) 589 | { 590 | return !GfIsClose(transform.offset[0], 0.0f, 1e-5f) || 591 | !GfIsClose(transform.offset[1], 0.0f, 1e-5f) || 592 | !GfIsClose(transform.rotation, 0.0f, 1e-5f) || 593 | !GfIsClose(transform.scale[0], 1.0f, 1e-5f) || 594 | !GfIsClose(transform.scale[1], 1.0f, 1e-5f); 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /src/libguc/src/cgltf_util.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | namespace guc 22 | { 23 | bool load_gltf(const char* gltfPath, cgltf_data** data); 24 | 25 | void free_gltf(cgltf_data* data); 26 | 27 | const char* cgltf_error_string(cgltf_result result); 28 | 29 | const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* primitive, 30 | const char* name); 31 | 32 | bool cgltf_transform_required(const cgltf_texture_transform& transform); 33 | } 34 | -------------------------------------------------------------------------------- /src/libguc/src/converter.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | #include "materialx.h" 30 | #include "usdpreviewsurface.h" 31 | 32 | namespace fs = std::filesystem; 33 | using namespace PXR_NS; 34 | 35 | namespace guc 36 | { 37 | class Converter 38 | { 39 | public: 40 | struct Params 41 | { 42 | fs::path srcDir; 43 | fs::path dstDir; 44 | fs::path mtlxFileName; 45 | bool copyExistingFiles; 46 | bool genRelativePaths; 47 | bool emitMtlx; 48 | bool mtlxAsUsdShade; 49 | int defaultMaterialVariant; 50 | }; 51 | 52 | public: 53 | Converter(const cgltf_data* data, UsdStageRefPtr stage, const Params& params); 54 | 55 | public: 56 | struct FileExport 57 | { 58 | std::string filePath; 59 | std::string refPath; 60 | }; 61 | using FileExports = std::vector; 62 | 63 | void convert(FileExports& fileExports); 64 | 65 | private: 66 | void createMaterials(FileExports& fileExports, bool createDefaultMaterial); 67 | void createNodesRecursively(const cgltf_node* nodeData, SdfPath path); 68 | void createOrOverCamera(const cgltf_camera* cameraData, SdfPath path); 69 | void createOrOverLight(const cgltf_light* lightData, SdfPath path); 70 | void createOrOverMesh(const cgltf_mesh* meshData, SdfPath path); 71 | void createMaterialBinding(UsdPrim& prim, const std::string& materialName); 72 | bool createPrimitive(const cgltf_primitive* primitiveData, SdfPath path, UsdPrim& prim); 73 | 74 | private: 75 | bool overridePrimInPathMap(void* dataPtr, const SdfPath& path, UsdPrim& prim); 76 | bool isValidTexture(const cgltf_texture_view& textureView); 77 | 78 | private: 79 | const cgltf_data* m_data; 80 | UsdStageRefPtr m_stage; 81 | const Params& m_params; 82 | 83 | private: 84 | ImageMetadataMap m_imgMetadata; 85 | MaterialX::DocumentPtr m_mtlxDoc; 86 | MaterialXMaterialConverter m_mtlxConverter; 87 | UsdPreviewSurfaceMaterialConverter m_usdPreviewSurfaceConverter; 88 | std::unordered_map m_uniquePaths; 89 | std::vector m_materialNames; 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/libguc/src/debugCodes.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "debugCodes.h" 18 | 19 | #include 20 | #include 21 | 22 | PXR_NAMESPACE_OPEN_SCOPE 23 | 24 | TF_REGISTRY_FUNCTION(TfDebug) 25 | { 26 | TF_DEBUG_ENVIRONMENT_SYMBOL(GUC, "GUC debug logging"); 27 | } 28 | 29 | PXR_NAMESPACE_CLOSE_SCOPE 30 | -------------------------------------------------------------------------------- /src/libguc/src/debugCodes.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | PXR_NAMESPACE_OPEN_SCOPE 22 | 23 | TF_DEBUG_CODES(GUC) 24 | 25 | PXR_NAMESPACE_CLOSE_SCOPE 26 | -------------------------------------------------------------------------------- /src/libguc/src/fileFormat.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "fileFormat.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | 31 | #include "cgltf_util.h" 32 | #include "converter.h" 33 | #include "debugCodes.h" 34 | 35 | using namespace guc; 36 | namespace fs = std::filesystem; 37 | 38 | PXR_NAMESPACE_OPEN_SCOPE 39 | 40 | TF_DEFINE_PUBLIC_TOKENS( 41 | UsdGlTFFileFormatTokens, 42 | USDGLTF_FILE_FORMAT_TOKENS 43 | ); 44 | 45 | TF_DEFINE_PRIVATE_TOKENS( 46 | _tokens, 47 | (gltf) 48 | (glb) 49 | (emitMtlx) 50 | ); 51 | 52 | TF_REGISTRY_FUNCTION(TfType) 53 | { 54 | SDF_DEFINE_FILE_FORMAT(UsdGlTFFileFormat, SdfFileFormat); 55 | } 56 | 57 | // glTF files can contain embedded images. In order to support them in our Sdf file 58 | // format plugin, we create a temporary directory for each glTF file, write the images 59 | // to it, and reference them. Afterwards, this directory gets deleted, however I was 60 | // unable to use the Sdf file format destructor for this purpose, as it does not seem 61 | // to get called. Instead, we instantiate an object with a static lifetime. 62 | class UsdGlTFTmpDirHolder 63 | { 64 | private: 65 | std::vector m_dirPaths; 66 | 67 | public: 68 | std::string makeDir() 69 | { 70 | std::string dir = ArchMakeTmpSubdir(ArchGetTmpDir(), "usdGlTF"); 71 | TF_DEBUG(GUC).Msg("created temp dir %s\n", dir.c_str()); 72 | m_dirPaths.push_back(dir); 73 | return dir; 74 | } 75 | ~UsdGlTFTmpDirHolder() 76 | { 77 | for (const std::string& dir : m_dirPaths) 78 | { 79 | TF_DEBUG(GUC).Msg("deleting temp dir %s\n", dir.c_str()); 80 | fs::remove_all(fs::path(dir)); 81 | } 82 | } 83 | }; 84 | 85 | static UsdGlTFTmpDirHolder s_tmpDirHolder; 86 | 87 | UsdGlTFFileFormat::UsdGlTFFileFormat() 88 | : SdfFileFormat( 89 | UsdGlTFFileFormatTokens->Id, 90 | UsdGlTFFileFormatTokens->Version, 91 | UsdGlTFFileFormatTokens->Target, 92 | /* extensions: */{ _tokens->gltf, _tokens->glb }) 93 | { 94 | } 95 | 96 | UsdGlTFFileFormat::~UsdGlTFFileFormat() 97 | { 98 | } 99 | 100 | SdfAbstractDataRefPtr UsdGlTFFileFormat::InitData(const FileFormatArguments& args) const 101 | { 102 | UsdGlTFDataRefPtr data(new UsdGlTFData()); 103 | 104 | auto emitMtlxIt = args.find(_tokens->emitMtlx.GetText()); 105 | if (emitMtlxIt != args.end()) 106 | { 107 | data->emitMtlx = TfUnstringify(emitMtlxIt->second); 108 | } 109 | 110 | return data; 111 | } 112 | 113 | bool UsdGlTFFileFormat::CanRead(const std::string& filePath) const 114 | { 115 | // FIXME: implement? In my tests, this is not even called. 116 | return true; 117 | } 118 | 119 | bool UsdGlTFFileFormat::Read(SdfLayer* layer, 120 | const std::string& resolvedPath, 121 | bool metadataOnly) const 122 | { 123 | fs::path srcDir = fs::path(resolvedPath).parent_path(); 124 | 125 | // In case we're accessing an image file in a sibling or parent folder, ArResolver needs 126 | // to know the root glTF directory in order to be able to resolve relative file paths. 127 | ArDefaultResolverContext ctx({srcDir.string()}); 128 | ArResolverContextBinder binder(ctx); 129 | 130 | cgltf_data* gltf_data = nullptr; 131 | if (!load_gltf(resolvedPath.c_str(), &gltf_data)) 132 | { 133 | TF_RUNTIME_ERROR("unable to load glTF file %s", resolvedPath.c_str()); 134 | return false; 135 | } 136 | 137 | SdfAbstractDataRefPtr layerData = InitData(layer->GetFileFormatArguments()); 138 | UsdGlTFDataConstPtr data = TfDynamic_cast(layerData); 139 | 140 | Converter::Params params = {}; 141 | params.srcDir = srcDir; 142 | params.dstDir = s_tmpDirHolder.makeDir(); 143 | params.mtlxFileName = ""; // Not needed because of Mtlx-as-UsdShade option 144 | params.copyExistingFiles = false; 145 | params.genRelativePaths = false; 146 | params.emitMtlx = data->emitMtlx; 147 | params.mtlxAsUsdShade = true; 148 | params.defaultMaterialVariant = 0; 149 | 150 | SdfLayerRefPtr tmpLayer = SdfLayer::CreateAnonymous(".usdc"); 151 | UsdStageRefPtr stage = UsdStage::Open(tmpLayer); 152 | 153 | Converter converter(gltf_data, stage, params); 154 | 155 | Converter::FileExports fileExports; // only used for USDZ 156 | converter.convert(fileExports); 157 | 158 | free_gltf(gltf_data); 159 | 160 | layer->TransferContent(tmpLayer); 161 | 162 | return true; 163 | } 164 | 165 | bool UsdGlTFFileFormat::ReadFromString(SdfLayer* layer, 166 | const std::string& str) const 167 | { 168 | // glTF files often reference other files (e.g. a .bin payload or images). 169 | // Hence, without a file location, most glTF files can not be loaded correctly. 170 | // TODO: but we could still try and return false on failure 171 | return false; 172 | } 173 | 174 | bool UsdGlTFFileFormat::WriteToString(const SdfLayer& layer, 175 | std::string* str, 176 | const std::string& comment) const 177 | { 178 | // Not supported, and never will be. Write USDC instead. 179 | SdfFileFormatConstPtr usdcFormat = SdfFileFormat::FindById(UsdUsdcFileFormatTokens->Id); 180 | return usdcFormat->WriteToString(layer, str, comment); 181 | } 182 | 183 | bool UsdGlTFFileFormat::WriteToStream(const SdfSpecHandle &spec, 184 | std::ostream& out, 185 | size_t indent) const 186 | { 187 | // Not supported, and never will be. Write USDC instead. 188 | SdfFileFormatConstPtr usdcFormat = SdfFileFormat::FindById(UsdUsdcFileFormatTokens->Id); 189 | return usdcFormat->WriteToStream(spec, out, indent); 190 | } 191 | 192 | void UsdGlTFFileFormat::ComposeFieldsForFileFormatArguments(const std::string& assetPath, 193 | const PcpDynamicFileFormatContext& context, 194 | FileFormatArguments* args, 195 | VtValue *dependencyContextData) const 196 | { 197 | VtValue emitMtlxValue; 198 | if (context.ComposeValue(_tokens->emitMtlx, &emitMtlxValue)) 199 | { 200 | (*args)[_tokens->emitMtlx] = TfStringify(emitMtlxValue); 201 | } 202 | } 203 | 204 | PXR_NAMESPACE_CLOSE_SCOPE 205 | -------------------------------------------------------------------------------- /src/libguc/src/fileFormat.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | PXR_NAMESPACE_OPEN_SCOPE 28 | 29 | #define USDGLTF_FILE_FORMAT_TOKENS \ 30 | ((Id, "gltf")) \ 31 | ((Version, GUC_VERSION_STRING)) \ 32 | ((Target, "usd")) 33 | 34 | TF_DECLARE_PUBLIC_TOKENS(UsdGlTFFileFormatTokens, USDGLTF_FILE_FORMAT_TOKENS); 35 | 36 | TF_DECLARE_WEAK_AND_REF_PTRS(UsdGlTFFileFormat); 37 | TF_DECLARE_WEAK_AND_REF_PTRS(UsdGlTFData); 38 | 39 | class UsdGlTFFileFormat : public SdfFileFormat, public PcpDynamicFileFormatInterface 40 | { 41 | protected: 42 | SDF_FILE_FORMAT_FACTORY_ACCESS; 43 | 44 | UsdGlTFFileFormat(); 45 | 46 | virtual ~UsdGlTFFileFormat(); 47 | 48 | public: 49 | SdfAbstractDataRefPtr InitData(const FileFormatArguments& args) const override; 50 | 51 | bool CanRead(const std::string &file) const override; 52 | 53 | bool Read(SdfLayer* layer, 54 | const std::string& resolvedPath, 55 | bool metadataOnly) const override; 56 | 57 | bool ReadFromString(SdfLayer* layer, 58 | const std::string& str) const override; 59 | 60 | bool WriteToString(const SdfLayer& layer, 61 | std::string* str, 62 | const std::string& comment = std::string()) 63 | const override; 64 | 65 | bool WriteToStream(const SdfSpecHandle &spec, 66 | std::ostream& out, 67 | size_t indent) const override; 68 | 69 | public: 70 | void ComposeFieldsForFileFormatArguments(const std::string& assetPath, 71 | const PcpDynamicFileFormatContext& context, 72 | FileFormatArguments* args, 73 | VtValue *dependencyContextData) const override; 74 | }; 75 | 76 | class UsdGlTFData : public SdfData 77 | { 78 | public: 79 | bool emitMtlx = false; 80 | }; 81 | 82 | PXR_NAMESPACE_CLOSE_SCOPE 83 | 84 | -------------------------------------------------------------------------------- /src/libguc/src/guc.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "guc.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | 29 | #include "debugCodes.h" 30 | #include "cgltf_util.h" 31 | #include "converter.h" 32 | 33 | using namespace guc; 34 | namespace fs = std::filesystem; 35 | 36 | bool convertToUsd(fs::path src_dir, 37 | const cgltf_data* gltf_data, 38 | fs::path usd_path, 39 | bool copyExistingFiles, 40 | const guc_options* options, 41 | Converter::FileExports& fileExports) 42 | { 43 | UsdStageRefPtr stage = UsdStage::CreateNew(usd_path.string()); 44 | if (!stage) 45 | { 46 | TF_RUNTIME_ERROR("unable to open stage at %s", usd_path.string().c_str()); 47 | return false; 48 | } 49 | 50 | Converter::Params params = {}; 51 | params.srcDir = src_dir; 52 | params.dstDir = usd_path.parent_path(); 53 | params.mtlxFileName = usd_path.filename().replace_extension(".mtlx"); 54 | params.copyExistingFiles = copyExistingFiles; 55 | params.genRelativePaths = true; 56 | params.emitMtlx = options->emit_mtlx; 57 | params.mtlxAsUsdShade = options->mtlx_as_usdshade; 58 | params.defaultMaterialVariant = options->default_material_variant; 59 | 60 | Converter converter(gltf_data, stage, params); 61 | 62 | converter.convert(fileExports); 63 | 64 | TF_DEBUG(GUC).Msg("saving stage to %s\n", usd_path.string().c_str()); 65 | stage->Save(); 66 | 67 | return true; 68 | } 69 | 70 | bool guc_convert(const char* gltf_path, 71 | const char* usd_path, 72 | const guc_options* options) 73 | { 74 | fs::path src_dir = fs::path(gltf_path).parent_path(); 75 | 76 | // We unconditionally use USD's asset resolver which needs to be able to resolve 77 | // relative file paths. 78 | ArDefaultResolverContext ctx({src_dir.string()}); 79 | ArResolverContextBinder binder(ctx); 80 | 81 | // The path we write USDA/USDC files to. If the user wants a USDZ file, we first 82 | // write these files to a temporary location, zip them, and copy the ZIP file to 83 | // the destination directory. 84 | fs::path final_usd_path = usd_path; 85 | fs::path base_usd_path = usd_path; 86 | fs::path dst_dir = base_usd_path.parent_path(); 87 | 88 | bool export_usdz = base_usd_path.extension() == ".usdz"; 89 | 90 | if (export_usdz) 91 | { 92 | dst_dir = ArchMakeTmpSubdir(ArchGetTmpDir(), "guc"); 93 | TF_DEBUG(GUC).Msg("using temp dir %s\n", dst_dir.string().c_str()); 94 | 95 | if (dst_dir.empty()) 96 | { 97 | TF_RUNTIME_ERROR("unable to create temporary directory for USDZ contents"); 98 | return false; 99 | } 100 | 101 | auto new_usd_filename = base_usd_path.replace_extension(".usdc").filename(); 102 | base_usd_path = dst_dir / new_usd_filename; 103 | TF_DEBUG(GUC).Msg("temporary USD path: %s\n", base_usd_path.string().c_str()); 104 | } 105 | 106 | cgltf_data* gltf_data = nullptr; 107 | if (!load_gltf(gltf_path, &gltf_data)) 108 | { 109 | TF_RUNTIME_ERROR("unable to load glTF file %s", gltf_path); 110 | return false; 111 | } 112 | 113 | bool copyExistingFiles = !export_usdz; // Add source files directly to archive in case of USDZ 114 | 115 | Converter::FileExports fileExports; 116 | bool result = convertToUsd(src_dir, gltf_data, base_usd_path, copyExistingFiles, options, fileExports); 117 | 118 | free_gltf(gltf_data); 119 | 120 | if (!result) 121 | { 122 | return false; 123 | } 124 | 125 | // In case of USDZ, we have now written the USDC file and all image files to a 126 | // temporary directory. Next, we invoke Pixar's USDZ API in order to zip them. 127 | if (export_usdz) 128 | { 129 | auto usdz_dst_dir = fs::absolute(final_usd_path).parent_path(); 130 | if (!fs::exists(usdz_dst_dir) && !fs::create_directories(usdz_dst_dir)) 131 | { 132 | TF_RUNTIME_ERROR("unable to create destination directory"); 133 | return false; 134 | } 135 | 136 | TF_DEBUG(GUC).Msg("creating USDZ archive %s\n", final_usd_path.string().c_str()); 137 | UsdZipFileWriter writer = UsdZipFileWriter::CreateNew(final_usd_path.string()); 138 | 139 | TF_DEBUG(GUC).Msg("adding %s to USDZ archive at ./%s\n", base_usd_path.string().c_str(), base_usd_path.filename().string().c_str()); 140 | if (writer.AddFile(base_usd_path.string(), base_usd_path.filename().string()) == "") 141 | { 142 | TF_RUNTIME_ERROR("unable to usdzip %s to %s", base_usd_path.string().c_str(), base_usd_path.filename().string().c_str()); 143 | return false; // Fatal error 144 | } 145 | for (const auto& fileExport : fileExports) 146 | { 147 | // We need to pass explicit source paths to UsdZipFileWriter because it uses the lower-level file 148 | // API instead of USD's ArResolver: https://github.com/PixarAnimationStudios/OpenUSD/issues/2374 149 | std::string srcPath = (src_dir / fileExport.filePath).string(); 150 | std::string dstPathInUsdz = fileExport.refPath; 151 | 152 | TF_DEBUG(GUC).Msg("adding %s to USDZ archive at ./%s\n", srcPath.c_str(), dstPathInUsdz.c_str()); 153 | if (writer.AddFile(srcPath, dstPathInUsdz) == "") 154 | { 155 | TF_RUNTIME_ERROR("unable to usdzip %s to %s", srcPath.c_str(), dstPathInUsdz.c_str()); 156 | // (non-fatal error) 157 | } 158 | } 159 | 160 | if (!writer.Save()) 161 | { 162 | TF_RUNTIME_ERROR("unable to save USDZ file"); 163 | return false; 164 | } 165 | 166 | TF_DEBUG(GUC).Msg("removing temporary directory %s\n", dst_dir.string().c_str()); 167 | fs::remove_all(dst_dir); 168 | } 169 | 170 | return true; 171 | } 172 | -------------------------------------------------------------------------------- /src/libguc/src/image.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "image.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #ifdef GUC_USE_OIIO 26 | #include 27 | #include 28 | #else 29 | #define STB_IMAGE_IMPLEMENTATION 30 | #include 31 | #endif 32 | 33 | #include 34 | #include 35 | 36 | #include "cgltf_util.h" 37 | #include "debugCodes.h" 38 | #include "naming.h" 39 | 40 | namespace fs = std::filesystem; 41 | 42 | namespace detail 43 | { 44 | using namespace guc; 45 | 46 | bool readImageDataFromBufferView(const cgltf_buffer_view* bufferView, 47 | size_t& dstSize, 48 | std::shared_ptr& dstData) 49 | { 50 | const char* srcData = (const char*) bufferView->buffer->data; 51 | 52 | if (bufferView->data) 53 | { 54 | TF_WARN("buffer view contains unsupported extension data"); 55 | } 56 | 57 | if (!srcData) 58 | { 59 | TF_RUNTIME_ERROR("unable to read buffer view; data is NULL"); 60 | return false; 61 | } 62 | 63 | srcData += bufferView->offset; 64 | 65 | dstSize = bufferView->size; 66 | dstData = std::shared_ptr(srcData, [](const char* ptr) { 67 | // Do not free the memory - it's owned by cgltf_data which is 68 | // externally managed and exceeds the lifetime of this shared_ptr. 69 | }); 70 | return true; 71 | } 72 | 73 | bool readImageDataFromBase64(const char* base64Str, 74 | size_t& size, 75 | std::shared_ptr& data) 76 | { 77 | size = strlen(base64Str); 78 | 79 | size_t padding = 0; 80 | if (size >= 2 && base64Str[size - 2] == '=') 81 | { 82 | padding = 2; 83 | } 84 | else if (size >= 1 && base64Str[size - 1] == '=') 85 | { 86 | padding = 1; 87 | } 88 | 89 | size = (size / 4) * 3 - padding; 90 | 91 | if (size == 0) 92 | { 93 | TF_WARN("base64 string has no payload"); 94 | return false; 95 | } 96 | 97 | void* dataPtr; 98 | cgltf_options options = {}; 99 | cgltf_result result = cgltf_load_buffer_base64(&options, size, base64Str, &dataPtr); 100 | if (result != cgltf_result_success) 101 | { 102 | TF_RUNTIME_ERROR("unable to read base64-encoded data"); 103 | return false; 104 | } 105 | 106 | data = std::shared_ptr((const char*) dataPtr, [](const char* ptr) { 107 | free((void*) ptr); 108 | }); 109 | return true; 110 | } 111 | 112 | bool readImageFromFile(const char* path, 113 | size_t& size, 114 | std::shared_ptr& data) 115 | { 116 | TF_DEBUG(GUC).Msg("reading image %s\n", path); 117 | 118 | ArResolver& resolver = ArGetResolver(); 119 | std::string identifier = resolver.CreateIdentifier(path); 120 | TF_DEBUG(GUC).Msg("normalized path to %s\n", identifier.c_str()); 121 | 122 | ArResolvedPath resolvedPath = resolver.Resolve(identifier); 123 | if (!resolvedPath) 124 | { 125 | TF_RUNTIME_ERROR("unable to resolve %s", path); 126 | return false; 127 | } 128 | 129 | std::string resolvedPathStr = resolvedPath.GetPathString(); 130 | TF_DEBUG(GUC).Msg("resolved path to %s\n", resolvedPathStr.c_str()); 131 | 132 | std::shared_ptr asset = resolver.OpenAsset(resolvedPath); 133 | if (!asset) 134 | { 135 | TF_RUNTIME_ERROR("unable to open asset %s", resolvedPathStr.c_str()); 136 | return false; 137 | } 138 | 139 | std::shared_ptr buffer = asset->GetBuffer(); 140 | if (!buffer) 141 | { 142 | TF_RUNTIME_ERROR("unable to open buffer for %s", resolvedPathStr.c_str()); 143 | return false; 144 | } 145 | 146 | size = asset->GetSize(); 147 | data = asset->GetBuffer(); 148 | return true; 149 | } 150 | 151 | bool writeImageData(const char* filePath, size_t size, const std::shared_ptr& data) 152 | { 153 | FILE* file = ArchOpenFile(filePath, "wb"); 154 | if (!file) 155 | { 156 | TF_RUNTIME_ERROR("unable to open file for writing: %s", filePath); 157 | return false; 158 | } 159 | 160 | int64_t result = ArchPWrite(file, data.get(), size, 0); 161 | 162 | ArchCloseFile(ArchFileNo(file)); 163 | 164 | if (result == -1) 165 | { 166 | TF_RUNTIME_ERROR("unable to read from file %s", filePath); 167 | return false; 168 | } 169 | return true; 170 | } 171 | 172 | bool readExtensionFromDataSignature(size_t size, const std::shared_ptr& data, std::string& extension) 173 | { 174 | const static std::array JPEG_HEADER = { 0xFF, 0xD8, 0xFF }; 175 | const static std::array PNG_HEADER = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; 176 | 177 | if (size < JPEG_HEADER.size()) 178 | { 179 | return false; 180 | } 181 | else if (memcmp(data.get(), JPEG_HEADER.data(), JPEG_HEADER.size()) == 0) 182 | { 183 | extension = ".jpg"; 184 | return true; 185 | } 186 | else if (size < PNG_HEADER.size()) 187 | { 188 | return false; 189 | } 190 | else if (memcmp(data.get(), PNG_HEADER.data(), PNG_HEADER.size()) == 0) 191 | { 192 | extension = ".png"; 193 | return true; 194 | } 195 | 196 | return false; 197 | } 198 | 199 | bool decodeImageMetadata(const std::shared_ptr& buffer, 200 | size_t bufferSize, 201 | const char* path, // only a hint 202 | int& channelCount) 203 | { 204 | #ifdef GUC_USE_OIIO 205 | OIIO::Filesystem::IOMemReader memReader((void*) buffer.get(), bufferSize); 206 | 207 | auto image = OIIO::ImageInput::open(path, nullptr, &memReader); 208 | if (image) 209 | { 210 | TF_VERIFY(image->supports("ioproxy")); 211 | 212 | const OIIO::ImageSpec& spec = image->spec(); 213 | channelCount = spec.nchannels; 214 | image->close(); 215 | return true; 216 | } 217 | else 218 | { 219 | std::string errStr = OIIO::geterror(); 220 | TF_RUNTIME_ERROR("OpenImageIO %s", errStr.c_str()); 221 | } 222 | #else 223 | int width, height; 224 | int ok = stbi_info_from_memory((const stbi_uc*) buffer.get(), bufferSize, &width, &height, &channelCount); 225 | if (ok) 226 | { 227 | return true; 228 | } 229 | #endif 230 | TF_RUNTIME_ERROR("unable to open file for reading: %s", path); 231 | return false; 232 | } 233 | 234 | bool readImageMetadata(const char* path, int& channelCount) 235 | { 236 | size_t size; 237 | std::shared_ptr data; 238 | if (!readImageFromFile(path, size, data)) 239 | { 240 | return false; 241 | } 242 | 243 | return decodeImageMetadata(data, size, path, channelCount); 244 | } 245 | 246 | std::optional processImage(const cgltf_image* image, 247 | const fs::path& srcDir, 248 | const fs::path& dstDir, 249 | bool copyExistingFiles, 250 | bool genRelativePaths, 251 | std::unordered_set& generatedFileNames) 252 | { 253 | size_t size = 0; 254 | std::shared_ptr data; 255 | std::string srcFilePath; 256 | 257 | const char* uri = image->uri; 258 | if (uri && strncmp(uri, "data:", 5) == 0) 259 | { 260 | const char* comma = strchr(uri, ','); 261 | if (comma && (comma - uri < 7 || strncmp(comma - 7, ";base64", 7) != 0)) 262 | { 263 | return std::nullopt; 264 | } 265 | 266 | if (!readImageDataFromBase64(comma + 1, size, data)) 267 | { 268 | return std::nullopt; 269 | } 270 | } 271 | else if (uri) 272 | { 273 | srcFilePath = std::string(uri); 274 | cgltf_decode_uri(srcFilePath.data()); 275 | 276 | if (!readImageFromFile(srcFilePath.c_str(), size, data)) 277 | { 278 | return std::nullopt; 279 | } 280 | } 281 | else if (image->buffer_view) 282 | { 283 | if (!readImageDataFromBufferView(image->buffer_view, size, data)) 284 | { 285 | return std::nullopt; 286 | } 287 | } 288 | else 289 | { 290 | TF_WARN("no image source; probably defined by unsupported extension"); 291 | return std::nullopt; 292 | } 293 | 294 | std::string fileExt; 295 | if (!readExtensionFromDataSignature(size, data, fileExt)) 296 | { 297 | // Doesn't matter what the mime type or path extension is if the image can not be read 298 | const char* hint = image->name; 299 | if (!srcFilePath.empty()) 300 | { 301 | hint = srcFilePath.c_str(); 302 | } 303 | if (!hint || !strcmp(hint, "")) 304 | { 305 | hint = "embedded"; 306 | } 307 | TF_RUNTIME_ERROR("unable to determine image data type (hint: %s)", hint); 308 | return std::nullopt; 309 | } 310 | 311 | bool genNewFileName = srcFilePath.empty() || genRelativePaths; 312 | bool writeNewFile = srcFilePath.empty() || copyExistingFiles; 313 | 314 | std::string dstRefPath = srcFilePath; 315 | if (genNewFileName) 316 | { 317 | std::string srcFileName = fs::path(srcFilePath).filename().string(); 318 | std::string dstFileName = makeUniqueImageFileName(image->name, srcFileName, fileExt, generatedFileNames); 319 | 320 | generatedFileNames.insert(dstFileName); 321 | 322 | dstRefPath = dstFileName; 323 | } 324 | 325 | std::string dstFilePath = srcFilePath; 326 | if (writeNewFile) 327 | { 328 | TF_VERIFY(genNewFileName); // Makes no sense to write a file to its source path 329 | 330 | std::string writeFilePath = (dstDir / fs::path(dstRefPath)).string(); 331 | TF_DEBUG(GUC).Msg("writing img %s\n", writeFilePath.c_str()); 332 | if (!writeImageData(writeFilePath.c_str(), size, data)) 333 | { 334 | return std::nullopt; 335 | } 336 | 337 | dstFilePath = writeFilePath; 338 | 339 | if (!genRelativePaths) 340 | { 341 | dstRefPath = writeFilePath; 342 | } 343 | } 344 | 345 | ImageMetadata metadata; 346 | metadata.filePath = dstFilePath; 347 | metadata.refPath = dstRefPath; 348 | 349 | // Read the metadata required for MaterialX shading network creation 350 | if (!readImageMetadata(dstFilePath.c_str(), metadata.channelCount)) 351 | { 352 | TF_RUNTIME_ERROR("unable to read metadata of image %s", dstFilePath.c_str()); 353 | return std::nullopt; 354 | } 355 | 356 | return metadata; 357 | } 358 | } 359 | 360 | namespace guc 361 | { 362 | void processImages(const cgltf_image* images, 363 | size_t imageCount, 364 | const fs::path& srcDir, 365 | const fs::path& dstDir, 366 | bool copyExistingFiles, 367 | bool genRelativePaths, 368 | ImageMetadataMap& metadata) 369 | { 370 | std::unordered_set generatedFileNames; 371 | 372 | for (size_t i = 0; i < imageCount; i++) 373 | { 374 | const cgltf_image* image = &images[i]; 375 | 376 | auto meta = detail::processImage(image, srcDir, dstDir, copyExistingFiles, genRelativePaths, generatedFileNames); 377 | 378 | if (meta.has_value()) 379 | { 380 | metadata[image] = meta.value(); 381 | } 382 | } 383 | 384 | TF_DEBUG(GUC).Msg("processed %d images\n", int(metadata.size())); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/libguc/src/image.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | namespace fs = std::filesystem; 26 | 27 | namespace guc 28 | { 29 | struct ImageMetadata 30 | { 31 | std::string filePath; 32 | std::string refPath; 33 | int channelCount; // Needed to determine the type of MaterialX nodes 34 | }; 35 | 36 | using ImageMetadataMap = std::unordered_map; 37 | 38 | void processImages(const cgltf_image* images, 39 | size_t imageCount, 40 | const fs::path& srcDir, 41 | const fs::path& dstDir, 42 | bool copyExistingFiles, 43 | bool genRelativePaths, 44 | ImageMetadataMap& metadata); 45 | } 46 | -------------------------------------------------------------------------------- /src/libguc/src/materialx.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | #include 22 | 23 | #include "image.h" 24 | 25 | namespace mx = MaterialX; 26 | 27 | namespace guc 28 | { 29 | class MaterialXMaterialConverter 30 | { 31 | public: 32 | MaterialXMaterialConverter(mx::DocumentPtr doc, 33 | const ImageMetadataMap& imageMetadataMap); 34 | 35 | void convert(const cgltf_material* material, const std::string& materialName); 36 | 37 | private: 38 | mx::DocumentPtr m_doc; 39 | const ImageMetadataMap& m_imageMetadataMap; 40 | std::string m_defaultColorSetName; 41 | std::string m_defaultOpacitySetName; 42 | 43 | private: 44 | void createUnlitSurfaceNodes(const cgltf_material* material, 45 | const std::string& materialName); 46 | 47 | void createGltfPbrNodes(const cgltf_material* material, 48 | const std::string& materialName); 49 | 50 | using ShaderNodeCreationCallback = 51 | std::function; 52 | 53 | void createMaterialNodes(const cgltf_material* material, 54 | const std::string& materialName, 55 | const std::string& shaderNodeType, 56 | ShaderNodeCreationCallback callback); 57 | 58 | private: 59 | void addGltfPbrInputs(const cgltf_material* material, 60 | mx::NodeGraphPtr nodeGraph, 61 | mx::NodePtr shaderNode); 62 | 63 | void addDiffuseTextureInput(mx::NodeGraphPtr nodeGraph, 64 | mx::NodePtr shaderNode, 65 | const std::string& inputName, 66 | const cgltf_texture_view* textureView, 67 | const mx::Color3& factor); 68 | 69 | void addAlphaTextureInput(mx::NodeGraphPtr nodeGraph, 70 | mx::NodePtr shaderNode, 71 | const std::string& inputName, 72 | const cgltf_texture_view* textureView, 73 | float factor); 74 | 75 | void addNormalTextureInput(mx::NodeGraphPtr nodeGraph, 76 | mx::NodePtr shaderNode, 77 | const std::string& inputName, 78 | const cgltf_texture_view& textureView); 79 | 80 | void addOcclusionTextureInput(mx::NodeGraphPtr nodeGraph, 81 | mx::NodePtr shaderNode, 82 | const cgltf_texture_view& textureView); 83 | 84 | void addIridescenceThicknessInput(mx::NodeGraphPtr nodeGraph, 85 | mx::NodePtr shaderNode, 86 | const cgltf_iridescence* iridescence); 87 | 88 | private: 89 | void addSrgbTextureInput(mx::NodeGraphPtr nodeGraph, 90 | mx::NodePtr shaderNode, 91 | const std::string& inputName, 92 | const cgltf_texture_view& textureView, 93 | mx::Color3 factor, 94 | mx::Color3 factorDefault); 95 | 96 | void addFloatTextureInput(mx::NodeGraphPtr nodeGraph, 97 | mx::NodePtr shaderNode, 98 | const std::string& inputName, 99 | const cgltf_texture_view& textureView, 100 | int channelIndex, 101 | float factor, 102 | float factorDefault); 103 | 104 | private: 105 | // These two functions not only set up the image nodes with the correct value 106 | // types and sampling properties, but also resolve mismatches between the desired and 107 | // given component types. Resolution is handled according to this table: 108 | // 109 | // texture type 110 | // (#channels) 111 | // +---------------+---------------+--------------------+ 112 | // desired | | | color3 | 113 | // type | | float | (/vector3) | 114 | // +---------------+---------------+--------------------+ 115 | // | | | img + | 116 | // | greyscale (1) | img | convert_color3 | 117 | // +---------------+---------------+--------------------+ 118 | // | | | img + | 119 | // | greyscale + | img + | extract_float(0) + | 120 | // | alpha (2) | extract_float | convert_color3 | 121 | // +---------------+---------------+--------------------+ 122 | // | | img + | | 123 | // | RGB (3) | extract_float | img  | 124 | // +---------------+---------------+--------------------+ 125 | // | | img + | img + | 126 | // | RGBA (4) | extract_float | convert_color3 | 127 | // +---------------+---------------+--------------------+ 128 | // 129 | mx::NodePtr addFloatTextureNodes(mx::NodeGraphPtr nodeGraph, 130 | const cgltf_texture_view& textureView, 131 | std::string& filePath, 132 | int channelIndex); 133 | 134 | mx::NodePtr addFloat3TextureNodes(mx::NodeGraphPtr nodeGraph, 135 | const cgltf_texture_view& textureView, 136 | std::string& filePath, 137 | bool color, 138 | mx::ValuePtr defaultValue); 139 | 140 | mx::NodePtr addTextureTransformNode(mx::NodeGraphPtr nodeGraph, 141 | mx::NodePtr texcoordNode, 142 | const cgltf_texture_transform& transform); 143 | 144 | mx::NodePtr addTextureNode(mx::NodeGraphPtr nodeGraph, 145 | const std::string& filePath, 146 | const std::string& textureType, 147 | bool isSrgb, 148 | const cgltf_texture_view& textureView, 149 | mx::ValuePtr defaultValue); 150 | 151 | mx::NodePtr makeGeompropValueNode(mx::NodeGraphPtr nodeGraph, 152 | const std::string& geompropName, 153 | const std::string& geompropValueTypeName, 154 | mx::ValuePtr defaultValue = nullptr); 155 | 156 | void connectNodeGraphNodeToShaderInput(mx::NodeGraphPtr nodeGraph, mx::InputPtr input, mx::NodePtr node); 157 | 158 | private: 159 | bool getTextureMetadata(const cgltf_texture_view& textureView, ImageMetadata& metadata) const; 160 | bool getTextureFilePath(const cgltf_texture_view& textureView, std::string& filePath) const; 161 | int getTextureChannelCount(const cgltf_texture_view& textureView) const; 162 | 163 | std::string getTextureValueType(const cgltf_texture_view& textureView, bool color) const; 164 | }; 165 | } 166 | -------------------------------------------------------------------------------- /src/libguc/src/mesh.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "mesh.h" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | #include "debugCodes.h" 26 | 27 | namespace guc 28 | { 29 | bool createGeometryRepresentation(const cgltf_primitive* prim, 30 | const VtIntArray& inIndices, 31 | VtIntArray& outIndices, 32 | VtIntArray& faceVertexCounts) 33 | { 34 | const char* INDICES_MISMATCH_ERROR_MSG = "indices count does not match primitive type"; 35 | 36 | switch (prim->type) 37 | { 38 | case cgltf_primitive_type_points: { 39 | faceVertexCounts = VtIntArray(inIndices.size(), 1); 40 | outIndices = inIndices; 41 | break; 42 | } 43 | case cgltf_primitive_type_lines: { 44 | if ((inIndices.size() % 2) != 0) 45 | { 46 | TF_RUNTIME_ERROR("%s", INDICES_MISMATCH_ERROR_MSG); 47 | return false; 48 | } 49 | faceVertexCounts = VtIntArray(inIndices.size() / 2, 2); 50 | outIndices = inIndices; 51 | break; 52 | } 53 | case cgltf_primitive_type_triangles: { 54 | if ((inIndices.size() % 3) != 0) 55 | { 56 | TF_RUNTIME_ERROR("%s", INDICES_MISMATCH_ERROR_MSG); 57 | return false; 58 | } 59 | faceVertexCounts = VtIntArray(inIndices.size() / 3, 3); 60 | outIndices = inIndices; 61 | break; 62 | } 63 | case cgltf_primitive_type_line_strip: { 64 | if (inIndices.size() < 2) 65 | { 66 | TF_RUNTIME_ERROR("%s", INDICES_MISMATCH_ERROR_MSG); 67 | return false; 68 | } 69 | faceVertexCounts = VtIntArray(inIndices.size() - 1, 2); 70 | outIndices.resize(faceVertexCounts.size() * 2); 71 | for (size_t i = 0; i < faceVertexCounts.size(); i++) 72 | { 73 | outIndices[i * 2 + 0] = inIndices[i + 0]; 74 | outIndices[i * 2 + 1] = inIndices[i + 1]; 75 | } 76 | break; 77 | } 78 | case cgltf_primitive_type_line_loop: { 79 | if (inIndices.size() < 2) 80 | { 81 | TF_RUNTIME_ERROR("%s", INDICES_MISMATCH_ERROR_MSG); 82 | return false; 83 | } 84 | faceVertexCounts = VtIntArray(inIndices.size(), 2); 85 | outIndices.resize(inIndices.size() * 2); 86 | size_t i; 87 | for (i = 0; i < inIndices.size() - 1; i++) 88 | { 89 | outIndices[i * 2 + 0] = inIndices[i + 0]; 90 | outIndices[i * 2 + 1] = inIndices[i + 1]; 91 | } 92 | outIndices[i + 0] = inIndices[inIndices.size() - 1]; 93 | outIndices[i + 1] = inIndices[0]; 94 | break; 95 | } 96 | case cgltf_primitive_type_triangle_strip: { 97 | if (inIndices.size() < 3) 98 | { 99 | TF_RUNTIME_ERROR("%s", INDICES_MISMATCH_ERROR_MSG); 100 | return false; 101 | } 102 | faceVertexCounts = VtIntArray(inIndices.size() - 2, 3); 103 | outIndices.resize(faceVertexCounts.size() * 3); 104 | bool forward = true; 105 | for (size_t i = 0; i < faceVertexCounts.size(); i++) 106 | { 107 | int i0 = i + 0; 108 | int i1 = forward ? (i + 1) : (i + 2); 109 | int i2 = forward ? (i + 2) : (i + 1); 110 | outIndices[i * 3 + 0] = inIndices[i0]; 111 | outIndices[i * 3 + 1] = inIndices[i1]; 112 | outIndices[i * 3 + 2] = inIndices[i2]; 113 | forward = !forward; 114 | } 115 | break; 116 | } 117 | case cgltf_primitive_type_triangle_fan: { 118 | if (inIndices.size() < 3) 119 | { 120 | TF_RUNTIME_ERROR("%s", INDICES_MISMATCH_ERROR_MSG); 121 | return false; 122 | } 123 | faceVertexCounts = VtIntArray(inIndices.size() - 2, 3); 124 | outIndices.resize(faceVertexCounts.size() * 3); 125 | for (size_t i = 0; i < faceVertexCounts.size(); i++) 126 | { 127 | outIndices[i * 3 + 0] = inIndices[0]; 128 | outIndices[i * 3 + 1] = inIndices[i + 1]; 129 | outIndices[i * 3 + 2] = inIndices[i + 2]; 130 | } 131 | break; 132 | } 133 | default: 134 | TF_CODING_ERROR("unhandled primitive type %d", int(prim->type)); 135 | return false; 136 | } 137 | return true; 138 | } 139 | 140 | void createFlatNormals(const VtIntArray& indices, 141 | const VtVec3fArray& positions, 142 | VtVec3fArray& normals) 143 | { 144 | TF_VERIFY((indices.size() % 3) == 0); 145 | normals.resize(positions.size()); 146 | 147 | for (size_t i = 0; i < indices.size(); i += 3) 148 | { 149 | int i0 = indices[i + 0]; 150 | int i1 = indices[i + 1]; 151 | int i2 = indices[i + 2]; 152 | 153 | const GfVec3f& p0 = positions[i0]; 154 | const GfVec3f& p1 = positions[i1]; 155 | const GfVec3f& p2 = positions[i2]; 156 | 157 | GfVec3f e1 = (p1 - p0); 158 | GfVec3f e2 = (p2 - p0); 159 | e1.Normalize(); 160 | e2.Normalize(); 161 | 162 | GfVec3f n = GfCross(e1, e2); 163 | n.Normalize(); 164 | 165 | normals[i0] = n; 166 | normals[i1] = n; 167 | normals[i2] = n; 168 | } 169 | } 170 | 171 | bool createTangents(const VtIntArray& indices, 172 | const VtVec3fArray& positions, 173 | const VtVec3fArray& normals, 174 | const VtVec2fArray& texcoords, 175 | VtFloatArray& unindexedSigns, 176 | VtVec3fArray& unindexedTangents) 177 | { 178 | TF_VERIFY(!texcoords.empty()); 179 | 180 | int vertexCount = indices.size(); 181 | unindexedTangents.resize(vertexCount); 182 | unindexedSigns.resize(vertexCount); 183 | 184 | struct UserData { 185 | const VtIntArray& indices; 186 | const VtVec3fArray& positions; 187 | const VtVec3fArray& normals; 188 | const VtVec2fArray& texcoords; 189 | VtFloatArray& unindexedSigns; 190 | VtVec3fArray& unindexedTangents; 191 | } userData = { 192 | indices, positions, normals, texcoords, unindexedSigns, unindexedTangents 193 | }; 194 | 195 | auto getNumFacesFunc = [](const SMikkTSpaceContext* pContext) { 196 | UserData* userData = (UserData*) pContext->m_pUserData; 197 | return (int) userData->indices.size() / 3; 198 | }; 199 | 200 | auto getNumVerticesOfFaceFunc = [](const SMikkTSpaceContext* pContext, const int iFace) { 201 | return 3; 202 | }; 203 | 204 | auto getPositionFunc = [](const SMikkTSpaceContext* pContext, float fvPosOut[], const int iFace, const int iVert) { 205 | UserData* userData = (UserData*) pContext->m_pUserData; 206 | int vertexIndex = userData->indices[iFace * 3 + iVert]; 207 | const GfVec3f& position = userData->positions[vertexIndex]; 208 | fvPosOut[0] = position[0]; 209 | fvPosOut[1] = position[1]; 210 | fvPosOut[2] = position[2]; 211 | }; 212 | 213 | auto getNormalFunc = [](const SMikkTSpaceContext* pContext, float fvNormOut[], const int iFace, const int iVert) { 214 | UserData* userData = (UserData*) pContext->m_pUserData; 215 | int vertexIndex = userData->indices[iFace * 3 + iVert]; 216 | const GfVec3f& normal = userData->normals[vertexIndex]; 217 | fvNormOut[0] = normal[0]; 218 | fvNormOut[1] = normal[1]; 219 | fvNormOut[2] = normal[2]; 220 | }; 221 | 222 | auto getTexCoordFunc = [](const SMikkTSpaceContext* pContext, float fvTexcOut[], const int iFace, const int iVert) { 223 | UserData* userData = (UserData*) pContext->m_pUserData; 224 | int vertexIndex = userData->indices[iFace * 3 + iVert]; 225 | const GfVec2f& texcoord = userData->texcoords[vertexIndex]; 226 | fvTexcOut[0] = texcoord[0]; 227 | fvTexcOut[1] = texcoord[1]; 228 | }; 229 | 230 | auto setTSpaceBasicFunc = [](const SMikkTSpaceContext* pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert) { 231 | UserData* userData = (UserData*) pContext->m_pUserData; 232 | int newVertexIndex = iFace * 3 + iVert; 233 | userData->unindexedTangents[newVertexIndex] = GfVec3f(fvTangent[0], fvTangent[1], fvTangent[2]); 234 | userData->unindexedSigns[newVertexIndex] = fSign; 235 | }; 236 | 237 | SMikkTSpaceInterface interface; 238 | interface.m_getNumFaces = getNumFacesFunc; 239 | interface.m_getNumVerticesOfFace= getNumVerticesOfFaceFunc; 240 | interface.m_getPosition = getPositionFunc; 241 | interface.m_getNormal = getNormalFunc; 242 | interface.m_getTexCoord = getTexCoordFunc; 243 | interface.m_setTSpaceBasic = setTSpaceBasicFunc; 244 | interface.m_setTSpace = nullptr; 245 | 246 | SMikkTSpaceContext context; 247 | context.m_pInterface = &interface; 248 | context.m_pUserData = &userData; 249 | 250 | return genTangSpaceDefault(&context); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/libguc/src/mesh.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | #include 22 | 23 | using namespace PXR_NS; 24 | 25 | namespace guc 26 | { 27 | bool createGeometryRepresentation(const cgltf_primitive* prim, 28 | const VtIntArray& inIndices, 29 | VtIntArray& outIndices, 30 | VtIntArray& faceVertexCounts); 31 | 32 | void createFlatNormals(const VtIntArray& indices, 33 | const VtVec3fArray& positions, 34 | VtVec3fArray& normals); 35 | 36 | bool createTangents(const VtIntArray& indices, 37 | const VtVec3fArray& positions, 38 | const VtVec3fArray& normals, 39 | const VtVec2fArray& texcoords, 40 | VtFloatArray& signs, 41 | VtVec3fArray& tangents); 42 | } 43 | -------------------------------------------------------------------------------- /src/libguc/src/naming.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "naming.h" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | namespace fs = std::filesystem; 26 | namespace mx = MaterialX; 27 | 28 | namespace guc 29 | { 30 | static const SdfPath s_entryPaths[] = { 31 | SdfPath{ "/Asset" }, 32 | SdfPath{ "/Asset/Scenes" }, 33 | SdfPath{ "/Asset/Nodes" }, 34 | SdfPath{ "/Asset/Materials" }, 35 | SdfPath{ "/Asset/Materials/UsdPreviewSurface" }, 36 | SdfPath{ "/Asset/Materials/MaterialX" }, 37 | SdfPath{ "/Asset/Meshes" }, 38 | SdfPath{ "/Asset/Cameras" }, 39 | SdfPath{ "/Asset/Lights" } 40 | }; 41 | static_assert(sizeof(s_entryPaths) / sizeof(s_entryPaths[0]) == size_t(EntryPathType::ENUM_SIZE)); 42 | 43 | const SdfPath& getEntryPath(EntryPathType type) 44 | { 45 | return s_entryPaths[size_t(type)]; 46 | } 47 | 48 | const char* getMaterialVariantSetName() 49 | { 50 | return "shadingVariant"; 51 | } 52 | 53 | std::string normalizeVariantName(const std::string& name) 54 | { 55 | return TfMakeValidIdentifier(name); 56 | } 57 | 58 | std::string makeStSetName(int index) 59 | { 60 | std::string name = UsdUtilsGetPrimaryUVSetName(); // likely to be "st" 61 | if (index == 0) 62 | { 63 | return name; 64 | } 65 | return name + std::to_string(index); 66 | } 67 | 68 | std::string makeColorSetName(int index) 69 | { 70 | // The primvar name for colors is not standardized. I have chosen 'color' for it, 71 | // and give reasons against the other suggestions discussed in this forum thread: 72 | // https://groups.google.com/g/usd-interest/c/VOkh0aj-8bU/m/zxrMQ-pJAgAJ 73 | // 74 | // 'colorSet': Maya seems to use this primvar name, however if there's a colorSet, 75 | // there should also be a texCoordSet / stSet. 76 | // 'vertexColor': includes the interpolation mode, of which USD has a few. We don't 77 | // use "vertexTangents" etc., although we emit per-vertex tangents. 78 | // 79 | // Furthermore, 'color' maps directly to the COLOR_ glTF attribute name and goes well 80 | // with the already existing 'displayColor' primvar. It's just not for the 'display' 81 | // purpose, but rather part of the actual data used for shading. 82 | std::string name = "color"; 83 | if (index == 0) 84 | { 85 | return name; 86 | } 87 | return name + std::to_string(index); 88 | } 89 | 90 | std::string makeOpacitySetName(int index) 91 | { 92 | std::string name = "opacity"; 93 | if (index == 0) 94 | { 95 | return name; 96 | } 97 | return name + std::to_string(index); 98 | } 99 | 100 | const static std::unordered_set MTLX_TYPE_NAME_SET = { 101 | /* Basic data types */ 102 | "integer", "boolean", "float", "color3", "color4", "vector2", "vector3", 103 | "vector4", "matrix33", "matrix44", "string", "filename", "geomname", "integerarray", 104 | "floatarray", "color3array", "color4array", "vector2array", "vector3array", 105 | "vector4array", "stringarray", "geomnamearray", 106 | /* Custom data types */ 107 | "color", "shader", "material" 108 | }; 109 | 110 | const char* DEFAULT_MATERIAL_NAME = "mat"; 111 | 112 | std::string makeUniqueMaterialName(std::string baseName, 113 | const std::unordered_set& existingNames) 114 | { 115 | if (baseName.empty()) 116 | { 117 | baseName = DEFAULT_MATERIAL_NAME; 118 | } 119 | else 120 | { 121 | baseName = mx::createValidName(baseName); 122 | baseName = TfMakeValidIdentifier(baseName); 123 | 124 | // Remove underscore prefix to prevent compile errors in HdStorm 125 | if (baseName[0] == '_') 126 | { 127 | baseName = DEFAULT_MATERIAL_NAME + baseName; 128 | } 129 | } 130 | 131 | std::string name = baseName; 132 | 133 | int i = 1; 134 | while (existingNames.find(name) != existingNames.end() || 135 | MTLX_TYPE_NAME_SET.find(name) != MTLX_TYPE_NAME_SET.end()) 136 | { 137 | name = baseName + "_" + std::to_string(i); 138 | i++; 139 | } 140 | 141 | return name; 142 | } 143 | 144 | const char* DEFAULT_IMAGE_FILENAME = "img"; 145 | 146 | std::string makeUniqueImageFileName(const char* nameHint, 147 | const std::string& fileName, 148 | const std::string& fileExt, 149 | const std::unordered_set& existingNames) 150 | { 151 | std::string baseName = fileName; 152 | 153 | if (baseName.empty() && nameHint) 154 | { 155 | baseName = TfMakeValidIdentifier(nameHint); 156 | } 157 | 158 | baseName = fs::path(baseName).replace_extension("").string(); // remove ext if already in img name 159 | 160 | if (baseName.empty()) 161 | { 162 | baseName = DEFAULT_IMAGE_FILENAME; 163 | } 164 | 165 | auto finalName = baseName + fileExt; 166 | 167 | int i = 1; 168 | while (existingNames.find(finalName) != existingNames.end()) 169 | { 170 | finalName = baseName + "_" + std::to_string(i) + fileExt; 171 | i++; 172 | } 173 | 174 | return finalName; 175 | } 176 | 177 | SdfPath makeUniqueStageSubpath(UsdStageRefPtr stage, 178 | const SdfPath& root, 179 | const std::string& baseName, 180 | const std::string& delimiter) 181 | { 182 | SdfPath finalPath = root.AppendElementString(TfMakeValidIdentifier(baseName)); 183 | 184 | std::string basePath = finalPath.GetAsString(); 185 | // FIXME: evaluate performance impact of GetObjectAtPath compared to simple hashmap 186 | for (int i = 1; stage->GetObjectAtPath(finalPath); i++) 187 | { 188 | auto newPathStr = basePath + delimiter + std::to_string(i); 189 | TF_VERIFY(SdfPath::IsValidPathString(newPathStr)); 190 | finalPath = SdfPath(newPathStr); 191 | } 192 | 193 | return finalPath; 194 | } 195 | 196 | SdfPath makeMtlxMaterialPath(const std::string& materialName) 197 | { 198 | return getEntryPath(EntryPathType::MaterialXMaterials).AppendElementString("Materials").AppendElementString(materialName); 199 | } 200 | 201 | SdfPath makeUsdPreviewSurfaceMaterialPath(const std::string& materialName) 202 | { 203 | return getEntryPath(EntryPathType::PreviewMaterials).AppendElementString("Materials").AppendElementString(materialName); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/libguc/src/naming.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | #include 22 | 23 | using namespace PXR_NS; 24 | 25 | namespace guc 26 | { 27 | enum class EntryPathType 28 | { 29 | Root = 0, 30 | Scenes, 31 | Nodes, 32 | Materials, 33 | PreviewMaterials, 34 | MaterialXMaterials, 35 | Meshes, 36 | Cameras, 37 | Lights, 38 | ENUM_SIZE 39 | }; 40 | const SdfPath& getEntryPath(EntryPathType type); 41 | 42 | const char* getMaterialVariantSetName(); 43 | std::string normalizeVariantName(const std::string& name); 44 | 45 | std::string makeStSetName(int index); 46 | std::string makeColorSetName(int index); 47 | std::string makeOpacitySetName(int index); 48 | 49 | std::string makeUniqueMaterialName(std::string baseName, 50 | const std::unordered_set& existingNames); 51 | 52 | std::string makeUniqueImageFileName(const char* nameHint, 53 | const std::string& fileName, 54 | const std::string& fileExt, 55 | const std::unordered_set& existingNames); 56 | 57 | SdfPath makeUniqueStageSubpath(UsdStageRefPtr stage, 58 | const SdfPath& root, 59 | const std::string& baseName, 60 | const std::string& delimiter = "_"); 61 | 62 | SdfPath makeMtlxMaterialPath(const std::string& materialName); 63 | 64 | SdfPath makeUsdPreviewSurfaceMaterialPath(const std::string& materialName); 65 | } 66 | -------------------------------------------------------------------------------- /src/libguc/src/usdpreviewsurface.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "usdpreviewsurface.h" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | #include "naming.h" 26 | #include "debugCodes.h" 27 | #include "cgltf_util.h" 28 | 29 | namespace fs = std::filesystem; 30 | 31 | TF_DEFINE_PRIVATE_TOKENS( 32 | _tokens, 33 | // Shading node IDs 34 | (UsdPreviewSurface) 35 | (UsdUVTexture) 36 | (UsdPrimvarReader_float2) 37 | (UsdTransform2d) 38 | // UsdPreviewSurface inputs 39 | (emissiveColor) 40 | (occlusion) 41 | (normal) 42 | (opacityThreshold) 43 | (diffuseColor) 44 | (opacity) 45 | (metallic) 46 | (roughness) 47 | (clearcoat) 48 | (clearcoatRoughness) 49 | (ior) 50 | (specularColor) 51 | (useSpecularWorkflow) 52 | // UsdUVTexture inputs 53 | (st) 54 | (file) 55 | (scale) 56 | (bias) 57 | (fallback) 58 | (wrapS) 59 | (wrapT) 60 | (sourceColorSpace) 61 | // UsdUVTexture outputs 62 | (r) 63 | (g) 64 | (b) 65 | (a) 66 | (rgb) 67 | // UsdUVTexture wrap modes 68 | (clamp) 69 | (mirror) 70 | (repeat) 71 | // UsdUVTexture color spaces 72 | (raw) 73 | (sRGB) 74 | // UsdPrimvarReader_float2 input and output 75 | (varname) 76 | (result) 77 | // UsdTransform2d inputs 78 | (in) 79 | (rotation) 80 | (translation) 81 | ); 82 | 83 | namespace detail 84 | { 85 | void setChannelInputValues(const UsdShadeInput& input, GfVec4f value, const TfToken& channels) 86 | { 87 | if (channels == _tokens->rgb) { 88 | input.Set(GfVec3f(value[0], value[1], value[2])); 89 | } 90 | else if (channels == _tokens->r) { 91 | input.Set(value[0]); 92 | } 93 | else if (channels == _tokens->g) { 94 | input.Set(value[1]); 95 | } 96 | else if (channels == _tokens->b) { 97 | input.Set(value[2]); 98 | } 99 | else if (channels == _tokens->a) { 100 | input.Set(value[3]); 101 | } 102 | else { 103 | TF_CODING_ERROR("unhandled input channel"); 104 | } 105 | } 106 | 107 | TfToken convertWrapMode(int wrapMode) 108 | { 109 | switch (wrapMode) 110 | { 111 | case 33071 /* CLAMP_TO_EDGE */: 112 | return _tokens->clamp; 113 | case 33648 /* MIRRORED_REPEAT */: 114 | return _tokens->mirror; 115 | default: 116 | TF_CODING_ERROR("invalid wrap mode"); 117 | case 0: 118 | // use glTF default 119 | case 10497 /* REPEAT */: 120 | return _tokens->repeat; 121 | } 122 | } 123 | 124 | void connectTextureInputOutput(UsdShadeInput& input, UsdShadeShader& node, const TfToken& channels) 125 | { 126 | auto valueType = channels == _tokens->rgb ? SdfValueTypeNames->Float3 : SdfValueTypeNames->Float; 127 | auto output = node.CreateOutput(channels, valueType); 128 | input.ConnectToSource(output); 129 | } 130 | } 131 | 132 | namespace guc 133 | { 134 | UsdPreviewSurfaceMaterialConverter::UsdPreviewSurfaceMaterialConverter(UsdStageRefPtr stage, 135 | const ImageMetadataMap& imageMetadataMap) 136 | : m_stage(stage) 137 | , m_imageMetadataMap(imageMetadataMap) 138 | { 139 | } 140 | 141 | void UsdPreviewSurfaceMaterialConverter::convert(const cgltf_material* material, const SdfPath& path) 142 | { 143 | auto shadeMaterial = UsdShadeMaterial::Define(m_stage, path); 144 | auto surfaceOutput = shadeMaterial.CreateSurfaceOutput(UsdShadeTokens->universalRenderContext); 145 | 146 | // FIXME: the first node will be called 'node' while MaterialX's first node is 'node1' 147 | const char* nodeNameNumberDelimiter = ""; // mimic MaterialX nodename generation with no delimiter between "node" and number 148 | auto shaderPath = makeUniqueStageSubpath(m_stage, path, "node", nodeNameNumberDelimiter); 149 | auto shader = UsdShadeShader::Define(m_stage, shaderPath); 150 | shader.CreateIdAttr(VtValue(_tokens->UsdPreviewSurface)); 151 | auto shaderOutput = shader.CreateOutput(UsdShadeTokens->surface, SdfValueTypeNames->Token); 152 | surfaceOutput.ConnectToSource(shaderOutput); 153 | 154 | auto setAlphaTextureInput = [&](const cgltf_pbr_metallic_roughness* pbrMetallicRoughness) 155 | { 156 | if (material->alpha_mode == cgltf_alpha_mode_opaque) 157 | { 158 | // Do nothing. 159 | return; 160 | } 161 | 162 | GfVec4f opacityFallback(1.0f); // image fallback value is 0.0, but opacity default should be 1.0 163 | 164 | auto opacityInput = shader.CreateInput(_tokens->opacity, SdfValueTypeNames->Float); 165 | setFloatTextureInput(path, opacityInput, pbrMetallicRoughness->base_color_texture, _tokens->a, GfVec4f(pbrMetallicRoughness->base_color_factor[3]), &opacityFallback); 166 | 167 | if (material->alpha_mode == cgltf_alpha_mode_mask) 168 | { 169 | auto opacityThresholdInput = shader.CreateInput(_tokens->opacityThreshold, SdfValueTypeNames->Float); 170 | opacityThresholdInput.Set(material->alpha_cutoff); 171 | } 172 | }; 173 | 174 | auto emissiveColorInput = shader.CreateInput(_tokens->emissiveColor, SdfValueTypeNames->Color3f); 175 | auto emissiveFactor = GfVec4f(material->emissive_factor); 176 | 177 | if (material->unlit) // Not really 'unlit', but there's no better way 178 | { 179 | if (material->has_pbr_metallic_roughness) 180 | { 181 | const cgltf_pbr_metallic_roughness* pbrMetallicRoughness = &material->pbr_metallic_roughness; 182 | 183 | GfVec4f unlitColorFallback(1.0f); // not specified 184 | 185 | setSrgbTextureInput(path, emissiveColorInput, pbrMetallicRoughness->base_color_texture, GfVec4f(pbrMetallicRoughness->base_color_factor), &unlitColorFallback); 186 | setAlphaTextureInput(pbrMetallicRoughness); 187 | } 188 | else 189 | { 190 | emissiveColorInput.Set(GfVec3f(1.0f)); 191 | } 192 | return; 193 | } 194 | 195 | if (material->has_emissive_strength) 196 | { 197 | const cgltf_emissive_strength* emissiveStrength = &material->emissive_strength; 198 | emissiveFactor *= emissiveStrength->emissive_strength; 199 | } 200 | setSrgbTextureInput(path, emissiveColorInput, material->emissive_texture, emissiveFactor); 201 | 202 | auto occlusionInput = shader.CreateInput(_tokens->occlusion, SdfValueTypeNames->Float); 203 | setOcclusionTextureInput(path, occlusionInput, material->occlusion_texture); 204 | 205 | auto normalInput = shader.CreateInput(_tokens->normal, SdfValueTypeNames->Normal3f); 206 | setNormalTextureInput(path, normalInput, material->normal_texture); 207 | 208 | // We need to set these values regardless of whether pbrMetallicRoughness is present or not, because UsdPreviewSurface's 209 | // default values differ (and we want to come as close as possible to the MaterialX look, although the shading model differs). 210 | auto diffuseColorInput = shader.CreateInput(_tokens->diffuseColor, SdfValueTypeNames->Color3f); 211 | auto metallicInput = shader.CreateInput(_tokens->metallic, SdfValueTypeNames->Float); 212 | auto roughnessInput = shader.CreateInput(_tokens->roughness, SdfValueTypeNames->Float); 213 | 214 | if (material->has_pbr_metallic_roughness) 215 | { 216 | const cgltf_pbr_metallic_roughness* pbrMetallicRoughness = &material->pbr_metallic_roughness; 217 | 218 | GfVec4f diffuseColorFallback(1.0f); // same as glTF spec sec. 5.22.2: "When undefined, the texture MUST be sampled as having 1.0 in all components." 219 | setSrgbTextureInput(path, diffuseColorInput, pbrMetallicRoughness->base_color_texture, GfVec4f(pbrMetallicRoughness->base_color_factor), &diffuseColorFallback); 220 | GfVec4f metallicRoughnessFallback(1.0f); // same as glTF spec sec. 5.22.5: "When undefined, the texture MUST be sampled as having 1.0 in G and B components." 221 | setFloatTextureInput(path, metallicInput, pbrMetallicRoughness->metallic_roughness_texture, _tokens->b, GfVec4f(pbrMetallicRoughness->metallic_factor), &metallicRoughnessFallback); 222 | setFloatTextureInput(path, roughnessInput, pbrMetallicRoughness->metallic_roughness_texture, _tokens->g, GfVec4f(pbrMetallicRoughness->roughness_factor), &metallicRoughnessFallback); 223 | setAlphaTextureInput(pbrMetallicRoughness); 224 | } 225 | else 226 | { 227 | diffuseColorInput.Set(GfVec3f(1.0f)); // 0.18 in UsdPreviewSurface spec 228 | metallicInput.Set(1.0f); // 0.0 in UsdPreviewSurface spec 229 | roughnessInput.Set(1.0f); // 0.5 in UsdPreviewSurface spec 230 | } 231 | 232 | if (material->has_clearcoat) 233 | { 234 | const cgltf_clearcoat* clearcoat = &material->clearcoat; 235 | 236 | // see glTF clearcoat extension spec: "If the clearcoatTexture or clearcoatRoughnessTexture is not given, respective texture components are assumed to have a value of 1.0." 237 | GfVec4f clearcoatFallback(1.0f); 238 | GfVec4f clearcoatRoughnessFallback(1.0f); 239 | 240 | auto clearcoatInput = shader.CreateInput(_tokens->clearcoat, SdfValueTypeNames->Float); 241 | setFloatTextureInput(path, clearcoatInput, clearcoat->clearcoat_texture, _tokens->r, GfVec4f(clearcoat->clearcoat_factor), &clearcoatFallback); 242 | 243 | auto clearcoatRoughnessInput = shader.CreateInput(_tokens->clearcoatRoughness, SdfValueTypeNames->Float); 244 | setFloatTextureInput(path, clearcoatRoughnessInput, clearcoat->clearcoat_roughness_texture, _tokens->g, GfVec4f(clearcoat->clearcoat_roughness_factor), &clearcoatRoughnessFallback); 245 | } 246 | 247 | if (material->has_ior) 248 | { 249 | auto iorInput = shader.CreateInput(_tokens->ior, SdfValueTypeNames->Float); 250 | 251 | const cgltf_ior* ior = &material->ior; 252 | iorInput.Set(ior->ior); 253 | } 254 | 255 | if (material->has_specular) 256 | { 257 | const cgltf_specular* specular = &material->specular; 258 | 259 | GfVec4f specularColorFallback(1.0f); // use default from glTF specular ext spec 260 | 261 | auto specularColorInput = shader.CreateInput(_tokens->specularColor, SdfValueTypeNames->Color3f); 262 | setSrgbTextureInput(path, specularColorInput, specular->specular_color_texture, GfVec4f(specular->specular_color_factor), &specularColorFallback); 263 | 264 | auto useSpecularWorkflowInput = shader.CreateInput(_tokens->useSpecularWorkflow, SdfValueTypeNames->Int); 265 | useSpecularWorkflowInput.Set(1); 266 | } 267 | 268 | // Transmissive materials are rendered translucently (but prefer explicit alpha) 269 | if (material->has_transmission && material->alpha_mode == cgltf_alpha_mode_opaque && material->has_pbr_metallic_roughness) 270 | { 271 | const cgltf_transmission* transmission = &material->transmission; 272 | const cgltf_pbr_metallic_roughness* pbrMetallicRoughness = &material->pbr_metallic_roughness; 273 | const cgltf_float* baseColorFactor = pbrMetallicRoughness->base_color_factor; 274 | 275 | // Hand-crafted heuristic based on HSV 'saturation' value. range: [0.25, 1.0] 276 | float baseColorMaxValue = std::max(baseColorFactor[0], std::max(baseColorFactor[1], baseColorFactor[2])); 277 | float baseColorMinValue = std::min(baseColorFactor[0], std::min(baseColorFactor[1], baseColorFactor[2])); 278 | float baseColorRange = baseColorMaxValue - baseColorMinValue; 279 | float saturation = (baseColorMaxValue <= std::numeric_limits::min()) ? 0.0f : (baseColorRange / baseColorMaxValue); 280 | float opacity = 0.25f + 0.75f * saturation * (1.0f - transmission->transmission_factor); 281 | 282 | auto opacityInput = shader.CreateInput(_tokens->opacity, SdfValueTypeNames->Float); // not set because of OPAQUE mode check 283 | opacityInput.Set(opacity); 284 | } 285 | } 286 | 287 | void UsdPreviewSurfaceMaterialConverter::setNormalTextureInput(const SdfPath& basePath, 288 | UsdShadeInput& shaderInput, 289 | const cgltf_texture_view& textureView) 290 | { 291 | // glTF spec 2.0 3.9.3: transform [0, 1] value range to [-1, 1]. 292 | // We also scale the normal although this does not guarantee that the resulting vector is normalized. 293 | float xyScale = 2.0f * textureView.scale; 294 | float xyBias = -1.0f * textureView.scale; 295 | GfVec4f scale(xyScale, xyScale, 2.0f, 1.0f); 296 | GfVec4f bias(xyBias, xyBias, -1.0f, 0.0f); 297 | GfVec4f fallback(0.5f, 0.5f, 1.0f, 0.0f); // glTF fallback normal 298 | 299 | UsdShadeShader textureNode; 300 | if (!addTextureNode(basePath, textureView, _tokens->raw, &scale, &bias, &fallback, textureNode)) 301 | { 302 | return; 303 | } 304 | 305 | detail::connectTextureInputOutput(shaderInput, textureNode, _tokens->rgb); 306 | } 307 | 308 | void UsdPreviewSurfaceMaterialConverter::setOcclusionTextureInput(const SdfPath& basePath, 309 | UsdShadeInput& shaderInput, 310 | const cgltf_texture_view& textureView) 311 | { 312 | // glTF spec 2.0 3.9.3. 313 | // if 'strength' attribute is present, it affects occlusion as follows: 314 | // 1.0 + strength * (occlusionTexture - 1.0) 315 | // 316 | // we multiply that out since we only have scale and bias (value * scale + bias) to 317 | // occlusionTexture * strength + (1.0 - strength) 318 | // ---------------- ++++++++ ~~~~~~~~~~~~~~~~ 319 | // (value) * scale + bias 320 | GfVec4f scale(textureView.scale); 321 | GfVec4f bias(1.0f - textureView.scale); 322 | GfVec4f fallback(1.0f); // image fallback value is 0.0, but default occlusion value should be 1.0 323 | 324 | UsdShadeShader textureNode; 325 | if (!addTextureNode(basePath, textureView, _tokens->raw, &scale, &bias, &fallback, textureNode)) 326 | { 327 | return; 328 | } 329 | 330 | detail::connectTextureInputOutput(shaderInput, textureNode, _tokens->r); 331 | } 332 | 333 | void UsdPreviewSurfaceMaterialConverter::setSrgbTextureInput(const SdfPath& basePath, 334 | UsdShadeInput& shaderInput, 335 | const cgltf_texture_view& textureView, 336 | const GfVec4f& factor, 337 | const GfVec4f* fallback) 338 | { 339 | setTextureInput(basePath, shaderInput, textureView, _tokens->rgb, _tokens->sRGB, &factor, nullptr, fallback); 340 | } 341 | 342 | void UsdPreviewSurfaceMaterialConverter::setFloatTextureInput(const SdfPath& basePath, 343 | UsdShadeInput& shaderInput, 344 | const cgltf_texture_view& textureView, 345 | const TfToken& channel, 346 | const GfVec4f& factor, 347 | const GfVec4f* fallback) 348 | { 349 | setTextureInput(basePath, shaderInput, textureView, channel, _tokens->raw, &factor, nullptr, fallback); 350 | } 351 | 352 | void UsdPreviewSurfaceMaterialConverter::setTextureInput(const SdfPath& basePath, 353 | UsdShadeInput& shaderInput, 354 | const cgltf_texture_view& textureView, 355 | const TfToken& channels, 356 | const TfToken& colorSpace, 357 | const GfVec4f* scale, 358 | const GfVec4f* bias, 359 | const GfVec4f* fallback) 360 | { 361 | UsdShadeShader textureNode; 362 | if (addTextureNode(basePath, textureView, colorSpace, scale, bias, fallback, textureNode)) 363 | { 364 | // "If a two-channel texture is fed into a UsdUVTexture, the r, g, and b components of the rgb output will 365 | // repeat the first channel's value, while the single a output will be set to the second channel's value." 366 | int channelCount = getTextureChannelCount(textureView); 367 | bool remapChannelToAlpha = (channelCount == 2 && channels == _tokens->g); 368 | 369 | detail::connectTextureInputOutput(shaderInput, textureNode, remapChannelToAlpha ? _tokens->a : channels); 370 | } 371 | else if (scale) 372 | { 373 | detail::setChannelInputValues(shaderInput, *scale, channels); 374 | } 375 | } 376 | 377 | void UsdPreviewSurfaceMaterialConverter::addTextureTransformNode(const SdfPath& basePath, 378 | const cgltf_texture_transform& transform, 379 | int stIndex, 380 | UsdShadeInput& textureStInput) 381 | { 382 | auto nodePath = makeUniqueStageSubpath(m_stage, basePath, "node", ""); 383 | 384 | UsdShadeShader node = UsdShadeShader::Define(m_stage, nodePath); 385 | node.CreateIdAttr(VtValue(_tokens->UsdTransform2d)); 386 | 387 | auto rotationInput = node.CreateInput(_tokens->rotation, SdfValueTypeNames->Float); 388 | rotationInput.Set(float(GfRadiansToDegrees(transform.rotation))); 389 | 390 | auto scaleInput = node.CreateInput(_tokens->scale, SdfValueTypeNames->Float2); 391 | scaleInput.Set(GfVec2f(transform.scale[0], transform.scale[1])); 392 | 393 | // Modify offset to account for glTF-USD differences in rotation pivot and y-origin 394 | GfVec2f offset(transform.offset[0], -transform.offset[1]); 395 | offset[0] += sinf(transform.rotation) * transform.scale[1]; 396 | offset[1] += 1.0f - (cosf(transform.rotation) * transform.scale[1]); 397 | 398 | auto translationInput = node.CreateInput(_tokens->translation, SdfValueTypeNames->Float2); 399 | translationInput.Set(offset); 400 | 401 | auto untransformedInput = node.CreateInput(_tokens->in, SdfValueTypeNames->Float2); 402 | setStPrimvarInput(untransformedInput, basePath, stIndex); 403 | 404 | auto transformedOutput = node.CreateOutput(_tokens->result, SdfValueTypeNames->Float2); 405 | textureStInput.ConnectToSource(transformedOutput); 406 | } 407 | 408 | bool UsdPreviewSurfaceMaterialConverter::addTextureNode(const SdfPath& basePath, 409 | const cgltf_texture_view& textureView, 410 | const TfToken& colorSpace, 411 | const GfVec4f* scale, 412 | const GfVec4f* bias, 413 | const GfVec4f* fallback, 414 | UsdShadeShader& node) 415 | { 416 | std::string filePath; 417 | if (!getTextureFilePath(textureView, filePath)) 418 | { 419 | return false; 420 | } 421 | 422 | auto nodePath = makeUniqueStageSubpath(m_stage, basePath, "node", ""); 423 | node = UsdShadeShader::Define(m_stage, nodePath); 424 | node.CreateIdAttr(VtValue(_tokens->UsdUVTexture)); 425 | 426 | auto fileInput = node.CreateInput(_tokens->file, SdfValueTypeNames->Asset); 427 | fileInput.Set(SdfAssetPath(filePath)); 428 | 429 | if (scale) 430 | { 431 | auto scaleInput = node.CreateInput(_tokens->scale, SdfValueTypeNames->Float4); 432 | scaleInput.Set(*scale); 433 | } 434 | 435 | if (bias) 436 | { 437 | auto biasInput = node.CreateInput(_tokens->bias, SdfValueTypeNames->Float4); 438 | biasInput.Set(*bias); 439 | } 440 | 441 | if (fallback) 442 | { 443 | auto fallbackInput = node.CreateInput(_tokens->fallback, SdfValueTypeNames->Float4); 444 | fallbackInput.Set(*fallback); 445 | } 446 | 447 | auto sourceColorSpaceInput = node.CreateInput(_tokens->sourceColorSpace, SdfValueTypeNames->Token); 448 | sourceColorSpaceInput.Set(colorSpace); 449 | 450 | const cgltf_sampler* sampler = textureView.texture->sampler; 451 | 452 | // glTF spec sec. 5.29.1. texture sampler: "When undefined, a sampler with repeat wrapping [..] SHOULD be used." 453 | auto wrapSInput = node.CreateInput(_tokens->wrapS, SdfValueTypeNames->Token); 454 | wrapSInput.Set(sampler ? detail::convertWrapMode(sampler->wrap_s) : _tokens->repeat); 455 | 456 | auto wrapTInput = node.CreateInput(_tokens->wrapT, SdfValueTypeNames->Token); 457 | wrapTInput.Set(sampler ? detail::convertWrapMode(sampler->wrap_t) : _tokens->repeat); 458 | 459 | // Tex coords come from either a primvar or the output of a UsdTransform2d node 460 | auto stInput = node.CreateInput(_tokens->st, SdfValueTypeNames->Float2); 461 | 462 | const cgltf_texture_transform& transform = textureView.transform; 463 | int stIndex = (textureView.has_transform && transform.has_texcoord) ? transform.texcoord : textureView.texcoord; 464 | 465 | if (textureView.has_transform && cgltf_transform_required(transform)) 466 | { 467 | addTextureTransformNode(basePath, transform, stIndex, stInput); 468 | } 469 | else 470 | { 471 | setStPrimvarInput(stInput, basePath, stIndex); 472 | } 473 | 474 | return true; 475 | } 476 | 477 | void UsdPreviewSurfaceMaterialConverter::setStPrimvarInput(UsdShadeInput& input, 478 | const SdfPath& nodeBasePath, 479 | int stIndex) 480 | { 481 | auto nodePath = makeUniqueStageSubpath(m_stage, nodeBasePath, "node", ""); 482 | auto node = UsdShadeShader::Define(m_stage, nodePath); 483 | node.CreateIdAttr(VtValue(_tokens->UsdPrimvarReader_float2)); 484 | 485 | auto varnameInput = node.CreateInput(_tokens->varname, SdfValueTypeNames->String); 486 | varnameInput.Set(makeStSetName(stIndex)); 487 | 488 | auto output = node.CreateOutput(_tokens->result, SdfValueTypeNames->Float2); 489 | input.ConnectToSource(output); 490 | } 491 | 492 | bool UsdPreviewSurfaceMaterialConverter::getTextureMetadata(const cgltf_texture_view& textureView, ImageMetadata& metadata) const 493 | { 494 | const cgltf_texture* texture = textureView.texture; 495 | if (!texture) 496 | { 497 | return false; 498 | } 499 | 500 | const cgltf_image* image = texture->image; 501 | if (!image) 502 | { 503 | return false; 504 | } 505 | 506 | auto iter = m_imageMetadataMap.find(image); 507 | if (iter == m_imageMetadataMap.end()) 508 | { 509 | return false; 510 | } 511 | 512 | metadata = iter->second; 513 | return true; 514 | } 515 | 516 | bool UsdPreviewSurfaceMaterialConverter::getTextureFilePath(const cgltf_texture_view& textureView, std::string& fileName) const 517 | { 518 | ImageMetadata metadata; 519 | if (!getTextureMetadata(textureView, metadata)) 520 | { 521 | return false; 522 | } 523 | fileName = metadata.refPath; 524 | return true; 525 | } 526 | 527 | int UsdPreviewSurfaceMaterialConverter::getTextureChannelCount(const cgltf_texture_view& textureView) const 528 | { 529 | ImageMetadata metadata; 530 | TF_VERIFY(getTextureMetadata(textureView, metadata)); 531 | return metadata.channelCount; 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /src/libguc/src/usdpreviewsurface.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Pablo Delgado Krämer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | 24 | #include "image.h" 25 | 26 | using namespace PXR_NS; 27 | 28 | namespace guc 29 | { 30 | class UsdPreviewSurfaceMaterialConverter 31 | { 32 | public: 33 | UsdPreviewSurfaceMaterialConverter(UsdStageRefPtr stage, 34 | const ImageMetadataMap& imageMetadataMap); 35 | 36 | void convert(const cgltf_material* material, const SdfPath& path); 37 | 38 | private: 39 | UsdStageRefPtr m_stage; 40 | const ImageMetadataMap& m_imageMetadataMap; 41 | 42 | private: 43 | void setNormalTextureInput(const SdfPath& basePath, 44 | UsdShadeInput& shaderInput, 45 | const cgltf_texture_view& textureView); 46 | 47 | void setOcclusionTextureInput(const SdfPath& basePath, 48 | UsdShadeInput& shaderInput, 49 | const cgltf_texture_view& textureView); 50 | 51 | void setSrgbTextureInput(const SdfPath& basePath, 52 | UsdShadeInput& shaderInput, 53 | const cgltf_texture_view& textureView, 54 | const GfVec4f& factor, 55 | const GfVec4f* fallback = nullptr); 56 | 57 | void setFloatTextureInput(const SdfPath& basePath, 58 | UsdShadeInput& shaderInput, 59 | const cgltf_texture_view& textureView, 60 | const TfToken& channel, 61 | const GfVec4f& factor, 62 | const GfVec4f* fallback = nullptr); 63 | 64 | private: 65 | void setTextureInput(const SdfPath& basePath, 66 | UsdShadeInput& shaderInput, 67 | const cgltf_texture_view& textureView, 68 | const TfToken& channels, 69 | const TfToken& colorSpace, 70 | const GfVec4f* scale, 71 | const GfVec4f* bias, 72 | const GfVec4f* fallback); 73 | 74 | void addTextureTransformNode(const SdfPath& basePath, 75 | const cgltf_texture_transform& transform, 76 | int stIndex, 77 | UsdShadeInput& textureStInput); 78 | 79 | bool addTextureNode(const SdfPath& basePath, 80 | const cgltf_texture_view& textureView, 81 | const TfToken& colorSpace, 82 | const GfVec4f* scale, 83 | const GfVec4f* bias, 84 | const GfVec4f* fallback, 85 | UsdShadeShader& node); 86 | 87 | void setStPrimvarInput(UsdShadeInput& input, const SdfPath& nodeBasePath, int stIndex); 88 | 89 | private: 90 | bool getTextureMetadata(const cgltf_texture_view& textureView, ImageMetadata& metadata) const; 91 | bool getTextureFilePath(const cgltf_texture_view& textureView, std::string& filePath) const; 92 | int getTextureChannelCount(const cgltf_texture_view& textureView) const; 93 | }; 94 | } 95 | --------------------------------------------------------------------------------