├── .github └── workflows │ └── publish.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── live.yml ├── marc-build.sh ├── nuke.sh ├── push.sh ├── usd_schemas ├── LICENSE │ └── LICENSE.txt ├── README.md └── usdInteractive │ ├── CMakeLists.txt │ ├── __init__.py │ ├── module.cpp │ ├── moduleDeps.cpp │ └── schema.usda └── usdzconvert ├── LICENSE.txt ├── fixOpacity ├── help.txt ├── iOS12LegacyModifier.py ├── usdARKitChecker ├── usdMaterialWithObjMtl.py ├── usdStageWithFbx.py ├── usdStageWithGlTF.py ├── usdStageWithObj.py ├── usdUtils.py ├── usdzaudioimport ├── usdzconvert ├── usdzcreateassetlib ├── validateMaterial.py └── validateMesh.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: docker publish 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | build-amd64: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 360 10 | steps: 11 | - name: checkout code 12 | uses: actions/checkout@v3 13 | - name: setup qemu 14 | uses: docker/setup-qemu-action@v2 15 | - name: setup buildx 16 | id: buildx 17 | uses: docker/setup-buildx-action@v2 18 | - name: print available platforms 19 | run: echo ${{ steps.buildx.outputs.platforms }} 20 | - name: set version from tag 21 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 22 | - name: print version number 23 | run: echo ${{ env.RELEASE_VERSION }} 24 | - name: login to dockerhub 25 | uses: docker/login-action@v2 26 | with: 27 | username: ${{ secrets.DOCKER_UPLOAD_USERNAME }} 28 | password: ${{ secrets.DOCKER_UPLOAD_TOKEN }} 29 | - name: build & upload 30 | run: | 31 | docker buildx build \ 32 | --push \ 33 | --tag plattar/python-usd-ar:version-${{ env.RELEASE_VERSION }}-amd64 \ 34 | --platform linux/amd64 \ 35 | --file Dockerfile . 36 | build-arm64: 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 360 39 | steps: 40 | - name: checkout code 41 | uses: actions/checkout@v3 42 | - name: setup qemu 43 | uses: docker/setup-qemu-action@v2 44 | - name: setup buildx 45 | id: buildx 46 | uses: docker/setup-buildx-action@v2 47 | - name: print available platforms 48 | run: echo ${{ steps.buildx.outputs.platforms }} 49 | - name: set version from tag 50 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 51 | - name: print version number 52 | run: echo ${{ env.RELEASE_VERSION }} 53 | - name: login to dockerhub 54 | uses: docker/login-action@v2 55 | with: 56 | username: ${{ secrets.DOCKER_UPLOAD_USERNAME }} 57 | password: ${{ secrets.DOCKER_UPLOAD_TOKEN }} 58 | - name: build & upload 59 | run: | 60 | docker buildx build \ 61 | --push \ 62 | --tag plattar/python-usd-ar:version-${{ env.RELEASE_VERSION }}-arm64 \ 63 | --platform linux/arm64 \ 64 | --file Dockerfile . 65 | build-manifest: 66 | runs-on: ubuntu-latest 67 | timeout-minutes: 360 68 | needs: [build-amd64, build-arm64] 69 | steps: 70 | - name: checkout code 71 | uses: actions/checkout@v3 72 | - name: set version from tag 73 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 74 | - name: print version number 75 | run: echo ${{ env.RELEASE_VERSION }} 76 | - name: login to dockerhub 77 | uses: docker/login-action@v2 78 | with: 79 | username: ${{ secrets.DOCKER_UPLOAD_USERNAME }} 80 | password: ${{ secrets.DOCKER_UPLOAD_TOKEN }} 81 | - name: create manifest 82 | run: docker manifest create plattar/python-usd-ar:version-${{ env.RELEASE_VERSION }} plattar/python-usd-ar:version-${{ env.RELEASE_VERSION }}-amd64 plattar/python-usd-ar:version-${{ env.RELEASE_VERSION }}-arm64 83 | - name: upload manifest 84 | run: docker manifest push plattar/python-usd-ar:version-${{ env.RELEASE_VERSION }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Create a base from a pre-compiled version of USD tools 2 | # More info @ https://github.com/Plattar/python-usd 3 | # Unlike the python-usd container, python-usd-ar also contains the Schema 4 | # Definitions for ARKit and is useful for generating USDZ files with various 5 | # AR Features. 6 | # NOTE: As of 30/06/2022 this respository also builds and sets up 7 | # usdzconvert tools 8 | # For more info on usdconvert, visit https://developer.apple.com/augmented-reality/tools/ 9 | FROM plattar/python-usd:version-22.05b-slim-bullseye 10 | 11 | LABEL MAINTAINER PLATTAR(www.plattar.com) 12 | 13 | ENV USD_SCHEMA_FOLDER="usd_schemas" 14 | ENV USDZCONVERT_FOLDER="usdzconvert" 15 | ENV USDZCONVERT_VERSION="0.66" 16 | 17 | WORKDIR /usr/src/app 18 | 19 | # Update the environment path for USDZ Convert Tools 20 | ENV USDZCONVERT_BIN_PATH="/usr/src/app/xrutils/${USDZCONVERT_FOLDER}" 21 | ENV PATH="${PATH}:${USDZCONVERT_BIN_PATH}" 22 | 23 | # copy source folders into container 24 | COPY /${USD_SCHEMA_FOLDER} /usr/src/app/${USD_SCHEMA_FOLDER} 25 | COPY /${USDZCONVERT_FOLDER} ${USDZCONVERT_BIN_PATH} 26 | 27 | RUN apt-get update && apt-get install -y --no-install-recommends \ 28 | git \ 29 | build-essential \ 30 | cmake \ 31 | nasm \ 32 | libxrandr-dev \ 33 | libxcursor-dev \ 34 | libxinerama-dev \ 35 | libxi-dev && \ 36 | rm -rf /var/lib/apt/lists/* && \ 37 | # Clone the USD Repository 38 | git clone --branch "v${USD_VERSION}" --depth 1 https://github.com/PixarAnimationStudios/USD.git usdsrc && \ 39 | # Copy the AR Schema Components into the examples folder 40 | cp -a /usr/src/app/${USD_SCHEMA_FOLDER}/usdInteractive/ usdsrc/pxr/usd/ && \ 41 | # Use usdGenSchema to Generate all CPP source files that will be built 42 | cd usdsrc/pxr/usd/usdInteractive && usdGenSchema schema.usda . && cd /usr/src/app && \ 43 | # Add the directories into the CMakeLists.txt so everything gets built 44 | echo "add_subdirectory(usdInteractive)" >> usdsrc/pxr/usd/CMakeLists.txt && \ 45 | # Remove the old USD installation 46 | rm -rf ${USD_BUILD_PATH} && \ 47 | # build a new version with our new schemas 48 | python3 usdsrc/build_scripts/build_usd.py --no-examples --no-tutorials --no-imaging --no-usdview --no-draco --no-docs --no-tests ${USD_BUILD_PATH} && \ 49 | # remove source code as we don't need it anymore 50 | rm -rf usdsrc && \ 51 | rm -rf ${USD_SCHEMA_FOLDER} && \ 52 | # remove build files we no longer need to save space 53 | rm -rf ${USD_BUILD_PATH}/build && \ 54 | rm -rf ${USD_BUILD_PATH}/cmake && \ 55 | rm -rf ${USD_BUILD_PATH}/pxrConfig.cmake && \ 56 | rm -rf ${USD_BUILD_PATH}/share && \ 57 | rm -rf ${USD_BUILD_PATH}/src && \ 58 | # remove packages we no longer need/require 59 | # this keeps the container as small as possible 60 | # if others need them, they can install when extending 61 | apt-get purge -y git \ 62 | build-essential \ 63 | cmake \ 64 | nasm \ 65 | libxrandr-dev \ 66 | libxcursor-dev \ 67 | libxinerama-dev \ 68 | libxi-dev && \ 69 | apt autoremove -y && \ 70 | apt-get autoclean -y -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Twitter: @plattarglobal](https://img.shields.io/badge/contact-@plattarglobal-blue.svg?style=flat)](https://twitter.com/plattarglobal) 2 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat)](LICENSE) 3 | 4 | _Python USD AR_ is a docker container that contains pre-built versions of [Pixar USD](https://github.com/PixarAnimationStudios/USD) toolchain and [Apple USDZ Schema](https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar) definitions. Due to the amount of time it takes to build these tools, this container can serve as a useful base for other applications. Check out the Plattar [dockerhub](https://hub.docker.com/r/plattar/python-usd-ar) repository for the latest pre-built images. 5 | 6 | Looking for the standard _Python USD_ images without Apple USDZ Schema Definitions? Check out the [python-usd](https://github.com/Plattar/python-usd) repository. 7 | 8 | ### Acknowledgements 9 | 10 | This tool relies on the following open source projects. 11 | 12 | - [Apple AR Tools](https://developer.apple.com/augmented-reality/tools/) 13 | - [Apple USDZ Schemas](https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar) 14 | - [Plattar Python USD](https://github.com/Plattar/python-usd) 15 | -------------------------------------------------------------------------------- /live.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | plattar-usd-ar: 4 | build: . 5 | container_name: plattar-usd-ar 6 | image: plattar/python-usd-ar:latest 7 | command: 8 | - /bin/sh 9 | - -c 10 | - | 11 | tail -f /dev/null -------------------------------------------------------------------------------- /marc-build.sh: -------------------------------------------------------------------------------- 1 | docker buildx create --name plattar_python_usd_ar_builder 2 | docker buildx use plattar_python_usd_ar_builder 3 | docker buildx build --push --tag plattar/python-usd-ar:version-$1 --platform linux/amd64,linux/arm64 --file Dockerfile . -------------------------------------------------------------------------------- /nuke.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Nukes all built docker images 4 | docker stop plattar-usd-ar 5 | docker rm -v plattar-usd-ar 6 | docker rmi plattar/python-usd-ar:latest --force -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # push a local build into dockerhub 4 | docker tag plattar/python-usd-ar:latest plattar/python-usd-ar:version-$1-slim-bullseye 5 | docker push plattar/python-usd-ar:version-$1-slim-bullseye 6 | 7 | # revert for future use 8 | docker tag plattar/python-usd-ar:version-$1-slim-bullseye plattar/python-usd-ar:latest -------------------------------------------------------------------------------- /usd_schemas/LICENSE/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2020 Apple Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /usd_schemas/README.md: -------------------------------------------------------------------------------- 1 | # Schema Definitions for Third-Party Digital Content Creation (DCC) 2 | 3 | Update your local USD library to add interactive and augmented reality features. 4 | 5 | ## Overview 6 | 7 | These schema definition files contain a codified version of the specification addendum defined by [USDZ Schemas for AR][1]. As a developer of third-party digital content creation (DCC) software, you enable your users to configure interactive and AR features in their 3D assets by implementing the specification and providing additional UI. 8 | 9 | ## Integrate Interactive and AR Schemas 10 | 11 | To recognize and validate syntax, and to participate in USD features such as transform hierarchies, incorporate the new interactive and AR schemas into your DCC by copying the `schema.usda` files into your USD library and rebuilding. For more information on updating your USD library, see [Generating New Schema Classes][2]. 12 | 13 | [1]:https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar 14 | [2]:https://graphics.pixar.com/usd/docs/Generating-New-Schema-Classes.html 15 | -------------------------------------------------------------------------------- /usd_schemas/usdInteractive/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(PXR_PACKAGE usdInteractive) 2 | 3 | pxr_plugin(${PXR_PACKAGE} 4 | LIBRARIES 5 | tf 6 | sdf 7 | usd 8 | vt 9 | usdGeom 10 | 11 | INCLUDE_DIRS 12 | ${Boost_INCLUDE_DIRS} 13 | ${PYTHON_INCLUDE_DIRS} 14 | 15 | PUBLIC_HEADERS 16 | api.h 17 | 18 | PUBLIC_CLASSES 19 | preliminary_Action 20 | preliminary_AnchoringAPI 21 | preliminary_Behavior 22 | preliminary_ReferenceImage 23 | preliminary_Text 24 | preliminary_Trigger 25 | tokens 26 | 27 | PYTHON_CPPFILES 28 | moduleDeps.cpp 29 | 30 | PYMODULE_FILES 31 | __init__.py 32 | 33 | PYMODULE_CPPFILES 34 | module.cpp 35 | wrapPreliminary_Action.cpp 36 | wrapPreliminary_AnchoringAPI.cpp 37 | wrapPreliminary_Behavior.cpp 38 | wrapPreliminary_ReferenceImage.cpp 39 | wrapPreliminary_Text.cpp 40 | wrapPreliminary_Trigger.cpp 41 | wrapTokens.cpp 42 | 43 | RESOURCE_FILES 44 | generatedSchema.usda 45 | plugInfo.json 46 | schema.usda:usdInteractive/schema.usda 47 | ) -------------------------------------------------------------------------------- /usd_schemas/usdInteractive/__init__.py: -------------------------------------------------------------------------------- 1 | from . import _usdInteractive 2 | from pxr import Tf 3 | Tf.PrepareModule(_usdInteractive, locals()) 4 | del Tf 5 | 6 | try: 7 | from . import __DOC 8 | __DOC.Execute(locals()) 9 | del __DOC 10 | except Exception: 11 | pass -------------------------------------------------------------------------------- /usd_schemas/usdInteractive/module.cpp: -------------------------------------------------------------------------------- 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 | #include "pxr/base/tf/pySafePython.h" 25 | #include "pxr/pxr.h" 26 | #include "pxr/base/tf/pyModule.h" 27 | 28 | PXR_NAMESPACE_USING_DIRECTIVE 29 | 30 | TF_WRAP_MODULE 31 | { 32 | TF_WRAP(UsdInteractivePreliminary_Action); 33 | TF_WRAP(UsdInteractivePreliminary_AnchoringAPI); 34 | TF_WRAP(UsdInteractivePreliminary_Behavior); 35 | TF_WRAP(UsdInteractivePreliminary_ReferenceImage); 36 | TF_WRAP(UsdInteractivePreliminary_Text); 37 | TF_WRAP(UsdInteractivePreliminary_Trigger); 38 | } 39 | -------------------------------------------------------------------------------- /usd_schemas/usdInteractive/moduleDeps.cpp: -------------------------------------------------------------------------------- 1 | #include "pxr/pxr.h" 2 | #include "pxr/base/tf/registryManager.h" 3 | #include "pxr/base/tf/scriptModuleLoader.h" 4 | #include "pxr/base/tf/token.h" 5 | 6 | #include 7 | 8 | PXR_NAMESPACE_OPEN_SCOPE 9 | 10 | TF_REGISTRY_FUNCTION(TfScriptModuleLoader) { 11 | // List of direct dependencies for this library. 12 | const std::vector reqs = { 13 | TfToken("sdf"), 14 | TfToken("tf"), 15 | TfToken("usd"), 16 | TfToken("vt"), 17 | TfToken("usdGeom") 18 | }; 19 | TfScriptModuleLoader::GetInstance(). 20 | RegisterLibrary(TfToken("usdInteractive"), TfToken("pxr.UsdInteractive"), reqs); 21 | } 22 | 23 | PXR_NAMESPACE_CLOSE_SCOPE -------------------------------------------------------------------------------- /usd_schemas/usdInteractive/schema.usda: -------------------------------------------------------------------------------- 1 | #usda 1.0 2 | ( 3 | """ 4 | Copyright © 2020 Apple Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | This file describes the USD Interactive schemata for code generation. 13 | """ 14 | subLayers = [ 15 | @usd/schema.usda@, 16 | @usdGeom/schema.usda@ 17 | ] 18 | ) 19 | 20 | over "GLOBAL" ( 21 | customData = { 22 | string libraryName = "usdInteractive" 23 | string libraryPath = "pxr/usd/usdInteractive" 24 | string libraryPrefix = "UsdInteractive" 25 | string tokensPrefix = "UsdInteractive" 26 | } 27 | ) { 28 | } 29 | 30 | class "Preliminary_AnchoringAPI" 31 | ( 32 | inherits = 33 | customData = { 34 | token apiSchemaType = "singleApply" 35 | } 36 | doc = """ 37 | API schema that specifies that the prim and its children should be 38 | placed relative to a detected plane, image, or face. 39 | 40 | When applied to a prim, this API schema allows the runtime to determine 41 | the transform of that prim and its children independently from its 42 | parent transform. 43 | 44 | \\section Anchor Layering 45 | 46 | When one or more anchorable prims are defined beneath another anchorable 47 | prim, each anchorable prim will be positioned independently and the 48 | positions of its non-anchorable children will be located relative to it. 49 | 50 | \\note 51 | Due to the independent nature of anchorable prims, it is recommended 52 | that each anchorable prim be placed at the top level of your content. 53 | This also helps make it clear that each subtree has its transform 54 | independently calculated by the runtime. 55 | """ 56 | ) 57 | { 58 | uniform token preliminary:anchoring:type ( 59 | allowedTokens = ["plane", "image", "face", "none"] 60 | doc = """ 61 | Defines the type of anchoring for the prim. This is a required 62 | property for this schema. 63 | 64 | plane: The content will be placed along the normal and the center of 65 | the detected plane. 66 | image: The content will be placed along the normal and center of the 67 | detected image. 68 | face: The content will be placed along the normal and at the center 69 | of the detected face. 70 | none: The content will not be anchored. This is equivalent to not 71 | applying the anchoring API schema to a prim at all. 72 | """ 73 | ) 74 | 75 | uniform token preliminary:planeAnchoring:alignment ( 76 | allowedTokens = ["horizontal", "vertical", "any"] 77 | doc = """ 78 | Specifies the kind of detected plane the prim and its children 79 | should be placed relative to. This property is only active if the 80 | anchoring type is "plane". 81 | 82 | horizontal: Horizontal planes include floors, tables, ceilings, and more. 83 | vertical: Vertical planes include walls, doors, windows, and more. 84 | """ 85 | ) 86 | 87 | rel preliminary:imageAnchoring:referenceImage ( 88 | doc = """ 89 | Specifies the kind of detected image reference the prim and its 90 | children should be placed relative to. This property is only active 91 | if the anchoring type is "image". 92 | 93 | \\note 94 | This should point to a prim with the type "ReferenceImage". 95 | """ 96 | ) 97 | } 98 | 99 | class Preliminary_ReferenceImage "Preliminary_ReferenceImage" ( 100 | doc = """ 101 | Defines an image anchoring reference, which includes the image and its 102 | physical width. 103 | """ 104 | inherits = 105 | ) 106 | { 107 | uniform asset image ( 108 | doc = """ 109 | The image to which this prim should be placed relative to. This 110 | should point to an image. This property is only active if the 111 | anchoring type is "image". 112 | 113 | \\note 114 | In a USDZ, the only valid image types are png and jpeg (any of the 115 | multiple common extensions for jpeg). 116 | """ 117 | ) 118 | 119 | uniform double physicalWidth = 0.0 ( 120 | doc = """ 121 | Specifies the physical, real-world width, defined in centimeters to 122 | avoid unit changes due to composition, of the image to which this prim 123 | should be placed relative to. This property can be used as a reference 124 | for AR runtimes to determine the approximate image size to look for 125 | and anchor this content to. This property is only active if the 126 | anchoring type is "image". 127 | 128 | \\note 129 | This property is not affected by its transform hierarchy as it 130 | describes a physical width in the real world. 131 | 132 | \\note 133 | The height is not required because it can be determined based on the 134 | aspect ratio of the image. 135 | """ 136 | ) 137 | } 138 | 139 | class Preliminary_Behavior "Preliminary_Behavior" ( 140 | doc = """A Behavior encapsulates a set of triggers and their associated actions.""" 141 | inherits = 142 | customData = { 143 | string className = "Preliminary_Behavior" 144 | } 145 | ) 146 | { 147 | rel triggers ( 148 | doc = """ 149 | List of \a Trigger prims that will execute the list of \p actions. 150 | """ 151 | ) 152 | rel actions ( 153 | doc = """ 154 | List of \a Action prims that are performed when elements of \p triggers 155 | are executed. These actions are executed serially. @see GroupAction 156 | """ 157 | ) 158 | uniform bool exclusive = false ( 159 | doc = """ 160 | Determines whether this behavior can be executed exclusively to other 161 | behaviors. 162 | Valid values are: 163 | - true: If a trigger in this behavior is executed, other exclusive 164 | behaviors will stop performing their actions. 165 | - false: Other actions in other behaviors can run concurrently 166 | with this behavior. (Default) 167 | """ 168 | ) 169 | } 170 | 171 | class "Preliminary_Trigger" ( 172 | doc = """A Trigger represents an event that when executed, causes an *action* 173 | to be performed. 174 | Triggers can be executed by: 175 | - User input: e.g. a tap gesture 176 | - Scene state: e.g. proximity to the camera 177 | - Programmatically: e.g. as a result of application state or other event 178 | This is the base class for all Behavior triggers. 179 | """ 180 | inherits = 181 | customData = { 182 | string className = "Preliminary_Trigger" 183 | } 184 | ) 185 | { 186 | uniform token info:id ( 187 | doc = """The id is the identifier for the type or purpose of the trigger. 188 | E.g. TapGesture, ProximityToCamera 189 | The value of this id is interpreted by the runtime implementation of the 190 | behavior system. 191 | """ 192 | ) 193 | } 194 | 195 | class "Preliminary_Action" ( 196 | doc = """An Action is performed when a *Trigger* is executed. 197 | Performing an action is how a Behavior modifies the state of the scene dynamically. 198 | For example, an action might start an animation playing, change the transform 199 | of an *Xformable*, or start looping audio. 200 | 201 | This is the base class for Behavior actions""" 202 | inherits = 203 | customData = { 204 | string className = "Preliminary_Action" 205 | } 206 | ) 207 | { 208 | uniform token info:id ( 209 | doc = """The id is the identifier for the type or purpose of the action. 210 | E.g. Impulse, Group 211 | The value of this id is interpreted by the runtime implementation of the 212 | behavior system. 213 | """ 214 | ) 215 | uniform token multiplePerformOperation= "ignore" ( 216 | allowedTokens = ["ignore", "allow", "stop"] 217 | doc = """Defines how this action handles a request be performed again while 218 | already running. 219 | Valid values are: 220 | - allow: Perform the action again, effectively restarting it. 221 | - ignore: Ignore the perform request, and continue running the current action. 222 | - stop: Stops the current action. 223 | """ 224 | ) 225 | } 226 | 227 | class Preliminary_Text "Preliminary_Text" ( 228 | doc = """Defines 3D extruded text geometry in the scene""" 229 | inherits = 230 | ) 231 | { 232 | string content = "" ( 233 | doc = """ 234 | Text contents. This string may include line breaks which will be honored. 235 | """ 236 | ) 237 | string[] font ( 238 | doc = """ 239 | An array of font names. They will be traversed in order and the first one that matches an 240 | available font will be used. If no font matches exactly the behavior is undefined, although 241 | there may be some attempt to find a related font. The font name string contains the family 242 | and any styling attributes. 243 | """ 244 | ) 245 | float pointSize = 144.0 ( 246 | doc = """ 247 | Font size in points. 248 | """ 249 | ) 250 | float width ( 251 | doc = """ 252 | Width (X) of the text bounding rectangle in scene units. Must be positive. Is ignored 253 | if wrapMode is set to singleLine. 254 | """ 255 | ) 256 | float height ( 257 | doc = """ 258 | Height (Y) of the text bounding rectangle in scene units. Must be positive. Is ignored 259 | if wrapMode is set to singleLine. 260 | """ 261 | ) 262 | float depth = 0 ( 263 | doc = """ 264 | Extrusion depth (Z) in scene units. Must be non-negative. The geometry is visible from 265 | both sides even for a zero extrusion depth. 266 | """ 267 | ) 268 | token wrapMode = "flowing" ( 269 | allowedTokens = ["singleLine", "hardBreaks", "flowing"] 270 | doc = """ 271 | Hint about the intent of the text flow. 272 | singleLine: The entire content is a single line 273 | hardBreaks: The content contains line breaks and no other line breaking is allowed 274 | flowing: The content can flow in the bounds by adding line breaks 275 | """ 276 | ) 277 | token horizontalAlignment = "center" ( 278 | allowedTokens = ["left", "center", "right", "justified"] 279 | doc = """ 280 | Placement of each line relative to the bounding rectangle. 281 | left: Left-align each line 282 | center: Center-align each line 283 | right: Right-align each line 284 | justified: Left-align each line, and add spacing between words to right-align also, if possible 285 | """ 286 | ) 287 | token verticalAlignment = "middle" ( 288 | allowedTokens = ["top", "middle", "lowerMiddle", "baseline", "bottom"] 289 | doc = """ 290 | Vertical placement of the text. 291 | For a single line the alignment is relative to font features: 292 | top: ascender 293 | middle: center of capital letters 294 | lowerMiddle: center of lowercase letters 295 | baseline: baseline 296 | bottom: descender 297 | For multi-line text the alignment is relative to the bounds: 298 | top: lines aligned with the top 299 | middle, lowerMiddle: lines together with equal space above and below 300 | baseline, bottom: lines aligned with the bottom 301 | """ 302 | ) 303 | } 304 | -------------------------------------------------------------------------------- /usdzconvert/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2018 Apple Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /usdzconvert/fixOpacity: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Fix opacity wiring for usdz generated with xcode's usdz_converter 4 | import zipfile 5 | import os, shutil, sys, argparse 6 | import fnmatch 7 | from pxr import * 8 | # TODO: remove PIL to replace with binary 9 | from PIL import Image 10 | 11 | verboseOutput = False 12 | 13 | def unzip(filePath, outputFolder): 14 | # unzip to folder/fileName 15 | foldePath, file = os.path.split(filePath) 16 | fileName = file.split(".")[0] 17 | outputDir = os.path.join(outputFolder, fileName) 18 | if not os.path.exists(outputDir): 19 | os.makedirs(outputDir) 20 | else: 21 | # clear existing folder 22 | shutil.rmtree(outputDir) 23 | os.makedirs(outputDir) 24 | 25 | with zipfile.ZipFile(filePath) as zf: 26 | zf.extractall(outputDir) 27 | 28 | return outputDir 29 | 30 | def gatherAllUSDCFiles(inputDir): 31 | usdcs = [] 32 | for root, dirnames, filenames in os.walk(inputDir): 33 | for filename in filenames: 34 | # usdz_convert only allows usdc files in usdz archive 35 | if filename.endswith(".usdc") or filename.endswith(".USDC"): 36 | usdcs.append(os.path.join(root, filename)) 37 | 38 | return usdcs 39 | 40 | def gatherMaterials(stage): 41 | predicate = Usd.TraverseInstanceProxies(Usd.PrimIsActive & Usd.PrimIsDefined & ~Usd.PrimIsAbstract) 42 | materialPrims = set() 43 | for prim in stage.Traverse(predicate): 44 | if prim.GetTypeName() == "Mesh": 45 | subsets = UsdGeom.Subset.GetGeomSubsets(UsdGeom.Mesh(prim)) 46 | for subset in subsets: 47 | material = UsdShade.MaterialBindingAPI(subset).ComputeBoundMaterial() 48 | if material is not None and material[0]: 49 | materialPrims.add(material[0]) 50 | # there's no guarantee all face will be covered by geom subsets 51 | primMaterial = UsdShade.MaterialBindingAPI(prim).ComputeBoundMaterial() 52 | if primMaterial is not None and primMaterial[0]: 53 | materialPrims.add(primMaterial[0]) 54 | 55 | return materialPrims 56 | 57 | def getPBRShader(materialPrim): 58 | material = UsdShade.Material(materialPrim) 59 | sourcePrim = UsdShade.ConnectableAPI(materialPrim) 60 | try: 61 | connection = sourcePrim.GetConnectedSource(sourcePrim.GetOutput("surface")) 62 | except: 63 | connection = None 64 | if connection is None: return None 65 | 66 | shader = UsdShade.Shader(connection[0]) 67 | try: 68 | shaderType = shader.GetIdAttr().Get() 69 | except: 70 | return None 71 | if shaderType == "UsdPreviewSurface": 72 | return shader 73 | return None 74 | 75 | # return a tuple between diffuse texture and PBR shader 76 | def gatherDiffuseTexture(materialPrim): 77 | pbrShader = getPBRShader(materialPrim) 78 | if pbrShader is None: 79 | return None 80 | 81 | sourcePrim = UsdShade.ConnectableAPI(pbrShader) 82 | try: 83 | connection = sourcePrim.GetConnectedSource(sourcePrim.GetInput("diffuseColor")) 84 | except: 85 | return None 86 | 87 | if connection is None: return None 88 | connectedPrim = connection[0] 89 | shader = UsdShade.Shader(connectedPrim) 90 | 91 | try: 92 | shaderType = shader.GetIdAttr().Get() 93 | except: 94 | return None 95 | if shaderType == "UsdUVTexture": 96 | try: 97 | assetPath = shader.GetInput("file").Get() 98 | except: 99 | if verboseOutput: print("Warning: unable to find texture in shader " + pbrShader.GetPath().pathString) 100 | return None 101 | if assetPath.resolvedPath is not "": 102 | return (assetPath.resolvedPath, pbrShader.GetPath().pathString) 103 | else: 104 | print("Warning: unable to find texture" + assetPath.path + " in archive") 105 | return None 106 | 107 | # check if pbrShader already has valid opacity connection 108 | def hasOpacityConnectionOrNonUnitOpacityValue(pbrShader): 109 | sourcePrim = UsdShade.ConnectableAPI(pbrShader) 110 | try: 111 | opacityValue = sourcePrim.GetInput("opacity").Get() 112 | if opacityValue != None and opacityValue < 1.0: 113 | return True 114 | connection = sourcePrim.GetConnectedSource(sourcePrim.GetInput("opacity")) 115 | except: 116 | return False 117 | if connection is not None: 118 | shader = UsdShade.Shader(connection[0]) 119 | if shader: 120 | return True 121 | return False 122 | 123 | # TODO: remove this function to use binary 124 | def textureHasAlpha(texturePath): 125 | img = Image.open(texturePath, 'r') 126 | if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info): 127 | converted = img.convert('RGBA') 128 | pixeldata = list(converted.getdata()) 129 | for i, pixel in enumerate(pixeldata): 130 | if pixel[3] < 255: 131 | return True 132 | return False 133 | 134 | def updateMaterialsWithTexture(pbrShader): 135 | sourcePrim = UsdShade.ConnectableAPI(pbrShader) 136 | try: 137 | connection = sourcePrim.GetConnectedSource(sourcePrim.GetInput("diffuseColor")) 138 | except: 139 | return 140 | if connection is not None: 141 | shader = UsdShade.Shader(connection[0]) 142 | if shader: 143 | output = shader.CreateOutput("a", Sdf.ValueTypeNames.Float) 144 | if sourcePrim.GetInput("opacity").Get() == None: 145 | sourcePrim.CreateInput('opacity', Sdf.ValueTypeNames.Float) 146 | else: 147 | sourcePrim.GetInput("opacity").GetAttr().Clear() 148 | UsdShade.ConnectableAPI.ConnectToSource(sourcePrim.GetInput("opacity"), output) 149 | return False 150 | 151 | parser = argparse.ArgumentParser(description='Fix opacity material definition for\ 152 | materials implicitly using diffuseColor texture\'s alpha channel as opacity input.') 153 | parser.add_argument("-v", "--verbose", action='store_true', help="Verbose mode.") 154 | parser.add_argument("files", nargs='*', help="Input assets") 155 | parser.add_argument("-o", "--output", action='store', help="Output folder for fixed usdz files.") 156 | 157 | if len(sys.argv) < 2: 158 | parser.print_help() 159 | sys.exit(0) 160 | 161 | args=parser.parse_args() 162 | 163 | verboseOutput = args.verbose 164 | 165 | if len(args.files) > 1 and os.path.isdir(args.output) == False: 166 | print("Warning: please specify output directory for more than one input files") 167 | sys.exit(1) 168 | 169 | for filename in args.files: 170 | if verboseOutput: 171 | print("Checking file " + filename) 172 | if os.path.isabs(filename): 173 | cwd = os.getcwd() 174 | filename = os.path.join(cwd, filename) 175 | 176 | usds=[] 177 | extension = os.path.splitext(filename)[-1].lower() 178 | if not os.path.exists(filename): 179 | print("Warning: " + filename + " does not exist") 180 | continue 181 | 182 | if extension == '.usdz': 183 | tempDir = unzip(filename, "/tmp/fixOpacity") 184 | usds = gatherAllUSDCFiles(tempDir) 185 | elif extension in ['.usd', '.usda', '.usdc']: 186 | usds.append(filename) 187 | else: 188 | if verboseOutput: print("Error: input file " + filename + " is not a USD file") 189 | continue 190 | 191 | updated = False 192 | for usdFile in usds: 193 | stage = Usd.Stage.Open(usdFile) 194 | materials = gatherMaterials(stage) 195 | diffuseTextures = dict() 196 | 197 | for material in materials: 198 | result = gatherDiffuseTexture(material) 199 | if result is None: 200 | continue 201 | if hasOpacityConnectionOrNonUnitOpacityValue(stage.GetPrimAtPath(result[1])): 202 | continue 203 | 204 | if result[0] in diffuseTextures.keys(): 205 | diffuseTextures[result[0]].add(result[1]) 206 | else: 207 | diffuseTextures[result[0]] = set([result[1]]) 208 | 209 | for i, (texturePath, pbrShaders) in enumerate(diffuseTextures.items()): 210 | if textureHasAlpha(texturePath): 211 | for pbrShader in pbrShaders: 212 | updateMaterialsWithTexture(stage.GetPrimAtPath(pbrShader)) 213 | updated = True 214 | stage.Save() 215 | 216 | if updated: 217 | inputPath, file = os.path.split(filename) 218 | if args.output != None: 219 | outputPath = os.path.join(args.output, file) 220 | else: 221 | outputDir = os.path.join(inputPath,'fixOpacityOutput') 222 | if not os.path.exists(outputDir): 223 | os.makedirs(outputDir) 224 | outputPath = os.path.join(outputDir, file) 225 | if verboseOutput: print("Export updated usdz asset to " + outputPath) 226 | UsdUtils.CreateNewARKitUsdzPackage(usds[0], outputPath) 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /usdzconvert/help.txt: -------------------------------------------------------------------------------- 1 | Converts 3D model file to usd/usda/usdc/usdz. 2 | 3 | positional argument: 4 | inputFile Input file: OBJ/glTF(.gltf/glb)/FBX/Alembic(.abc)/USD(.usd/usda/usdc/usdz) files. 5 | 6 | optional arguments: 7 | outputFile Output .usd/usda/usdc/usdz files. 8 | -h, --help Show this help message and exit. 9 | -version Show version of converter and exit. 10 | -f Read arguments from 11 | -v Verbose output. 12 | -path 13 | Add search paths to find textures 14 | -url Add URL metadata 15 | -creator Set custom creator in USD metadata 16 | -copyright "copyright message" 17 | Add copyright metadata 18 | -copytextures Copy texture files (for .usd/usda/usdc) workflows 19 | -metersPerUnit value Set metersPerUnit attribute with float value 20 | -useObjMtl Load materials from mtl file for obj 21 | -loop Set animation loop flag to 1 22 | -no-loop Set animation loop flag to 0 23 | -m materialName Subsequent material arguments apply to this material. 24 | If no material is present in input file, a material of 25 | this name will be generated. 26 | -iOS12 Make output file compatible with iOS 12 frameworks 27 | -texCoordSet name The name of the texture coordinates to use for current 28 | material. Default texture coordinate set is "st". 29 | -wrapS mode Texture wrap mode for texture S-coordinate. 30 | mode can be: black, clamp, repeat, mirror, or useMetadata (default) 31 | -wrapT mode Texture wrap mode for texture T-coordinate. 32 | mode can be: black, clamp, repeat, mirror, or useMetadata (default) 33 | 34 | -diffuseColor r,g,b Set diffuseColor to constant color r,g,b with values in 35 | the range [0 .. 1] 36 | -diffuseColor fr,fg,fb 37 | Use as texture for diffuseColor. 38 | fr,fg,fb: (optional) constant fallback color, with 39 | values in the range [0..1]. 40 | 41 | -normal x,y,z Set normal to constant value x,y,z in tangent space 42 | [(-1, -1, -1), (1, 1, 1)]. 43 | -normal fx,fy,fz 44 | Use as texture for normal. 45 | fx,fy,fz: (optional) constant fallback value, with 46 | values in the range [-1..1]. 47 | 48 | -emissiveColor r,g,b Set emissiveColor to constant color r,g,b with values in 49 | the range [0..1] 50 | -emissiveColor fr,fg,fb 51 | Use as texture for emissiveColor. 52 | fr,fg,fb: (optional) constant fallback color, with 53 | values in the range [0..1]. 54 | 55 | -metallic c Set metallic to constant c, in the range [0..1] 56 | -metallic ch fc 57 | Use as texture for metallic. 58 | ch: (optional) texture color channel (r, g, b or a). 59 | fc: (optional) fallback constant in the range [0..1] 60 | 61 | -roughness c Set roughness to constant c, in the range [0..1] 62 | -roughness ch fc 63 | Use as texture for roughness. 64 | ch: (optional) texture color channel (r, g, b or a). 65 | fc: (optional) fallback constant in the range [0..1] 66 | 67 | -occlusion c Set occlusion to constant c, in the range [0..1] 68 | -occlusion ch fc 69 | Use as texture for occlusion. 70 | ch: (optional) texture color channel (r, g, b or a). 71 | fc: (optional) fallback constant in the range [0..1] 72 | 73 | -opacity c Set opacity to constant c, in the range [0..1] 74 | -opacity ch fc Use as texture for opacity. 75 | ch: (optional) texture color channel (r, g, b or a). 76 | fc: (optional) fallback constant in the range [0..1] 77 | -clearcoat c Set clearcoat to constant c, in the range [0..1] 78 | -clearcoat ch fc 79 | Use as texture for clearcoat. 80 | ch: (optional) texture color channel (r, g, b or a). 81 | fc: (optional) fallback constant in the range [0..1] 82 | -clearcoatRoughness c Set clearcoat roughness to constant c, in the range [0..1] 83 | -clearcoatRoughness ch fc 84 | Use as texture for clearcoat roughness. 85 | ch: (optional) texture color channel (r, g, b or a). 86 | fc: (optional) fallback constant in the range [0..1] 87 | 88 | examples: 89 | usdzconvert chicken.gltf 90 | 91 | usdzconvert cube.obj -diffuseColor albedo.png 92 | 93 | usdzconvert cube.obj -diffuseColor albedo.png -opacity a albedo.png 94 | 95 | usdzconvert vase.obj -m bodyMaterial -diffuseColor body.png -opacity a body.png -metallic r metallicRoughness.png -roughness g metallicRoughness.png -normal normal.png -occlusion ao.png 96 | 97 | usdzconvert subset.obj -m leftMaterial -diffuseColor left.png -m rightMaterial -diffuseColor right.png 98 | -------------------------------------------------------------------------------- /usdzconvert/iOS12LegacyModifier.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from shutil import copyfile 3 | import imp 4 | from pxr import * 5 | 6 | import usdUtils 7 | 8 | 9 | _pilLibraryLoaded = True 10 | try: 11 | imp.find_module('PIL') 12 | from PIL import Image 13 | except ImportError: 14 | usdUtils.printError('failed to import PIL. Please install module, e.g. using "$ sudo pip3 install pillow".') 15 | _pilLibraryLoaded = False 16 | 17 | 18 | 19 | class iOS12LegacyModifier: 20 | def __init__(self): 21 | self.oneChannelTextures = {} 22 | 23 | 24 | def eulerWithQuat(self, quat): 25 | rot = Gf.Rotation() 26 | rot.SetQuat(quat) 27 | return rot.Decompose(Gf.Vec3d(1, 0, 0), Gf.Vec3d(0, 1, 0), Gf.Vec3d(0, 0, 1)) 28 | 29 | 30 | def getEulerFromData(self, data, offset): 31 | quat = Gf.Quatf(float(data[offset + 3]), Gf.Vec3f(float(data[offset]), float(data[offset + 1]), float(data[offset + 2]))) 32 | return self.eulerWithQuat(quat) 33 | 34 | 35 | def makeOneChannelTexture(self, srcFile, dstFolder, channel, verbose): 36 | if not _pilLibraryLoaded: 37 | return '' 38 | pilChannel = channel.upper() 39 | if pilChannel != 'R' and pilChannel != 'G' and pilChannel != 'B': 40 | return '' 41 | 42 | basename = os.path.basename(srcFile) 43 | (name, ext) = os.path.splitext(basename) 44 | textureFilename = name + '_' + channel + ext 45 | lenDstFolder = len(dstFolder) 46 | newPath = dstFolder 47 | if lenDstFolder > 0 and dstFolder[lenDstFolder-1] != '/' and dstFolder[lenDstFolder-1] != '\\': 48 | newPath += '/' 49 | newPath += textureFilename 50 | if newPath in self.oneChannelTextures: 51 | return self.oneChannelTextures[newPath] 52 | 53 | image = None 54 | try: 55 | image = Image.open(srcFile) 56 | image = image.getchannel(pilChannel) 57 | except: 58 | usdUtils.printWarning("can't get channel " + pilChannel + " from texture " + basename) 59 | return '' 60 | 61 | if image is not None: 62 | image.save(newPath) 63 | self.oneChannelTextures[newPath] = textureFilename 64 | if verbose: 65 | print('One channel texture: ' + textureFilename) 66 | return textureFilename 67 | return '' 68 | 69 | 70 | def makeORMTextures(self, material, folder, verbose): 71 | inputNames = [ 72 | usdUtils.InputName.occlusion, 73 | usdUtils.InputName.roughness, 74 | usdUtils.InputName.metallic 75 | ] 76 | 77 | for inputName in inputNames: 78 | texture = self._getMapTextureFilename(material, inputName) 79 | if texture: 80 | map = material.inputs[inputName] 81 | file = self.makeOneChannelTexture(folder + '/' + texture, folder, map.channels, verbose) 82 | if file: 83 | map.file = file 84 | map.channels = 'r' 85 | 86 | 87 | def addSkelAnimToMesh(self, usdMesh, skeleton): 88 | if skeleton.usdSkelAnim is not None: 89 | usdSkelBinding = UsdSkel.BindingAPI(usdMesh) 90 | usdSkelBinding.CreateAnimationSourceRel().AddTarget(skeleton.usdSkelAnim.GetPath()) 91 | 92 | 93 | def opacityAndDiffuseOneTexture(self, material): 94 | opacity = material.inputs[usdUtils.InputName.opacity] if usdUtils.InputName.opacity in material.inputs else None 95 | if not isinstance(opacity, usdUtils.Map): 96 | return 97 | diffuse = material.inputs[usdUtils.InputName.diffuseColor] if usdUtils.InputName.diffuseColor in material.inputs else None 98 | if not isinstance(diffuse, usdUtils.Map): 99 | return 100 | if opacity.file and diffuse.file and opacity.file != diffuse.file: 101 | usdUtils.printError('iOS12 compatibility: material ' + material.name + ' has different texture files for diffuseColor and opacity.') 102 | raise usdUtils.ConvertError() 103 | 104 | 105 | def _getMapTextureFilename(self, material, inputName): 106 | if not inputName in material.inputs: 107 | return None 108 | input = material.inputs[inputName] 109 | if not isinstance(input, usdUtils.Map): 110 | return None 111 | return input.file 112 | 113 | 114 | 115 | def createLegacyModifier(): 116 | return iOS12LegacyModifier() 117 | 118 | -------------------------------------------------------------------------------- /usdzconvert/usdARKitChecker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import subprocess, sys, os, argparse 4 | from pxr import * 5 | from validateMesh import validateMesh 6 | from validateMaterial import validateMaterial 7 | 8 | def validateFile(file, verbose, errorData): 9 | stage = Usd.Stage.Open(file) 10 | success = True 11 | predicate = Usd.TraverseInstanceProxies(Usd.PrimIsActive & Usd.PrimIsDefined & ~Usd.PrimIsAbstract) 12 | for prim in stage.Traverse(predicate): 13 | if prim.GetTypeName() == "Mesh": 14 | success = validateMesh(prim, verbose, errorData) and success 15 | if prim.GetTypeName() == "Material": 16 | success = validateMaterial(prim, verbose, errorData) and success 17 | return success 18 | 19 | def runValidators(filename, verboseOutput, errorData): 20 | checker = UsdUtils.ComplianceChecker(arkit=True, 21 | skipARKitRootLayerCheck=False, rootPackageOnly=False, 22 | skipVariants=False, verbose=False) 23 | 24 | checker.CheckCompliance(filename) 25 | errors = checker.GetErrors() 26 | failedChecks = checker.GetFailedChecks() 27 | for rule in checker._rules: 28 | error = rule.__class__.__name__ 29 | failures = rule.GetFailedChecks() 30 | if len(failures) > 0: 31 | errorData.append({ "code": "PXR_" + error }) 32 | errors.append(error) 33 | 34 | usdCheckerResult = len(errors) == 0 35 | mdlValidation = validateFile(filename, verboseOutput, errorData) 36 | 37 | success = usdCheckerResult and mdlValidation 38 | print("usdARKitChecker: " + ("[Pass]" if success else "[Fail]") + " " + filename) 39 | 40 | def main(argumentList, outErrorList=None): 41 | parser = argparse.ArgumentParser() 42 | parser.add_argument("--verbose", "-v", action='store_true', help="Verbose mode.") 43 | parser.add_argument('files', nargs='*') 44 | args=parser.parse_args(argumentList) 45 | 46 | verboseOutput = args.verbose 47 | totalSuccess = True 48 | for filename in args.files: 49 | errorData = [] 50 | runValidators(filename, verboseOutput, errorData) 51 | if outErrorList is not None: 52 | outErrorList.append({ "file": filename, "errors": errorData }) 53 | totalSuccess = totalSuccess and len(errorData) == 0 54 | 55 | if totalSuccess: 56 | return 0 57 | else: 58 | return 1 59 | 60 | if __name__ == '__main__': 61 | argumentList = sys.argv[1:] 62 | sys.exit(main(argumentList)) 63 | -------------------------------------------------------------------------------- /usdzconvert/usdMaterialWithObjMtl.py: -------------------------------------------------------------------------------- 1 | from pxr import * 2 | 3 | import struct 4 | import sys 5 | import os.path 6 | import time 7 | 8 | import usdUtils 9 | 10 | 11 | __all__ = ['usdMaterialWithObjMtl'] 12 | 13 | 14 | def linesContinuation(fileHandle): 15 | for line in fileHandle: 16 | line = line.rstrip('\n') 17 | line = line.rstrip() 18 | while line.endswith('\\'): 19 | thisLine = line[:-1] 20 | nextLine = next(fileHandle).rstrip('\n') 21 | nextLine = nextLine.strip() 22 | line = thisLine + ' ' + nextLine 23 | yield line 24 | 25 | 26 | 27 | def usdMaterialWithObjMtl(converter, filename): 28 | if not os.path.isfile(filename): 29 | usdUtils.printWarning("Can't load material file. File not found: " + filename) 30 | return 31 | 32 | with open(filename, errors='ignore') as file: 33 | material = None 34 | 35 | primvarName = 'st' 36 | wrapS = usdUtils.WrapMode.repeat 37 | wrapT = usdUtils.WrapMode.repeat 38 | scaleFactor=None 39 | 40 | for line in linesContinuation(file): 41 | line = line.strip() 42 | if not line or '#' == line[0]: 43 | continue 44 | 45 | arguments = list(filter(None, line.split(' '))) 46 | command = arguments[0] 47 | arguments = arguments[1:] 48 | 49 | if 'newmtl' == command: 50 | primvarName = 'st' 51 | wrapS = usdUtils.WrapMode.repeat 52 | wrapT = usdUtils.WrapMode.repeat 53 | scaleFactor=None 54 | 55 | matName = ' '.join(arguments) 56 | converter.setMaterial(matName) 57 | material = usdUtils.Material(matName) 58 | converter.materialsByName[matName] = material 59 | elif material is not None: 60 | if 'Kd' == command: 61 | diffuseColor = arguments 62 | if usdUtils.InputName.diffuseColor in material.inputs: 63 | material.inputs[usdUtils.InputName.diffuseColor].scale = diffuseColor 64 | else: 65 | material.inputs[usdUtils.InputName.diffuseColor] = diffuseColor 66 | scaleFactor = diffuseColor 67 | elif 'd' == command: 68 | material.inputs[usdUtils.InputName.opacity] = arguments[0] if len(arguments) > 0 else 1 69 | elif 'map_Kd' == command: 70 | textureFilename = usdUtils.resolvePath(' '.join(arguments), converter.srcFolder, converter.searchPaths) 71 | material.inputs[usdUtils.InputName.diffuseColor] = usdUtils.Map('rgb', textureFilename, None, primvarName, wrapS, wrapT, scaleFactor) 72 | elif 'map_bump' == command or 'bump' == command: 73 | textureFilename = usdUtils.resolvePath(' '.join(arguments), converter.srcFolder, converter.searchPaths) 74 | material.inputs[usdUtils.InputName.normal] = usdUtils.Map('rgb', textureFilename, None, primvarName, wrapS, wrapT) 75 | elif 'map_ao' == command: 76 | textureFilename = usdUtils.resolvePath(' '.join(arguments), converter.srcFolder, converter.searchPaths) 77 | material.inputs[usdUtils.InputName.occlusion] = usdUtils.Map('rgb', textureFilename, None, primvarName, wrapS, wrapT) 78 | elif 'map_metallic' == command: 79 | textureFilename = usdUtils.resolvePath(' '.join(arguments), converter.srcFolder, converter.searchPaths) 80 | material.inputs[usdUtils.InputName.metallic] = usdUtils.Map('rgb', textureFilename, None, primvarName, wrapS, wrapT) 81 | elif 'map_roughness' == command: 82 | textureFilename = usdUtils.resolvePath(' '.join(arguments), converter.srcFolder, converter.searchPaths) 83 | material.inputs[usdUtils.InputName.roughness] = usdUtils.Map('rgb', textureFilename, None, primvarName, wrapS, wrapT) 84 | 85 | 86 | -------------------------------------------------------------------------------- /usdzconvert/usdStageWithObj.py: -------------------------------------------------------------------------------- 1 | from pxr import * 2 | 3 | import struct 4 | import sys 5 | import os.path 6 | import time 7 | import importlib 8 | 9 | import usdUtils 10 | 11 | 12 | __all__ = ['usdStageWithObj'] 13 | 14 | 15 | INVALID_INDEX = -1 16 | LAST_ELEMENT = -1 17 | 18 | 19 | def convertObjIndexToUsd(strIndex, elementsCount): 20 | if not strIndex: 21 | return INVALID_INDEX 22 | index = int(strIndex) 23 | # OBJ indices starts from 1, USD indices starts from 0 24 | if 0 < index and index <= elementsCount: 25 | return index - 1 26 | # OBJ indices can be negative as reverse indexing 27 | if index < 0: 28 | return elementsCount + index 29 | return INVALID_INDEX 30 | 31 | 32 | def fixExponent(value): 33 | # allow for scientific notation with X.Y(+/-)eZ 34 | return float(value.lower().replace('+e', 'e+').replace('-e', 'e-')) 35 | 36 | 37 | def floatList(v): 38 | try: 39 | return list(map(float, v)) 40 | except ValueError: 41 | return list(map(fixExponent, v)) 42 | except: 43 | raise 44 | 45 | 46 | def linesContinuation(fileHandle): 47 | for line in fileHandle: 48 | line = line.rstrip('\n') 49 | line = line.rstrip() 50 | while line.endswith('\\'): 51 | thisLine = line[:-1] 52 | nextLine = next(fileHandle).rstrip('\n') 53 | nextLine = nextLine.strip() 54 | line = thisLine + ' ' + nextLine 55 | yield line 56 | 57 | 58 | 59 | class Subset: 60 | def __init__(self, materialIndex): 61 | self.faces = [] 62 | self.materialIndex = materialIndex 63 | 64 | 65 | class Group: 66 | def __init__(self, materialIndex): 67 | self.subsets = [] 68 | self.currentSubset = None 69 | 70 | self.vertexIndices = [] 71 | 72 | self.uvIndices = [] 73 | self.uvsHaveOwnIndices = False # avoid creating indexed uv UsdAttribute if uv indices are identical to vertex indices 74 | 75 | self.normalIndices = [] 76 | self.normalsHaveOwnIndices = False # avoid creating indexed normal UsdAttribute if normal indices are identical to vertex indices 77 | 78 | self.faceVertexCounts = [] 79 | self.setMaterial(materialIndex) 80 | 81 | 82 | def setMaterial(self, materialIndex): 83 | self.currentSubset = None 84 | for subset in self.subsets: 85 | if subset.materialIndex == materialIndex: 86 | self.currentSubset = subset 87 | break 88 | # if currentSubset does not exist, create new one and append to subsets 89 | if self.currentSubset == None: 90 | # remove last empty subset 91 | if len(self.subsets) and len(self.subsets[LAST_ELEMENT].faces) == 0: 92 | del self.subsets[LAST_ELEMENT] 93 | 94 | self.currentSubset = Subset(materialIndex) 95 | self.subsets.append(self.currentSubset) 96 | 97 | 98 | def appendIndices(self, vertexIndex, uvIndex, normalIndex): 99 | self.vertexIndices.append(vertexIndex) 100 | self.uvIndices.append(uvIndex) 101 | self.normalIndices.append(normalIndex) 102 | 103 | 104 | 105 | class ObjConverter: 106 | def __init__(self, objPath, usdPath, useMtl, openParameters): 107 | self.usdPath = usdPath 108 | self.useMtl = useMtl 109 | self.searchPaths = openParameters.searchPaths 110 | self.verbose = openParameters.verbose 111 | 112 | filenameFull = objPath.split('/')[-1] 113 | self.srcFolder = objPath[:len(objPath)-len(filenameFull)] 114 | 115 | self.vertices = [] 116 | self.colors = [] 117 | self.uvs = [] 118 | self.normals = [] 119 | 120 | self.groups = {} 121 | self.currentGroup = None 122 | 123 | self.materialNames = [] 124 | self.materialIndicesByName = {} 125 | self.currentMaterial = INVALID_INDEX 126 | self.materialsByName = {} # created with .mtl files 127 | self.usdMaterials = [] 128 | self.usdDefaultMaterial = None 129 | self.asset = None 130 | self.setGroup() 131 | 132 | self.parseObjFile(objPath) 133 | openParameters.metersPerUnit = 0.01 134 | 135 | 136 | def setMaterial(self, name): 137 | materialName = name if name else 'white' # white by spec 138 | if self.verbose: 139 | print(' setting material: ' + materialName) 140 | # find material 141 | self.currentMaterial = self.materialIndicesByName.get(materialName, INVALID_INDEX) 142 | if self.currentMaterial == INVALID_INDEX: 143 | self.materialNames.append(materialName) 144 | self.currentMaterial = len(self.materialNames) - 1 145 | self.materialIndicesByName[materialName] = self.currentMaterial 146 | 147 | if self.currentGroup != None: 148 | self.currentGroup.setMaterial(self.currentMaterial) 149 | 150 | 151 | def setGroup(self, name=''): 152 | groupName = name if name else 'default' # default by spec 153 | self.currentGroup = self.groups.get(groupName) 154 | if self.currentGroup == None: 155 | if self.verbose: 156 | print(' creating group: ' + groupName) 157 | self.currentGroup = Group(self.currentMaterial) 158 | self.groups[groupName] = self.currentGroup 159 | else: 160 | if self.verbose: 161 | print(' setting group: ' + groupName) 162 | self.currentGroup.setMaterial(self.currentMaterial) 163 | 164 | 165 | def addVertex(self, v): 166 | v = floatList(v) 167 | vLen = len(v) 168 | self.vertices.append(Gf.Vec3f(v[0:3]) if vLen >= 3 else Gf.Vec3f()) 169 | if vLen >= 6: 170 | self.colors.append(Gf.Vec3f(v[3:6])) 171 | 172 | 173 | def addUV(self, v): 174 | v = floatList(v) 175 | self.uvs.append(Gf.Vec2f(v[0:2]) if len(v) >= 2 else Gf.Vec2f()) 176 | 177 | 178 | def addNormal(self, v): 179 | v = floatList(v) 180 | self.normals.append(Gf.Vec3f(v[0:3]) if len(v) >= 3 else Gf.Vec3f()) 181 | 182 | 183 | def addFace(self, arguments): 184 | # arguments have format like this: ['1/1/1', '2/2/2', '3/3/3'] 185 | faceVertexCount = 0 186 | for indexStr in arguments: 187 | indices = indexStr.split('/') 188 | 189 | vertexIndex = convertObjIndexToUsd(indices[0], len(self.vertices)) 190 | if vertexIndex == INVALID_INDEX: 191 | break 192 | 193 | uvIndex = INVALID_INDEX 194 | if 1 < len(indices): 195 | uvIndex = convertObjIndexToUsd(indices[1], len(self.uvs)) 196 | if uvIndex != vertexIndex: 197 | self.currentGroup.uvsHaveOwnIndices = True 198 | 199 | normalIndex = INVALID_INDEX 200 | if 2 < len(indices): 201 | normalIndex = convertObjIndexToUsd(indices[2], len(self.normals)) 202 | if normalIndex != vertexIndex: 203 | self.currentGroup.normalsHaveOwnIndices = True 204 | 205 | self.currentGroup.appendIndices(vertexIndex, uvIndex, normalIndex) 206 | faceVertexCount += 1 207 | 208 | if faceVertexCount > 0: 209 | self.currentGroup.currentSubset.faces.append(len(self.currentGroup.faceVertexCounts)) 210 | self.currentGroup.faceVertexCounts.append(faceVertexCount) 211 | 212 | 213 | def checkLastSubsets(self): 214 | for groupName, group in self.groups.items(): 215 | if len(group.subsets) > 1 and len(group.subsets[LAST_ELEMENT].faces) == 0: 216 | del group.subsets[LAST_ELEMENT] 217 | 218 | 219 | def getUsdMaterial(self, materialIndex): 220 | if 0 <= materialIndex and materialIndex < len(self.usdMaterials): 221 | return self.usdMaterials[materialIndex] 222 | else: 223 | if self.usdDefaultMaterial is None: 224 | material = usdUtils.Material("defaultMaterial") 225 | self.usdDefaultMaterial = material.makeUsdMaterial(self.asset) 226 | return self.usdDefaultMaterial 227 | 228 | 229 | def createMesh(self, geomPath, group, groupName, usdStage): 230 | if len(group.faceVertexCounts) == 0: 231 | return False 232 | 233 | groupName = usdUtils.makeValidIdentifier(groupName) 234 | if self.verbose: 235 | print(' creating USD mesh: ' + groupName + ('(subsets: ' + str(len(group.subsets)) + ')' if len(group.subsets) > 1 else '')) 236 | usdMesh = UsdGeom.Mesh.Define(usdStage, geomPath + '/' + groupName) 237 | usdMesh.CreateSubdivisionSchemeAttr(UsdGeom.Tokens.none) 238 | 239 | usdMesh.CreateFaceVertexCountsAttr(group.faceVertexCounts) 240 | 241 | # vertices 242 | minVertexIndex = min(group.vertexIndices) 243 | maxVertexIndex = max(group.vertexIndices) 244 | 245 | groupVertices = self.vertices[minVertexIndex:maxVertexIndex+1] 246 | usdMesh.CreatePointsAttr(groupVertices) 247 | if minVertexIndex == 0: # optimization 248 | usdMesh.CreateFaceVertexIndicesAttr(group.vertexIndices) 249 | else: 250 | usdMesh.CreateFaceVertexIndicesAttr(list(map(lambda x: x - minVertexIndex, group.vertexIndices))) 251 | 252 | extent = Gf.Range3f() 253 | for pt in groupVertices: 254 | extent.UnionWith(Gf.Vec3f(pt)) 255 | usdMesh.CreateExtentAttr([extent.GetMin(), extent.GetMax()]) 256 | 257 | # vertex colors 258 | if len(self.colors) == len(self.vertices): 259 | colorAttr = usdMesh.CreateDisplayColorPrimvar(UsdGeom.Tokens.vertex) 260 | colorAttr.Set(self.colors[minVertexIndex:maxVertexIndex+1]) 261 | 262 | # texture coordinates 263 | minUvIndex = min(group.uvIndices) 264 | maxUvIndex = max(group.uvIndices) 265 | 266 | if minUvIndex >= 0: 267 | if group.uvsHaveOwnIndices: 268 | uvPrimvar = usdMesh.CreatePrimvar('st', Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.faceVarying) 269 | uvPrimvar.Set(self.uvs[minUvIndex:maxUvIndex+1]) 270 | if minUvIndex == 0: # optimization 271 | uvPrimvar.SetIndices(Vt.IntArray(group.uvIndices)) 272 | else: 273 | uvPrimvar.SetIndices(Vt.IntArray(list(map(lambda x: x - minUvIndex, group.uvIndices)))) 274 | else: 275 | uvPrimvar = usdMesh.CreatePrimvar('st', Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.vertex) 276 | uvPrimvar.Set(self.uvs[minUvIndex:maxUvIndex+1]) 277 | 278 | # normals 279 | minNormalIndex = min(group.normalIndices) 280 | maxNormalIndex = max(group.normalIndices) 281 | 282 | if minNormalIndex >= 0: 283 | if group.normalsHaveOwnIndices: 284 | normalPrimvar = usdMesh.CreatePrimvar('normals', Sdf.ValueTypeNames.Normal3fArray, UsdGeom.Tokens.faceVarying) 285 | normalPrimvar.Set(self.normals[minNormalIndex:maxNormalIndex+1]) 286 | if minNormalIndex == 0: # optimization 287 | normalPrimvar.SetIndices(Vt.IntArray(group.normalIndices)) 288 | else: 289 | normalPrimvar.SetIndices(Vt.IntArray(list(map(lambda x: x - minNormalIndex, group.normalIndices)))) 290 | else: 291 | normalPrimvar = usdMesh.CreatePrimvar('normals', Sdf.ValueTypeNames.Normal3fArray, UsdGeom.Tokens.vertex) 292 | normalPrimvar.Set(self.normals[minNormalIndex:maxNormalIndex+1]) 293 | 294 | # materials 295 | if len(group.subsets) == 1: 296 | materialIndex = group.subsets[0].materialIndex 297 | if self.verbose: 298 | if 0 <= materialIndex and materialIndex < len(self.usdMaterials): 299 | print(usdUtils.makeValidIdentifier(self.materialNames[materialIndex])) 300 | else: 301 | print('defaultMaterial') 302 | UsdShade.MaterialBindingAPI(usdMesh).Bind(self.getUsdMaterial(materialIndex)) 303 | else: 304 | bindingAPI = UsdShade.MaterialBindingAPI(usdMesh) 305 | for subset in group.subsets: 306 | materialIndex = subset.materialIndex 307 | if len(subset.faces) > 0: 308 | materialName = 'defaultMaterial' 309 | if 0 <= materialIndex and materialIndex < len(self.usdMaterials): 310 | materialName = usdUtils.makeValidIdentifier(self.materialNames[materialIndex]) 311 | subsetName = materialName + 'Subset' 312 | if self.verbose: 313 | print(' subset: ' + subsetName + ' faces: ' + str(len(subset.faces))) 314 | usdSubset = UsdShade.MaterialBindingAPI.CreateMaterialBindSubset(bindingAPI, subsetName, Vt.IntArray(subset.faces)) 315 | UsdShade.MaterialBindingAPI(usdSubset).Bind(self.getUsdMaterial(materialIndex)) 316 | 317 | 318 | def loadMaterialsFromMTLFile(self, filename): 319 | global usdMaterialWithObjMtl_module 320 | usdMaterialWithObjMtl_module = importlib.import_module("usdMaterialWithObjMtl") 321 | usdStage = usdMaterialWithObjMtl_module.usdMaterialWithObjMtl(self, filename) 322 | 323 | 324 | def parseObjFile(self, objPath): 325 | with open(objPath, errors='ignore') as file: 326 | for line in linesContinuation(file): 327 | line = line.strip() 328 | if not line or '#' == line[0]: 329 | continue 330 | 331 | arguments = list(filter(None, line.split(' '))) 332 | command = arguments[0] 333 | arguments = arguments[1:] 334 | 335 | if 'v' == command: 336 | self.addVertex(arguments) 337 | elif 'vt' == command: 338 | self.addUV(arguments) 339 | elif 'vn' == command: 340 | self.addNormal(arguments) 341 | elif 'f' == command: 342 | self.addFace(arguments) 343 | elif 'g' == command or 'o' == command: 344 | self.setGroup(' '.join(arguments)) 345 | elif 'usemtl' == command: 346 | self.setMaterial(' '.join(arguments)) 347 | elif 'mtllib' == command: 348 | if self.useMtl: 349 | filename = os.path.dirname(objPath) + '/' + (' '.join(arguments)) 350 | self.loadMaterialsFromMTLFile(filename) 351 | 352 | self.checkLastSubsets() 353 | 354 | 355 | def makeUsdStage(self): 356 | self.asset = usdUtils.Asset(self.usdPath) 357 | usdStage = self.asset.makeUsdStage() 358 | 359 | # create all materials 360 | for matName in self.materialNames: 361 | if matName in self.materialsByName: 362 | material = self.materialsByName[matName] 363 | else: 364 | material = usdUtils.Material(matName) 365 | usdMaterial = material.makeUsdMaterial(self.asset) 366 | self.usdMaterials.append(usdMaterial) 367 | 368 | if len(self.vertices) == 0: 369 | return usdStage 370 | 371 | # create all meshes 372 | geomPath = self.asset.getGeomPath() 373 | for groupName, group in self.groups.items(): 374 | self.createMesh(geomPath, group, groupName, usdStage) 375 | 376 | return usdStage 377 | 378 | 379 | 380 | def usdStageWithObj(objPath, usdPath, useMtl, openParameters): 381 | start = time.time() 382 | converter = ObjConverter(objPath, usdPath, useMtl, openParameters) 383 | usdStage = converter.makeUsdStage() 384 | if openParameters.verbose: 385 | print(' creating stage from obj file: ' + str(time.time() - start) + ' sec') 386 | return usdStage 387 | -------------------------------------------------------------------------------- /usdzconvert/usdUtils.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from shutil import copyfile 3 | import re 4 | import math 5 | from pxr import * 6 | 7 | 8 | class ConvertError(Exception): 9 | pass 10 | 11 | class ConvertExit(Exception): 12 | pass 13 | 14 | 15 | def printError(message): 16 | print(' \033[91m' + 'Error: ' + message + '\033[0m') 17 | 18 | 19 | def printWarning(message): 20 | print(' \033[93m' + 'Warning: ' + message + '\033[0m') 21 | 22 | 23 | def makeValidIdentifier(path): 24 | if len(path) > 0: 25 | path = re.sub('[^A-Za-z0-9]', '_', path) 26 | if path[0].isdigit(): 27 | path = '_' + path 28 | if Sdf.Path.IsValidIdentifier(path): 29 | return path 30 | return 'defaultIdentifier' 31 | 32 | 33 | def makeValidPath(path): 34 | if len(path) > 0: 35 | path = re.sub('[^A-Za-z0-9/.]', '_', path) 36 | if path[0].isdigit(): 37 | path = '_' + path 38 | return path 39 | 40 | 41 | def getIndexByChannel(channel): 42 | if channel == 'g': 43 | return 1 44 | if channel == 'b': 45 | return 2 46 | if channel == 'a': 47 | return 3 48 | return 0 49 | 50 | 51 | def copy(srcFile, dstFile, verbose=False): 52 | if verbose: 53 | print('Copying file: ' + srcFile + ' ' + dstFile) 54 | if os.path.isfile(srcFile): 55 | dstFolder = os.path.dirname(dstFile) 56 | if dstFolder != '' and not os.path.isdir(dstFolder): 57 | os.makedirs(dstFolder) 58 | copyfile(srcFile, dstFile) 59 | else: 60 | printWarning("can't find " + srcFile) 61 | 62 | 63 | def resolvePath(textureFileName, folder, searchPaths=None): 64 | if textureFileName == '': 65 | return '' 66 | if os.path.isfile(textureFileName): 67 | return textureFileName 68 | 69 | if folder == '': 70 | folder = os.getcwd() 71 | 72 | path = textureFileName.replace('\\', '/') 73 | basename = os.path.basename(path) 74 | if os.path.isfile(folder + basename): 75 | return folder + basename 76 | 77 | # TODO: try more precise finding with folders info 78 | 79 | for root, dirnames, filenames in os.walk(folder): 80 | for filename in filenames: 81 | if filename == basename: 82 | return os.path.join(root, filename) 83 | 84 | if searchPaths is not None: 85 | for searchPath in searchPaths: 86 | for root, dirnames, filenames in os.walk(searchPath): 87 | for filename in filenames: 88 | if filename == basename: 89 | return os.path.join(root, filename) 90 | 91 | return textureFileName 92 | 93 | 94 | 95 | class WrapMode: 96 | black = 'black' 97 | clamp = 'clamp' 98 | repeat = 'repeat' 99 | mirror = 'mirror' 100 | useMetadata = 'useMetadata' 101 | 102 | 103 | def isWrapModeCorrect(mode): 104 | modes = [WrapMode.black, WrapMode.clamp, WrapMode.repeat, WrapMode.mirror, WrapMode.useMetadata] 105 | if mode in modes: 106 | return True 107 | return False 108 | 109 | 110 | class Asset: 111 | materialsFolder = 'Materials' 112 | geomFolder = 'Geom' 113 | animationsFolder = 'Animations' 114 | 115 | def __init__(self, usdPath, usdStage=None): 116 | fileName = os.path.basename(usdPath) 117 | self.name = fileName[:fileName.find('.')] 118 | self.name = makeValidIdentifier(self.name) 119 | self.usdPath = usdPath 120 | self.usdStage = usdStage 121 | self.defaultPrim = None 122 | self.beginTime = float('inf') 123 | self.endTime = float('-inf') 124 | self.timeCodesPerSecond = 24 # default for USD 125 | self._geomPath = '' 126 | self._materialsPath = '' 127 | self._animationsPath = '' 128 | 129 | 130 | def getPath(self): 131 | return '/' + self.name 132 | 133 | 134 | def getMaterialsPath(self): 135 | # debug 136 | # assert self.usdStage is not None, 'Using materials path before usdStage was created' 137 | if not self._materialsPath: 138 | self._materialsPath = self.getPath() + '/' + Asset.materialsFolder 139 | self.usdStage.DefinePrim(self._materialsPath, 'Scope') 140 | return self._materialsPath 141 | 142 | 143 | def getGeomPath(self): 144 | # debug 145 | # assert self.usdStage is not None, 'Using geom path before usdStage was created' 146 | if not self._geomPath: 147 | self._geomPath = self.getPath() + '/' + Asset.geomFolder 148 | self.usdStage.DefinePrim(self._geomPath, 'Scope') 149 | return self._geomPath 150 | 151 | 152 | def getAnimationsPath(self): 153 | # debug 154 | # assert self.usdStage is not None, 'Using animations path before usdStage was created' 155 | if not self._animationsPath: 156 | self._animationsPath = self.getPath() + '/' + Asset.animationsFolder 157 | self.usdStage.DefinePrim(self._animationsPath, 'Scope') 158 | return self._animationsPath 159 | 160 | 161 | def setFPS(self, fps): 162 | # set one time code per frame 163 | self.timeCodesPerSecond = fps 164 | 165 | 166 | def extentTime(self, time): 167 | if math.isinf(self.endTime): 168 | self.beginTime = time 169 | self.endTime = time 170 | return 171 | if self.beginTime > time: 172 | self.beginTime = time 173 | if self.endTime < time: 174 | self.endTime = time 175 | 176 | 177 | def toTimeCode(self, time, extentTime=False): 178 | if extentTime: 179 | self.extentTime(time) 180 | real = time * self.timeCodesPerSecond 181 | round = int(real + 0.5) 182 | epsilon = 0.001 183 | if abs(real - round) < epsilon: 184 | return round 185 | return real 186 | 187 | 188 | def makeUniqueBlendShapeName(self, name, path): 189 | geomPath = self.getGeomPath() 190 | if len(path) > len(geomPath) and path[:len(geomPath)] == geomPath: 191 | path = path[len(geomPath):] 192 | 193 | blendShapeName = path.replace("/", ":") + ":" + name 194 | if blendShapeName[0] == ":": 195 | blendShapeName = blendShapeName[1:] 196 | return blendShapeName 197 | 198 | 199 | def makeUsdStage(self): 200 | # debug 201 | # assert self.usdStage is None, 'Trying to create another usdStage' 202 | self.usdStage = Usd.Stage.CreateNew(self.usdPath) 203 | UsdGeom.SetStageUpAxis(self.usdStage, UsdGeom.Tokens.y) 204 | 205 | # make default prim 206 | self.defaultPrim = self.usdStage.DefinePrim(self.getPath(), 'Xform') 207 | self.defaultPrim.SetAssetInfoByKey('name', self.name) 208 | Usd.ModelAPI(self.defaultPrim).SetKind('component') 209 | self.usdStage.SetDefaultPrim(self.defaultPrim) 210 | 211 | return self.usdStage 212 | 213 | 214 | def finalize(self): 215 | if not math.isinf(self.endTime): 216 | self.usdStage.SetStartTimeCode(self.toTimeCode(self.beginTime)) 217 | self.usdStage.SetEndTimeCode(self.toTimeCode(self.endTime)) 218 | self.usdStage.SetTimeCodesPerSecond(self.timeCodesPerSecond) 219 | 220 | 221 | 222 | class InputName: 223 | normal = 'normal' 224 | diffuseColor = 'diffuseColor' 225 | opacity = 'opacity' 226 | emissiveColor = 'emissiveColor' 227 | metallic = 'metallic' 228 | roughness = 'roughness' 229 | occlusion = 'occlusion' 230 | clearcoat = 'clearcoat' 231 | clearcoatRoughness = 'clearcoatRoughness' 232 | 233 | 234 | 235 | class Input: 236 | names = [InputName.normal, InputName.diffuseColor, InputName.opacity, InputName.emissiveColor, InputName.metallic, InputName.roughness, InputName.occlusion, InputName.clearcoat, InputName.clearcoatRoughness] 237 | channels = ['rgb', 'rgb', 'a', 'rgb', 'r', 'r', 'r', 'r', 'r'] 238 | types = [Sdf.ValueTypeNames.Normal3f, Sdf.ValueTypeNames.Color3f, Sdf.ValueTypeNames.Float, 239 | Sdf.ValueTypeNames.Color3f, Sdf.ValueTypeNames.Float, Sdf.ValueTypeNames.Float, Sdf.ValueTypeNames.Float, Sdf.ValueTypeNames.Float, Sdf.ValueTypeNames.Float] 240 | 241 | 242 | 243 | class MapTransform: 244 | def __init__(self, translation, scale, rotation): 245 | self.translation = translation 246 | self.scale = scale 247 | self.rotation = rotation 248 | 249 | 250 | 251 | class Map: 252 | def __init__(self, channels, file, fallback=None, texCoordSet='st', wrapS=WrapMode.useMetadata, wrapT=WrapMode.useMetadata, scale=None, transform=None): 253 | self.file = file 254 | self.channels = channels 255 | self.fallback = fallback 256 | self.texCoordSet = texCoordSet 257 | self.textureShaderName = '' 258 | self.wrapS = wrapS 259 | self.wrapT = wrapT 260 | self.scale = scale 261 | self.transform = transform 262 | 263 | 264 | 265 | class Material: 266 | def __init__(self, name): 267 | if name.find('/') != -1: 268 | self.path = makeValidPath(name) 269 | self.name = makeValidIdentifier(os.path.basename(name)) 270 | else: 271 | self.path = '' 272 | self.name = makeValidIdentifier(name) if name != '' else '' 273 | self.inputs = {} 274 | self.opacityThreshold = None 275 | 276 | 277 | def isEmpty(self): 278 | if len(self.inputs.keys()) == 0: 279 | return True 280 | return False 281 | 282 | 283 | def getUsdSurfaceShader(self, usdMaterial, usdStage): 284 | for usdShadeOutput in usdMaterial.GetOutputs(): 285 | if UsdShade.ConnectableAPI.HasConnectedSource(usdShadeOutput) == True: 286 | (sourceAPI, sourceName, sourceType) = UsdShade.ConnectableAPI.GetConnectedSource(usdShadeOutput) 287 | if sourceName == 'surface': 288 | return UsdShade.Shader(sourceAPI) 289 | return self._createSurfaceShader(usdMaterial, usdStage) 290 | 291 | 292 | def updateUsdMaterial(self, usdMaterial, surfaceShader, usdStage): 293 | self._makeTextureShaderNames() 294 | for inputIdx in range(len(Input.names)): 295 | self._addMapToUsdMaterial(inputIdx, usdMaterial, surfaceShader, usdStage) 296 | 297 | 298 | def makeUsdMaterial(self, asset): 299 | matPath = self.path if self.path else asset.getMaterialsPath() + '/' + self.name 300 | usdMaterial = UsdShade.Material.Define(asset.usdStage, matPath) 301 | surfaceShader = self._createSurfaceShader(usdMaterial, asset.usdStage) 302 | 303 | if self.isEmpty(): 304 | return usdMaterial 305 | 306 | self.updateUsdMaterial(usdMaterial, surfaceShader, asset.usdStage) 307 | return usdMaterial 308 | 309 | 310 | # private methods: 311 | 312 | def _createSurfaceShader(self, usdMaterial, usdStage): 313 | matPath = str(usdMaterial.GetPath()) 314 | surfaceShader = UsdShade.Shader.Define(usdStage, matPath + '/surfaceShader') 315 | surfaceShader.CreateIdAttr('UsdPreviewSurface') 316 | surfaceOutput = surfaceShader.CreateOutput('surface', Sdf.ValueTypeNames.Token) 317 | usdMaterial.CreateOutput('surface', Sdf.ValueTypeNames.Token).ConnectToSource(surfaceOutput) 318 | if self.opacityThreshold is not None: 319 | surfaceShader.CreateInput('opacityThreshold', Sdf.ValueTypeNames.Float).Set(float(self.opacityThreshold)) 320 | return surfaceShader 321 | 322 | 323 | def _makeTextureShaderNames(self): 324 | # combine texture shaders with the same texture 325 | for i in range(0, len(Input.names)): 326 | inputName = Input.names[i] 327 | if inputName in self.inputs: 328 | map = self.inputs[inputName] 329 | if not isinstance(map, Map): 330 | continue 331 | if map.textureShaderName != '': 332 | continue 333 | textureShaderName = inputName 334 | maps = [map] 335 | if inputName != InputName.normal: 336 | for j in range(i + 1, len(Input.names)): 337 | inputName2 = Input.names[j] 338 | map2 = self.inputs[inputName2] if inputName2 in self.inputs else None 339 | if not isinstance(map2, Map): 340 | continue 341 | if map2 != None and map2.file == map.file: 342 | # channel factors (scales) shouldn't be rewritten 343 | split = (map.scale is not None and map2.scale is not None and 344 | len(map.channels) == 1 and len(map2.channels) == 1 and 345 | map.channels == map2.channels and map.scale != map2.scale) 346 | if not split: 347 | textureShaderName += '_' + inputName2 348 | maps.append(map2) 349 | for map3 in maps: 350 | map3.textureShaderName = textureShaderName 351 | 352 | 353 | def _makeUsdUVTexture(self, matPath, map, inputName, channels, uvInput, usdStage): 354 | uvReaderPath = matPath + '/uvReader_' + map.texCoordSet 355 | uvReader = usdStage.GetPrimAtPath(uvReaderPath) 356 | if uvReader: 357 | uvReader = UsdShade.Shader(uvReader) 358 | else: 359 | uvReader = UsdShade.Shader.Define(usdStage, uvReaderPath) 360 | uvReader.CreateIdAttr('UsdPrimvarReader_float2') 361 | if uvInput != None: 362 | # token inputs:varname.connect = 363 | uvReader.CreateInput('varname', Sdf.ValueTypeNames.Token).ConnectToSource(uvInput) 364 | else: 365 | uvReader.CreateInput('varname',Sdf.ValueTypeNames.Token).Set(map.texCoordSet) 366 | uvReader.CreateOutput('result', Sdf.ValueTypeNames.Float2) 367 | 368 | # texture transform 369 | if map.transform != None: 370 | transformShaderPath = matPath + '/' + map.textureShaderName + '_transform2D' 371 | transformShader = UsdShade.Shader.Define(usdStage, transformShaderPath) 372 | transformShader.SetSdrMetadataByKey("role", "math") 373 | transformShader.CreateIdAttr('UsdTransform2d') 374 | transformShader.CreateInput('in', Sdf.ValueTypeNames.Float2).ConnectToSource(uvReader.GetOutput('result')) 375 | 376 | if map.transform.translation[0] != 0 or map.transform.translation[1] != 0: 377 | transformShader.CreateInput('translation', Sdf.ValueTypeNames.Float2).Set(Gf.Vec2f(map.transform.translation[0], map.transform.translation[1])) 378 | if map.transform.scale[0] != 1 or map.transform.scale[1] != 1: 379 | transformShader.CreateInput('scale', Sdf.ValueTypeNames.Float2).Set(Gf.Vec2f(map.transform.scale[0], map.transform.scale[1])) 380 | if map.transform.rotation != 0: 381 | transformShader.CreateInput('rotation', Sdf.ValueTypeNames.Float).Set(float(map.transform.rotation)) 382 | 383 | transformShader.CreateOutput('result', Sdf.ValueTypeNames.Float2) 384 | uvReader = transformShader 385 | 386 | # create texture shader node 387 | textureShader = UsdShade.Shader.Define(usdStage, matPath + '/' + map.textureShaderName + '_texture') 388 | textureShader.CreateIdAttr('UsdUVTexture') 389 | 390 | if inputName == InputName.normal: 391 | # float4 inputs:scale = (2, 2, 2, 2) 392 | textureShader.CreateInput('scale', Sdf.ValueTypeNames.Float4).Set(Gf.Vec4f(2, 2, 2, 2)) 393 | # float4 inputs:bias = (-1, -1, -1, -1) 394 | textureShader.CreateInput('bias', Sdf.ValueTypeNames.Float4).Set(Gf.Vec4f(-1, -1, -1, -1)) 395 | else: 396 | if map.scale != None: 397 | gfScale = Gf.Vec4f(1) 398 | scaleInput = textureShader.GetInput('scale') 399 | if scaleInput is not None and scaleInput.Get() is not None: 400 | gfScale = scaleInput.Get() 401 | if channels == 'rgb': 402 | if isinstance(map.scale, list): 403 | gfScale[0] = float(map.scale[0]) 404 | gfScale[1] = float(map.scale[1]) 405 | gfScale[2] = float(map.scale[2]) 406 | else: 407 | printError('Scale value ' + map.scale + ' for ' + inputName + ' is incorrect.') 408 | raise 409 | else: 410 | gfScale[getIndexByChannel(channels)] = float(map.scale) 411 | if Gf.Vec4f(1) != gfScale: # skip default value 412 | textureShader.CreateInput('scale', Sdf.ValueTypeNames.Float4).Set(gfScale) 413 | 414 | fileAndExt = os.path.splitext(map.file) 415 | if len(fileAndExt) == 1 or (fileAndExt[-1] != '.png' and fileAndExt[-1] != '.jpg'): 416 | printWarning('texture file ' + map.file + ' is not .png or .jpg') 417 | 418 | textureShader.CreateInput('file', Sdf.ValueTypeNames.Asset).Set(map.file) 419 | textureShader.CreateInput('st', Sdf.ValueTypeNames.Float2).ConnectToSource(uvReader.GetOutput('result')) 420 | dataType = Sdf.ValueTypeNames.Float3 if len(channels) == 3 else Sdf.ValueTypeNames.Float 421 | textureShader.CreateOutput(channels, dataType) 422 | 423 | # wrapping mode 424 | if map.wrapS != WrapMode.useMetadata: 425 | textureShader.CreateInput('wrapS', Sdf.ValueTypeNames.Token).Set(map.wrapS) 426 | if map.wrapT != WrapMode.useMetadata: 427 | textureShader.CreateInput('wrapT', Sdf.ValueTypeNames.Token).Set(map.wrapT) 428 | 429 | # fallback value is used if loading of the texture file is failed 430 | if map.fallback != None: 431 | # update if exists in combined textures like for ORM 432 | gfFallback = textureShader.GetInput('fallback').Get() 433 | if gfFallback is None: 434 | # default by Pixar spec 435 | gfFallback = Gf.Vec4f(0, 0, 0, 1) 436 | if channels == 'rgb': 437 | if isinstance(map.fallback, list): 438 | gfFallback[0] = float(map.fallback[0]) 439 | gfFallback[1] = float(map.fallback[1]) 440 | gfFallback[2] = float(map.fallback[2]) 441 | # do not update alpha channel! 442 | else: 443 | printWarning('fallback value ' + map.fallback + ' for ' + inputName + ' is incorrect.') 444 | else: 445 | gfFallback[getIndexByChannel(channels)] = float(map.fallback) 446 | 447 | if inputName == InputName.normal: 448 | #normal map fallback is within 0 - 1 449 | gfFallback = 0.5*(gfFallback + Gf.Vec4f(1.0)) 450 | if Gf.Vec4f(0, 0, 0, 1) != gfFallback: # skip default value 451 | textureShader.CreateInput('fallback', Sdf.ValueTypeNames.Float4).Set(gfFallback) 452 | 453 | return textureShader 454 | 455 | 456 | def _isDefaultValue(self, inputName): 457 | input = self.inputs[inputName] 458 | if isinstance(input, Map): 459 | return False 460 | 461 | if isinstance(input, list): 462 | gfVec3d = Gf.Vec3d(float(input[0]), float(input[1]), float(input[2])) 463 | if InputName.diffuseColor == inputName and gfVec3d == Gf.Vec3d(0.18, 0.18, 0.18): 464 | return True 465 | if InputName.emissiveColor == inputName and gfVec3d == Gf.Vec3d(0, 0, 0): 466 | return True 467 | if InputName.normal == inputName and gfVec3d == Gf.Vec3d(0, 0, 1.0): 468 | return True 469 | else: 470 | if InputName.metallic == inputName and float(input) == 0.0: 471 | return True 472 | if InputName.roughness == inputName and float(input) == 0.5: 473 | return True 474 | if InputName.clearcoat == inputName and float(input) == 0.0: 475 | return True 476 | if InputName.clearcoatRoughness == inputName and float(input) == 0.01: 477 | return True 478 | if InputName.opacity == inputName and float(input) == 1.0: 479 | return True 480 | if InputName.occlusion == inputName and float(input) == 1.0: 481 | return True 482 | return False 483 | 484 | 485 | def _addMapToUsdMaterial(self, inputIdx, usdMaterial, surfaceShader, usdStage): 486 | inputName = Input.names[inputIdx] 487 | if inputName not in self.inputs: 488 | return 489 | 490 | if self._isDefaultValue(inputName): 491 | return 492 | 493 | input = self.inputs[inputName] 494 | inputType = Input.types[inputIdx] 495 | 496 | if isinstance(input, Map): 497 | map = input 498 | defaultChannels = Input.channels[inputIdx] 499 | channels = map.channels if len(map.channels) == len(defaultChannels) else defaultChannels 500 | uvInput = None 501 | if inputName == InputName.normal: 502 | # token inputs:frame:stPrimvarName = "st" 503 | uvInput = usdMaterial.CreateInput('frame:stPrimvarName', Sdf.ValueTypeNames.Token) 504 | uvInput.Set(map.texCoordSet) 505 | matPath = str(usdMaterial.GetPath()) 506 | textureShader = self._makeUsdUVTexture(matPath, map, inputName, channels, uvInput, usdStage) 507 | surfaceShader.CreateInput(inputName, inputType).ConnectToSource(textureShader.GetOutput(channels)) 508 | elif isinstance(input, list): 509 | gfVec3d = Gf.Vec3d(float(input[0]), float(input[1]), float(input[2])) 510 | surfaceShader.CreateInput(inputName, inputType).Set(gfVec3d) 511 | else: 512 | surfaceShader.CreateInput(inputName, inputType).Set(float(input)) 513 | 514 | 515 | 516 | class NodeManager: 517 | def __init__(self): 518 | pass 519 | 520 | def overrideGetName(self, node): 521 | # take care about valid identifier 522 | # debug 523 | # assert 0, "Can't find overriden method overrideGetName for node manager" 524 | pass 525 | 526 | def overrideGetChildren(self, node): 527 | # debug 528 | # assert 0, "Can't find overriden method overrideGetChildren for node manager" 529 | pass 530 | 531 | def overrideGetLocalTransformGfMatrix4d(self, node): 532 | # debug 533 | # assert 0, "Can't find overriden method overrideGetLocaLTransform for node manager" 534 | pass 535 | 536 | def overrideGetWorldTransformGfMatrix4d(self, node): 537 | pass 538 | 539 | def overrideGetParent(self, node): 540 | pass 541 | 542 | 543 | def getCommonParent(self, node1, node2): 544 | parent1 = node1 545 | while parent1 is not None: 546 | parent2 = node2 547 | while parent2 is not None: 548 | if parent1 == parent2: 549 | return parent2 550 | parent2 = self.overrideGetParent(parent2) 551 | parent1 = self.overrideGetParent(parent1) 552 | return None 553 | 554 | 555 | def findRoot(self, nodes): 556 | if len(nodes) == 0: 557 | return None 558 | if len(nodes) == 1: 559 | return nodes[0] 560 | parent = nodes[0] 561 | for i in range(1, len(nodes)): 562 | parent = self.getCommonParent(parent, nodes[i]) 563 | return parent 564 | 565 | 566 | 567 | class Skin: 568 | def __init__(self, root=None): 569 | self.root = root 570 | self.joints = [] 571 | self.bindMatrices = {} 572 | self.skeleton = None 573 | self._toSkeletonIndices = {} 574 | 575 | 576 | def remapIndex(self, index): 577 | return self._toSkeletonIndices[str(index)] 578 | 579 | 580 | # private: 581 | def _setSkeleton(self, skeleton): 582 | self.skeleton = skeleton 583 | for joint in self.joints: 584 | self.skeleton.bindMatrices[joint] = self.bindMatrices[joint] 585 | 586 | 587 | def _prepareIndexRemapping(self): 588 | for jointIdx in range(len(self.joints)): 589 | joint = self.joints[jointIdx] 590 | self._toSkeletonIndices[str(jointIdx)] = self.skeleton.getJointIndex(joint) 591 | 592 | 593 | 594 | class Skeleton: 595 | def __init__(self): 596 | self.joints = [] 597 | self.jointPaths = {} # jointPaths[joint] 598 | self.restMatrices ={} # restMatrices[joint] 599 | self.bindMatrices = {} # bindMatrices[joint] 600 | self.usdSkeleton = None 601 | self.usdSkelAnim = None 602 | self.sdfPath = None 603 | 604 | 605 | def getJointIndex(self, joint): 606 | for jointIdx in range(len(self.joints)): 607 | if joint == self.joints[jointIdx]: 608 | return jointIdx 609 | return -1 610 | 611 | 612 | def getRoot(self): 613 | return self.joints[0] # TODO: check if does exist 614 | 615 | 616 | def makeUsdSkeleton(self, usdStage, sdfPath, nodeManager): 617 | if self.usdSkeleton is not None: 618 | return self.usdSkeleton 619 | self.sdfPath = sdfPath 620 | jointPaths = [] 621 | restMatrices = [] 622 | bindMatrices = [] 623 | for joint in self.joints: 624 | if joint is None: 625 | continue 626 | jointPaths.append(self.jointPaths[joint]) 627 | restMatrices.append(self.restMatrices[joint]) 628 | if joint in self.bindMatrices: 629 | bindMatrices.append(self.bindMatrices[joint]) 630 | else: 631 | bindMatrices.append(nodeManager.overrideGetWorldTransformGfMatrix4d(joint)) 632 | 633 | usdGeom = UsdSkel.Root.Define(usdStage, sdfPath) 634 | 635 | self.usdSkeleton = UsdSkel.Skeleton.Define(usdStage, sdfPath + '/Skeleton') 636 | self.usdSkeleton.CreateJointsAttr(jointPaths) 637 | self.usdSkeleton.CreateRestTransformsAttr(restMatrices) 638 | self.usdSkeleton.CreateBindTransformsAttr(bindMatrices) 639 | return usdGeom 640 | 641 | 642 | def bindRigidDeformation(self, joint, usdMesh, bindTransform): 643 | # debug 644 | # assert self.usdSkeleton, "Trying to bind rigid deforamtion before USD Skeleton has been created." 645 | jointIndex = self.getJointIndex(joint) 646 | if jointIndex == -1: 647 | return 648 | usdSkelBinding = UsdSkel.BindingAPI(usdMesh) 649 | 650 | usdSkelBinding.CreateJointIndicesPrimvar(True, 1).Set([jointIndex]) 651 | usdSkelBinding.CreateJointWeightsPrimvar(True, 1).Set([1]) 652 | usdSkelBinding.CreateGeomBindTransformAttr(bindTransform) 653 | 654 | usdSkelBinding.CreateSkeletonRel().AddTarget(self.usdSkeleton.GetPath()) 655 | 656 | 657 | def setSkeletalAnimation(self, usdSkelAnim): 658 | if self.usdSkelAnim != None: 659 | # default animation is the first one 660 | return 661 | 662 | if self.usdSkeleton is None: 663 | printWarning('trying to assign Skeletal Animation before USD Skeleton has been created.') 664 | return 665 | 666 | usdSkelBinding = UsdSkel.BindingAPI(self.usdSkeleton) 667 | usdSkelBinding.CreateAnimationSourceRel().AddTarget(usdSkelAnim.GetPath()) 668 | self.usdSkelAnim = usdSkelAnim 669 | 670 | 671 | # private: 672 | def _collectJoints(self, node, path, nodeManager): 673 | self.joints.append(node) 674 | name = nodeManager.overrideGetName(node) 675 | newPath = path + name 676 | self.jointPaths[node] = newPath 677 | self.restMatrices[node] = nodeManager.overrideGetLocalTransformGfMatrix4d(node) 678 | for child in nodeManager.overrideGetChildren(node): 679 | self._collectJoints(child, newPath + '/', nodeManager) 680 | 681 | 682 | class Skinning: 683 | def __init__(self, nodeManager): 684 | self.skins = [] 685 | self.skeletons = [] 686 | self.nodeManager = nodeManager 687 | self.joints = {} # joint set 688 | 689 | 690 | def createSkeleton(self, root): 691 | skeleton = Skeleton() 692 | skeleton._collectJoints(root, '', self.nodeManager) 693 | self.skeletons.append(skeleton) 694 | return skeleton 695 | 696 | 697 | def createSkeletonsFromSkins(self): 698 | for skin in self.skins: 699 | if len(skin.joints) < 1: 700 | continue 701 | if skin.root == None: 702 | skin.root = self.nodeManager.findRoot(skin.joints) 703 | skeleton = self.findSkeletonByJoint(skin.joints[0]) 704 | if skeleton is None: 705 | skeleton = self.createSkeleton(skin.root) 706 | for joint in skin.joints: 707 | self.joints[joint] = joint 708 | skin._setSkeleton(skeleton) 709 | 710 | # check if existed skeletons are subpart of this one 711 | skeletonsToRemove = [] 712 | for subSkeleton in self.skeletons: 713 | if subSkeleton == skeleton: 714 | continue 715 | if skeleton.getJointIndex(subSkeleton.getRoot()) != -1: 716 | for skin in self.skins: 717 | if skin.skeleton == subSkeleton: 718 | skin._setSkeleton(skeleton) 719 | skeletonsToRemove.append(subSkeleton) 720 | for skeletonToRemove in skeletonsToRemove: 721 | self.skeletons.remove(skeletonToRemove) 722 | 723 | 724 | for skin in self.skins: 725 | skin._prepareIndexRemapping() 726 | 727 | 728 | def isJoint(self, node): 729 | return True if node in self.joints else False 730 | 731 | 732 | def findSkeletonByRoot(self, node): 733 | for skeleton in self.skeletons: 734 | if skeleton.getRoot() == node: 735 | return skeleton 736 | return None 737 | 738 | 739 | def findSkeletonByJoint(self, node): 740 | for skeleton in self.skeletons: 741 | if skeleton.getJointIndex(node) != -1: 742 | return skeleton 743 | return None 744 | 745 | 746 | 747 | class BlendShape: 748 | def __init__(self, weightsCount): 749 | self.weightsCount = weightsCount 750 | self.usdSkeleton = None 751 | self.usdSkelAnim = None 752 | self.sdfPath = None 753 | self.skeleton = None 754 | self.blendShapeList = [] 755 | 756 | 757 | def makeUsdSkeleton(self, usdStage, sdfPath): 758 | if self.usdSkeleton is not None: 759 | return self.usdSkeleton 760 | self.sdfPath = sdfPath 761 | 762 | usdGeom = UsdSkel.Root.Define(usdStage, sdfPath) 763 | 764 | self.usdSkeleton = UsdSkel.Skeleton.Define(usdStage, sdfPath + '/Skeleton') 765 | 766 | usdSkelBlendShapeBinding = UsdSkel.BindingAPI(usdGeom) 767 | usdSkelBlendShapeBinding.CreateSkeletonRel().AddTarget("Skeleton") 768 | 769 | return usdGeom 770 | 771 | 772 | def setSkeletalAnimation(self, usdSkelAnim): 773 | if self.usdSkelAnim != None: 774 | # default animation is the first one 775 | return 776 | 777 | if self.usdSkeleton is None: 778 | printWarning('trying to assign Skeletal Animation before USD Skeleton has been created.') 779 | return 780 | 781 | usdSkelBinding = UsdSkel.BindingAPI(self.usdSkeleton) 782 | usdSkelBinding.CreateAnimationSourceRel().AddTarget(usdSkelAnim.GetPath()) 783 | self.usdSkelAnim = usdSkelAnim 784 | 785 | 786 | def addBlendShapeList(self, blendShapeList): 787 | # TODO: combine lists? 788 | self.blendShapeList = blendShapeList 789 | 790 | 791 | 792 | class ShapeBlending: 793 | def __init__(self): 794 | self.blendShapes = [] 795 | 796 | 797 | def createBlendShape(self, weightsCount): 798 | blendShape = BlendShape(weightsCount) 799 | self.blendShapes.append(blendShape) 800 | return blendShape 801 | 802 | 803 | def flush(self): 804 | for blendShape in self.blendShapes: 805 | if blendShape.usdSkelAnim is not None: 806 | blendShape.usdSkelAnim.CreateBlendShapesAttr(blendShape.blendShapeList) 807 | 808 | 809 | -------------------------------------------------------------------------------- /usdzconvert/usdzaudioimport: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # add audio file to usdz: 4 | # usdzaudioimport input.usdz output.usdz --audio 5 | 6 | import zipfile 7 | import os, shutil, sys 8 | import re 9 | from pxr import * 10 | 11 | 12 | scriptName = "usdzaudioimport" 13 | scriptVersion = 0.01 14 | 15 | 16 | def printError(message): 17 | print(' \033[91m' + 'Error: ' + message + '\033[0m') 18 | 19 | 20 | def printWarning(message): 21 | print(' \033[93m' + 'Warning:' + message + '\033[0m') 22 | 23 | 24 | def makeValidPath(path): 25 | if len(path) > 0: 26 | path = re.sub('[^A-Za-z0-9/.]', '_', path) 27 | if path[0].isdigit(): 28 | path = '_' + path 29 | return path 30 | 31 | 32 | 33 | class Audio: 34 | def __init__(self, path, file): 35 | self.path = makeValidPath(path) 36 | self.file = file 37 | 38 | self.auralMode = None 39 | self.playbackMode = None 40 | self.startTime = None 41 | self.endTime = None 42 | self.mediaOffset = None 43 | self.gain = None 44 | 45 | 46 | 47 | def isFloat(value): 48 | try: 49 | val = float(value) 50 | return True 51 | except ValueError: 52 | return False 53 | 54 | 55 | 56 | class ParserOut: 57 | def __init__(self): 58 | self.inFilePath = '' 59 | self.outFilePath = '' 60 | self.verbose = False 61 | self.audios = [] 62 | 63 | 64 | 65 | class Parser: 66 | def __init__(self): 67 | self.out = ParserOut() 68 | self.arguments = [] 69 | self.argumentIndex = 0 70 | 71 | 72 | def printConvertNameAndVersion(self): 73 | print(scriptName + " " + str(scriptVersion)) 74 | 75 | 76 | 77 | def printUsage(self): 78 | self.printConvertNameAndVersion() 79 | print('usage: usdzaudioimport inputFile [outputFile]\n\ 80 | [-h] [-v]\n\ 81 | [-a sdfPath audioFile] [--audio sdfPath audioFile]\n\ 82 | [-auralMode mode]\n\ 83 | [-playbackMode mode]\n\ 84 | [-startTime value]\n\ 85 | [-endTime value]\n\ 86 | [-mediaOffset value]\n\ 87 | [-gain value]') 88 | 89 | 90 | def printHelpAndExit(self): 91 | self.printUsage() 92 | print('\n\ 93 | Adds audio files to usdz files and creates SpatialAudio nodes.\n\ 94 | \npositional argument:\n\ 95 | inputFile Input usdz file.\n\ 96 | \noptional arguments:\n\ 97 | outputFile Output .usd/usda/usdc/usdz files.\n\ 98 | -h, --help Show this help message and exit.\n\ 99 | -v Verbose output.\n\ 100 | -a sdfPath audioFile\n\ 101 | --audio sdfPath audioFile\n\ 102 | Create new SpatialAudio node with usd path and audio file path\n\ 103 | -auralMode mode How audio should be played\n\ 104 | mode can be: spatial or nonSpatial\n\ 105 | -playbackMode mode Playback mode\n\ 106 | mode can be: onceFromStart, onceFromStartToEnd, loopFromStart, loopFromStartToEnd, or loopFromStage\n\ 107 | -startTime value Start time for audio in time codes\n\ 108 | -endTime value End time for audio in time codes\n\ 109 | -mediaOffset value Media offset in seconds\n\ 110 | -gain value Multiplier on the incoming audio signal\n\ 111 | \n\ 112 | examples:\n\ 113 | usdzaudioimport input.usdz output.usdz --audio /AssetName/Sounds/Sound1 love.mp3 -auralMode nonSpatial\n\ 114 | \n\ 115 | usdzaudioimport input.usdz output.usdz -a /AssetName/Sounds/Sound1 love.mp3 -auralMode nonSpatial -a /AssetName/Sounds/Sound2 /tmp/hate.mp3 -playbackMode onceFromStart\n\ 116 | ') 117 | 118 | exit(0) 119 | 120 | 121 | def printErrorUsageAndExit(self, message): 122 | self.printConvertNameAndVersion() 123 | printError(message) 124 | print('For more information, run "' + scriptName + ' -h"') 125 | exit(1) 126 | 127 | 128 | def getParameters(self, count, argument): 129 | if self.argumentIndex + count >= len(self.arguments): 130 | self.printErrorUsageAndExit('argument ' + argument + ' needs more parameters') 131 | 132 | self.argumentIndex += count 133 | if count == 1: 134 | parameter = self.arguments[self.argumentIndex] 135 | if parameter[0] == '-' and not isFloat(parameter): 136 | self.printErrorUsageAndExit('unexpected parameter ' + parameter + ' for argument ' + argument) 137 | return self.arguments[self.argumentIndex] 138 | else: 139 | parameters = self.arguments[(self.argumentIndex - count + 1):(self.argumentIndex + 1)] 140 | for parameter in parameters: 141 | if parameter[0] == '-' and not isFloat(parameter): 142 | self.printErrorUsageAndExit('unexpected parameter ' + parameter + ' for argument ' + argument) 143 | return parameters 144 | 145 | 146 | def parse(self, arguments): 147 | self.arguments = [] 148 | for arg in arguments: 149 | if arg.find(',') != -1: 150 | newargs = filter(None, arg.replace(',',' ').split(' ')) 151 | for newarg in newargs: 152 | self.arguments.append(newarg) 153 | else: 154 | self.arguments.append(arg) 155 | 156 | if len(arguments) == 0: 157 | self.printUsage() 158 | print('For more information, run "' + scriptName + ' -h"') 159 | exit(1) 160 | 161 | while self.argumentIndex < len(self.arguments): 162 | argument = self.arguments[self.argumentIndex] 163 | if not argument: 164 | continue 165 | if '-' == argument[0]: 166 | # parse optional arguments 167 | if '-v' == argument: 168 | self.out.verbose = True 169 | elif '-a' == argument or '--audio' == argument: 170 | (audioPath, audioFile) = self.getParameters(2, argument) 171 | audio = Audio(audioPath, audioFile) 172 | self.out.audios.append(audio) 173 | elif '-auralMode' == argument: 174 | possibleValues = ['spatial', 'notSpatial'] 175 | auralMode = self.getParameters(1, argument) 176 | if auralMode not in possibleValues: 177 | self.printErrorUsageAndExit("Incorrect value '" + auralMode + "' for argument " + argument + ", should be from " + str(possibleValues)) 178 | if len(self.out.audios) > 0: 179 | self.out.audios[-1].auralMode = auralMode 180 | elif '-playbackMode' == argument: 181 | possibleValues = ['onceFromStart', 'onceFromStartToEnd', 'loopFromStart', 'loopFromStartToEnd', 'loopFromStage'] 182 | playbackMode = self.getParameters(1, argument) 183 | if playbackMode not in possibleValues: 184 | self.printErrorUsageAndExit("Incorrect value '" + playbackMode + "' for argument " + argument + ", should be from " + str(possibleValues)) 185 | if len(self.out.audios) > 0: 186 | self.out.audios[-1].playbackMode = playbackMode 187 | elif '-startTime' == argument: 188 | startTime = self.getParameters(1, argument) 189 | if not isFloat(startTime): 190 | self.printErrorUsageAndExit('expected float value for argument ' + argument) 191 | if len(self.out.audios) > 0: 192 | self.out.audios[-1].startTime = float(startTime) 193 | elif '-endTime' == argument: 194 | endTime = self.getParameters(1, argument) 195 | if not isFloat(endTime): 196 | self.printErrorUsageAndExit('expected float value for argument ' + argument) 197 | if len(self.out.audios) > 0: 198 | self.out.audios[-1].endTime = float(endTime) 199 | elif '-mediaOffset' == argument: 200 | mediaOffset = self.getParameters(1, argument) 201 | if not isFloat(mediaOffset): 202 | self.printErrorUsageAndExit('expected float value for argument ' + argument) 203 | if len(self.out.audios) > 0: 204 | self.out.audios[-1].mediaOffset = float(mediaOffset) 205 | elif '-gain' == argument: 206 | gain = self.getParameters(1, argument) 207 | if not isFloat(gain): 208 | self.printErrorUsageAndExit('expected float value for argument ' + argument) 209 | if len(self.out.audios) > 0: 210 | self.out.audios[-1].gain = float(gain) 211 | elif '-h' == argument or '--help' == argument: 212 | self.printHelpAndExit() 213 | else: 214 | self.printErrorUsageAndExit('unknown argument ' + argument) 215 | else: 216 | # parse input/output filenames 217 | if self.out.inFilePath == '': 218 | self.out.inFilePath = argument 219 | elif self.out.outFilePath == '': 220 | self.out.outFilePath = argument 221 | else: 222 | print('Input file: ' + self.out.inFilePath) 223 | print('Output file:' + self.out.outFilePath) 224 | self.printErrorUsageAndExit('unknown argument ' + argument) 225 | 226 | self.argumentIndex += 1 227 | 228 | if self.out.inFilePath == '': 229 | self.printErrorUsageAndExit('too few arguments') 230 | 231 | return self.out 232 | 233 | 234 | 235 | def unzip(filePath, outputFolder): 236 | # unzip to folder/fileName 237 | foldePath, file = os.path.split(filePath) 238 | fileName = file.split(".")[0] 239 | outputDir = os.path.join(outputFolder, fileName) 240 | if not os.path.exists(outputDir): 241 | os.makedirs(outputDir) 242 | else: 243 | # clear existing folder 244 | shutil.rmtree(outputDir) 245 | os.makedirs(outputDir) 246 | 247 | with zipfile.ZipFile(filePath) as zf: 248 | zf.extractall(outputDir) 249 | 250 | return outputDir 251 | 252 | 253 | def gatherAllUSDCFiles(inputDir): 254 | usdcs = [] 255 | for root, dirnames, filenames in os.walk(inputDir): 256 | for filename in filenames: 257 | # usdz_convert only allows usdc files in usdz archive 258 | if filename.endswith(".usdc") or filename.endswith(".USDC"): 259 | usdcs.append(os.path.join(root, filename)) 260 | 261 | return usdcs 262 | 263 | 264 | 265 | parser = Parser() 266 | parserOut = parser.parse(sys.argv[1:]) 267 | 268 | if not os.path.exists(parserOut.inFilePath): 269 | parser.printErrorUsageAndExit("file '" + parserOut.inFilePath + "' does not exist.") 270 | 271 | fileAndExt = os.path.splitext(parserOut.inFilePath) 272 | if len(fileAndExt) != 2: 273 | parser.printErrorUsageAndExit('input file ' + parserOut.inFilePath + ' has unsupported file extension.') 274 | 275 | print('Input file: ' + parserOut.inFilePath) 276 | srcExt = fileAndExt[1].lower() 277 | if srcExt != '.usdz': 278 | parser.printErrorUsageAndExit('input file ' + parserOut.inFilePath + ' is not an usdz file.') 279 | 280 | if parserOut.outFilePath == '': 281 | parserOut.outFilePath = parserOut.inFilePath 282 | print('Output file:' + parserOut.outFilePath) 283 | 284 | 285 | fileAndExt = os.path.splitext(parserOut.outFilePath) 286 | dstExt = fileAndExt[1].lower() 287 | 288 | 289 | tmpFolder = "/tmp/usdzupdate" 290 | tempDir = unzip(parserOut.inFilePath, tmpFolder) 291 | usds = gatherAllUSDCFiles(tempDir) 292 | usdFile = usds[0] 293 | 294 | usdStage = Usd.Stage.Open(usdFile) 295 | 296 | stageStartTimeCode = usdStage.GetStartTimeCode() 297 | stageEndTimeCode = usdStage.GetEndTimeCode() 298 | 299 | 300 | for audio in parserOut.audios: 301 | if parserOut.verbose: 302 | print(' Add SpatialAudio ' + audio.path) 303 | 304 | usdAudio = UsdMedia.SpatialAudio.Define(usdStage, audio.path) 305 | 306 | usdAudio.CreateFilePathAttr(audio.file) 307 | 308 | if audio.auralMode is not None: 309 | usdAudio.CreateAuralModeAttr(audio.auralMode) 310 | if audio.playbackMode is not None: 311 | usdAudio.CreatePlaybackModeAttr(audio.playbackMode) 312 | if audio.startTime is not None: 313 | usdAudio.CreateStartTimeAttr(audio.startTime) 314 | if stageStartTimeCode > audio.startTime: 315 | printWarning("startTime for audio (" + str(audio.startTime) + ") is before than stage start time (" + str(stageStartTimeCode) + ")") 316 | if audio.endTime is not None: 317 | usdAudio.CreateEndTimeAttr(audio.endTime) 318 | if stageEndTimeCode != 0 and stageEndTimeCode < audio.endTime: 319 | printWarning("endTime for audio (" + str(audio.endTime) + ") is later than stage end time (" + str(stageEndTimeCode) + ")") 320 | if audio.mediaOffset is not None: 321 | usdAudio.CreateMediaOffsetAttr(audio.mediaOffset) 322 | if audio.gain is not None: 323 | usdAudio.CreateGainAttr(audio.gain) 324 | 325 | 326 | outputDir = os.path.dirname(parserOut.outFilePath) 327 | if not os.path.exists(outputDir): 328 | os.makedirs(outputDir) 329 | 330 | if dstExt == '.usdz': 331 | UsdUtils.CreateNewARKitUsdzPackage(usds[0], parserOut.outFilePath) 332 | else: 333 | usdStage.GetRootLayer().Export(parserOut.outFilePath) 334 | 335 | shutil.rmtree(tmpFolder, ignore_errors=True) 336 | 337 | -------------------------------------------------------------------------------- /usdzconvert/usdzconvert: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os.path 3 | from os import chdir 4 | import sys 5 | import importlib 6 | import tempfile 7 | from shutil import rmtree 8 | import zipfile 9 | 10 | usdLibLoaded = True 11 | kConvertErrorReturnValue = 2 12 | 13 | 14 | if sys.version_info.major != 3: 15 | print(' \033[93mWarning: It is recommended to use Python 3. Current version is ' + str(sys.version_info.major) + '.\033[0m') 16 | 17 | try: 18 | from pxr import * 19 | import usdUtils 20 | except ImportError: 21 | print(' \033[91mError: failed to import pxr module. Please add path to USD Python bindings to your PYTHONPATH\033[0m') 22 | usdLibLoaded = False 23 | 24 | __all__ = ['convert'] 25 | 26 | 27 | class USDParameters: 28 | version = 0.66 29 | materialsPath = '/Materials' 30 | 31 | def __init__(self, usdStage, verbose, url, creator, copyright, assetPath): 32 | self.usdStage = usdStage 33 | self.verbose = verbose 34 | self.url = url 35 | self.creator = creator 36 | self.copyright = copyright 37 | self.usdMaterials = {} # store materials by path 38 | self.usdMaterialsByName = {} # store materials by name 39 | self.defaultMaterial = None 40 | self.assetName = '' 41 | self.asset = usdUtils.Asset(assetPath, usdStage) 42 | 43 | 44 | # parameters from command line 45 | class ParserOut: 46 | def __init__(self): 47 | self.inFilePath = '' 48 | self.outFilePath = '' 49 | self.argumentFile = '' 50 | self.materials = [] 51 | self.verbose = False 52 | self.copyTextures = False 53 | self.iOS12 = False 54 | self.paths = None 55 | self.url = '' 56 | self.creator = '' 57 | self.copyright = '' 58 | self.metersPerUnit = 0 # set by user 59 | self.preferredIblVersion = -1 60 | self.loop = False 61 | self.noloop = False 62 | self.useObjMtl = False 63 | material = usdUtils.Material('') 64 | self.materials.append(material) 65 | 66 | 67 | # in/out parameters for converters 68 | class OpenParameters: 69 | def __init__(self): 70 | self.copyTextures = False 71 | self.searchPaths = None 72 | self.verbose = False 73 | self.metersPerUnit = 0 # set by converters 74 | 75 | 76 | class Parser: 77 | def __init__(self): 78 | self.out = ParserOut() 79 | self.arguments = [] 80 | self.argumentIndex = 0 81 | self.texCoordSet = 'st' 82 | self.wrapS = usdUtils.WrapMode.useMetadata 83 | self.wrapT = usdUtils.WrapMode.useMetadata 84 | 85 | 86 | def printConvertNameAndVersion(self): 87 | print('usdzconvert ' + str(USDParameters.version)) 88 | 89 | 90 | 91 | def printUsage(self): 92 | self.printConvertNameAndVersion() 93 | print('usage: usdzconvert inputFile [outputFile]\n\ 94 | [-h] [-version] [-f file] [-v]\n\ 95 | [-path path[+path2[...]]]\n\ 96 | [-url url]\n\ 97 | [-copyright copyright]\n\ 98 | [-copytextures]\n\ 99 | [-metersPerUnit value]\n\ 100 | [-useObjMtl]\n\ 101 | [-preferredIblVersion value]\n\ 102 | [-loop]\n\ 103 | [-no-loop]\n\ 104 | [-iOS12]\n\ 105 | [-m materialName]\n\ 106 | [-texCoordSet name]\n\ 107 | [-wrapS mode]\n\ 108 | [-wrapT mode]\n\ 109 | [-diffuseColor r,g,b]\n\ 110 | [-diffuseColor fr,fg,fb]\n\ 111 | [-normal x,y,z]\n\ 112 | [-normal fx,fy,fz]\n\ 113 | [-emissiveColor r,g,b]\n\ 114 | [-emissiveColor fr,fb,fg]\n\ 115 | [-metallic c]\n\ 116 | [-metallic ch fc]\n\ 117 | [-roughness c]\n\ 118 | [-roughness ch fc]\n\ 119 | [-occlusion c]\n\ 120 | [-occlusion ch fc]\n\ 121 | [-opacity c]\n\ 122 | [-opacity ch fc]\n\ 123 | [-clearcoat c]\n\ 124 | [-clearcoat ch fc]\n\ 125 | [-clearcoatRoughness c]\n\ 126 | [-clearcoatRoughness ch fc]\n') 127 | 128 | 129 | def printHelpAndExit(self): 130 | self.printUsage() 131 | with open(os.path.join(sys.path[0], 'help.txt'), 'r') as file: 132 | print (file.read()) 133 | file.close() 134 | raise usdUtils.ConvertExit() 135 | 136 | 137 | def printVersionAndExit(self): 138 | print(USDParameters.version) 139 | raise usdUtils.ConvertExit() 140 | 141 | 142 | def printErrorUsageAndExit(self, message): 143 | self.printConvertNameAndVersion() 144 | usdUtils.printError(message) 145 | print('For more information, run "usdzconvert -h"') 146 | raise usdUtils.ConvertError() 147 | 148 | 149 | def loadArgumentsFromFile(self, filename): 150 | self.out.argumentFile = '' 151 | if os.path.isfile(filename): 152 | self.out.argumentFile = filename 153 | elif self.out.inFilePath: 154 | filename = os.path.dirname(self.out.inFilePath) + '/' + filename 155 | if os.path.isfile(filename): 156 | self.out.argumentFile = filename 157 | if self.out.argumentFile == '': 158 | self.printErrorUsageAndExit("failed to load argument file:" + filename) 159 | 160 | with open(self.out.argumentFile) as file: 161 | for line in file: 162 | line = line.strip() 163 | if '' == line: 164 | continue 165 | 166 | line = line.replace('\t', ' ') 167 | line = line.replace(',', ' ') 168 | # arguments, like file names, can be with spaces in quotes 169 | quotes = line.split('"') 170 | if len(quotes) > 1: 171 | for i in range(1, len(quotes), 2): 172 | quotes[i] = quotes[i].replace(' ', '\t') 173 | line = ''.join(quotes) 174 | 175 | arguments = line.split(' ') 176 | for argument in arguments: 177 | argument = argument.replace('\t', ' ').strip() 178 | if argument: 179 | self.arguments.append(argument) 180 | 181 | 182 | def getParameters(self, count, argument): 183 | if self.argumentIndex + count >= len(self.arguments): 184 | self.printErrorUsageAndExit('argument ' + argument + ' needs more parameters') 185 | 186 | self.argumentIndex += count 187 | if count == 1: 188 | parameter = self.arguments[self.argumentIndex] 189 | if parameter[0] == '-' and not isFloat(parameter): 190 | self.printErrorUsageAndExit('unexpected parameter ' + parameter + ' for argument ' + argument) 191 | return self.arguments[self.argumentIndex] 192 | else: 193 | parameters = self.arguments[(self.argumentIndex - count + 1):(self.argumentIndex + 1)] 194 | for parameter in parameters: 195 | if parameter[0] == '-' and not isFloat(parameter): 196 | self.printErrorUsageAndExit('unexpected parameter ' + parameter + ' for argument ' + argument) 197 | return parameters 198 | 199 | 200 | def isNextArgumentsAreFloats(self, count): 201 | if self.argumentIndex + count >= len(self.arguments): 202 | return False 203 | for i in range(count): 204 | argument = self.arguments[self.argumentIndex + 1 + i] 205 | if not isFloat(argument): 206 | return False 207 | return True 208 | 209 | 210 | def processInputArgument(self, argument): 211 | Ok = 0 212 | Error = 1 213 | inputIdx = -1 214 | for i in range(len(usdUtils.Input.names)): 215 | inputName = usdUtils.Input.names[i] 216 | if '-' + inputName == argument: 217 | inputIdx = i 218 | break 219 | if inputIdx == -1: 220 | return Error 221 | 222 | defaultChannels = usdUtils.Input.channels[inputIdx] 223 | channelsCount = len(defaultChannels) 224 | inputName = usdUtils.Input.names[inputIdx] 225 | if self.isNextArgumentsAreFloats(channelsCount): 226 | # constant or RGB value for input 227 | self.out.materials[-1].inputs[inputName] = self.getParameters(channelsCount, argument) 228 | return Ok 229 | 230 | # texture file 231 | channels = '' 232 | filename = '' 233 | parameter = self.getParameters(1, argument) 234 | if 'r' == parameter or 'g' == parameter or 'b' == parameter or 'a' == parameter or 'rgb' == parameter: 235 | channels = parameter 236 | filename = self.getParameters(1, argument) 237 | else: 238 | filename = parameter 239 | 240 | if channelsCount != 1 and channels != '' or channels == 'rgb': 241 | usdUtils.printWarning('invalid channel ' + channels + ' for argument ' + argument) 242 | channels = '' 243 | 244 | 245 | fallback = None 246 | if self.isNextArgumentsAreFloats(channelsCount): 247 | fallback = self.getParameters(channelsCount, argument) 248 | 249 | if channels == '': 250 | index = usdUtils.Input.names.index(inputName) 251 | channels = usdUtils.Input.channels[index] 252 | 253 | self.out.materials[-1].inputs[inputName] = usdUtils.Map(channels, filename, fallback, self.texCoordSet, self.wrapS, self.wrapT) 254 | return Ok 255 | 256 | 257 | def processPath(self, pathLine): 258 | paths = pathLine.split('+') 259 | outPath = [] 260 | for path in paths: 261 | if path: 262 | if os.path.isdir(path): 263 | outPath.append(path) 264 | else: 265 | usdUtils.printWarning("Folder '" + path + "' does not exist. Ignored.") 266 | return outPath 267 | 268 | 269 | def parse(self, arguments): 270 | self.arguments = [] 271 | for arg in arguments: 272 | if arg.find(',') != -1: 273 | newargs = filter(None, arg.replace(',',' ').split(' ')) 274 | for newarg in newargs: 275 | self.arguments.append(newarg) 276 | else: 277 | self.arguments.append(arg) 278 | 279 | if len(arguments) == 0: 280 | self.printUsage() 281 | print('For more information, run "usdzconvert -h"') 282 | raise usdUtils.ConvertExit() 283 | 284 | while self.argumentIndex < len(self.arguments): 285 | argument = self.arguments[self.argumentIndex] 286 | if not argument: 287 | continue 288 | if '-' == argument[0]: 289 | # parse optional arguments 290 | if '-v' == argument: 291 | self.out.verbose = True 292 | elif '-copytextures' == argument: 293 | self.out.copyTextures = True 294 | elif '-iOS12' == argument or '-ios12' == argument: 295 | self.out.iOS12 = True 296 | elif '-path' == argument: 297 | self.out.paths = self.processPath(self.getParameters(1, argument)) 298 | elif '-copyright' == argument: 299 | self.out.copyright = self.getParameters(1, argument) 300 | elif '-url' == argument: 301 | self.out.url = self.getParameters(1, argument) 302 | elif '-creator' == argument: 303 | self.out.creator = self.getParameters(1, argument) 304 | elif '-metersPerUnit' == argument: 305 | metersPerUnit = self.getParameters(1, argument) 306 | if not isFloat(metersPerUnit) or float(metersPerUnit) <= 0: 307 | self.printErrorUsageAndExit('expected positive float value for argument ' + argument) 308 | self.out.metersPerUnit = float(metersPerUnit) 309 | elif '-preferredIblVersion' == argument or '--preferredIblVersion' == argument or '--preferrediblversion' == argument: 310 | preferredIblVersion = self.getParameters(1, argument) 311 | if not isFloat(preferredIblVersion) or float(preferredIblVersion) < 0 or 2 < float(preferredIblVersion): 312 | self.printErrorUsageAndExit('expected positive integer value [0, 1, 2] for argument ' + argument) 313 | self.out.preferredIblVersion = int(float(preferredIblVersion)) 314 | elif '-m' == argument: 315 | name = self.getParameters(1, argument) 316 | material = usdUtils.Material(name) 317 | self.out.materials.append(material) 318 | self.texCoordSet = 'st' # drop to default 319 | elif '-texCoordSet' == argument: 320 | self.texCoordSet = self.getParameters(1, argument) 321 | elif '-wraps' == argument.lower(): 322 | self.wrapS = self.getParameters(1, argument) 323 | if not usdUtils.isWrapModeCorrect(self.wrapS): 324 | self.printErrorUsageAndExit('wrap mode \'' + self.wrapS + '\' is incorrect for ' + argument) 325 | elif '-wrapt' == argument.lower(): 326 | self.wrapT = self.getParameters(1, argument) 327 | if not usdUtils.isWrapModeCorrect(self.wrapT): 328 | self.printErrorUsageAndExit('wrap mode \'' + self.wrapT + '\' is incorrect for ' + argument) 329 | elif '-loop' == argument or '--loop' == argument: 330 | self.out.loop = True 331 | elif '-no-loop' == argument or '--no-loop' == argument: 332 | self.out.noloop = True 333 | elif '-useObjMtl' == argument: 334 | self.out.useObjMtl = True 335 | elif '-h' == argument or '--help' == argument: 336 | self.printHelpAndExit() 337 | elif '-version' == argument or '--version' == argument: 338 | self.printVersionAndExit() 339 | elif '-f' == argument: 340 | self.loadArgumentsFromFile(self.getParameters(1, argument)) 341 | else: 342 | errorValue = self.processInputArgument(argument) 343 | if errorValue: 344 | self.printErrorUsageAndExit('unknown argument ' + argument) 345 | else: 346 | # parse input/output filenames 347 | if self.out.inFilePath == '': 348 | self.out.inFilePath = argument 349 | elif self.out.outFilePath == '': 350 | self.out.outFilePath = argument 351 | else: 352 | print('Input file: ' + self.out.inFilePath) 353 | print('Output file:' + self.out.outFilePath) 354 | self.printErrorUsageAndExit('unknown argument ' + argument) 355 | 356 | self.argumentIndex += 1 357 | 358 | if self.out.inFilePath == '': 359 | self.printErrorUsageAndExit('too few arguments') 360 | 361 | if self.out.loop and self.out.noloop: 362 | self.printErrorUsageAndExit("can't use -loop and -no-loop flags together") 363 | 364 | return self.out 365 | 366 | 367 | def isFloat(value): 368 | try: 369 | val = float(value) 370 | return True 371 | except ValueError: 372 | return False 373 | 374 | 375 | def createMaterial(params, materialName): 376 | matPath = params.materialsPath + '/' + materialName 377 | 378 | if params.verbose: 379 | print(' creating material at path: ' + matPath) 380 | if not Sdf.Path.IsValidIdentifier(materialName): 381 | usdUtils.printError("failed to create material by specified path.") 382 | raise usdUtils.ConvertError() 383 | 384 | surfaceShader = UsdShade.Shader.Define(params.usdStage, matPath + '/Shader') 385 | surfaceShader.CreateIdAttr('UsdPreviewSurface') 386 | surfaceOutput = surfaceShader.CreateOutput('surface', Sdf.ValueTypeNames.Token) 387 | usdMaterial = UsdShade.Material.Define(params.usdStage, matPath) 388 | usdMaterial.CreateOutput('surface', Sdf.ValueTypeNames.Token).ConnectToSource(surfaceOutput) 389 | 390 | params.usdMaterials[matPath] = usdMaterial 391 | params.usdMaterialsByName[materialName] = usdMaterial 392 | return usdMaterial 393 | 394 | 395 | def getAllUsdMaterials(params, usdParentPrim): 396 | for usdPrim in usdParentPrim.GetChildren(): 397 | if usdPrim.IsA(UsdGeom.Mesh) or usdPrim.IsA(UsdGeom.Subset): 398 | bindAPI = UsdShade.MaterialBindingAPI(usdPrim) 399 | if bindAPI != None: 400 | usdShadeMaterial = None 401 | directBinding = bindAPI.GetDirectBinding() 402 | matPath = str(directBinding.GetMaterialPath()) 403 | 404 | if matPath != '': 405 | if params.usdStage.GetObjectAtPath(matPath).IsValid(): 406 | usdShadeMaterial = directBinding.GetMaterial() 407 | elif params.verbose: 408 | usdUtils.printWarning("Mesh has material '" + matPath + "' which is not exist.") 409 | 410 | if usdShadeMaterial != None and matPath not in params.usdMaterials: 411 | params.usdMaterials[matPath] = usdShadeMaterial 412 | materialNameSplitted = matPath.split('/') 413 | materialName = materialNameSplitted[len(materialNameSplitted) - 1] 414 | params.usdMaterialsByName[materialName] = usdShadeMaterial 415 | 416 | getAllUsdMaterials(params, usdPrim) 417 | 418 | 419 | def addDefaultMaterialToGeometries(params, usdParentPrim): 420 | for usdPrim in usdParentPrim.GetChildren(): 421 | if usdPrim.IsA(UsdGeom.Mesh) or usdPrim.IsA(UsdGeom.Subset): 422 | bindAPI = UsdShade.MaterialBindingAPI(usdPrim) 423 | if bindAPI != None: 424 | usdShadeMaterial = None 425 | directBinding = bindAPI.GetDirectBinding() 426 | matPath = str(directBinding.GetMaterialPath()) 427 | 428 | if matPath != '': 429 | usdShadeMaterial = directBinding.GetMaterial() 430 | 431 | if usdShadeMaterial == None: 432 | if params.defaultMaterial == None: 433 | params.defaultMaterial = createMaterial(params, 'defaultMaterial') 434 | matPath = params.materialsPath + '/defaultMaterial' 435 | usdShadeMaterial = params.defaultMaterial 436 | bindAPI.Bind(usdShadeMaterial) 437 | 438 | if matPath not in params.usdMaterials: 439 | params.usdMaterials[matPath] = usdShadeMaterial 440 | materialNameSplitted = matPath.split('/') 441 | materialName = materialNameSplitted[len(materialNameSplitted) - 1] 442 | params.usdMaterialsByName[materialName] = usdShadeMaterial 443 | 444 | addDefaultMaterialToGeometries(params, usdPrim) 445 | 446 | 447 | def findUsdMaterialRecursively(params, usdParentPrim, name, byPath): 448 | for usdPrim in usdParentPrim.GetChildren(): 449 | if usdPrim.IsA(UsdShade.Material): 450 | path = usdPrim.GetPath() 451 | if byPath: 452 | if path == name: 453 | return UsdShade.Material(usdPrim) 454 | else: 455 | matName = os.path.basename(str(path)) 456 | if matName == name: 457 | return UsdShade.Material(usdPrim) 458 | usdMaterial = findUsdMaterialRecursively(params, usdPrim, name, byPath) 459 | if usdMaterial is not None: 460 | return usdMaterial 461 | return None 462 | 463 | 464 | def findUsdMaterial(params, name): 465 | if not name or len(name) < 1: 466 | return None 467 | 468 | # first try to find by material path 469 | if name in params.usdMaterials: 470 | return params.usdMaterials[name] 471 | 472 | # try to find by material name 473 | materialName = usdUtils.makeValidIdentifier(name) 474 | if materialName in params.usdMaterialsByName: 475 | return params.usdMaterialsByName[materialName] 476 | 477 | # try other options 478 | testMaterialName = '/Materials/' + materialName 479 | if testMaterialName in params.usdMaterials: 480 | return params.usdMaterials[testMaterialName] 481 | 482 | testMaterialName = '/' + materialName 483 | if testMaterialName in params.usdMaterials: 484 | return params.usdMaterials[testMaterialName] 485 | 486 | byPath = '/' == name[0] 487 | return findUsdMaterialRecursively(params, params.usdStage.GetPseudoRoot(), name, byPath) 488 | 489 | 490 | def copyTexturesFromStageToFolder(params, srcPath, folder): 491 | copiedFiles = {} 492 | srcFolder = os.path.dirname(srcPath) 493 | for path, usdMaterial in params.usdMaterials.items(): 494 | for childShader in usdMaterial.GetPrim().GetChildren(): 495 | idAttribute = childShader.GetAttribute('info:id') 496 | if idAttribute is None: 497 | continue 498 | id = idAttribute.Get() 499 | if id != 'UsdUVTexture': 500 | continue 501 | fileAttribute = childShader.GetAttribute('inputs:file') 502 | if fileAttribute is None or fileAttribute.Get() is None: 503 | continue 504 | filename = fileAttribute.Get().path 505 | if not filename: 506 | continue 507 | if filename in copiedFiles: 508 | continue 509 | if srcFolder and filename[0] != '/': 510 | filePath = srcFolder + '/' + filename 511 | else: 512 | filePath = filename 513 | usdUtils.copy(filePath, folder + '/' + filename, params.verbose) 514 | copiedFiles[filename] = filename 515 | 516 | 517 | def copyMaterialTextures(params, material, srcPath, dstPath, folder): 518 | srcFolder = os.path.dirname(srcPath) 519 | dstFolder = os.path.dirname(dstPath) 520 | for inputName, input in material.inputs.items(): 521 | if not isinstance(input, usdUtils.Map): 522 | continue 523 | if not input.file: 524 | continue 525 | 526 | if srcFolder: 527 | if os.path.isfile(srcFolder + '/' + input.file): 528 | usdUtils.copy(srcFolder + '/' + input.file, folder + '/' + input.file, params.verbose) 529 | continue 530 | 531 | if dstFolder and dstFolder != srcFolder: 532 | if os.path.isfile(dstFolder + '/' + input.file): 533 | usdUtils.copy(dstFolder + '/' + input.file, folder + '/' + input.file, params.verbose) 534 | continue 535 | 536 | if os.path.isfile(input.file): 537 | if srcFolder and len(srcFolder) < len(input.file) and srcFolder + '/' == input.file[0:(len(srcFolder)+1)]: 538 | input.file = input.file[(len(srcFolder)+1):] 539 | usdUtils.copy(srcFolder + '/' + input.file, folder + '/' + input.file, params.verbose) 540 | continue 541 | 542 | if dstFolder and dstFolder != srcFolder and len(dstFolder) < len(input.file) and dstFolder + '/' == input.file[0:(len(dstFolder)+1)]: 543 | input.file = input.file[(len(dstFolder)+1):] 544 | usdUtils.copy(dstFolder + '/' + input.file, folder + '/' + input.file, params.verbose) 545 | continue 546 | 547 | basename = 'textures/' + os.path.basename(input.file) 548 | usdUtils.copy(input.file, folder + '/' + basename, params.verbose) 549 | input.file = basename 550 | 551 | 552 | def createStageMetadata(params): 553 | if params.creator != '': 554 | params.usdStage.SetMetadataByDictKey("customLayerData", "creator", str(params.creator)) 555 | else: 556 | params.usdStage.SetMetadataByDictKey("customLayerData", "creator", "usdzconvert preview " + str(params.version)) 557 | if params.url != '': 558 | params.usdStage.SetMetadataByDictKey("customLayerData", "url", str(params.url)) 559 | if params.copyright != '': 560 | params.usdStage.SetMetadataByDictKey("customLayerData", "copyright", str(params.copyright)) 561 | 562 | 563 | def unzip(filePath, outputDir): 564 | firstFile = '' 565 | with zipfile.ZipFile(filePath) as zf: 566 | zf.extractall(outputDir) 567 | namelist = zf.namelist() 568 | if len(namelist) > 0: 569 | firstFile = namelist[0] 570 | return firstFile 571 | 572 | 573 | def process(argumentList): 574 | parser = Parser() 575 | parserOut = parser.parse(argumentList) 576 | 577 | srcPath = '' 578 | if os.path.isfile(parserOut.inFilePath): 579 | srcPath = parserOut.inFilePath 580 | elif os.path.dirname(parserOut.inFilePath) == '' and parserOut.argumentFile: 581 | # try to find input file in argument file folder which is specified by -f in command line 582 | argumentFileDir = os.path.dirname(parserOut.argumentFile) 583 | if argumentFileDir: 584 | os.chdir(argumentFileDir) 585 | if os.path.isfile(parserOut.inFilePath): 586 | srcPath = parserOut.inFilePath 587 | 588 | if srcPath == '': 589 | parser.printErrorUsageAndExit('input file ' + parserOut.inFilePath + ' does not exist.') 590 | 591 | fileAndExt = os.path.splitext(srcPath) 592 | if len(fileAndExt) != 2: 593 | parser.printErrorUsageAndExit('input file ' + parserOut.inFilePath + ' has unsupported file extension.') 594 | 595 | print('Input file: ' + srcPath) 596 | srcExt = fileAndExt[1].lower() 597 | 598 | dstIsUsdz = False 599 | dstPath = parserOut.outFilePath 600 | dstExt = '' 601 | if dstPath == '': 602 | # default destination file is .usdz file in the same folder as source file 603 | dstExt = '.usdz' 604 | dstPath = fileAndExt[0] + dstExt 605 | dstIsUsdz = True 606 | 607 | dstFileAndExt = os.path.splitext(dstPath) 608 | if len(dstFileAndExt) != 2: 609 | parser.printErrorUsageAndExit('output file ' + dstPath + ' has unsupported file extension.') 610 | 611 | if not dstIsUsdz: 612 | dstExt = dstFileAndExt[1].lower() 613 | if dstExt == '.usdz': 614 | dstIsUsdz = True 615 | elif dstExt != '.usd' and dstExt != '.usdc' and dstExt != '.usda': 616 | parser.printErrorUsageAndExit('output file ' + dstPath + ' should have .usdz, .usdc, .usda or .usd extension.') 617 | 618 | tmpFolder = tempfile.mkdtemp('usdzconvert') 619 | 620 | legacyModifier = None 621 | if parserOut.iOS12: 622 | iOS12Compatible_module = importlib.import_module("iOS12LegacyModifier") 623 | legacyModifier = iOS12Compatible_module.createLegacyModifier() 624 | print('Converting in iOS12 compatiblity mode.') 625 | 626 | tmpPath = dstFileAndExt[0] + '.usdc' if dstIsUsdz else dstPath 627 | tmpBasename = os.path.basename(tmpPath) 628 | tmpPath = tmpFolder + '/' + tmpBasename 629 | 630 | if parserOut.verbose and parserOut.copyTextures and dstIsUsdz: 631 | usdUtils.printWarning('argument -copytextures works for .usda and .usdc output files only.') 632 | 633 | openParameters = OpenParameters() 634 | openParameters.copyTextures = parserOut.copyTextures and not dstIsUsdz 635 | openParameters.searchPaths = parserOut.paths 636 | openParameters.verbose = parserOut.verbose 637 | 638 | srcIsUsd = False 639 | srcIsUsdz = False 640 | usdStage = None 641 | if '.obj' == srcExt: 642 | global usdStageWithObj_module 643 | usdStageWithObj_module = importlib.import_module("usdStageWithObj") 644 | # this line can be updated with Pixar's backend loader 645 | usdStage = usdStageWithObj_module.usdStageWithObj(srcPath, tmpPath, parserOut.useObjMtl, openParameters) 646 | elif '.gltf' == srcExt or '.glb' == srcExt: 647 | global usdStageWithGlTF_module 648 | usdStageWithGlTF_module = importlib.import_module("usdStageWithGlTF") 649 | usdStage = usdStageWithGlTF_module.usdStageWithGlTF(srcPath, tmpPath, legacyModifier, openParameters) 650 | elif '.fbx' == srcExt: 651 | global usdStageWithFbx_module 652 | usdStageWithFbx_module = importlib.import_module("usdStageWithFbx") 653 | usdStage = usdStageWithFbx_module.usdStageWithFbx(srcPath, tmpPath, legacyModifier, openParameters) 654 | elif '.usd' == srcExt or '.usda' == srcExt or '.usdc' == srcExt: 655 | usdStage = Usd.Stage.Open(srcPath) 656 | srcIsUsd = True 657 | openParameters.metersPerUnit = usdStage.GetMetadata("metersPerUnit") 658 | elif '.usdz' == srcExt: 659 | tmpUSDC = unzip(srcPath, tmpFolder) 660 | if tmpUSDC == '': 661 | parser.printErrorUsageAndExit("can't open input usdz file " + parserOut.inFilePath) 662 | usdStage = Usd.Stage.Open(tmpFolder + '/' + tmpUSDC) 663 | srcIsUsdz = True 664 | elif '.abc' == srcExt: 665 | usdStage = Usd.Stage.Open(srcPath) 666 | # To update Alembic USD Stage, first save it to temporary .usdc and reload it 667 | tmpUSDC = tmpPath + '.usdc' 668 | usdStage.GetRootLayer().Export(tmpUSDC) 669 | if parserOut.verbose: 670 | print('Temporary USDC file: ' + tmpUSDC) 671 | usdStage = Usd.Stage.Open(tmpUSDC) 672 | else: 673 | parser.printErrorUsageAndExit('input file ' + parserOut.inFilePath + ' has unsupported file extension.') 674 | 675 | if usdStage == None: 676 | usdUtils.printError("failed to create USD stage.") 677 | raise usdUtils.ConvertError() 678 | 679 | params = USDParameters(usdStage, parserOut.verbose, parserOut.url, parserOut.creator, parserOut.copyright, tmpPath) 680 | createStageMetadata(params) 681 | 682 | if parserOut.loop and (srcIsUsd or srcIsUsdz): 683 | usdStage.SetMetadataByDictKey("customLayerData", "loopStartToEndTimeCode", True) 684 | 685 | if parserOut.noloop: 686 | usdStage.SetMetadataByDictKey("customLayerData", "loopStartToEndTimeCode", False) 687 | 688 | if parserOut.preferredIblVersion != -1: 689 | appleDict = usdStage.GetMetadataByDictKey("customLayerData", "Apple") 690 | if appleDict is None or type(appleDict) is not dict: 691 | appleDict = {} 692 | appleDict["preferredIblVersion"] = parserOut.preferredIblVersion 693 | usdStage.SetMetadataByDictKey("customLayerData", "Apple", appleDict) 694 | 695 | rootPrim = None 696 | if usdStage.HasDefaultPrim(): 697 | rootPrim = usdStage.GetDefaultPrim() 698 | 699 | if rootPrim != None: 700 | params.assetName = rootPrim.GetName() 701 | params.materialsPath = '/' + params.assetName + '/Materials' 702 | 703 | metersPerUnit = openParameters.metersPerUnit # set by converter 704 | if parserOut.metersPerUnit != 0: 705 | metersPerUnit = parserOut.metersPerUnit # set by user 706 | if metersPerUnit == 0: 707 | metersPerUnit = 0.01 708 | if legacyModifier is None: 709 | usdStage.SetMetadata("metersPerUnit", metersPerUnit) 710 | else: 711 | if rootPrim != None: 712 | usdMetersPerUnit = 0.01 713 | scale = metersPerUnit / usdMetersPerUnit 714 | if scale != 1: 715 | rootXform = UsdGeom.Xform(rootPrim) 716 | rootXform.AddScaleOp(UsdGeom.XformOp.PrecisionFloat, "metersPerUnit").Set(Gf.Vec3f(scale, scale, scale)) 717 | 718 | getAllUsdMaterials(params, params.usdStage.GetPseudoRoot()) 719 | 720 | if srcIsUsd and dstIsUsdz: 721 | # copy textures to temporary folder while creating usdz 722 | copyTexturesFromStageToFolder(params, srcPath, tmpFolder) 723 | 724 | if srcIsUsd: 725 | if not (len(parserOut.materials) == 1 and parserOut.materials[0].isEmpty()): 726 | usdUtils.printWarning('Material arguments are ignored for .usda/usdc input files.') 727 | else: 728 | # update usd materials with command line material arguments 729 | for material in parserOut.materials: 730 | 731 | if legacyModifier is not None: 732 | legacyModifier.opacityAndDiffuseOneTexture(material) 733 | 734 | if material.name == '': 735 | # if materials are not specified, then apply default material to all materials 736 | if not material.isEmpty(): 737 | addDefaultMaterialToGeometries(params, params.usdStage.GetPseudoRoot()) 738 | 739 | copyMaterialTextures(params, material, srcPath, dstPath, tmpFolder) 740 | if legacyModifier is not None: 741 | legacyModifier.makeORMTextures(material, tmpFolder, parserOut.verbose) 742 | 743 | for path, usdMaterial in params.usdMaterials.items(): 744 | surfaceShader = material.getUsdSurfaceShader(usdMaterial, params.usdStage) 745 | material.updateUsdMaterial(usdMaterial, surfaceShader, params.usdStage) 746 | continue 747 | 748 | usdMaterial = findUsdMaterial(params, material.path if material.path else material.name) 749 | 750 | if usdMaterial is not None: 751 | # if material does exist remove it 752 | matPath = str(usdMaterial.GetPrim().GetPath()) 753 | if matPath in params.usdMaterials: 754 | del params.usdMaterials[matPath] 755 | usdStage.RemovePrim(matPath) 756 | usdMaterial = None 757 | 758 | copyMaterialTextures(params, material, srcPath, dstPath, tmpFolder) 759 | if legacyModifier is not None: 760 | legacyModifier.makeORMTextures(material, tmpFolder, parserOut.verbose) 761 | 762 | usdMaterial = material.makeUsdMaterial(params.asset) 763 | if usdMaterial is None: 764 | continue 765 | 766 | surfaceShader = material.getUsdSurfaceShader(usdMaterial, params.usdStage) 767 | material.updateUsdMaterial(usdMaterial, surfaceShader, params.usdStage) 768 | params.usdMaterials[str(usdMaterial.GetPrim().GetPath())] = usdMaterial 769 | 770 | usdStage.GetRootLayer().Export(tmpPath) 771 | 772 | # prepare destination folder 773 | dstFolder = os.path.dirname(dstPath) 774 | if dstFolder != '' and not os.path.isdir(dstFolder): 775 | if parserOut.verbose: 776 | print('Creating folder: ' + dstFolder) 777 | os.makedirs(dstFolder) 778 | 779 | if dstIsUsdz: 780 | # construct .usdz archive from the .usdc file 781 | UsdUtils.CreateNewARKitUsdzPackage(Sdf.AssetPath(tmpPath), dstPath) 782 | else: 783 | usdUtils.copy(tmpPath, dstPath) 784 | 785 | # copy textures with usda and usdc 786 | if openParameters.copyTextures: 787 | copyTexturesFromStageToFolder(params, tmpPath, dstFolder) 788 | 789 | rmtree(tmpFolder, ignore_errors=True) 790 | print('Output file: ' + dstPath) 791 | 792 | arkitCheckerReturn = 0 793 | if dstIsUsdz: 794 | # ARKit checker code 795 | usdcheckerArgs = [dstPath] 796 | if parserOut.verbose: 797 | usdcheckerArgs.append('-v') 798 | scriptFolder = os.path.dirname(os.path.realpath(__file__)) 799 | spec = importlib.util.spec_from_loader("usdARKitChecker", importlib.machinery.SourceFileLoader("usdARKitChecker", scriptFolder + '/usdARKitChecker')) 800 | usdARKitChecker = importlib.util.module_from_spec(spec) 801 | spec.loader.exec_module(usdARKitChecker) 802 | arkitCheckerReturn = usdARKitChecker.main(usdcheckerArgs) 803 | 804 | 805 | return arkitCheckerReturn 806 | 807 | def tryProcess(argumentList): 808 | try: 809 | ret = process(argumentList) 810 | except usdUtils.ConvertError: 811 | return kConvertErrorReturnValue 812 | except usdUtils.ConvertExit: 813 | return 0 814 | except: 815 | raise 816 | return ret 817 | 818 | 819 | def convert(fileList, optionDictionary): 820 | supportedFormats = ['.obj', '.gltf', '.glb', '.fbx', '.usd', '.usda', '.usdc', '.usdz', '.abc'] 821 | argumentList = [] 822 | 823 | for file in fileList: 824 | fileAndExt = os.path.splitext(file) 825 | if len(fileAndExt) == 2: 826 | ext = fileAndExt[1].lower() 827 | if ext in supportedFormats: 828 | # source file to convert 829 | argumentList.append(file) 830 | 831 | name = fileAndExt[0] 832 | 833 | for inputName in usdUtils.Input.names: 834 | if inputName in optionDictionary: 835 | option = optionDictionary[inputName] 836 | 837 | channel = '' 838 | 839 | optionAndChannel = option.split(':') 840 | if len(optionAndChannel) == 2: 841 | option = optionAndChannel[0] 842 | channel = optionAndChannel[1] 843 | 844 | if len(name) > len(option) and option==name[-len(option):]: 845 | argumentList.append('-' + inputName) 846 | if channel != '': 847 | argumentList.append(channel) 848 | argumentList.append(file) 849 | 850 | return tryProcess(argumentList) 851 | 852 | 853 | def main(): 854 | return tryProcess(sys.argv[1:]) 855 | 856 | 857 | if __name__ == '__main__': 858 | if usdLibLoaded: 859 | errorValue = main() 860 | else: 861 | errorValue = kConvertErrorReturnValue 862 | 863 | sys.exit(errorValue) 864 | 865 | -------------------------------------------------------------------------------- /usdzconvert/usdzcreateassetlib: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os, struct, sys, time, binascii 4 | from collections import namedtuple 5 | 6 | def computeExtraFieldSize(fileoffset): 7 | extra_headersize = 2*2 8 | usd_data_alignment = 64 9 | 10 | paddingbuffersize = extra_headersize + usd_data_alignment # Maximum size of buffer needed for padding bytes. 11 | 12 | required_padding = usd_data_alignment - (fileoffset % usd_data_alignment) 13 | if required_padding == usd_data_alignment: 14 | required_padding = 0 15 | elif required_padding < extra_headersize: 16 | required_padding += usd_data_alignment # If the amount of padding needed is too small to contain the header, bump the size up while maintaining the required alignment. 17 | 18 | if required_padding == 0: 19 | return bytearray([]) 20 | 21 | extrafield = [0] * required_padding 22 | extrafield[0] = 0x86 23 | extrafield[1] = 0x19 24 | extrafield[2] = required_padding - extra_headersize 25 | extrafield[3] = 0 26 | 27 | return bytearray(extrafield) 28 | 29 | FileInfo = namedtuple('FileInfo', 'dostime dosdate CRC filesize filename extra headerstart') 30 | 31 | def storeFile(arc_fp, filepath): 32 | localHeaderFmt = "<4s5H3L2H" 33 | localHeaderSize = struct.calcsize(localHeaderFmt) 34 | 35 | filename = os.path.basename(filepath) 36 | 37 | st = os.stat(filepath) 38 | filesize = st.st_size 39 | mtime = time.localtime(st.st_mtime) 40 | dt = mtime[0:6] 41 | dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] 42 | dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2) 43 | 44 | headerstart = arc_fp.tell() 45 | extra = computeExtraFieldSize(headerstart + localHeaderSize + len(filename)) 46 | 47 | with open(filepath, 'rb') as f: 48 | filedata = f.read() 49 | CRC = binascii.crc32(filedata) & 0xffffffff 50 | 51 | header = struct.pack(localHeaderFmt, 52 | "PK\003\004", # local file header signature 4 bytes (0x04034b50) 53 | 10, # version needed to extract 2 bytes 54 | 0, # general purpose bit flag 2 bytes 55 | 0, # compression method 2 bytes 56 | dostime, # last mod file time 2 bytes 57 | dosdate, # last mod file date 2 bytes 58 | CRC, # crc-32 4 bytes 59 | filesize, # compressed size 4 bytes 60 | filesize, # uncompressed size 4 bytes 61 | len(filename), # file name length 2 bytes 62 | len(extra)) # extra field length 2 bytes 63 | 64 | arc_fp.write(header) 65 | arc_fp.write(filename) 66 | arc_fp.write(extra) 67 | 68 | arc_fp.write(filedata) 69 | 70 | return FileInfo(dostime, dosdate, CRC, filesize, filename, extra, headerstart) 71 | 72 | return None 73 | 74 | 75 | def storeCentralDirectoryHeader(arc_fp, fileInfo): 76 | centralDirectoryHeaderFmt = "<4s6H3L5H2L" 77 | centralDirectoryHeaderSize = struct.calcsize(centralDirectoryHeaderFmt) 78 | 79 | header = struct.pack(centralDirectoryHeaderFmt, 80 | "PK\001\002", # central file header signature 4 bytes (0x02014b50) 81 | 0, # version made by 2 bytes 82 | 10, # version needed to extract 2 bytes 83 | 0, # general purpose bit flag 2 bytes 84 | 0, # compression method 2 bytes 85 | fileInfo.dostime, # last mod file time 2 bytes 86 | fileInfo.dosdate, # last mod file date 2 bytes 87 | fileInfo.CRC, # crc-32 4 bytes 88 | fileInfo.filesize, # compressed size 4 bytes 89 | fileInfo.filesize, # uncompressed size 4 bytes 90 | len(fileInfo.filename), # file name length 2 bytes 91 | len(fileInfo.extra), # extra field length 2 bytes 92 | 0, # file comment length 2 bytes 93 | 0, # disk number start 2 bytes 94 | 0, # internal file attributes 2 bytes 95 | 0, # external file attributes 4 bytes 96 | fileInfo.headerstart) # relative offset of local header 4 bytes 97 | 98 | arc_fp.write(header) 99 | arc_fp.write(fileInfo.filename) 100 | arc_fp.write(fileInfo.extra) 101 | 102 | def storeEndOfCentralDirectoryRecord(arc_fp, fileInfos, centralDirectoryStart): 103 | endOfCentralDirectoryRecordFmt = "<4s4H2LH" 104 | endOfCentralDirectoryRecordSize = struct.calcsize(endOfCentralDirectoryRecordFmt) 105 | 106 | centralDirectorySize = arc_fp.tell() - centralDirectoryStart 107 | 108 | eoCentralDirectory = struct.pack(endOfCentralDirectoryRecordFmt, 109 | "PK\005\006", # end of central dir signature 4 bytes (0x06054b50) 110 | 0, # number of this disk 2 bytes 111 | 0, # number of the disk with the start of the central directory 2 bytes 112 | len(fileInfos), # total number of entries in the central directory on this disk 2 bytes 113 | len(fileInfos), # total number of entries in the central directory 2 bytes 114 | centralDirectorySize, # size of the central directory 4 bytes 115 | centralDirectoryStart, # offset of start of central directory with respect to the starting disk number 4 bytes 116 | 0) # .ZIP file comment length 2 bytes 117 | 118 | arc_fp.write(eoCentralDirectory) 119 | 120 | def createPackageUsda(usdaFilename, files): 121 | prefixTemplate = """#usda 1.0 122 | ( 123 | defaultPrim = "Object" 124 | upAxis = "Y" 125 | customLayerData = {{ 126 | asset[] assetLibrary = {0} 127 | }} 128 | ) 129 | 130 | def Xform "Object" ( 131 | variants = {{ 132 | string Assets = "{1}" 133 | }} 134 | prepend variantSets = "Assets" 135 | ) 136 | {{ 137 | variantSet "Assets" = {{""" 138 | 139 | perAssetTemplate = """ 140 | "{0}" ( 141 | prepend references = @{1}@ 142 | ) {{ 143 | 144 | }}""" 145 | 146 | postFixTemplate = """ 147 | }} 148 | }} 149 | """ 150 | fileBasenames = str([os.path.basename(name) for name in files]).replace("'", "@") 151 | usda = prefixTemplate.format(fileBasenames, os.path.splitext(os.path.basename(files[0]))[0]) 152 | for filename in files: 153 | usda += perAssetTemplate.format(os.path.splitext(os.path.basename(filename))[0], os.path.basename(filename)) 154 | usda += postFixTemplate.format() 155 | 156 | file = open(usdaFilename, "w") 157 | file.write(usda) 158 | file.close() 159 | return 160 | 161 | allArgumentsUSDZ = True 162 | for filename in sys.argv[1:]: 163 | if os.path.splitext(filename)[1] != '.usdz': 164 | allArgumentsUSDZ = False 165 | 166 | if len(sys.argv) < 3 or not allArgumentsUSDZ: 167 | print 'usage:\n usdzcreateassetlib outputFile.usdz asset1.usdz [asset2.usdz [...]]' 168 | sys.exit(0) 169 | 170 | fileInfos = [] 171 | target_usdz = sys.argv[1] 172 | target_usda = os.path.splitext(target_usdz)[0]+".usda" 173 | 174 | createPackageUsda(target_usda, sys.argv[2:]) 175 | with open(target_usdz, 'wb') as arc_fp: 176 | fileInfo = storeFile(arc_fp, target_usda) 177 | fileInfos.append(fileInfo) 178 | 179 | for (index, fileName) in enumerate(sys.argv[2:]): 180 | fileInfo = storeFile(arc_fp, fileName) 181 | fileInfos.append(fileInfo) 182 | 183 | centralDirectoryStart = arc_fp.tell() 184 | 185 | for fileInfo in fileInfos: 186 | storeCentralDirectoryHeader(arc_fp, fileInfo) 187 | 188 | storeEndOfCentralDirectoryRecord(arc_fp, fileInfos, centralDirectoryStart) 189 | 190 | -------------------------------------------------------------------------------- /usdzconvert/validateMaterial.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os, shutil, sys 3 | 4 | from pxr import * 5 | 6 | class TermColors: 7 | WARN = '\033[93m' 8 | FAIL = '\033[91m' 9 | END = '\033[0m' 10 | 11 | def _Err(msg): 12 | sys.stderr.write(TermColors.FAIL + msg + TermColors.END + '\n') 13 | 14 | def _Warn(msg): 15 | sys.stderr.write(TermColors.WARN + msg + TermColors.END + '\n') 16 | 17 | def validateType(property, correctType, shaderPath, verboseOutput, errorData): 18 | if not property: 19 | return True 20 | if property.GetTypeName() != correctType: 21 | errorData.append({ 22 | "code": "ERR_INCORRECT_PROPERTY_TYPE", 23 | "shaderPath": shaderPath, 24 | "propertyName": property.GetFullName(), 25 | "propertyTypeName": str(property.GetTypeName()), 26 | "correctPropertyType": str(correctType) 27 | }) 28 | if verboseOutput:_Err("\t" + shaderPath + ": " + property.GetFullName() + " type " + 29 | str(property.GetTypeName()) + " is not correct type "+ str(correctType) + ".") 30 | return False 31 | return True 32 | 33 | def validateConnection(property, connection, verboseOutput, errorData): 34 | if not connection: 35 | return True 36 | elif connection[2] == UsdShade.AttributeType.Output: 37 | output = connection[0].GetOutput(connection[1]) 38 | if not output: 39 | errorData.append({ 40 | "code": "ERR_MISSING_OUTPUT", 41 | "connectionPath": connection[0].GetPrim().GetPath().pathString, 42 | "output": connection[1] 43 | }) 44 | if verboseOutput: _Err("\t" + connection[0].GetPrim().GetPath().pathString + ": is missing output " + 45 | connection[1] + ".") 46 | return False 47 | else: 48 | if property.GetTypeName().cppTypeName != output.GetTypeName().cppTypeName: 49 | errorData.append({ 50 | "code": "ERR_MISMATCHED_PROPERTY_TYPE", 51 | "connectionPath": connection[0].GetPrim().GetPath().pathString, 52 | "outputTypeName": str(output.GetTypeName()), 53 | "propertyTypeName": str(property.GetTypeName()) 54 | }) 55 | if verboseOutput: _Err("\t" + connection[0].GetPrim().GetPath().pathString + ": output type of " + 56 | str(output.GetTypeName()) + " mismatches connecting property type " + 57 | str(property.GetTypeName()) + ".") 58 | return False 59 | 60 | elif connection[2] == UsdShade.AttributeType.Input: 61 | input = connection[0].GetInput(connection[1]) 62 | if not input: 63 | errorData.append({ 64 | "code": "ERR_MISSING_INPUT", 65 | "connectionPath": connection[0].GetPrim().GetPath().pathString, 66 | "input": connection[1] 67 | }) 68 | if verboseOutput: _Err("\t" + connection[0].GetPrim().GetPath().pathString + ": is missing input " + 69 | connection[1] + ".") 70 | return False 71 | else: 72 | if property.GetTypeName().cppTypeName != input.GetTypeName().cppTypeName: 73 | errorData.append({ 74 | "code": "ERR_MISMATCHED_PROPERTY_TYPE", 75 | "connectionPath": connection[0].GetPrim().GetPath().pathString, 76 | "outputTypeName": str(output.GetTypeName()), 77 | "propertyTypeName": str(property.GetTypeName()) 78 | }) 79 | if verboseOutput: _Err("\t" + connection[0].GetPrim().GetPath().pathString + ": output type of " + 80 | str(input.GetTypeName()) + " mismatches connecting property type " + 81 | str(property.GetTypeName()) + ".") 82 | return False 83 | else: 84 | pass 85 | return True 86 | 87 | def validatePropertyType(shaderPath, property, verboseOutput, errorData): 88 | baseName = property.GetBaseName() 89 | if baseName in ["diffuseColor", "emissiveColor", "specularColor"]: 90 | if not validateType(property, Sdf.ValueTypeNames.Color3f, shaderPath, verboseOutput, errorData): 91 | return False 92 | elif baseName == "normal": 93 | if not validateType(property, Sdf.ValueTypeNames.Normal3f, shaderPath, verboseOutput, errorData): 94 | return False 95 | elif baseName in ["ior", "metallic", "roughness", "clearcoat", "clearcoatRoughness", "opacity", 96 | "opacityThreshold", "occlusion", "displacement"]: 97 | if not validateType(property, Sdf.ValueTypeNames.Float, shaderPath, verboseOutput, errorData): 98 | return False 99 | elif baseName == "useSpecularWorkflow": 100 | if not validateType(property, Sdf.ValueTypeNames.Int, shaderPath, verboseOutput, errorData): 101 | return False 102 | return True 103 | 104 | 105 | def validateTextureNode(shaderNode, verboseOutput, errorData): 106 | shaderPath = shaderNode.GetPrim().GetPath().pathString 107 | assetInput = shaderNode.GetInput("file") 108 | if not validateType(assetInput, Sdf.ValueTypeNames.Asset, shaderPath, verboseOutput, errorData): 109 | return False 110 | if not assetInput or assetInput.Get() == None: 111 | errorData.append({ 112 | "code": "WRN_NO_TEXTURE_FILE", 113 | "shaderPath": shaderPath 114 | }) 115 | if verboseOutput:_Warn("\t" + shaderPath + ": no texture file authored, fallback value will be used.") 116 | 117 | fallback = shaderNode.GetInput("fallback") 118 | default = shaderNode.GetInput("default") 119 | if default and not fallback: 120 | errorData.append({ 121 | "code": "WRN_INPUT_DEFAULT_DEPRECATED", 122 | "shaderPath": shaderPath 123 | }) 124 | if verboseOutput:_Warn("\t" + shaderPath+": input:default is deprecated, please author with input:fallback.") 125 | 126 | if not validateType(fallback, Sdf.ValueTypeNames.Float4, shaderPath, verboseOutput, errorData): 127 | return False 128 | 129 | if not validateType(shaderNode.GetInput("scale"), Sdf.ValueTypeNames.Float4, shaderPath, verboseOutput, errorData): 130 | return False 131 | 132 | if not validateType(shaderNode.GetInput("bias"), Sdf.ValueTypeNames.Float4, shaderPath, verboseOutput, errorData): 133 | return False 134 | 135 | if not validateType(shaderNode.GetInput("wrapS"), Sdf.ValueTypeNames.Token, shaderPath, verboseOutput, errorData): 136 | return False 137 | 138 | if not validateType(shaderNode.GetInput("wrapT"), Sdf.ValueTypeNames.Token, shaderPath, verboseOutput, errorData): 139 | return False 140 | 141 | st = shaderNode.GetInput("st") 142 | if not validateType(st, Sdf.ValueTypeNames.Float2, shaderPath, verboseOutput, errorData): 143 | return False 144 | 145 | if not st: 146 | errorData.append({ 147 | "code": "ERR_NO_ST", 148 | "shaderPath": shaderPath 149 | }) 150 | if verboseOutput: _Err("\t" + shaderPath + ": has no st input.") 151 | return False 152 | 153 | connect = UsdShade.ConnectableAPI.GetConnectedSource(st) 154 | if connect == None: 155 | return True 156 | 157 | if not validateConnection(st, connect, verboseOutput, errorData): 158 | return False 159 | 160 | connectable = UsdShade.Shader(connect[0]) 161 | shaderId = connectable.GetIdAttr().Get() 162 | if shaderId == "UsdTransform2d": 163 | return validateTransform2dNode(connectable, verboseOutput, errorData) 164 | elif shaderId == "UsdPrimvarReader_float2": 165 | return validatePrimvarReaderNode(connectable, verboseOutput, errorData) 166 | else: 167 | errorData.append({ 168 | "code": "ERR_ST_CONNECTION", 169 | "shaderPath": shaderPath, 170 | "shaderId": shaderId 171 | }) 172 | if verboseOutput: _Err("\t" + shaderPath + ": st connect to " + shaderId + ".") 173 | return False 174 | 175 | def validatePrimvarReaderNode(shaderNode, verboseOutput, errorData): 176 | shaderPath = shaderNode.GetPrim().GetPath().pathString 177 | shaderId = shaderNode.GetIdAttr().Get() 178 | primvarReaderType = shaderId[len('UsdPrimvarReader_'):] 179 | 180 | # TODO: support more types in preview surface proposal, we only check float2 for now 181 | if primvarReaderType != "float2": 182 | errorData.append({ 183 | "code": "WRN_FLOAT2_TYPE_ONLY", 184 | "shaderPath": shaderPath, 185 | "shaderId": str(shaderId) 186 | }) 187 | if verboseOutput: _Warn("\t" + shaderPath +": has shader id type " + str(shaderId) + 188 | ", currently not supported by this checker.") 189 | return True 190 | 191 | varname = shaderNode.GetInput("varname") 192 | if not varname: 193 | errorData.append({ 194 | "code": "ERR_NO_VARNAME", 195 | "shaderPath": shaderPath 196 | }) 197 | if verboseOutput: _Err("\t" + shaderPath + ": has no varname input.") 198 | return False 199 | varnameType = varname.GetTypeName() 200 | if not (varnameType == Sdf.ValueTypeNames.String or varnameType == Sdf.ValueTypeNames.Token): 201 | errorData.append({ 202 | "code": "ERR_INVALID_VARNAME_TYPE", 203 | "shaderPath": shaderPath, 204 | "varnameType": str(varnameType) 205 | }) 206 | if verboseOutput:_Err("\t" + shaderPath + ": has invalid varname type " + str(varnameType) + ".") 207 | return False 208 | 209 | connect = UsdShade.ConnectableAPI.GetConnectedSource(varname) 210 | if not validateConnection(varname, connect, verboseOutput, errorData): 211 | return False 212 | 213 | fallback = shaderNode.GetInput("fallback") 214 | if not validateType(fallback, Sdf.ValueTypeNames.Float2, shaderPath, verboseOutput, errorData): 215 | return False 216 | 217 | output = shaderNode.GetOutput("result") 218 | if not validateType(output, Sdf.ValueTypeNames.Float2, shaderPath, verboseOutput, errorData): 219 | return False 220 | return True 221 | 222 | def validateTransform2dNode(shaderNode, verboseOutput, errorData): 223 | shaderPath = shaderNode.GetPrim().GetPath().pathString 224 | 225 | input = shaderNode.GetInput("in") 226 | if not input: 227 | errorData.append({ 228 | "code": "ERR_NO_INPUTS_IN", 229 | "shaderPath": shaderPath 230 | }) 231 | if verboseOutput: _Err("\t" + shaderPath +": does not have inputs:in.") 232 | return False 233 | 234 | connect = UsdShade.ConnectableAPI.GetConnectedSource(input) 235 | 236 | if connect: 237 | if not validateConnection(input, connect, verboseOutput, errorData): 238 | return False 239 | else: 240 | connectable = UsdShade.Shader(connect[0]) 241 | shaderId = connectable.GetIdAttr().Get() 242 | if shaderId == "UsdPrimvarReader_float2": 243 | if not validatePrimvarReaderNode(connectable, verboseOutput, errorData): 244 | return False 245 | 246 | rotation = shaderNode.GetInput('rotation') 247 | if not validateType(rotation, Sdf.ValueTypeNames.Float, shaderPath, verboseOutput, errorData): 248 | return False 249 | 250 | scale = shaderNode.GetInput('scale') 251 | if not validateType(scale, Sdf.ValueTypeNames.Float2, shaderPath, verboseOutput, errorData): 252 | return False 253 | 254 | translation = shaderNode.GetInput('translation') 255 | if not validateType(translation, Sdf.ValueTypeNames.Float2, shaderPath, verboseOutput, errorData): 256 | return False 257 | return True 258 | 259 | 260 | def validateMaterialProperty(pbrShader, property, verboseOutput, errorData): 261 | pbrShaderPath = pbrShader.GetPrim().GetPath().pathString 262 | if not validatePropertyType(pbrShaderPath, property, verboseOutput, errorData): 263 | return False 264 | 265 | connection = UsdShade.ConnectableAPI.GetConnectedSource(property) 266 | if connection == None: 267 | # constant material property 268 | return True 269 | if not validateConnection(property, connection, verboseOutput, errorData): 270 | return False 271 | 272 | connectable = UsdShade.Shader(connection[0]) 273 | connectablePath = connectable.GetPrim().GetPath().pathString 274 | 275 | shaderId = connectable.GetIdAttr().Get() 276 | if shaderId == None: 277 | errorData.append({ 278 | "code": "WRN_MISSING_SHADER_ID", 279 | "connectablePath": connectablePath 280 | }) 281 | if verboseOutput: _Warn("\t" + connectablePath +": is missing shader id.") 282 | return False 283 | 284 | if shaderId == "UsdUVTexture": 285 | if not validateTextureNode(connectable, verboseOutput, errorData): 286 | return False 287 | elif shaderId.startswith("UsdPrimvarReader_"): 288 | if not validatePrimvarReaderNode(connectable, verboseOutput, errorData): 289 | return False 290 | else: 291 | errorData.append({ 292 | "code": "WRN_UNRECOGNIZED_SHADER_ID", 293 | "connectablePath": connectablePath, 294 | "shaderId": shaderId 295 | }) 296 | if verboseOutput:_Warn("\t" + connectablePath +": has unrecognized shaderId: " + shaderId + ".") 297 | return False 298 | 299 | return True 300 | 301 | def validateMaterial(materialPrim, verbose, errorData): 302 | verboseOutput = verbose 303 | material = UsdShade.Material(materialPrim) 304 | materialPath = material.GetPrim().GetPath().pathString 305 | 306 | surface = material.GetSurfaceOutput() 307 | connect = UsdShade.ConnectableAPI.GetConnectedSource(surface) 308 | if not validateConnection(surface, connect, verboseOutput, errorData): 309 | return False 310 | if connect is None or not connect[0].IsContainer(): 311 | # Empty material is valid 312 | return True 313 | 314 | connectable = connect[0] 315 | primPath = connectable.GetPrim().GetPath().pathString 316 | 317 | if not connectable.GetOutput("surface"): 318 | errorData.append({ 319 | "code": "ERR_MISSING_SURFACE_OUTPUT", 320 | "primPath": primPath 321 | }) 322 | if verboseOutput: _Err("\t" + primPath +": is missing surface output.") 323 | return False 324 | 325 | shader = UsdShade.Shader(connectable.GetPrim()) 326 | 327 | if shader.GetIdAttr().Get() != "UsdPreviewSurface": 328 | errorData.append({ 329 | "code": "ERR_NOT_USDPREVIEWSURFACE", 330 | "primPath": primPath 331 | }) 332 | if verboseOutput: _Err("\t" + primPath + ": is not UsdPreviewSurface shader.") 333 | return False 334 | 335 | for shaderInput in shader.GetInputs(): 336 | if not validateMaterialProperty(shader, shaderInput, verboseOutput, errorData): 337 | return False 338 | return True 339 | -------------------------------------------------------------------------------- /usdzconvert/validateMesh.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os, shutil, sys 3 | 4 | from pxr import * 5 | 6 | class TermColors: 7 | WARN = '\033[93m' 8 | FAIL = '\033[91m' 9 | END = '\033[0m' 10 | 11 | def _Err(msg): 12 | sys.stderr.write(TermColors.FAIL + msg + TermColors.END + '\n') 13 | 14 | def _Warn(msg): 15 | sys.stderr.write(TermColors.WARN + msg + TermColors.END + '\n') 16 | 17 | def validateTopology(faceVertexCounts, faceVertexIndices, pointsCount, meshPath, verboseOutput, errorData): 18 | if len(faceVertexIndices) < len(faceVertexCounts): 19 | errorData.append({ 20 | "code": "WRN_INDICES_VERTEX_COUNT_MISMATCH", 21 | "meshPath": meshPath 22 | }) 23 | if verboseOutput: _Warn("\t" + meshPath + ": faceVertexIndices's size is less then the size of faceVertexCounts.") 24 | return False 25 | return True 26 | 27 | def validateGeomsubset(subset, facesCount, subsetName, timeCode, verboseOutput, errorData): 28 | indicesAttr = subset.GetIndicesAttr() 29 | indices = [] 30 | if indicesAttr: 31 | indices = indicesAttr.Get(timeCode) 32 | 33 | if len(indices) == 0 or len(indices) > facesCount: 34 | errorData.append({ 35 | "code": "WRN_INVALID_FACEINDICES", 36 | "subsetName": subsetName 37 | }) 38 | if verboseOutput: _Warn("\tsubset " + subsetName + "'s faceIndices are invalid.") 39 | return False 40 | return True 41 | 42 | def validateMeshAttribute(meshPath, value, indices, attrName, typeName, interpolation, elementSize, facesCount, 43 | faceVertexIndicesCount, pointsCount, verboseOutput, errorData, unauthoredValueIndex = None): 44 | valueCount = len(value) 45 | if not typeName.isArray: 46 | valueCount = 1 47 | indicesCount = len(indices) 48 | 49 | if interpolation == UsdGeom.Tokens.constant: 50 | if not valueCount == elementSize: 51 | errorData.append({ 52 | "code": "WRN_CONSTANT_VALUE_SIZE_MISMATCH", 53 | "meshPath": meshPath, 54 | "attrName": attrName, 55 | "valueCount": str(valueCount), 56 | "elementSize": str(elementSize) 57 | }) 58 | if verboseOutput: _Warn("\t" + meshPath + ": " + attrName + " has constant interpolation and number of value " 59 | + str(valueCount) + " is not equal to element size " + str(elementSize) + ".") 60 | return False 61 | elif interpolation == UsdGeom.Tokens.vertex or interpolation == UsdGeom.Tokens.varying: 62 | if indicesCount > 0: 63 | if indicesCount != pointsCount: 64 | errorData.append({ 65 | "code": "WRN_VERTEX_INDICES_POINTS_MISMATCH", 66 | "meshPath": meshPath, 67 | "attrName": attrName, 68 | "interpolation": interpolation, 69 | "indicesCount": str(indicesCount), 70 | "pointsCount": str(pointsCount) 71 | }) 72 | if verboseOutput: _Warn("\t" + meshPath + ": " + attrName + " has " + interpolation + 73 | " interpolation and number of attribute indices " + str(indicesCount) + 74 | " is not equal to points count " + str(pointsCount) + ".") 75 | return False 76 | else: 77 | if valueCount != pointsCount * elementSize: 78 | errorData.append({ 79 | "code": "WRN_VERTEX_NO_INDICES", 80 | "meshPath": meshPath, 81 | "attrName": attrName, 82 | "interpolation": interpolation, 83 | "valueCount": str(valueCount), 84 | "pointsCount": str(pointsCount), 85 | "elementSize": str(elementSize) 86 | }) 87 | if verboseOutput: _Warn("\t" + meshPath + ": " + attrName + " has "+ interpolation + 88 | " interpolation and no indices. The number of value " + str(valueCount) + 89 | " is not equal to points count (" + str(pointsCount) + ") * elementSize (" + 90 | str(elementSize) + ").") 91 | return False 92 | elif interpolation == UsdGeom.Tokens.uniform: 93 | if indicesCount > 0: 94 | if indicesCount != facesCount: 95 | errorData.append({ 96 | "code": "WRN_UNIFORM_INDICES_FACES_MISMATCH", 97 | "meshPath": meshPath, 98 | "attrName": attrName, 99 | "indicesCount": str(indicesCount), 100 | "facesCount": str(facesCount) 101 | }) 102 | if verboseOutput: _Warn("\t" + meshPath + ": " + attrName + " has uniform interpolation and \ 103 | number of attribute indices " + str(indicesCount) + 104 | " is not equal to faces count " + str(facesCount) + ".") 105 | return False 106 | else: 107 | if valueCount != facesCount * elementSize: 108 | errorData.append({ 109 | "code": "WRN_UNIFORM_NO_INDICES", 110 | "meshPath": meshPath, 111 | "attrName": attrName, 112 | "valueCount": str(valueCount), 113 | "facesCount": str(faceCount), 114 | "elementSize": str(elementSize) 115 | }) 116 | if verboseOutput: _Warn("\t" + meshPath + ": " + attrName + " has uniform interpolation and no indices. \ 117 | The number of value " + str(valueCount) + " is not equal to faces count (" + 118 | str(facesCount) + ") * elementSize (" + str(elementSize) + ").") 119 | return False 120 | elif interpolation == UsdGeom.Tokens.faceVarying: 121 | if indicesCount > 0: 122 | if indicesCount != faceVertexIndicesCount: 123 | errorData.append({ 124 | "code": "WRN_FACE_VARYING_INDICES_FACES_MISMATCH", 125 | "meshPath": meshPath, 126 | "attrName": attrName, 127 | "indicesCount": str(indicesCount), 128 | "faceVertexIndicesCount": str(faceVertexIndicesCount) 129 | }) 130 | if verboseOutput: _Warn("\t" + meshPath + ": " + attrName + " has face varying interpolation and number \ 131 | of attribute indices " + str(indicesCount) + " is not equal to face vertices \ 132 | count " + str(faceVertexIndicesCount) + ".") 133 | return False 134 | else: 135 | if valueCount != faceVertexIndicesCount * elementSize: 136 | errorData.append({ 137 | "code": "WRN_FACE_VARYING_NO_INDICES", 138 | "meshPath": meshPath, 139 | "attrName": attrName, 140 | "valueCount": str(valueCount), 141 | "faceVertexIndicesCount": str(faceVertexIndicesCount), 142 | "elementSize": str(elementSize) 143 | }) 144 | if verboseOutput: _Warn("\t" + meshPath + ": " + attrName + " has face varying interpolation and no \ 145 | indices. The number of value " + str(valueCount) + " is not equal to face \ 146 | vertices count (" + str(faceVertexIndicesCount) + ") * elementSize (" + 147 | str(elementSize) + ").") 148 | return False 149 | else: 150 | errorData.append({ 151 | "code": "WRN_UNKNOWN_INTERPOLATION", 152 | "meshPath": meshPath, 153 | "attrName": attrName, 154 | "interpolation": interpolation 155 | }) 156 | if verboseOutput: _Warn("\t"+meshPath + ": " + attrName + " has unknown interpolation " + interpolation + ".") 157 | return False 158 | return True 159 | 160 | def validatePrimvar(meshPath, primvar, facesCount, faceVertexIndicesCount, pointsCount, timeCode, verboseOutput, errorData): 161 | if primvar.HasAuthoredValue(): 162 | indices = [] 163 | if primvar.IsIndexed(): 164 | indices = primvar.GetIndices(timeCode) 165 | attrName, typeName, interpolation, elementSize = primvar.GetDeclarationInfo() 166 | unauthoredValueIndex = primvar.GetUnauthoredValuesIndex() 167 | if not validateMeshAttribute(meshPath, primvar.Get(timeCode), indices, attrName, typeName, interpolation, elementSize, 168 | facesCount, faceVertexIndicesCount, pointsCount, verboseOutput, errorData, unauthoredValueIndex): 169 | return False 170 | return True 171 | 172 | def validateMesh(prim, verbose, errorData): 173 | verboseOutput = verbose 174 | meshPath = prim.GetPath().pathString 175 | mesh = UsdGeom.Mesh(prim) 176 | startTimeCode = prim.GetStage().GetStartTimeCode() 177 | 178 | faceVertexCounts = mesh.GetFaceVertexCountsAttr().Get(startTimeCode) 179 | if faceVertexCounts is None or len(faceVertexCounts) == 0: 180 | errorData.append({ 181 | "code": "WRN_NO_FACE_VERTEX_COUNTS", 182 | "meshPath": meshPath 183 | }) 184 | if verboseOutput: _Warn("\t" + meshPath + " has no face vertex counts data.") 185 | return True 186 | 187 | faceVertexIndices = mesh.GetFaceVertexIndicesAttr().Get(startTimeCode) 188 | if faceVertexIndices is None or len(faceVertexIndices) == 0: 189 | errorData.append({ 190 | "code": "WRN_NO_FACE_VERTEX_INDICES", 191 | "meshPath": meshPath 192 | }) 193 | if verboseOutput: _Warn("\t" + meshPath + " has no face vertex indices data.") 194 | return True 195 | 196 | points = mesh.GetPointsAttr().Get(startTimeCode) 197 | if points is None or len(points) == 0: 198 | errorData.append({ 199 | "code": "WRN_NO_POSITION_DATA", 200 | "meshPath": meshPath 201 | }) 202 | if verboseOutput: _Warn("\t" + meshPath + " has no position data.") 203 | return True 204 | 205 | pointsCount = len(points) 206 | if not validateTopology(faceVertexCounts, faceVertexIndices, pointsCount, meshPath, verboseOutput, errorData): 207 | errorData.append({ 208 | "code": "ERR_INVALID_TOPOLOGY", 209 | "meshPath": meshPath 210 | }) 211 | if verboseOutput: _Err("\t " + meshPath + " has invalid topology") 212 | return False 213 | 214 | facesCount = len(faceVertexCounts) 215 | faceVertexIndicesCount = len(faceVertexIndices) 216 | 217 | subsets = UsdGeom.Subset.GetGeomSubsets(mesh) 218 | for subset in subsets: 219 | if not validateGeomsubset(subset, facesCount, subset.GetPrim().GetName(), startTimeCode, verboseOutput, errorData): 220 | return False 221 | # handle normal attribute that's not authored as primvar 222 | normalAttr = mesh.GetNormalsAttr() 223 | if normalAttr.HasAuthoredValue(): 224 | if not validateMeshAttribute(meshPath, normalAttr.Get(startTimeCode), [], normalAttr.GetName(), 225 | Sdf.ValueTypeNames.Normal3fArray, mesh.GetNormalsInterpolation(), 1, facesCount, 226 | faceVertexIndicesCount, pointsCount, verboseOutput, errorData, None): 227 | return False 228 | 229 | prim = UsdGeom.PrimvarsAPI(mesh) 230 | # Find inherited primvars includes the primvars on prim 231 | inheritedPrimvars = prim.FindPrimvarsWithInheritance() 232 | for primvar in inheritedPrimvars: 233 | if not validatePrimvar(meshPath, primvar, facesCount, faceVertexIndicesCount, pointsCount, startTimeCode, verboseOutput, errorData): 234 | return False 235 | 236 | return True 237 | --------------------------------------------------------------------------------