├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Source ├── __init__.py ├── _gltf2usd │ ├── __init__.py │ ├── gltf2 │ │ ├── Animation.py │ │ ├── Asset.py │ │ ├── GLTFImage.py │ │ ├── Material.py │ │ ├── Mesh.py │ │ ├── Node.py │ │ ├── Scene.py │ │ ├── Skin.py │ │ └── __init__.py │ ├── gltf2loader.py │ ├── gltf2usdUtils.py │ ├── gltfUsdMaterialHelper.py │ ├── usd_material.py │ └── version.py ├── gltf2usd.py └── tests │ ├── __init__.py │ ├── assets │ └── Start_Walking │ │ ├── Boss_diffuse.png │ │ ├── Start_Walking.gltf │ │ └── buffer.bin │ └── testInitializeGLTFLoader.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Mac OS 107 | .DS_Store 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #Changelog 2 | 3 | ## 0.1.0 (2018-10-15) 4 | **Fixed Bugs:** 5 | - Fixed an issue where generating `usdz` wrote out absolute paths instead of relative paths 6 | 7 | **Changes:** 8 | - Reorganized repo by moving files to the `_gltf2usd` module 9 | - Renames unicode filenames to ascii to avoid name conflicts (https://github.com/kcoley/gltf2usd/issues/71) 10 | - Added versioning to the `gltf2usd` (https://github.com/kcoley/gltf2usd/issues/81) 11 | (https://github.com/kcoley/gltf2usd/issues/68) 12 | - Added changelog 13 | 14 | 15 | ## 0.1.1 (2018-10-21) 16 | **Fixed Bugs:** 17 | - Fixed issue with node transforms and node animations, where the glTF node transform would get applied on top of its animation, causing incorrect positions. 18 | 19 | **Changes:** 20 | - Convert usd file path to absolute path on load 21 | - Reformatted changelog 22 | - Switched some methods to properties 23 | - Increment version 24 | 25 | ## 0.1.2 (2018-10-21) 26 | **Fixed Bugs:** 27 | - Use only first animation group 28 | 29 | ## 0.1.3 (2018-10-26) 30 | **Fixed Bugs:** 31 | - Fixed strength 32 | 33 | ## 0.1.4 (2018-10-28) 34 | **Fixed Bugs:** 35 | - Opacity for base color factor is now used even wihout base color texture 36 | 37 | ## 0.1.5 (2018-11-2) 38 | **Fixed Bugs:** 39 | - Fixed bug in using specular workflow (https://github.com/kcoley/gltf2usd/pull/92) 40 | 41 | **Changes:** 42 | - Added alpha mode to Material (https://github.com/kcoley/gltf2usd/issues/88) 43 | - If opacity is set in glTF, overwrite the alpha to 1 on USD export. 44 | - Alpha mask is not supported in glTF so a warning is displayed and defaults to alpha blend 45 | 46 | ## 0.1.6 (2018-11-3) 47 | **Fixed Bugs:** 48 | - Fixed bug in joint names to allow support for animations for iOS (https://github.com/kcoley/gltf2usd/issues/79) 49 | 50 | ## 0.1.7 (2018-11-6) 51 | **Changes:** 52 | - When using alpha opaque, the base color texture is cloned 53 | 54 | ## 0.1.8 (2018-11-8) 55 | **Changes:** 56 | - If a normal texture has only one channel, the channel is applied to RGB as a new texture 57 | 58 | ## 0.1.9 (2018-11-10) 59 | **Fixed Bugs:** 60 | - Fixed a bug with transform animations not getting exported 61 | 62 | ## 0.1.10 (2018-11-12) 63 | **Changes:** 64 | - Added optimize-textures flag to help reduce texture size when generating usdz files 65 | 66 | ## 0.1.11 (2018-12-4) 67 | **Changes:** 68 | - Set vertex colors for Color4 (vertex color alpha currently not supported) 69 | - Resolves (https://github.com/kcoley/gltf2usd/issues/113) 70 | 71 | ## 0.1.12 (2018-12-17) 72 | **Changes:** 73 | - Generate a new texture if the KHR_texture_transform extension is used 74 | - Resolves (https://github.com/kcoley/gltf2usd/issues/104) 75 | 76 | ## 0.1.13 (2018-12-23) 77 | **Fixed Bugs:** 78 | - Fixed a bug where normalized ubytes and ushorts were not being normalized on load 79 | **Changes:** 80 | - Added support for loading embedded images in bufferviews 81 | - Resolves (https://github.com/kcoley/gltf2usd/issues/123) 82 | 83 | ## 0.1.14 (2018-12-23) 84 | **Fixed Bugs:** 85 | - Fixed a bug where multiple embedded textures within the same buffer were not extracted properly 86 | **Changes:** 87 | - Resolves (https://github.com/kcoley/gltf2usd/issues/125) 88 | 89 | ## 0.1.15 (2019-01-11) 90 | **Fixed Bugs:** 91 | - Fix for throwing when USD minor version is 19 and path version is 1 [spiderworm](https://github.com/kcoley/gltf2usd/pull/130) 92 | - Fixed bug where texture transform generation would fail on index out of range (https://github.com/kcoley/gltf2usd/issues/131) 93 | 94 | ## 0.1.16 (2019-01-17) 95 | **Changes:** 96 | - Add option to toggle texture transform texture generation (enable `--generate_texture_transform_texture`, disable `--no-generate_texture_transform_texture`) (https://github.com/kcoley/gltf2usd/issues/133) 97 | 98 | ## 0.1.17 (2019-01-24) 99 | **Changes:** 100 | - Preserve material names of gltf if present (https://github.com/kcoley/gltf2usd/issues/135) 101 | 102 | ## 0.1.18 (2019-01-28) 103 | **Changes:** 104 | - Cache accessor data to help with glTF import optimization (https://github.com/kcoley/gltf2usd/issues/137) 105 | - Adding logging for exporting mesh primitive data, behind the -v flag 106 | 107 | ## 0.1.19 (2019-01-30) 108 | **Changes:** 109 | - Prevent crash and display warning when glTF nodes have joints, but no skin (https://github.com/kcoley/gltf2usd/issues/140) 110 | 111 | ## 0.1.20 (2019-02-04) 112 | **Fixed Bugs:** 113 | - Fixed a bug where textures were being indexed through images (https://github.com/kcoley/gltf2usd/issues/142) 114 | 115 | ## 0.1.21 (2019-02-11) 116 | **Changes:** 117 | - Update rename regex to include square brackets (https://github.com/kcoley/gltf2usd/issues/144) 118 | 119 | ## 0.1.22 (2019-02-15) 120 | **Fixed Bugs:** 121 | - Resolve some issues with materials containing special characters by appending index to name (https://github.com/kcoley/gltf2usd/issues/147) 122 | 123 | ## 0.1.23 (2019-02-27) 124 | **Fixed Bugs:** 125 | - Fixed a bug where `TEXCOORD_1` attribute was not being read (https://github.com/kcoley/gltf2usd/issues/149) 126 | 127 | ## 0.2.0 (2019-02-27) 128 | **Fixed Bugs:** 129 | - Fix blend shapes 130 | **Changes:** 131 | - The Usd file generation now happens in a temp directory 132 | - Add `--scale-texture` optional parameter to multiply metallic/roughness factors by textures 133 | - Added creator metadata 134 | - Moved material scope to below root xform 135 | - connect opacity of diffuse texture if alpha blend or mask is used 136 | - Export extras data on nodes and glTF asset -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kacey Coley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gltf2usd 2 | 3 | This tool is a command-line Python script which converts glTF 2.0 models to USD, with the goal being simple pipeline conversion from glTF to usd, usda, usdc, or usdz. 4 | 5 | The tool is a **proof-of-concept**, to determine format conversion details, which could be useful for an actual C++ USD plugin. It has been developed and tested on both Windows 10 and Mac OS 10.14 Mojave Beta, using USD v18.09 and v18.11, and is built against the USD Python API. 6 | 7 | This tool currently only works on glTF 2.0 files, based on the core glTF 2.0 specification (no extensions except `PbrSpecularGlossiness` and `KHR_texture_transform`). 8 | 9 | ## Supported Features 10 | - glTF nodes are mapped to USD `Xform` 11 | - glTF `PbrMetallicRoughnessMaterial` is mapped to `USDPreviewSurface` 12 | - glTF [KHR_materials_pbrSpecularGlossiness](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness) extension 13 | - glTF [KHR_texture_transform](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform) extension (new textures are generated at the expense of a longer export time) 14 | - glTF Skeletal animation is mapped to `UsdSkel` 15 | - glTF node animations are supported 16 | - Currently supports `.gltf` conversion to `.usd`, `.usda`, `.usdc`, and `.usdz` 17 | 18 | 19 | ## Currently not implemented: 20 | - `.glb` files 21 | - glTF extensions (except `KHR_materials_pbrSpecularGlossiness` and `KHR_texture_transform`) 22 | - Primitive modes (other than triangles) 23 | 24 | ## Note: 25 | - The root node of the generated USD file is, by default, scaled by 100 to convert from glTF's meters to USD's centimeters. This scale is purely to be able to see the glTF models when using ARKit, or otherwise, they are too small. 26 | - There are several edge cases that have not been fully tested yet 27 | 28 | ## Dependencies: 29 | 30 | - You will need to initially have [USD v18.09 or v18.11](https://github.com/PixarAnimationStudios/USD) installed on your system 31 | and have the Python modules built 32 | - Linux users will need to build the tools themselves, or use [AnimalLogic's USD Docker Container](https://github.com/AnimalLogic/docker-usd) (recommended for non-CentOS users) 33 | - macOS users can use Apple's [Prebuilt USD Toolkit](https://developer.apple.com/go/?id=python-usd-library). Make sure you add the USD dir to your `PYTHONPATH` (an easy way to do this is to run the `path_to_usdpython/USD.command` shell command included with the prebuilt USD Toolkit). 34 | 35 | ### Python dependencies 36 | You can install the following python dependencies using `pip install -r requirements.txt`: 37 | 38 | - Pillow (Python module for image manipulation) 39 | - enum34 (Python module for enums in Python 2.7) 40 | 41 | 42 | ## Help Menu: 43 | ```Shell 44 | python gltf2usd.py -h 45 | usage: gltf2usd.py [-h] --gltf GLTF_FILE [--fps FPS] --output USD_FILE 46 | [--scale] [--verbose] [--arkit] 47 | 48 | Convert glTF to USD 49 | 50 | optional arguments: 51 | -h, --help show this help message and exit 52 | --gltf GLTF_FILE, -g GLTF_FILE 53 | glTF file (in .gltf format) 54 | --fps FPS The frames per second for the animations (defaults to 24 fps) 55 | --output USD_FILE, -o USD_FILE 56 | destination to store generated .usda file 57 | --scale SCALE, -s Scale the resulting USDA model 58 | --verbose, -v Enable verbose mode 59 | --arkit Check USD with ARKit compatibility before making USDZ 60 | file 61 | --use-euler-rotation sets euler rotations for node animations instead of 62 | quaternion rotations 63 | --optimize-textures Specifies if image file size should be optimized and 64 | reduced at the expense of longer export time 65 | --generate_texture_transform_texture 66 | Enables texture transform texture generation 67 | --no-generate_texture_transform_texture 68 | Disables texture transform texture generation 69 | ``` 70 | 71 | ## Sample usage: 72 | Create a .usda file 73 | ```Shell 74 | python gltf2usd.py -g ../path_to_read_glTF_file/file.gltf -o path_to_write_usd_file/file.usda 75 | ``` 76 | 77 | Create a .usdz file 78 | ```Shell 79 | python gltf2usd.py -g ../path_to_read_glTF_file/file.gltf -o path_to_write_usd_file/file.usdz 80 | ``` 81 | -------------------------------------------------------------------------------- /Source/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcoley/gltf2usd/c0752099313834a466acf04b89af33dfd5dc9a0b/Source/__init__.py -------------------------------------------------------------------------------- /Source/_gltf2usd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcoley/gltf2usd/c0752099313834a466acf04b89af33dfd5dc9a0b/Source/_gltf2usd/__init__.py -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/Animation.py: -------------------------------------------------------------------------------- 1 | from bisect import bisect_left 2 | 3 | from _gltf2usd.gltf2usdUtils import GLTF2USDUtils 4 | 5 | from pxr import Gf 6 | 7 | class AnimationSampler(object): 8 | def __init__(self, sampler_entry, animation): 9 | self._animation = animation 10 | self._input_accessor_index = sampler_entry['input'] 11 | self._input_accessor = self._animation._gltf_loader.json_data['accessors'][self._input_accessor_index] 12 | self._interpolation = sampler_entry['interpolation'] if ('interpolation' in sampler_entry) else 'LINEAR' 13 | self._output_accessor_index = sampler_entry['output'] 14 | self._output_accessor = self._animation._gltf_loader.json_data['accessors'][self._output_accessor_index] 15 | self._input_count = self._input_accessor['count'] 16 | self._input_min = self._input_accessor['min'] 17 | self._input_max = self._input_accessor['max'] 18 | self._output_count = self._output_accessor['count'] 19 | self._output_min = self._output_accessor['min'] if ('min' in self._output_accessor) else None 20 | self._output_max = self._output_accessor['max'] if ('max' in self._output_accessor) else None 21 | self._input_data = None 22 | self._output_data = None 23 | 24 | self._input_data = None 25 | self._output_data = None 26 | 27 | def get_input_count(self): 28 | return self._input_count 29 | 30 | def get_input_min(self): 31 | return self._input_min 32 | 33 | def get_input_max(self): 34 | return self._input_max 35 | 36 | def get_output_count(self): 37 | return self._output_count 38 | 39 | def get_output_min(self): 40 | return self._output_min 41 | 42 | def get_output_max(self): 43 | return self._output_max 44 | 45 | def get_input_data(self): 46 | if not self._input_data: 47 | accessor = self._animation._gltf_loader.json_data['accessors'][self._input_accessor_index] 48 | self._input_data = self._animation._gltf_loader.get_data(accessor, self._input_accessor_index) 49 | 50 | return self._input_data 51 | 52 | def get_output_data(self): 53 | if not self._output_data: 54 | accessor = self._animation._gltf_loader.json_data['accessors'][self._output_accessor_index] 55 | self._output_data = self._animation._gltf_loader.get_data(accessor, self._output_accessor_index) 56 | 57 | return self._output_data 58 | 59 | 60 | def get_interpolated_output_data(self, input_sample): 61 | input_data = self.get_input_data() 62 | output_data = self.get_output_data() 63 | 64 | closest_pos = bisect_left(input_data, input_sample) 65 | if closest_pos == 0: 66 | value = output_data[0] 67 | if len(value) == 4: 68 | return Gf.Quatf(value[3], value[0], value[1], value[2]) 69 | else: 70 | return value 71 | elif closest_pos == len(input_data): 72 | value = output_data[-1] 73 | if len(value) == 4: 74 | return Gf.Quatf(value[3], value[0], value[1], value[2]) 75 | else: 76 | return value 77 | else: 78 | left_output_sample = output_data[closest_pos - 1] 79 | right_output_sample = output_data[closest_pos] 80 | 81 | factor = float(input_sample - input_data[closest_pos-1])/(input_data[closest_pos] - input_data[closest_pos - 1]) 82 | if self._interpolation == 'LINEAR': 83 | return self._linear_interpolate_values(left_output_sample, right_output_sample, factor) 84 | elif self._interpolation == 'STEP': 85 | return self._step_interpolate_values(left_output_sample, right_output_sample, factor) 86 | else: 87 | print('cubic spline interpolation not yet implemented! Defaulting to linear for now...') 88 | return self._linear_interpolate_values(left_output_sample, right_output_sample, factor) 89 | 90 | def _linear_interpolate_values(self, value0, value1, factor): 91 | if len(value0) == 3: 92 | one_minus_factor = 1 - factor 93 | #translation or scale interpolation 94 | return [ 95 | (factor * value1[0] + (one_minus_factor * value0[0])), 96 | (factor * value1[1] + (one_minus_factor * value0[1])), 97 | (factor * value1[2] + (one_minus_factor * value0[2])) 98 | ] 99 | 100 | elif len(value0) == 4: 101 | #quaternion interpolation 102 | result = GLTF2USDUtils.slerp(value0, value1, factor) 103 | return result 104 | else: 105 | raise Exception('unsupported value type') 106 | 107 | def _step_interpolate_values(self, value0, value1, factor): 108 | if len(value0) == 3: 109 | #translation or scale interpolation 110 | return value0 111 | 112 | elif len(value0) == 4: 113 | #quaternion interpolation 114 | return Gf.Quatf(value0[3], value0[0], value0[1], value0[2]) 115 | else: 116 | raise Exception('unsupported value type') 117 | 118 | 119 | 120 | class AnimationChannelTarget(object): 121 | def __init__(self, animation_channel_target_entry): 122 | self._node_index = animation_channel_target_entry['node'] 123 | self._path = animation_channel_target_entry['path'] 124 | @property 125 | def path(self): 126 | return self._path 127 | 128 | class AnimationChannel(object): 129 | def __init__(self, channel_entry, animation): 130 | self._sampler_index = channel_entry['sampler'] 131 | self._target = AnimationChannelTarget(channel_entry['target']) 132 | self._animation = animation 133 | 134 | @property 135 | def target(self): 136 | return self._target 137 | 138 | def get_sampler_index(self): 139 | return self._sampler_index 140 | 141 | @property 142 | def sampler(self): 143 | return self._animation._samplers[self._sampler_index] 144 | 145 | 146 | class Animation(object): 147 | def __init__(self, animation_entry, index, gltf_loader): 148 | self._gltf_loader = gltf_loader 149 | self._name = animation_entry['name'] if ('name' in animation_entry) else 'animation_{}'.format(index) 150 | self._samplers = [AnimationSampler(sampler, self) for sampler in animation_entry['samplers']] 151 | self._channels = [AnimationChannel(channel, self) for channel in animation_entry['channels']] 152 | 153 | 154 | def get_animation_channel_for_node_and_path(self, node, path): 155 | for channel in self._channels: 156 | if (channel._target._node_index == node.get_index() and channel._target._path == path): 157 | return channel 158 | 159 | return None 160 | 161 | def get_animation_channels_for_node(self, node): 162 | return [channel for channel in self._channels if (channel._target._node_index == node.get_index())] 163 | 164 | def get_channels(self): 165 | return self._channels 166 | 167 | def get_samplers(self): 168 | return self._samplers 169 | 170 | def get_sampler_at_index(self, index): 171 | return self._samplers[index] 172 | 173 | -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/Asset.py: -------------------------------------------------------------------------------- 1 | class Asset(object): 2 | def __init__(self, asset_entry): 3 | self._generator = asset_entry['generator'] if 'generator' in asset_entry else None 4 | self._version = asset_entry['version'] if 'version' in asset_entry else None 5 | self._extras = asset_entry['extras'] if 'extras' in asset_entry else {} 6 | self._copyright = asset_entry['copyright'] if 'copyright' in asset_entry else None 7 | self._minversion = asset_entry['minversion'] if 'minversion' in asset_entry else None 8 | 9 | @property 10 | def generator(self): 11 | return self._generator 12 | 13 | @property 14 | def version(self): 15 | return self._version 16 | 17 | @property 18 | def minversion(self): 19 | return self._minversion 20 | 21 | @property 22 | def copyright(self): 23 | return self._copyright 24 | 25 | @property 26 | def extras(self): 27 | return self._extras 28 | 29 | -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/GLTFImage.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from io import BytesIO 3 | import math 4 | import ntpath 5 | import os 6 | 7 | from enum import Enum 8 | from PIL import Image 9 | import numpy as np 10 | 11 | class ImageColorChannels(Enum): 12 | RGB = 'RGB' 13 | RGBA = 'RGBA' 14 | R = 'R' 15 | G = 'G' 16 | B = 'B' 17 | A = 'A' 18 | 19 | class GLTFImage(object): 20 | def __init__(self, image_entry, image_index, gltf_loader, optimize_textures=False, generate_texture_transform_texture=True): 21 | self._generate_texture_transform_texture = generate_texture_transform_texture 22 | self._optimize_textures = optimize_textures 23 | if 'bufferView' in image_entry: 24 | #get image data from bufferview 25 | bufferview = gltf_loader.json_data['bufferViews'][image_entry['bufferView']] 26 | if 'byteOffset' in bufferview: 27 | buffer = gltf_loader.json_data['buffers'][bufferview['buffer']] 28 | 29 | img_base64 = buffer['uri'].split(',')[1] 30 | buff = BytesIO() 31 | buff.write(base64.b64decode(img_base64)) 32 | buff.seek(bufferview['byteOffset']) 33 | img = Image.open(BytesIO(buff.read(bufferview['byteLength']))) 34 | # NOTE: image might not have a name 35 | self._name = image_entry['name'] if 'name' in image_entry else 'image_{}.{}'.format(image_index, img.format.lower()) 36 | self._image_path = os.path.join(gltf_loader.root_dir, self._name) 37 | img.save(self._image_path, optimize=self._optimize_textures) 38 | else: 39 | if image_entry['uri'].startswith('data:image'): 40 | uri_data = image_entry['uri'].split(',')[1] 41 | img = Image.open(BytesIO(base64.b64decode(uri_data))) 42 | 43 | # NOTE: image might not have a name 44 | self._name = image_entry['name'] if 'name' in image_entry else 'image_{}.{}'.format(image_index, img.format.lower()) 45 | self._image_path = os.path.join(gltf_loader.root_dir, self._name) 46 | img.save(self._image_path, optimize=self._optimize_textures) 47 | else: 48 | self._uri = image_entry['uri'] 49 | self._name = ntpath.basename(self._uri) 50 | self._image_path = os.path.join(gltf_loader.root_dir, self._uri) 51 | 52 | #decode unicode name to ascii 53 | if isinstance(self._name, unicode): 54 | self._name = self._name.encode('utf-8') 55 | self._name = self._name.decode('ascii', 'ignore') 56 | 57 | def get_image_path(self): 58 | return self._image_path 59 | 60 | 61 | def write_to_directory(self, output_dir, channels, texture_prefix, offset = [0,0], scale = [1,1], rotation = 0, scale_factor=None): 62 | file_name = '{0}_{1}'.format(texture_prefix, ntpath.basename(self._name)) if texture_prefix else ntpath.basename(self._name) 63 | destination = os.path.join(output_dir, file_name) 64 | original_img = Image.open(self._image_path) 65 | img = original_img 66 | 67 | # this is helpful debug information 68 | debugTxt = "===> IMG INFO: {0} -> {1}".format(self._name, img.mode) 69 | print (debugTxt) 70 | 71 | # img.mode P means palettised which implies that only 1byte of colormap is used to represent 256 colors 72 | # We ran into several assets with diffuse map that are designated grayscale that only requires two texture channels 73 | # and when you split each channels, the img_channels array only has 2 channels but when you are trying to merge 74 | # all channels for temporary output, it tries to merge 3 channels. 75 | # In order to avoid this error, we need to cover for img mode L and LA and convert them to RGBA 76 | 77 | # 1/14/2021 - Looks like we found an asset with image mode "I"... adding this here... it is form of 78 | # grayscale... but something new that was not enountered before 79 | if img.mode == 'P' or img.mode == 'LA' or img.mode == 'L' or img.mode == "I": 80 | img = img.convert('RGBA') 81 | 82 | # now image channels should have 3 or channels 83 | img_channels = img.split() 84 | 85 | if len(img_channels) == 1: #distribute grayscale image across rgb 86 | img = Image.merge('RGB', (img_channels[0], img_channels[0], img_channels[0])) 87 | img_channels = img.split() 88 | 89 | if channels == ImageColorChannels.RGB: 90 | if img.mode == "RGBA": #Make a copy and add opaque 91 | file_name = '{0}_{1}'.format('RGB', file_name) 92 | destination = os.path.join(output_dir, file_name) 93 | 94 | img = Image.merge('RGB', (img_channels[0], img_channels[1], img_channels[2])) 95 | elif channels == ImageColorChannels.RGBA: 96 | img = original_img.convert('RGBA') 97 | elif channels == ImageColorChannels.R or channels == ImageColorChannels.G or channels == ImageColorChannels.B or channels == ImageColorChannels.A: 98 | if img.mode != 'L': 99 | img = img.getchannel(channels.value) 100 | else: 101 | raise Exception('Unsupported image channel format {}'.format(channels)) 102 | 103 | if destination.endswith('jpg') or destination.endswith('.jpeg'): 104 | img = img.convert('RGB') 105 | 106 | if scale_factor: 107 | width, height = img.size 108 | for x in range(width): 109 | for y in range(height): 110 | value = img.getpixel((x, y)) 111 | if isinstance(value, int): 112 | value = value * scale_factor[0] 113 | img.putpixel((x, y), (int(value))) 114 | else: 115 | value = list(value) 116 | value[0] = int(value[0] * scale_factor[0]) 117 | value[1] = int(value[1] * scale_factor[1]) 118 | value[2] = int(value[2] * scale_factor[2]) 119 | value = tuple(value) 120 | img.putpixel((x, y), (value)) 121 | 122 | #apply texture transform if necessary 123 | if offset != [0,0] or scale != [1,1] or rotation != 0: 124 | if not self._generate_texture_transform_texture: 125 | print('Texture transform texture modification has been disabled, so the resulting USD may look incorrect') 126 | else: 127 | texture_transform_prefix_name= 'o{0}{1}s{2}{3}r{4}_'.format(offset[0], offset[1], scale[0], scale[1], rotation).replace('.', '_') 128 | file_name = texture_transform_prefix_name + file_name 129 | destination = os.path.join(output_dir, file_name) 130 | print('Generating texture transformed image "{}" ...'.format(file_name)) 131 | img = self._transform_image(img, translate=offset, scale=scale, rotation=rotation) 132 | 133 | img.save(destination, optimize=self._optimize_textures) 134 | 135 | return file_name 136 | 137 | def _texture_transform_matrix(self, offset, scale, rotation): 138 | """Creates a texture transform matrix, based on specification for KHR_texture_transform (https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform) 139 | 140 | Arguments: 141 | offset {list} -- The offset of the UV coordinate origin as a factor of the texture dimensions 142 | scale {list} -- The scale factor applied to the components of the UV coordinates 143 | rotation {float} -- Rotate the UVs by this many radians counter-clockwise around the origin 144 | 145 | Returns: 146 | [numpy.matrix] -- texture transform matrix 147 | """ 148 | 149 | pivot_center = [0.0, -1.0] 150 | rotation *= -1 151 | pre_translation_matrix = np.matrix([[1, 0, -pivot_center[0]], [0, 1, -pivot_center[1]], [0, 0, 1]]) 152 | post_translation_matrix = np.matrix([[1, 0, pivot_center[0]], [0, 1, pivot_center[1]], [0, 0, 1]]) 153 | translation_matrix = np.matrix( 154 | [ 155 | [1,0,offset[0]], 156 | [0,1,offset[1]], 157 | [0, 0, 1] 158 | ] 159 | ) 160 | rotation_matrix = np.matrix( 161 | [ 162 | [math.cos(rotation), math.sin(rotation), 0], 163 | [-math.sin(rotation), math.cos(rotation), 0], 164 | [0, 0, 1] 165 | ] 166 | ) 167 | scale_matrix = np.matrix( 168 | [ 169 | [scale[0], 0, 0], 170 | [0, scale[1], 0], 171 | [0, 0, 1] 172 | ] 173 | ) 174 | transform_matrix = np.matmul(np.matmul(pre_translation_matrix, np.matmul(np.matmul(translation_matrix, rotation_matrix), scale_matrix)), post_translation_matrix) 175 | 176 | return transform_matrix 177 | 178 | def _transform_image(self, img, scale, rotation, translate): 179 | """Generates a new texture transformed image 180 | 181 | Arguments: 182 | img {Image} -- source image 183 | scale {list} -- scale of the new image 184 | rotation {float} -- rotation of the new image 185 | translate {list} -- translation of the new image 186 | 187 | Returns: 188 | [Image] -- transformed image 189 | """ 190 | 191 | def _normalized_texcoord(x): 192 | return x - int(x) if x >= 0 else 1 + (x - int(x)) 193 | 194 | texture_transform_matrix = self._texture_transform_matrix(translate, scale, rotation) 195 | width = img.width 196 | height = img.height 197 | 198 | res = np.matmul(texture_transform_matrix, np.array([0.0,0.0,1])) 199 | 200 | source_image_pixels = img.getdata() 201 | new_img = Image.new(img.mode, (img.width, img.height)) 202 | 203 | pixels = new_img.load() 204 | 205 | for col in range(new_img.size[0]): 206 | for row in range(new_img.size[1]): 207 | res = np.matmul(texture_transform_matrix, np.array([col/float(img.width),row/float(img.height),1])) 208 | 209 | c = min(int(round(_normalized_texcoord(res[0,0]) * height)), img.height - 1) 210 | r = min(int(round(_normalized_texcoord(res[0,1]) * width)), img.width - 1) 211 | pixel = source_image_pixels[r * width + c] 212 | pixels[col, row] = pixel 213 | 214 | return new_img 215 | 216 | 217 | -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/Material.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class TextureWrap(Enum): 4 | CLAMP_TO_EDGE = 33071 5 | MIRRORED_REPEAT = 33648 6 | REPEAT = 10497 7 | 8 | class AlphaMode(Enum): 9 | BLEND = 'BLEND' 10 | MASK = 'MASK' 11 | OPAQUE = 'OPAQUE' 12 | 13 | class Texture(object): 14 | def __init__(self, texture_entry, gltf_loader): 15 | index = 0 16 | self._name = texture_entry['name'] if ('name' in texture_entry) else 'texture_{}'.format(index) 17 | self._image = gltf_loader.get_images()[gltf_loader.json_data['textures'][texture_entry['index']]['source']] 18 | self._index = texture_entry['index'] if ('index' in texture_entry) else 0 19 | self._texcoord_index = texture_entry['texCoord'] if ('texCoord' in texture_entry) else 0 20 | sampler = gltf_loader.json_data['samplers'][texture_entry['sampler']] if ('sampler' in texture_entry) else (gltf_loader.json_data['samplers'][0] if ('samplers' in gltf_loader.json_data) else {}) 21 | self._wrap_s = TextureWrap(sampler['wrapS']) if ('wrapS' in sampler) else TextureWrap.REPEAT 22 | self._wrap_t = TextureWrap(sampler['wrapT']) if ('wrapT' in sampler) else TextureWrap.REPEAT 23 | self._extensions = {} 24 | self._tt_offset = [0.0,0.0] 25 | self._tt_scale = [1.0,1.0] 26 | self._tt_rotation = 0.0 27 | 28 | if 'extensions' in texture_entry and 'KHR_texture_transform' in texture_entry['extensions']: 29 | self._extensions['KHR_texture_transform'] = KHRTextureTransform(texture_entry['extensions']['KHR_texture_transform']) 30 | self._tt_offset = self._extensions['KHR_texture_transform']._offset 31 | self._tt_scale = self._extensions['KHR_texture_transform']._scale 32 | self._tt_rotation = self._extensions['KHR_texture_transform']._rotation 33 | 34 | def get_name(self): 35 | return self._name 36 | 37 | def get_image_path(self): 38 | return self._image.get_image_path() 39 | 40 | def get_wrap_s(self): 41 | return self._wrap_s 42 | 43 | def get_wrap_t(self): 44 | return self._wrap_t 45 | 46 | def write_to_directory(self, output_directory, channels, texture_prefix="", scale_factor=None): 47 | return self._image.write_to_directory(output_directory, channels, texture_prefix, self._tt_offset, self._tt_scale, self._tt_rotation, scale_factor) 48 | 49 | def get_texcoord_index(self): 50 | return self._texcoord_index 51 | 52 | @property 53 | def extensions(self): 54 | return self._extensions 55 | 56 | class NormalTexture(Texture): 57 | def __init__(self, normal_texture_entry, gltf_loader): 58 | super(NormalTexture, self).__init__(normal_texture_entry, gltf_loader) 59 | self._scale = normal_texture_entry['scale'] if ('scale' in normal_texture_entry) else 1.0 60 | 61 | @property 62 | def scale(self): 63 | return self._scale 64 | 65 | 66 | class OcclusionTexture(Texture): 67 | def __init__(self, occlusion_texture_entry, gltf_loader): 68 | super(OcclusionTexture, self).__init__(occlusion_texture_entry, gltf_loader) 69 | self._strength = occlusion_texture_entry['strength'] if ('strength' in occlusion_texture_entry) else 1.0 70 | 71 | @property 72 | def strength(self): 73 | return self._strength 74 | 75 | class PbrMetallicRoughness(object): 76 | def __init__(self, pbr_metallic_roughness_entry, gltf_loader): 77 | self._name = pbr_metallic_roughness_entry['name'] if ('name' in pbr_metallic_roughness_entry) else 'pbr_mat_roughness_texture' 78 | self._base_color_factor = pbr_metallic_roughness_entry['baseColorFactor'] if ('baseColorFactor' in pbr_metallic_roughness_entry) else [1.0,1.0,1.0, 1.0] 79 | self._metallic_factor = pbr_metallic_roughness_entry['metallicFactor'] if ('metallicFactor' in pbr_metallic_roughness_entry) else 1.0 80 | self._roughness_factor = pbr_metallic_roughness_entry['roughnessFactor'] if ('roughnessFactor' in pbr_metallic_roughness_entry) else 1.0 81 | self._base_color_texture = Texture(pbr_metallic_roughness_entry['baseColorTexture'], gltf_loader) if ('baseColorTexture' in pbr_metallic_roughness_entry) else None 82 | self._metallic_roughness_texture = Texture(pbr_metallic_roughness_entry['metallicRoughnessTexture'], gltf_loader) if ('metallicRoughnessTexture' in pbr_metallic_roughness_entry) else None 83 | 84 | def get_base_color_texture(self): 85 | return self._base_color_texture 86 | 87 | def get_base_color_factor(self): 88 | return self._base_color_factor 89 | 90 | def get_metallic_roughness_texture(self): 91 | return self._metallic_roughness_texture 92 | 93 | def get_metallic_factor(self): 94 | return self._metallic_factor 95 | 96 | def get_roughness_factor(self): 97 | return self._roughness_factor 98 | 99 | class PbrSpecularGlossiness(object): 100 | def __init__(self, pbr_specular_glossiness_entry, gltf_loader): 101 | self._diffuse_factor = pbr_specular_glossiness_entry['diffuseFactor'] if ('diffuseFactor' in pbr_specular_glossiness_entry) else [1.0,1.0,1.0,1.0] 102 | self._diffuse_texture = Texture(pbr_specular_glossiness_entry['diffuseTexture'], gltf_loader) if ('diffuseTexture' in pbr_specular_glossiness_entry) else None 103 | self._specular_factor = pbr_specular_glossiness_entry['specularFactor'] if ('specularFactor' in pbr_specular_glossiness_entry) else [1.0,1.0,1.0] 104 | self._glossiness_factor = pbr_specular_glossiness_entry['glossinessFactor'] if ('glossinessFactor' in pbr_specular_glossiness_entry) else 1.0 105 | self._specular_glossiness_texture = Texture(pbr_specular_glossiness_entry['specularGlossinessTexture'], gltf_loader) if ('specularGlossinessTexture' in pbr_specular_glossiness_entry) else None 106 | 107 | def get_diffuse_factor(self): 108 | return self._diffuse_factor 109 | 110 | def get_specular_glossiness_texture(self): 111 | return self._specular_glossiness_texture 112 | 113 | def get_specular_factor(self): 114 | return self._specular_factor 115 | 116 | def get_glossiness_factor(self): 117 | return self._glossiness_factor 118 | 119 | def get_diffuse_texture(self): 120 | return self._diffuse_texture 121 | 122 | class KHRTextureTransform(object): 123 | def __init__(self, khr_texture_transform_entry): 124 | self._offset = khr_texture_transform_entry['offset'] if 'offset' in khr_texture_transform_entry else [0.0,0.0] 125 | self._scale = khr_texture_transform_entry['scale'] if 'scale' in khr_texture_transform_entry else [1.0,1.0] 126 | self._rotation = khr_texture_transform_entry['rotation'] if 'rotation' in khr_texture_transform_entry else 0.0 127 | 128 | @property 129 | def offset(self): 130 | return self._offset 131 | 132 | @property 133 | def scale(self): 134 | return self._scale 135 | 136 | @property 137 | def rotation(self): 138 | return self._rotation 139 | 140 | 141 | class Material: 142 | def __init__(self, material_entry, material_index, gltf_loader): 143 | self._name = material_entry['name'] if ('name' in material_entry) else 'material_{}'.format(material_index) 144 | self._index = material_index 145 | self._double_sided = material_entry['doubleSided'] if ('doubleSided' in material_entry) else False 146 | 147 | self._pbr_metallic_roughness = PbrMetallicRoughness(material_entry['pbrMetallicRoughness'], gltf_loader) if ('pbrMetallicRoughness' in material_entry) else None 148 | 149 | self._alpha_mode = material_entry['alphaMode'] if ('alphaMode' in material_entry) else AlphaMode.OPAQUE 150 | self._alpha_cutoff = material_entry ['alphaCutoff'] if ('alphaCutoff' in material_entry) else 0.5 151 | 152 | self._normal_texture = NormalTexture(material_entry['normalTexture'], gltf_loader) if ('normalTexture' in material_entry) else None 153 | self._emissive_factor = material_entry['emissiveFactor'] if ('emissiveFactor' in material_entry) else [0,0,0] 154 | self._emissive_texture = Texture(material_entry['emissiveTexture'], gltf_loader) if ('emissiveTexture' in material_entry) else None 155 | self._occlusion_texture = OcclusionTexture(material_entry['occlusionTexture'], gltf_loader) if ('occlusionTexture' in material_entry) else None 156 | 157 | self._extensions = {} 158 | if 'extensions' in material_entry and 'KHR_materials_pbrSpecularGlossiness' in material_entry['extensions']: 159 | self._extensions['KHR_materials_pbrSpecularGlossiness'] = PbrSpecularGlossiness(material_entry['extensions']['KHR_materials_pbrSpecularGlossiness'], gltf_loader) 160 | 161 | def is_double_sided(self): 162 | return self._double_sided 163 | 164 | @property 165 | def alpha_mode(self): 166 | return self._alpha_mode 167 | 168 | def get_index(self): 169 | return self._index 170 | 171 | def get_pbr_metallic_roughness(self): 172 | return self._pbr_metallic_roughness 173 | 174 | def get_name(self): 175 | return self._name 176 | 177 | def get_extensions(self): 178 | return self._extensions 179 | 180 | def get_normal_texture(self): 181 | return self._normal_texture 182 | 183 | def get_occlusion_texture(self): 184 | return self._occlusion_texture 185 | 186 | def get_emissive_texture(self): 187 | return self._emissive_texture 188 | 189 | def get_emissive_factor(self): 190 | return self._emissive_factor 191 | 192 | def alpha_cutoff(self): 193 | return self._alpha_cutoff -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/Mesh.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class PrimitiveModeType(Enum): 4 | POINTS = 0 5 | LINES = 1 6 | LINE_LOOP = 2 7 | LINE_STRIP = 3 8 | TRIANGLES = 4 9 | TRIANGLE_STRIP = 5 10 | TRIANGLE_FAN = 6 11 | 12 | class PrimitiveAttribute(object): 13 | def __init__(self, attribute_name, attribute_data, accessor_type, min_value=None, max_value=None): 14 | self._attribute_type = attribute_name 15 | self._attribute_data = attribute_data 16 | self._accessor_type = accessor_type 17 | self._min_value = min_value 18 | self._max_value = max_value 19 | 20 | @property 21 | def attribute_type(self): 22 | return self._attribute_type 23 | 24 | @property 25 | def accessor_type(self): 26 | return self._accessor_type 27 | 28 | def get_min_value(self): 29 | return self._min_value 30 | 31 | def get_max_value(self): 32 | return self._max_value 33 | 34 | def get_data(self): 35 | return self._attribute_data 36 | 37 | 38 | class PrimitiveTarget(object): 39 | def __init__(self, target_entry, target_index, gltf_loader): 40 | self._name = None 41 | self._attributes = {} 42 | for entry in target_entry: 43 | accessor = gltf_loader.json_data['accessors'][target_entry[entry]] 44 | if self._name == None: 45 | self._name = accessor['name'] if ('name' in accessor) else 'shape_{}'.format(target_index) 46 | data = gltf_loader.get_data(accessor, target_entry[entry]) 47 | self._attributes[entry] = data 48 | 49 | def get_attributes(self): 50 | return self._attributes 51 | 52 | def get_name(self): 53 | return self._name 54 | 55 | 56 | class Primitive(object): 57 | def __init__(self, primitive_entry, i, gltf_mesh, gltf_loader): 58 | self._name = primitive_entry['name'] if ('name' in primitive_entry) else 'primitive_{}'.format(i) 59 | self._attributes = {} 60 | self._material = None 61 | self._targets = [] 62 | 63 | if 'attributes' in primitive_entry: 64 | for attribute_name in primitive_entry['attributes']: 65 | accessor_index = primitive_entry['attributes'][attribute_name] 66 | accessor = gltf_loader.json_data['accessors'][accessor_index] 67 | data = gltf_loader.get_data(accessor, accessor_index) 68 | if data: 69 | min_value = accessor['min'] if ('min' in accessor) else None 70 | max_value = accessor['max'] if ('max' in accessor) else None 71 | 72 | self._attributes[attribute_name] = PrimitiveAttribute(attribute_name, data, accessor['type'], min_value, max_value) 73 | 74 | self._indices = self._get_indices(primitive_entry, gltf_loader) 75 | self._mode = PrimitiveModeType(primitive_entry['mode']) if ('mode' in primitive_entry) else PrimitiveModeType.TRIANGLES 76 | if 'material' in primitive_entry: 77 | self._material = gltf_loader.get_materials()[primitive_entry['material']] 78 | 79 | # Fetch the names and accessors of the blendshapes 80 | if 'targets' in primitive_entry: 81 | for i, target_entry in enumerate(primitive_entry['targets']): 82 | target = PrimitiveTarget(target_entry, i, gltf_loader) 83 | self._targets.append(target) 84 | 85 | def _get_indices(self, primitive_entry, gltf_loader): 86 | if 'indices' in primitive_entry: 87 | accessor_index = primitive_entry['indices'] 88 | accessor = gltf_loader.json_data['accessors'][accessor_index] 89 | data = gltf_loader.get_data(accessor, accessor_index) 90 | return data 91 | 92 | else: 93 | position_accessor = gltf_loader.json_data['accessors'][primitive_entry['attributes']['POSITION']] 94 | count = position_accessor['count'] 95 | return range(0, count) 96 | 97 | def get_attributes(self): 98 | return self._attributes 99 | 100 | def get_morph_targets(self): 101 | return self._targets 102 | 103 | def get_indices(self): 104 | return self._indices 105 | 106 | def get_material(self): 107 | return self._material 108 | 109 | def get_name(self): 110 | return self._name 111 | 112 | 113 | 114 | class Mesh(object): 115 | def __init__(self, mesh_entry, mesh_index, gltf_loader): 116 | self._name = mesh_entry['name'] if 'name' in mesh_entry else 'mesh_{}'.format(mesh_index) 117 | self._primitives = [] 118 | self._weights = [] 119 | self._index = mesh_index 120 | if 'weights' in mesh_entry: 121 | self._weights = mesh_entry['weights'] 122 | if 'primitives' in mesh_entry: 123 | for i, primitive_entry in enumerate(mesh_entry['primitives']): 124 | primitive = Primitive(primitive_entry, i, self, gltf_loader) 125 | self._primitives.append(primitive) 126 | 127 | @property 128 | def name(self): 129 | return self._name 130 | 131 | def get_weights(self): 132 | return self._weights 133 | 134 | def get_primitives(self): 135 | return self._primitives 136 | 137 | 138 | -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/Node.py: -------------------------------------------------------------------------------- 1 | import unicodedata 2 | 3 | class Node(object): 4 | """Create a glTF node object 5 | """ 6 | 7 | def __init__(self, node_dict, node_index, gltf_loader): 8 | self._parent = None 9 | self._name = node_dict['name'] if ('name' in node_dict and len(node_dict['name']) > 0) else 'node_{}'.format(node_index) 10 | 11 | if isinstance(self._name, unicode): 12 | self._name = unicodedata.normalize('NFKD', self._name).encode('ascii', 'ignore') 13 | 14 | self._name = '{0}_{1}'.format(self._name, node_index) 15 | self._node_index = node_index 16 | self._matrix = node_dict['matrix'] if ('matrix' in node_dict) else None 17 | self._translation = node_dict['translation'] if ('translation' in node_dict) else [0,0,0] 18 | self._rotation = node_dict['rotation'] if ('rotation' in node_dict) else [0,0,0,1] 19 | self._scale = node_dict['scale'] if ('scale' in node_dict) else [1,1,1] 20 | self._skin_index = node_dict['skin'] if ('skin' in node_dict) else None 21 | self._skin = None 22 | 23 | self._mesh = gltf_loader.get_meshes()[node_dict['mesh']] if ('mesh' in node_dict) else None 24 | 25 | self._children_indices = node_dict['children'] if ('children' in node_dict) else [] 26 | self._children = [] 27 | self._extras = node_dict['extras'] if 'extras' in node_dict else {} 28 | 29 | @property 30 | def name(self): 31 | return self._name 32 | 33 | @property 34 | def translation(self): 35 | return self._translation 36 | 37 | @property 38 | def rotation(self): 39 | return self._rotation 40 | 41 | @property 42 | def scale(self): 43 | return self._scale 44 | 45 | def get_children(self): 46 | return self._children 47 | 48 | @property 49 | def parent(self): 50 | return self._parent 51 | 52 | @property 53 | def matrix(self): 54 | return self._matrix 55 | 56 | def get_index(self): 57 | return self._node_index 58 | 59 | def get_mesh(self): 60 | return self._mesh 61 | 62 | def get_skin(self): 63 | return self._skin 64 | 65 | @property 66 | def index(self): 67 | return self._node_index 68 | 69 | @property 70 | def extras(self): 71 | return self._extras -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/Scene.py: -------------------------------------------------------------------------------- 1 | class Scene(object): 2 | def __init__(self, scene_entry, scene_index, nodes): 3 | self._nodes = [] 4 | if 'nodes' in scene_entry: 5 | for node_index in scene_entry['nodes']: 6 | self._nodes.append(nodes[node_index]) 7 | 8 | def get_nodes(self): 9 | """Returns the node from the scene 10 | 11 | Returns: 12 | Scene[] -- nodes in the scene 13 | """ 14 | 15 | return self._nodes -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/Skin.py: -------------------------------------------------------------------------------- 1 | from sets import Set 2 | class Skin(object): 3 | """Represents a glTF Skin 4 | """ 5 | 6 | def __init__(self, gltf2_loader, skin_entry): 7 | self._inverse_bind_matrices = self._init_inverse_bind_matrices(gltf2_loader, skin_entry) 8 | self._joints = [gltf2_loader.get_nodes()[joint_index] for joint_index in skin_entry['joints']] 9 | self._root_skeletons = self._init_root_skeletons(gltf2_loader, skin_entry) 10 | 11 | def _init_inverse_bind_matrices(self, gltf2_loader, skin_entry): 12 | inverse_bind_matrices = [] 13 | if 'inverseBindMatrices' in skin_entry: 14 | inverse_bind_matrices = gltf2_loader.get_data(accessor=gltf2_loader.json_data['accessors'][skin_entry['inverseBindMatrices']], accessor_index=skin_entry['inverseBindMatrices']) 15 | 16 | return inverse_bind_matrices 17 | 18 | def get_inverse_bind_matrices(self): 19 | return self._inverse_bind_matrices 20 | 21 | def get_joints(self): 22 | return self._joints 23 | 24 | def _init_root_skeletons(self, gltf2_loader, skin_entry): 25 | root_joints = Set() 26 | for joint_index in skin_entry['joints']: 27 | joint = gltf2_loader.nodes[joint_index] 28 | parent = joint.parent 29 | if parent == None or parent.index not in skin_entry['joints']: 30 | root_joints.add(joint) 31 | 32 | 33 | return list(root_joints) 34 | 35 | def get_bind_transforms(self): 36 | pass 37 | 38 | def get_rest_transforms(self): 39 | pass 40 | 41 | def get_joint_names(self): 42 | pass 43 | 44 | @property 45 | def root_joints(self): 46 | """The root joints of the skeleton 47 | 48 | Returns: 49 | [[Node]] -- list of root joints for the skeleton 50 | """ 51 | 52 | return self._root_skeletons -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2/__init__.py: -------------------------------------------------------------------------------- 1 | from Skin import Skin 2 | from Node import Node 3 | from Animation import Animation 4 | from Scene import Scene 5 | from Mesh import Mesh 6 | from Material import Material -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2loader.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | from enum import Enum 3 | import base64 4 | import json 5 | import os 6 | import re 7 | import struct 8 | 9 | import gltf2usdUtils 10 | 11 | from gltf2 import Skin, Node, Animation, Scene, Mesh, Material, GLTFImage, Asset 12 | 13 | 14 | class AccessorType(Enum): 15 | SCALAR = 'SCALAR' 16 | VEC2 = 'VEC2' 17 | VEC3 = 'VEC3' 18 | VEC4 = 'VEC4' 19 | MAT2 = 'MAT2' 20 | MAT3 = 'MAT3' 21 | MAT4 = 'MAT4' 22 | 23 | class AccessorComponentType(Enum): 24 | BYTE = 5120 25 | UNSIGNED_BYTE = 5121 26 | SHORT = 5122 27 | UNSIGNED_SHORT = 5123 28 | UNSIGNED_INT = 5125 29 | FLOAT = 5126 30 | 31 | class TextureWrap(Enum): 32 | CLAMP_TO_EDGE = 33071 33 | MIRRORED_REPEAT = 33648 34 | REPEAT = 10497 35 | 36 | class MagFilter(Enum): 37 | NEAREST = 9728 38 | LINEAR = 9729 39 | 40 | class MinFilter(Enum): 41 | NEAREST = 9728 42 | LINEAR = 9729 43 | NEAREST_MIPMAP_NEAREST = 9984 44 | LINEAR_MIPMAP_NEAREST = 9985 45 | NEAREST_MIPMAP_LINEAR = 9986 46 | LINEAR_MIPMAP_LINEAR = 9987 47 | 48 | class AccessorTypeCount(Enum): 49 | SCALAR = 1 50 | VEC2 = 2 51 | VEC3 = 3 52 | VEC4 = 4 53 | MAT2 = 4 54 | MAT3 = 9 55 | MAT4 = 16 56 | 57 | def accessor_type_count(x): 58 | return { 59 | 'SCALAR': 1, 60 | 'VEC2': 2, 61 | 'VEC3': 3, 62 | 'VEC4': 4, 63 | 'MAT2': 4, 64 | 'MAT3': 9, 65 | 'MAT4': 16 66 | }[x] 67 | 68 | def PrimitiveMode(Enum): 69 | POINTS = 0 70 | LINES = 1 71 | LINE_LOOP = 2 72 | LINE_STRIP = 3 73 | TRIANGLES = 4 74 | TRIANGLE_STRIP = 5 75 | TRIANGLE_FAN = 6 76 | 77 | def accessor_component_type_bytesize(x): 78 | return { 79 | AccessorComponentType.BYTE: 1, 80 | AccessorComponentType.UNSIGNED_BYTE: 1, 81 | AccessorComponentType.SHORT: 2, 82 | AccessorComponentType.UNSIGNED_SHORT: 2, 83 | AccessorComponentType.UNSIGNED_INT: 4, 84 | AccessorComponentType.FLOAT: 4, 85 | }[x] 86 | 87 | 88 | 89 | 90 | class GLTF2Loader(object): 91 | """A very simple glTF loader. It is essentially a utility to load data from accessors 92 | """ 93 | 94 | def __init__(self, gltf_file, optimize_textures=False, generate_texture_transform_texture=True): 95 | """Initializes the glTF 2.0 loader 96 | 97 | Arguments: 98 | gltf_file {str} -- Path to glTF file 99 | """ 100 | if not os.path.isfile(gltf_file): 101 | raise Exception("file {} does not exist".format(gltf_file)) 102 | 103 | if not gltf_file.endswith('.gltf'): 104 | raise Exception('Can only accept .gltf files') 105 | 106 | self._accessor_data_map = {} 107 | self.root_dir = os.path.dirname(gltf_file) 108 | self._optimize_textures = optimize_textures 109 | self._generate_texture_transform_texture = generate_texture_transform_texture 110 | try: 111 | with codecs.open(gltf_file, encoding='utf-8', errors='strict') as f: 112 | self.json_data = json.load(f) 113 | except UnicodeDecodeError: 114 | with open(gltf_file) as f: 115 | self.json_data = json.load(f) 116 | 117 | self._initialize() 118 | 119 | def _initialize(self): 120 | """Initializes the glTF loader 121 | """ 122 | self._initialize_asset() 123 | self._initialize_images() 124 | self._initialize_materials() 125 | self._initialize_meshes() 126 | self._initialize_nodes() 127 | self._initialize_skins() 128 | self._initialize_scenes() 129 | 130 | self._initialize_animations() 131 | 132 | def _initialize_asset(self): 133 | if 'asset' in self.json_data: 134 | self._asset = Asset.Asset(self.json_data['asset']) 135 | else: 136 | self._asset = None 137 | 138 | def _initialize_images(self): 139 | self._images = [] 140 | if 'images' in self.json_data: 141 | for i, image_entry in enumerate(self.json_data['images']): 142 | self._images.append(GLTFImage.GLTFImage(image_entry, i, self, self._optimize_textures, self._generate_texture_transform_texture)) 143 | 144 | 145 | def _initialize_nodes(self): 146 | self.nodes = [] 147 | if 'nodes' in self.json_data: 148 | for i, node_entry in enumerate(self.json_data['nodes']): 149 | node = Node(node_entry, i, self) 150 | self.nodes.append(node) 151 | 152 | for i, node_entry in enumerate(self.json_data['nodes']): 153 | if 'children' in node_entry: 154 | parent = self.nodes[i] 155 | for child_index in node_entry['children']: 156 | child = self.nodes[child_index] 157 | child._parent = parent 158 | parent._children.append(child) 159 | 160 | def _initialize_materials(self): 161 | self._materials = [] 162 | 163 | if 'materials' in self.json_data: 164 | for i, material_entry in enumerate(self.json_data['materials']): 165 | material = Material(material_entry, i, self) 166 | self._materials.append(material) 167 | 168 | def _initialize_scenes(self): 169 | self._scenes = [] 170 | self._main_scene = None 171 | if 'scenes' in self.json_data: 172 | for i, scene_entry in enumerate(self.json_data['scenes']): 173 | scene = Scene(scene_entry, i, self.nodes) 174 | self._scenes.append(scene) 175 | 176 | if 'scene' in self.json_data: 177 | self._main_scene = self._scenes[self.json_data['scene']] 178 | else: 179 | self._main_scene = self._scenes[0] 180 | 181 | def get_images(self): 182 | return self._images 183 | 184 | def get_scenes(self): 185 | """Get the scene objects from the glTF file 186 | 187 | Returns: 188 | Scene[] -- glTF scene objects 189 | """ 190 | 191 | return self._scenes 192 | 193 | def get_main_scene(self): 194 | """Returns the main scene in the glTF file, or none if there are no scenes 195 | 196 | Returns: 197 | Scene -- glTF scene 198 | """ 199 | 200 | return self._main_scene 201 | 202 | def get_materials(self): 203 | return self._materials 204 | 205 | def get_meshes(self): 206 | return self._meshes 207 | 208 | def _initialize_meshes(self): 209 | self._meshes = [] 210 | if 'meshes' in self.json_data: 211 | for i, mesh_entry in enumerate(self.json_data['meshes']): 212 | mesh = Mesh(mesh_entry, i, self) 213 | self._meshes.append(mesh) 214 | 215 | 216 | 217 | def _initialize_animations(self): 218 | self.animations = [] 219 | if 'animations' in self.json_data: 220 | for i, animation_entry in enumerate(self.json_data['animations']): 221 | animation = Animation(animation_entry, i, self) 222 | self.animations.append(animation) 223 | 224 | 225 | def _initialize_skins(self): 226 | self.skins = [] 227 | if 'skins' in self.json_data: 228 | self.skins = [Skin(self, skin) for skin in self.json_data['skins']] 229 | for node in self.nodes: 230 | if node._skin_index != None: 231 | node._skin = self.skins[node._skin_index] 232 | 233 | 234 | def get_asset(self): 235 | return self._asset 236 | 237 | def get_nodes(self): 238 | return self.nodes 239 | 240 | def get_skins(self): 241 | return self.skins 242 | 243 | def get_animations(self): 244 | return self.animations 245 | 246 | 247 | def align(self, value, size): 248 | remainder = value % size 249 | return value if (remainder == 0) else (value + size - remainder) 250 | 251 | def get_data(self, accessor, accessor_index): 252 | if accessor_index in self._accessor_data_map.keys(): 253 | return self._accessor_data_map[accessor_index] 254 | 255 | bufferview = self.json_data['bufferViews'][accessor['bufferView']] 256 | buffer = self.json_data['buffers'][bufferview['buffer']] 257 | accessor_type = AccessorType(accessor['type']) 258 | uri = buffer['uri'] 259 | buffer_data = '' 260 | 261 | if re.match(r'^data:.*?;base64,', uri): 262 | uri_data = uri.split(',')[1] 263 | buffer_data = base64.b64decode(uri_data) 264 | if 'byteOffset' in bufferview: 265 | buffer_data = buffer_data[bufferview['byteOffset']:] 266 | else: 267 | buffer_file = os.path.join(self.root_dir, uri) 268 | with open(buffer_file, 'rb') as buffer_fptr: 269 | if 'byteOffset' in bufferview: 270 | buffer_fptr.seek(bufferview['byteOffset'], 1) 271 | 272 | buffer_data = buffer_fptr.read(bufferview['byteLength']) 273 | 274 | data_arr = [] 275 | accessor_component_type = AccessorComponentType(accessor['componentType']) 276 | 277 | accessor_type_size = accessor_type_count(accessor['type']) 278 | accessor_component_type_size = accessor_component_type_bytesize(accessor_component_type) 279 | 280 | bytestride = int(bufferview['byteStride']) if ('byteStride' in bufferview) else (accessor_type_size * accessor_component_type_size) 281 | offset = int(accessor['byteOffset']) if 'byteOffset' in accessor else 0 282 | 283 | data_type = '' 284 | data_type_size = 4 285 | normalize_divisor = 1.0 #used if the value needs to be normalized 286 | if accessor_component_type == AccessorComponentType.FLOAT: 287 | data_type = 'f' 288 | data_type_size = 4 289 | elif accessor_component_type == AccessorComponentType.UNSIGNED_INT: 290 | data_type = 'I' 291 | data_type_size = 4 292 | elif accessor_component_type == AccessorComponentType.UNSIGNED_SHORT: 293 | data_type = 'H' 294 | data_type_size = 2 295 | normalize_divisor = 65535.0 if 'normalized' in accessor and accessor['normalized'] == True else 1.0 296 | elif accessor_component_type == AccessorComponentType.UNSIGNED_BYTE: 297 | data_type = 'B' 298 | data_type_size = 1 299 | normalize_divisor = 255.0 if 'normalized' in accessor and accessor['normalized'] == True else 1.0 300 | else: 301 | raise Exception('unsupported accessor component type!') 302 | 303 | for i in range(0, accessor['count']): 304 | entries = [] 305 | for j in range(0, accessor_type_size): 306 | x = offset + j * accessor_component_type_size 307 | window = buffer_data[x:x + data_type_size] 308 | entries.append((struct.unpack(data_type, window)[0])/normalize_divisor) 309 | 310 | if len(entries) > 1: 311 | data_arr.append(tuple(entries)) 312 | else: 313 | data_arr.append(entries[0]) 314 | offset = offset + bytestride 315 | 316 | self._accessor_data_map[accessor_index] = data_arr 317 | return data_arr 318 | -------------------------------------------------------------------------------- /Source/_gltf2usd/gltf2usdUtils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pxr import Gf, UsdSkel, Tf 4 | 5 | class GLTF2USDUtils(object): 6 | @staticmethod 7 | def convert_to_usd_friendly_node_name(name): 8 | """Format a glTF name to make it more USD friendly 9 | 10 | Arguments: 11 | name {str} -- glTF node name 12 | 13 | Returns: 14 | str -- USD friendly name 15 | """ 16 | 17 | return Tf.MakeValidIdentifier(name) 18 | 19 | 20 | @staticmethod 21 | def get_skin_rest_transforms(gltf_skin): 22 | joints = gltf_skin.get_joints() 23 | for joint in joints: 24 | rest_matrix = GLTF2USDUtils.compute_usd_transform_matrix_from_gltf_node(joint) 25 | 26 | @staticmethod 27 | def compute_usd_transform_matrix_from_gltf_node(node): 28 | """Computes a transform matrix from a glTF node object 29 | 30 | Arguments: 31 | node {Node} -- glTF2 Node object 32 | 33 | Returns: 34 | Matrix-- USD matrix 35 | """ 36 | 37 | matrix = node.matrix 38 | if (matrix != None): 39 | return Gf.Matrix4d( 40 | matrix[0], matrix[1], matrix[2], matrix[3], 41 | matrix[4], matrix[5], matrix[6], matrix[7], 42 | matrix[8], matrix[9], matrix[10], matrix[11], 43 | matrix[12], matrix[13], matrix[14], matrix[15] 44 | ) 45 | else: 46 | translation = node.translation 47 | usd_translation = Gf.Vec3f(translation[0], translation[1], translation[2]) 48 | 49 | rotation = node.rotation 50 | usd_rotation = Gf.Quatf(rotation[3], rotation[0], rotation[1], rotation[2]) 51 | 52 | scale = node.scale 53 | usd_scale = Gf.Vec3h(scale[0], scale[1], scale[2]) 54 | 55 | return UsdSkel.MakeTransform(usd_translation, usd_rotation, usd_scale) 56 | 57 | @staticmethod 58 | def slerp(vec0, vec1, factor): 59 | quat0 = Gf.Quatf(vec0[3], vec0[0], vec0[1], vec0[2]) 60 | quat1 = Gf.Quatf(vec1[3], vec1[0], vec1[1], vec1[2]) 61 | result = Gf.Quatf(Gf.Slerp(factor, quat0, quat1)) 62 | 63 | return result 64 | -------------------------------------------------------------------------------- /Source/_gltf2usd/gltfUsdMaterialHelper.py: -------------------------------------------------------------------------------- 1 | class GLTFUSDMaterialHelper(object): 2 | def __init__(self): 3 | pass -------------------------------------------------------------------------------- /Source/_gltf2usd/usd_material.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pxr import Gf, Sdf, UsdGeom, UsdShade 4 | 5 | from gltf2 import Material, GLTFImage 6 | from _gltf2usd.gltf2usdUtils import GLTF2USDUtils 7 | from gltf2.Material import AlphaMode 8 | from gltf2loader import TextureWrap 9 | 10 | class USDMaterial(object): 11 | def __init__(self, stage, name, material_scope, index, gltf2loader, scale_texture=False): 12 | self._gltf2loader = gltf2loader 13 | self._stage = stage 14 | self._material_scope = material_scope 15 | self._material_path = Sdf.Path('{0}/{1}'.format('/root/Materials', name)) 16 | self._usd_material = UsdShade.Material.Define(stage, self._material_path) 17 | 18 | self._usd_material_surface_output = self._usd_material.CreateOutput("surface", Sdf.ValueTypeNames.Token) 19 | self._usd_material_displacement_output = self._usd_material.CreateOutput("displacement", Sdf.ValueTypeNames.Token) 20 | self._scale_texture = scale_texture 21 | 22 | def convert_material_to_usd_preview_surface(self, gltf_material, output_directory, material_name): 23 | usd_preview_surface = USDPreviewSurface(self._stage, gltf_material, self, output_directory, material_name, self._scale_texture) 24 | usd_preview_surface._name = material_name 25 | 26 | def get_usd_material(self): 27 | return self._usd_material 28 | 29 | 30 | 31 | 32 | class USDPreviewSurface(object): 33 | """Models a physically based surface for USD 34 | """ 35 | def __init__(self, stage, gltf_material, usd_material, output_directory, material_name, scale_texture=False): 36 | self._stage = stage 37 | self._scale_texture = scale_texture 38 | self._usd_material = usd_material 39 | self._output_directory = output_directory 40 | material_path = usd_material._usd_material.GetPath() 41 | material = UsdShade.Shader.Define(self._stage, material_path.AppendChild(material_name)) 42 | material.CreateIdAttr('UsdPreviewSurface') 43 | self._shader = material 44 | self._initialize_material(material, self) 45 | self._initialize_from_gltf_material(gltf_material) 46 | 47 | 48 | def _initialize_material(self, material, usd_preview_surface_material): 49 | shader = material 50 | self._use_specular_workflow = material.CreateInput('useSpecularWorkflow', Sdf.ValueTypeNames.Int) 51 | self._use_specular_workflow.Set(False) 52 | 53 | self._surface_output = shader.CreateOutput('surface', Sdf.ValueTypeNames.Token) 54 | self._usd_material._usd_material_surface_output.ConnectToSource(self._surface_output) 55 | 56 | 57 | self._displacement_output = shader.CreateOutput('displacement', Sdf.ValueTypeNames.Token) 58 | self._usd_material._usd_material_displacement_output.ConnectToSource(self._displacement_output) 59 | 60 | self._specular_color = material.CreateInput('specularColor', Sdf.ValueTypeNames.Color3f) 61 | self._specular_color.Set((1.0,1.0,1.0)) 62 | 63 | self._metallic = material.CreateInput('metallic', Sdf.ValueTypeNames.Float) 64 | 65 | self._roughness = material.CreateInput('roughness', Sdf.ValueTypeNames.Float) 66 | 67 | self._clearcoat = material.CreateInput('clearcoat', Sdf.ValueTypeNames.Float) 68 | self._clearcoat.Set(0.0) 69 | 70 | self._clearcoat_roughness = material.CreateInput('clearcoatRoughness', Sdf.ValueTypeNames.Float) 71 | self._clearcoat_roughness.Set(0.01) 72 | 73 | self._opacity = material.CreateInput('opacity', Sdf.ValueTypeNames.Float) 74 | self._opacity.Set(1.0) 75 | 76 | self._opacityThreshold = material.CreateInput('opacityThreshold', Sdf.ValueTypeNames.Float) 77 | self._opacityThreshold.Set(0) 78 | 79 | self._ior = material.CreateInput('ior', Sdf.ValueTypeNames.Float) 80 | self._ior.Set(1.5) 81 | 82 | self._normal = material.CreateInput('normal', Sdf.ValueTypeNames.Normal3f) 83 | 84 | self._displacement = material.CreateInput('displacement', Sdf.ValueTypeNames.Float) 85 | self._displacement.Set(0.0) 86 | 87 | self._occlusion = material.CreateInput('occlusion', Sdf.ValueTypeNames.Float) 88 | 89 | self._emissive_color = material.CreateInput('emissiveColor', Sdf.ValueTypeNames.Color3f) 90 | 91 | self._diffuse_color = material.CreateInput('diffuseColor', Sdf.ValueTypeNames.Color3f) 92 | 93 | self._st0 = USDPrimvarReaderFloat2(self._stage, self._usd_material._material_path, 'st0') 94 | self._st1 = USDPrimvarReaderFloat2(self._stage, self._usd_material._material_path, 'st1') 95 | 96 | def _initialize_from_gltf_material(self, gltf_material): 97 | self._set_normal_texture(gltf_material) 98 | self._set_emissive_texture(gltf_material) 99 | self._set_occlusion_texture(gltf_material) 100 | self._set_khr_material_pbr_specular_glossiness(gltf_material) 101 | 102 | def _set_normal_texture(self, gltf_material): 103 | normal_texture = gltf_material.get_normal_texture() 104 | if (not normal_texture): 105 | self._normal.Set((0,0,1)) 106 | else: 107 | destination = normal_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.RGB) 108 | normal_scale = normal_texture.scale 109 | scale_factor = (normal_scale, normal_scale, normal_scale, 1.0) 110 | usd_uv_texture = USDUVTexture("normalTexture", self._stage, self._usd_material._usd_material, normal_texture, [self._st0, self._st1]) 111 | usd_uv_texture._file_asset.Set(destination) 112 | usd_uv_texture._scale.Set(scale_factor) 113 | usd_uv_texture._fallback.Set(scale_factor) 114 | texture_shader = usd_uv_texture.get_shader() 115 | texture_shader.CreateOutput('rgb', Sdf.ValueTypeNames.Float3) 116 | self._normal.ConnectToSource(texture_shader, 'rgb') 117 | 118 | def _set_emissive_texture(self, gltf_material): 119 | emissive_texture = gltf_material.get_emissive_texture() 120 | emissive_factor = gltf_material.get_emissive_factor() 121 | if (not emissive_texture): 122 | self._emissive_color.Set((0,0,0)) 123 | else: 124 | destination = emissive_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.RGB) 125 | scale_factor = (emissive_factor[0], emissive_factor[1], emissive_factor[2], 1.0) 126 | usd_uv_texture = USDUVTexture("emissiveTexture", self._stage, self._usd_material._usd_material, emissive_texture, [self._st0, self._st1]) 127 | usd_uv_texture._file_asset.Set(destination) 128 | usd_uv_texture._scale.Set(scale_factor) 129 | usd_uv_texture._fallback.Set(scale_factor) 130 | texture_shader = usd_uv_texture.get_shader() 131 | texture_shader.CreateOutput('rgb', Sdf.ValueTypeNames.Float3) 132 | self._emissive_color.ConnectToSource(texture_shader, 'rgb') 133 | 134 | def _set_occlusion_texture(self, gltf_material): 135 | occlusion_texture = gltf_material.get_occlusion_texture() 136 | if (not occlusion_texture): 137 | self._occlusion.Set(1.0) 138 | else: 139 | destination = occlusion_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.R) 140 | occlusion_strength = occlusion_texture.strength 141 | strength_factor = (occlusion_strength, occlusion_strength, occlusion_strength, 1.0) 142 | usd_uv_texture = USDUVTexture("occlusionTexture", self._stage, self._usd_material._usd_material, occlusion_texture, [self._st0, self._st1]) 143 | usd_uv_texture._file_asset.Set(destination) 144 | usd_uv_texture._scale.Set(strength_factor) 145 | usd_uv_texture._fallback.Set(strength_factor) 146 | texture_shader = usd_uv_texture.get_shader() 147 | texture_shader.CreateOutput('r', Sdf.ValueTypeNames.Float) 148 | self._occlusion.ConnectToSource(texture_shader, 'r') 149 | 150 | def _set_pbr_metallic_roughness(self, gltf_material): 151 | pbr_metallic_roughness = gltf_material.get_pbr_metallic_roughness() 152 | if (pbr_metallic_roughness): 153 | self._set_pbr_base_color(pbr_metallic_roughness, gltf_material.alpha_mode, gltf_material.alpha_cutoff()) 154 | self._set_pbr_metallic(pbr_metallic_roughness) 155 | self._set_pbr_roughness(pbr_metallic_roughness) 156 | 157 | def _set_khr_material_pbr_specular_glossiness(self, gltf_material): 158 | extensions = gltf_material.get_extensions() 159 | if not 'KHR_materials_pbrSpecularGlossiness' in extensions: 160 | self._set_pbr_metallic_roughness(gltf_material) 161 | else: 162 | self._use_specular_workflow.Set(True) 163 | pbr_specular_glossiness = extensions['KHR_materials_pbrSpecularGlossiness'] 164 | self._set_pbr_specular_glossiness_diffuse(pbr_specular_glossiness) 165 | self._set_pbr_specular_glossiness_glossiness(pbr_specular_glossiness) 166 | self._set_pbr_specular_glossiness_specular(pbr_specular_glossiness) 167 | 168 | def _set_pbr_specular_glossiness_diffuse(self, pbr_specular_glossiness): 169 | diffuse_texture = pbr_specular_glossiness.get_diffuse_texture() 170 | diffuse_factor = pbr_specular_glossiness.get_diffuse_factor() 171 | if not diffuse_texture: 172 | self._diffuse_color.Set(Gf.Vec3f(diffuse_factor[0], diffuse_factor[1], diffuse_factor[2])) 173 | else: 174 | destination = diffuse_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.RGB) 175 | scale_factor = tuple(diffuse_factor) 176 | usd_uv_texture = USDUVTexture("diffuseTexture", self._stage, self._usd_material._usd_material, diffuse_texture, [self._st0, self._st1]) 177 | usd_uv_texture._file_asset.Set(destination) 178 | usd_uv_texture._scale.Set(scale_factor) 179 | usd_uv_texture._fallback.Set(scale_factor) 180 | texture_shader = usd_uv_texture.get_shader() 181 | texture_shader.CreateOutput('rgb', Sdf.ValueTypeNames.Float3) 182 | texture_shader.CreateOutput('a', Sdf.ValueTypeNames.Float) 183 | self._diffuse_color.ConnectToSource(texture_shader, 'rgb') 184 | self._opacity.ConnectToSource(texture_shader, 'a') 185 | 186 | def _set_pbr_specular_glossiness_specular(self, pbr_specular_glossiness): 187 | specular_glossiness_texture = pbr_specular_glossiness.get_specular_glossiness_texture() 188 | 189 | specular_factor = tuple(pbr_specular_glossiness.get_specular_factor()) 190 | if not specular_glossiness_texture: 191 | self._specular_color.Set(specular_factor) 192 | else: 193 | scale_factor = (specular_factor[0], specular_factor[1], specular_factor[2], 1) 194 | destination = specular_glossiness_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.RGB, "specular") 195 | 196 | usd_uv_texture = USDUVTexture("specularTexture", self._stage, self._usd_material._usd_material, specular_glossiness_texture, [self._st0, self._st1]) 197 | usd_uv_texture._file_asset.Set(destination) 198 | usd_uv_texture._scale.Set(scale_factor) 199 | usd_uv_texture._fallback.Set(scale_factor) 200 | texture_shader = usd_uv_texture.get_shader() 201 | texture_shader.CreateOutput('rgb', Sdf.ValueTypeNames.Float3) 202 | self._specular_color.ConnectToSource(texture_shader, 'rgb') 203 | 204 | def _set_pbr_specular_glossiness_glossiness(self, pbr_specular_glossiness): 205 | specular_glossiness_texture = pbr_specular_glossiness.get_specular_glossiness_texture() 206 | roughness_factor = 1 - pbr_specular_glossiness.get_glossiness_factor() 207 | if not specular_glossiness_texture: 208 | self._roughness.Set(roughness_factor) 209 | else: 210 | destination = specular_glossiness_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.A, "glossiness") 211 | scale_factor = (-1, -1, -1, -1) 212 | usd_uv_texture = USDUVTexture("glossinessTexture", self._stage, self._usd_material._usd_material, specular_glossiness_texture, [self._st0, self._st1]) 213 | usd_uv_texture._file_asset.Set(destination) 214 | usd_uv_texture._bias.Set((1.0, 1.0, 1.0, 1.0)) 215 | usd_uv_texture._scale.Set(scale_factor) 216 | usd_uv_texture._fallback.Set(scale_factor) 217 | texture_shader = usd_uv_texture.get_shader() 218 | texture_shader.CreateOutput('r', Sdf.ValueTypeNames.Float) 219 | self._roughness.ConnectToSource(texture_shader, 'r') 220 | 221 | 222 | def _set_pbr_base_color(self, pbr_metallic_roughness, alpha_mode, alpha_cutoff): 223 | base_color_texture = pbr_metallic_roughness.get_base_color_texture() 224 | base_color_scale = pbr_metallic_roughness.get_base_color_factor() 225 | 226 | if not base_color_texture: 227 | self._diffuse_color.Set(tuple(base_color_scale[0:3])) 228 | if AlphaMode(alpha_mode) != AlphaMode.OPAQUE: 229 | self._opacity.Set(base_color_scale[3]) 230 | else: 231 | 232 | defaultAlpha = 1 233 | if len(base_color_scale) >= 3: 234 | defaultAlpha = base_color_scale[3] 235 | 236 | if AlphaMode(alpha_mode) == AlphaMode.OPAQUE: 237 | destination = base_color_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.RGB) 238 | scale_factor = (base_color_scale[0], base_color_scale[1], base_color_scale[2], base_color_scale[3]) 239 | else: 240 | destination = base_color_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.RGBA) 241 | scale_factor = tuple(base_color_scale[0:4]) 242 | 243 | usd_uv_texture = USDUVTexture("baseColorTexture", self._stage, self._usd_material._usd_material, base_color_texture, [self._st0, self._st1]) 244 | usd_uv_texture._file_asset.Set(destination) 245 | usd_uv_texture._scale.Set(scale_factor) 246 | usd_uv_texture._fallback.Set(scale_factor) 247 | texture_shader = usd_uv_texture.get_shader() 248 | texture_shader.CreateOutput('rgb', Sdf.ValueTypeNames.Float3) 249 | texture_shader.CreateOutput('a', Sdf.ValueTypeNames.Float) 250 | self._diffuse_color.ConnectToSource(texture_shader, 'rgb') 251 | 252 | # set opacity and opacity threshold 253 | if AlphaMode(alpha_mode) == AlphaMode.OPAQUE: 254 | print("Opaque mode detected!") 255 | self._opacity.Set(1) 256 | 257 | if AlphaMode(alpha_mode) == AlphaMode.BLEND: 258 | print("Blend mode detected!") 259 | self._opacity.ConnectToSource(texture_shader, 'a') 260 | self._opacity.Set(defaultAlpha) 261 | 262 | if AlphaMode(alpha_mode) == AlphaMode.MASK: 263 | print("Mask mode detected defaulting to blend mode.") 264 | self._opacity.ConnectToSource(texture_shader, 'a') 265 | self._opacity.Set(defaultAlpha) 266 | 267 | self._opacityThreshold.Set(alpha_cutoff) 268 | 269 | def _set_pbr_metallic(self, pbr_metallic_roughness): 270 | metallic_roughness_texture = pbr_metallic_roughness.get_metallic_roughness_texture() 271 | metallic_factor = pbr_metallic_roughness.get_metallic_factor() 272 | if not metallic_roughness_texture or metallic_factor == 0: 273 | self._metallic.Set(metallic_factor) 274 | else: 275 | scale_texture = None 276 | scale_factor = tuple([metallic_factor]*4) 277 | usd_uv_texture = USDUVTexture("metallicTexture", self._stage, self._usd_material._usd_material, metallic_roughness_texture, [self._st0, self._st1]) 278 | if self._scale_texture: 279 | scale_texture = scale_factor 280 | usd_uv_texture._scale.Set(tuple([1.0]*4)) 281 | else: 282 | usd_uv_texture._scale.Set(scale_factor) 283 | destination = metallic_roughness_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.B, "metallic", scale_texture) 284 | 285 | usd_uv_texture._file_asset.Set(destination) 286 | usd_uv_texture._fallback.Set(scale_factor) 287 | texture_shader = usd_uv_texture.get_shader() 288 | texture_shader.CreateOutput('r', Sdf.ValueTypeNames.Float) 289 | self._metallic.ConnectToSource(texture_shader, 'r') 290 | 291 | def _set_pbr_roughness(self, pbr_metallic_roughness): 292 | metallic_roughness_texture = pbr_metallic_roughness.get_metallic_roughness_texture() 293 | roughness_factor = pbr_metallic_roughness.get_roughness_factor() 294 | if not metallic_roughness_texture or roughness_factor == 0: 295 | self._roughness.Set(roughness_factor) 296 | else: 297 | scale_texture = None 298 | scale_factor = tuple([roughness_factor]*4) 299 | usd_uv_texture = USDUVTexture("roughnessTexture", self._stage, self._usd_material._usd_material, metallic_roughness_texture, [self._st0, self._st1]) 300 | if self._scale_texture: 301 | scale_texture = scale_factor 302 | usd_uv_texture._scale.Set(tuple([1.0]*4)) 303 | else: 304 | usd_uv_texture._scale.Set(scale_factor) 305 | 306 | destination = metallic_roughness_texture.write_to_directory(self._output_directory, GLTFImage.ImageColorChannels.G, "roughness", scale_texture) 307 | usd_uv_texture._file_asset.Set(destination) 308 | usd_uv_texture._fallback.Set(scale_factor) 309 | texture_shader = usd_uv_texture.get_shader() 310 | texture_shader.CreateOutput('r', Sdf.ValueTypeNames.Float) 311 | self._roughness.ConnectToSource(texture_shader, 'r') 312 | 313 | def export_to_stage(self, usd_material): 314 | """Converts a glTF material to a usd preview surface 315 | 316 | Arguments: 317 | gltf_material {Material} -- glTF Material 318 | """ 319 | material = UsdShade.Shader.Define(name, usd_material._stage, usd_material._material_path.AppendChild(self._name)) 320 | material.CreateIdAttr('UsdPreviewSurface') 321 | material.CreateInput('useSpecularWorkflow', Sdf.ValueTypeNames.Int).Set(self._use_specular_workflow) 322 | surface_output = material.CreateOutput('surface', Sdf.ValueTypeNames.Token) 323 | usd_material._usd_material_surface_output.ConnectToSource(surface_output) 324 | displacement_output = material.CreateOutput('displacement', Sdf.ValueTypeNames.Token) 325 | usd_material._usd_material_displacement_output.ConnectToSource(displacement_output) 326 | 327 | 328 | class USDPrimvarReaderFloat2(object): 329 | def __init__(self, stage, material_path, var_name): 330 | primvar = UsdShade.Shader.Define(stage, material_path.AppendChild('primvar_{}'.format(var_name))) 331 | primvar.CreateIdAttr('UsdPrimvarReader_float2') 332 | primvar.CreateInput('fallback', Sdf.ValueTypeNames.Float2).Set((0,0)) 333 | primvar.CreateInput('varname', Sdf.ValueTypeNames.Token).Set(var_name) 334 | self._output = primvar.CreateOutput('result', Sdf.ValueTypeNames.Float2) 335 | 336 | def get_output(self): 337 | return self._output 338 | 339 | 340 | 341 | class USDUVTextureWrapMode(Enum): 342 | BLACK = 'black' 343 | CLAMP = 'clamp' 344 | REPEAT = 'repeat' 345 | MIRROR = 'mirror' 346 | 347 | 348 | class USDUVTexture(object): 349 | TEXTURE_SAMPLER_WRAP = { 350 | TextureWrap.CLAMP_TO_EDGE.name : 'clamp', 351 | TextureWrap.MIRRORED_REPEAT.name : 'mirror', 352 | TextureWrap.REPEAT.name: 'repeat', 353 | } 354 | def __init__(self, name, stage, usd_material, gltf_texture, usd_primvar_st_arr): 355 | 356 | material_path = usd_material.GetPath() 357 | 358 | self._texture_shader = UsdShade.Shader.Define(stage, material_path.AppendChild(name)) 359 | self._texture_shader.CreateIdAttr("UsdUVTexture") 360 | 361 | self._wrap_s = self._texture_shader.CreateInput('wrapS', Sdf.ValueTypeNames.Token) 362 | self._wrap_s.Set(USDUVTexture.TEXTURE_SAMPLER_WRAP[gltf_texture.get_wrap_s().name]) 363 | 364 | self._wrap_t =self._texture_shader.CreateInput('wrapT', Sdf.ValueTypeNames.Token) 365 | self._wrap_t.Set(USDUVTexture.TEXTURE_SAMPLER_WRAP[gltf_texture.get_wrap_t().name]) 366 | 367 | self._bias = self._texture_shader.CreateInput('bias', Sdf.ValueTypeNames.Float4) 368 | self._bias.Set((0,0,0,0)) 369 | 370 | self._scale = self._texture_shader.CreateInput('scale', Sdf.ValueTypeNames.Float4) 371 | self._scale.Set((1,1,1,1)) 372 | 373 | self._file_asset = self._texture_shader.CreateInput('file', Sdf.ValueTypeNames.Asset) 374 | self._file_asset.Set(gltf_texture.get_image_path()) 375 | 376 | self._fallback = self._texture_shader.CreateInput('fallback', Sdf.ValueTypeNames.Float4) 377 | self._fallback.Set((0,0,0,1)) 378 | 379 | self._st = self._texture_shader.CreateInput('st', Sdf.ValueTypeNames.Float2) 380 | 381 | self._st.ConnectToSource(usd_primvar_st_arr[gltf_texture.get_texcoord_index()].get_output()) 382 | 383 | 384 | def get_shader(self): 385 | return self._texture_shader 386 | 387 | 388 | 389 | 390 | -------------------------------------------------------------------------------- /Source/_gltf2usd/version.py: -------------------------------------------------------------------------------- 1 | class Version(object): 2 | """Version API for gltf2usd 3 | """ 4 | _major = 0 5 | _minor = 2 6 | _patch = 1 7 | @staticmethod 8 | def get_major_version_number(): 9 | """Returns the major version 10 | 11 | Returns: 12 | int -- major version number 13 | """ 14 | 15 | return Version._major 16 | 17 | @staticmethod 18 | def get_minor_version_number(): 19 | """Returns the minor version number 20 | 21 | Returns: 22 | int -- minor version number 23 | """ 24 | 25 | return Version._minor 26 | 27 | @staticmethod 28 | def get_patch_version_number(): 29 | """Patch version number 30 | 31 | Returns: 32 | int -- patch version number 33 | """ 34 | 35 | return Version._patch 36 | 37 | @staticmethod 38 | def get_version_name(): 39 | """Returns the version name 40 | 41 | Returns: 42 | str -- version mame 43 | """ 44 | 45 | return '{0}.{1}.{2}'.format(Version._major, Version._minor, Version._patch) -------------------------------------------------------------------------------- /Source/gltf2usd.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | import argparse 4 | import base64 5 | import collections 6 | import filecmp 7 | import json 8 | import logging 9 | import ntpath 10 | import numpy 11 | import os 12 | import tempfile 13 | import re 14 | import shutil 15 | from io import BytesIO 16 | 17 | from PIL import Image 18 | 19 | from pxr import Usd, UsdGeom, Sdf, UsdShade, Gf, UsdSkel, Vt, Ar, UsdUtils 20 | 21 | from _gltf2usd.gltf2loader import GLTF2Loader, PrimitiveMode, TextureWrap, MinFilter, MagFilter 22 | from _gltf2usd.gltf2usdUtils import GLTF2USDUtils 23 | from _gltf2usd.usd_material import USDMaterial 24 | 25 | from _gltf2usd import version 26 | 27 | __version__ = version.Version.get_version_name() 28 | 29 | class GLTF2USD(object): 30 | """ 31 | Class for converting glTF 2.0 models to Pixar's USD format. Currently openly supports .gltf files 32 | with non-embedded data and exports to .usda . 33 | """ 34 | 35 | TEXTURE_SAMPLER_WRAP = { 36 | TextureWrap.CLAMP_TO_EDGE : 'clamp', 37 | TextureWrap.MIRRORED_REPEAT : 'mirror', 38 | TextureWrap.REPEAT: 'repeat', 39 | } 40 | 41 | def __init__(self, gltf_file, usd_file, fps, scale, verbose=False, use_euler_rotation=False, optimize_textures=False, generate_texture_transform_texture=True, scale_texture=False): 42 | """Initializes the glTF to USD converter 43 | 44 | Arguments: 45 | gltf_file {str} -- path to the glTF file 46 | usd_file {str} -- path to store the generated usda file 47 | verbose {boolean} -- specifies if the output should be verbose from this tool 48 | """ 49 | self.logger = logging.getLogger('gltf2usd') 50 | self.logger.setLevel(logging.DEBUG) 51 | console_handler = logging.StreamHandler() 52 | console_handler.setLevel(logging.DEBUG) 53 | self.logger.addHandler(console_handler) 54 | 55 | self.fps = fps 56 | self.gltf_loader = GLTF2Loader(gltf_file, optimize_textures, generate_texture_transform_texture) 57 | self.verbose = verbose 58 | self.scale = scale 59 | self.use_euler_rotation = use_euler_rotation 60 | self._scale_texture = scale_texture 61 | 62 | self.output_dir = os.path.dirname(os.path.abspath(usd_file)) 63 | if self.verbose: 64 | self.logger.info("Converting {0} to {1}".format(gltf_file, usd_file)) 65 | 66 | #if usdz file is desired, change to usdc file 67 | if usd_file.endswith('usdz'): 68 | usd_file = usd_file[:-1] + 'c' 69 | self.stage = Usd.Stage.CreateNew(usd_file) 70 | self.gltf_usd_nodemap = {} 71 | self.gltf_usdskel_nodemap = {} 72 | self._usd_mesh_skin_map = {} 73 | self._joint_hierarchy_name_map = {} 74 | 75 | self.convert() 76 | 77 | 78 | def convert_nodes_to_xform(self): 79 | """ 80 | Converts the glTF nodes to USD Xforms. The models get a parent Xform that scales the geometry by 100 81 | to convert from meters (glTF) to centimeters (USD). 82 | """ 83 | parent_transform = UsdGeom.Xform.Define(self.stage, '/root') 84 | parent_transform.AddScaleOp().Set((self.scale, self.scale, self.scale)) 85 | 86 | main_scene = self.gltf_loader.get_main_scene() 87 | 88 | nodes = main_scene.get_nodes() 89 | root_nodes = [node for node in nodes if node.parent == None] 90 | for node in root_nodes: 91 | self._convert_node_to_xform(node, parent_transform) 92 | 93 | 94 | def _convert_node_to_xform(self, node, usd_xform): 95 | """Converts a glTF node to a USD transform node. 96 | 97 | Arguments: 98 | node {dict} -- glTF node 99 | node_index {int} -- glTF node index 100 | xform_name {str} -- USD xform name 101 | """ 102 | xformPrim = UsdGeom.Xform.Define(self.stage, '{0}/{1}'.format(usd_xform.GetPath(), GLTF2USDUtils.convert_to_usd_friendly_node_name(node.name))) 103 | 104 | if self._node_has_animations(node): 105 | self._convert_animation_to_usd(node, xformPrim) 106 | else: 107 | xformPrim.AddTransformOp().Set(self._compute_rest_matrix(node)) 108 | 109 | 110 | mesh = node.get_mesh() 111 | if mesh != None: 112 | usd_mesh = self._convert_mesh_to_xform(mesh, xformPrim, node) 113 | 114 | children = node.get_children() 115 | 116 | for child in children: 117 | self._convert_node_to_xform(child, xformPrim) 118 | 119 | if node.extras: 120 | self.stage.OverridePrim(xformPrim.GetPath()).SetCustomData(node.extras) 121 | 122 | 123 | def _create_usd_skeleton(self, gltf_skin, usd_xform, usd_joint_names): 124 | """Creates a USD skeleton from a glTF skin 125 | 126 | Arguments: 127 | gltf_skin {Skin} -- gltf skin 128 | usd_xform {Xform} -- USD Xform 129 | 130 | Returns: 131 | Skeleton -- USD skeleton 132 | """ 133 | 134 | # create skeleton 135 | root_joints = gltf_skin.root_joints 136 | root_joint_names = [GLTF2USDUtils.convert_to_usd_friendly_node_name(root_joint.name) for root_joint in root_joints] 137 | 138 | skeleton = None 139 | 140 | if len(root_joints) == 1: 141 | skeleton = UsdSkel.Skeleton.Define(self.stage, '{0}/{1}'.format(usd_xform.GetPath(), root_joint_names[0])) 142 | else: 143 | skeleton = UsdSkel.Skeleton.Define(self.stage, '{0}/{1}'.format(usd_xform.GetPath(), '__root__')) 144 | 145 | gltf_bind_transforms = [Gf.Matrix4d(*xform).GetInverse() for xform in gltf_skin.get_inverse_bind_matrices()] 146 | gltf_rest_transforms = [GLTF2USDUtils.compute_usd_transform_matrix_from_gltf_node(joint) for joint in gltf_skin.get_joints()] 147 | if len(root_joints) > 1: 148 | matrix = Gf.Matrix4d() 149 | matrix.SetIdentity() 150 | 151 | skeleton.CreateJointsAttr().Set(usd_joint_names) 152 | skeleton.CreateBindTransformsAttr(gltf_bind_transforms) 153 | skeleton.CreateRestTransformsAttr(gltf_rest_transforms) 154 | 155 | return skeleton 156 | 157 | def _create_usd_skeleton_animation(self, gltf_skin, usd_skeleton, joint_names): 158 | #get the animation data per joint 159 | skelAnim = None 160 | gltf_animations = self.gltf_loader.get_animations() 161 | 162 | if len(gltf_animations): 163 | skelAnim = UsdSkel.Animation.Define(self.stage, '{0}/{1}'.format(usd_skeleton.GetPath(), 'anim')) 164 | 165 | usd_skel_root_path = usd_skeleton.GetPath().GetParentPath() 166 | usd_skel_root = self.stage.GetPrimAtPath(usd_skel_root_path) 167 | 168 | skelAnim.CreateJointsAttr().Set(joint_names) 169 | gltf_animation = self.gltf_loader.get_animations()[0] 170 | min_sample = 999 171 | max_sample = -999 172 | for sampler in gltf_animation.get_samplers(): 173 | input_data = sampler.get_input_data() 174 | min_sample = min(min_sample, input_data[0]) 175 | max_sample = max(max_sample, input_data[-1]) 176 | 177 | rotate_attr = skelAnim.CreateRotationsAttr() 178 | for input_key in numpy.arange(min_sample, max_sample, 1./self.fps): 179 | entries = [] 180 | for joint in gltf_skin.get_joints(): 181 | anim = self._get_anim_data_for_joint_and_path(gltf_animation, joint, 'rotation', input_key) 182 | entries.append(anim) 183 | 184 | if len(gltf_skin.get_joints()) != len(entries): 185 | raise Exception('up oh!') 186 | 187 | rotate_attr.Set(Vt.QuatfArray(entries), Usd.TimeCode(input_key * self.fps)) 188 | 189 | translate_attr = skelAnim.CreateTranslationsAttr() 190 | for input_key in numpy.arange(min_sample, max_sample, 1./self.fps): 191 | entries = [] 192 | for joint in gltf_skin.get_joints(): 193 | anim = self._get_anim_data_for_joint_and_path(gltf_animation, joint, 'translation', input_key) 194 | entries.append(anim) 195 | 196 | if len(gltf_skin.get_joints()) != len(entries): 197 | raise Exception('up oh!') 198 | 199 | translate_attr.Set(entries, Usd.TimeCode(input_key * self.fps)) 200 | 201 | scale_attr = skelAnim.CreateScalesAttr() 202 | for input_key in numpy.arange(min_sample, max_sample, 1./self.fps): 203 | entries = [] 204 | for joint in gltf_skin.get_joints(): 205 | anim = self._get_anim_data_for_joint_and_path(gltf_animation, joint, 'scale', input_key) 206 | entries.append(anim) 207 | 208 | if len(gltf_skin.get_joints()) != len(entries): 209 | raise Exception('up oh!') 210 | 211 | scale_attr.Set(entries, Usd.TimeCode(input_key * self.fps)) 212 | 213 | return skelAnim 214 | 215 | def _get_anim_data_for_joint_and_path(self, gltf_animation, gltf_joint, path, time_sample): 216 | anim_channel = gltf_animation.get_animation_channel_for_node_and_path(gltf_joint, path) 217 | if not anim_channel: 218 | if path == 'translation': 219 | return gltf_joint.translation 220 | elif path == 'rotation': 221 | gltf_rotation = gltf_joint.rotation 222 | usd_rotation = Gf.Quatf(gltf_rotation[3], gltf_rotation[0], gltf_rotation[1], gltf_rotation[2]) 223 | return usd_rotation 224 | elif path == 'scale': 225 | return gltf_joint.scale 226 | 227 | else: 228 | if path == 'rotation': 229 | rotation = anim_channel.sampler.get_interpolated_output_data(time_sample) 230 | return rotation 231 | elif path == 'scale' or path =='translation': 232 | return anim_channel.sampler.get_interpolated_output_data(time_sample) 233 | else: 234 | raise Exception('unsupported animation type: {}'.format(path)) 235 | 236 | 237 | def _get_usd_joint_hierarchy_name(self, gltf_joint, root_joints): 238 | if gltf_joint in self._joint_hierarchy_name_map: 239 | return GLTF2USDUtils.convert_to_usd_friendly_node_name(self._joint_hierarchy_name_map[gltf_joint]) 240 | 241 | joint = gltf_joint 242 | joint_name_stack = [GLTF2USDUtils.convert_to_usd_friendly_node_name(joint.name)] 243 | 244 | while joint.parent != None and joint not in root_joints: 245 | joint = joint.parent 246 | joint_name_stack.append(GLTF2USDUtils.convert_to_usd_friendly_node_name(joint.name)) 247 | 248 | joint_name = '' 249 | while len(joint_name_stack) > 0: 250 | if joint_name: 251 | joint_name = '{0}/{1}'.format(joint_name, joint_name_stack.pop()) 252 | else: 253 | joint_name = joint_name_stack.pop() 254 | 255 | self._joint_hierarchy_name_map[gltf_joint] = joint_name 256 | return GLTF2USDUtils.convert_to_usd_friendly_node_name(joint_name) 257 | 258 | 259 | def _convert_mesh_to_xform(self, gltf_mesh, usd_node, gltf_node): 260 | """ 261 | Converts a glTF mesh to a USD Xform. 262 | Each primitive becomes a submesh of the Xform. 263 | 264 | Arguments: 265 | mesh {dict} -- glTF mesh 266 | parent_path {str} -- xform path 267 | node_index {int} -- glTF node index 268 | """ 269 | #for each mesh primitive, create a USD mesh 270 | primitive_count = 1 271 | max_primitive_count = len(gltf_mesh.get_primitives()) 272 | for primitive in gltf_mesh.get_primitives(): 273 | self._convert_primitive_to_mesh(primitive, usd_node, gltf_node, gltf_mesh) 274 | if self.verbose: 275 | self.logger.debug('mesh {0}: primitive {1}/{2}'.format(gltf_mesh.name, primitive_count, max_primitive_count)) 276 | 277 | primitive_count += 1 278 | 279 | def _convert_primitive_to_mesh(self, gltf_primitive, usd_node, gltf_node, gltf_mesh): 280 | """ 281 | Converts a glTF mesh primitive to a USD mesh 282 | 283 | Arguments: 284 | name {str} -- name of the primitive 285 | primitive {dict} -- glTF primitive 286 | usd_parent_node {str} -- USD parent xform 287 | node_index {int} -- glTF node index 288 | double_sided {bool} -- specifies if the primitive is double sided 289 | """ 290 | parent_node = usd_node 291 | parent_path = parent_node.GetPath() 292 | attributes = gltf_primitive.get_attributes() 293 | skel_root = None 294 | targets = gltf_primitive.get_morph_targets() 295 | if 'JOINTS_0' in attributes or len(targets) > 0: 296 | skeleton_path = '{0}/{1}'.format(usd_node.GetPath(), 'skeleton_root') 297 | skel_root = UsdSkel.Root.Define(self.stage, skeleton_path) 298 | parent_node = skel_root 299 | parent_path = parent_node.GetPath() 300 | mesh = UsdGeom.Mesh.Define(self.stage, '{0}/{1}'.format(parent_node.GetPath(), GLTF2USDUtils.convert_to_usd_friendly_node_name(gltf_primitive.get_name()))) 301 | mesh.CreateSubdivisionSchemeAttr().Set('none') 302 | 303 | material = gltf_primitive.get_material() 304 | if material != None: 305 | if material.is_double_sided(): 306 | mesh.CreateDoubleSidedAttr().Set(True) 307 | 308 | usd_material = self.usd_materials[material.get_index()] 309 | UsdShade.MaterialBindingAPI(mesh).Bind(usd_material.get_usd_material()) 310 | 311 | for attribute_name in attributes: 312 | attribute = attributes[attribute_name] 313 | if attribute_name == 'POSITION': 314 | override_prim = self.stage.OverridePrim(mesh.GetPath()) 315 | override_prim.CreateAttribute('extent', Sdf.ValueTypeNames.Float3Array).Set([attribute.get_min_value(), attribute.get_max_value()]) 316 | mesh.CreatePointsAttr(attribute.get_data()) 317 | 318 | if attribute_name == 'NORMAL': 319 | mesh.CreateNormalsAttr(attribute.get_data()) 320 | 321 | if attribute_name == 'COLOR_0': 322 | prim_var = UsdGeom.PrimvarsAPI(mesh) 323 | data = attribute.get_data() 324 | if attribute.accessor_type == 'VEC4': 325 | print('Vertex color alpha currently not supported. Defaulting to vertex color without alpha.') 326 | data = [Gf.Vec3f(entry[0:3]) for entry in attribute.get_data()] 327 | 328 | colors = prim_var.CreatePrimvar('displayColor', Sdf.ValueTypeNames.Color3f, 'vertex').Set(data) 329 | 330 | if attribute_name == 'TEXCOORD_0': 331 | data = attribute.get_data() 332 | invert_uvs = [] 333 | for uv in data: 334 | new_uv = (uv[0], 1 - uv[1]) 335 | invert_uvs.append(new_uv) 336 | prim_var = UsdGeom.PrimvarsAPI(mesh) 337 | uv = prim_var.CreatePrimvar('primvars:st0', Sdf.ValueTypeNames.TexCoord2fArray, 'vertex') 338 | uv.Set(invert_uvs) 339 | 340 | if attribute_name == 'TEXCOORD_1': 341 | data = attribute.get_data() 342 | invert_uvs = [] 343 | for uv in data: 344 | new_uv = (uv[0], 1 - uv[1]) 345 | invert_uvs.append(new_uv) 346 | prim_var = UsdGeom.PrimvarsAPI(mesh) 347 | uv = prim_var.CreatePrimvar('primvars:st1', Sdf.ValueTypeNames.TexCoord2fArray, 'vertex') 348 | uv.Set(invert_uvs) 349 | 350 | if attribute_name == 'JOINTS_0': 351 | self._convert_skin_to_usd(gltf_node, gltf_primitive, parent_node, mesh) 352 | 353 | weights = gltf_mesh.get_weights() 354 | if targets: 355 | skinBinding = UsdSkel.BindingAPI.Apply(mesh.GetPrim()) 356 | 357 | skeleton = UsdSkel.Skeleton.Define(self.stage, '{0}/skel'.format(parent_path)) 358 | 359 | # Create an animation for this mesh to hold the blendshapes 360 | skelAnim = UsdSkel.Animation.Define(self.stage, '{0}/skel/anim'.format(parent_path)) 361 | 362 | # link the skeleton animation to skelAnim 363 | skinBinding.CreateAnimationSourceRel().AddTarget(skelAnim.GetPath()) 364 | skeleton_skel_binding = UsdSkel.BindingAPI(skeleton) 365 | skeleton_skel_binding.CreateAnimationSourceRel().AddTarget(skelAnim.GetPath()) 366 | 367 | # link the skeleton to skeleton 368 | skinBinding.CreateSkeletonRel().AddTarget(skeleton.GetPath()) 369 | 370 | # Set blendshape names on the animation 371 | names = [] 372 | for i, _ in enumerate(gltf_mesh.get_weights()): 373 | targets[i].get_name() 374 | blend_shape_name = GLTF2USDUtils.convert_to_usd_friendly_node_name(targets[i].get_name()) 375 | names.append(blend_shape_name) 376 | 377 | skelAnim.CreateBlendShapesAttr().Set(names) 378 | self._set_blendshape_weights(skelAnim, usd_node, gltf_node) 379 | skinBinding.CreateBlendShapesAttr(names) 380 | 381 | # Set the starting weights of each blendshape to the weights defined in the glTF primitive 382 | blend_shape_targets = skinBinding.CreateBlendShapeTargetsRel() 383 | 384 | # Define offsets for each blendshape, and add them as skel:blendShapes and skel:blendShapeTargets 385 | for i, name in enumerate(names): 386 | offsets = targets[i].get_attributes()['POSITION'] 387 | blend_shape_name = '{0}/{1}'.format(mesh.GetPath(), name) 388 | 389 | # define blendshapes in the mesh 390 | blend_shape = UsdSkel.BlendShape.Define(self.stage, blend_shape_name) 391 | blend_shape.CreateOffsetsAttr(offsets) 392 | blend_shape_targets.AddTarget(name) 393 | 394 | indices = gltf_primitive.get_indices() 395 | num_faces = len(indices)/3 396 | face_count = [3] * num_faces 397 | mesh.CreateFaceVertexCountsAttr(face_count) 398 | mesh.CreateFaceVertexIndicesAttr(indices) 399 | 400 | def _get_texture__wrap_modes(self, texture): 401 | """Get the USD texture wrap modes from a glTF texture 402 | 403 | Arguments: 404 | texture {dict} -- glTF texture 405 | 406 | Returns: 407 | dict -- dictionary mapping wrapS and wrapT to 408 | a USD texture sampler mode 409 | """ 410 | 411 | texture_data = {'wrapS': 'repeat', 'wrapT': 'repeat'} 412 | if 'sampler' in texture: 413 | sampler = self.gltf_loader.json_data['samplers'][texture['sampler']] 414 | 415 | if 'wrapS' in sampler: 416 | texture_data['wrapS'] = GLTF2USD.TEXTURE_SAMPLER_WRAP[TextureWrap(sampler['wrapS'])] 417 | 418 | if 'wrapT' in sampler: 419 | texture_data['wrapT'] = GLTF2USD.TEXTURE_SAMPLER_WRAP[TextureWrap(sampler['wrapT'])] 420 | 421 | return texture_data 422 | 423 | def _convert_images_to_usd(self): 424 | """ 425 | Converts the glTF images to USD images 426 | """ 427 | 428 | if 'images' in self.gltf_loader.json_data: 429 | self.images = [] 430 | for i, image in enumerate(self.gltf_loader.json_data['images']): 431 | image_name = '' 432 | 433 | # save data-uri textures 434 | if 'bufferView' in image or image['uri'].startswith('data:image'): 435 | img = None 436 | if 'bufferView' in image: 437 | buffer_view = self.gltf_loader.json_data['bufferViews'][image['bufferView']] 438 | buffer = self.gltf_loader.json_data['buffers'][buffer_view['buffer']] 439 | img_base64 = buffer['uri'].split(',')[1] 440 | buff = BytesIO() 441 | buff.write(base64.b64decode(img_base64)) 442 | buff.seek(buffer_view['byteOffset']) 443 | img = Image.open(BytesIO(buff.read(buffer_view['byteLength']))) 444 | 445 | elif image['uri'].startswith('data:image'): 446 | uri_data = image['uri'].split(',')[1] 447 | img = Image.open(BytesIO(base64.b64decode(uri_data))) 448 | 449 | # NOTE: image might not have a name 450 | image_name = image['name'] if 'name' in image else 'image{}.{}'.format(i, img.format.lower()) 451 | image_path = os.path.join(self.gltf_loader.root_dir, image_name) 452 | img.save(image_path) 453 | 454 | # otherwise just copy the texture over 455 | else: 456 | image_path = os.path.join(self.gltf_loader.root_dir, image['uri']) 457 | image_name = os.path.join(self.output_dir, ntpath.basename(image_path)) 458 | 459 | if (self.gltf_loader.root_dir is not self.output_dir) and (image_path is not image_name): 460 | if not (os.path.isfile(image_name) and filecmp.cmp(image_path, image_name)): 461 | shutil.copyfile(image_path, image_name) 462 | 463 | self.images.append(ntpath.basename(image_name)) 464 | 465 | 466 | def _convert_materials_to_preview_surface_new(self): 467 | """ 468 | Converts the glTF materials to preview surfaces 469 | """ 470 | self.usd_materials = [] 471 | material_path_root = '/root/Materials' 472 | scope = UsdGeom.Scope.Define(self.stage, material_path_root) 473 | material_name_map = [] 474 | 475 | for i, material in enumerate(self.gltf_loader.get_materials()): 476 | material_name = '{0}_index_{1}'.format(GLTF2USDUtils.convert_to_usd_friendly_node_name(material.get_name()), i) 477 | #check for unique material name 478 | if material_name in material_name_map: 479 | count = 1 480 | new_material_name = '{0}_{1}'.format(material_name, count) 481 | 482 | while new_material_name in material_name_map: 483 | count += 1 484 | new_material_name = '{0}_{1}'.format(material_name, count) 485 | material_name = new_material_name 486 | 487 | material_name_map.append(material_name) 488 | usd_material = USDMaterial(self.stage, material_name, scope, i, self.gltf_loader, self._scale_texture) 489 | usd_material.convert_material_to_usd_preview_surface(material, self.output_dir, material_name) 490 | self.usd_materials.append(usd_material) 491 | 492 | def _node_has_animations(self, gltf_node): 493 | animations = self.gltf_loader.get_animations() 494 | for animation in animations: 495 | animation_channels = animation.get_animation_channels_for_node(gltf_node) 496 | if len(animation_channels) > 0: 497 | return True 498 | 499 | 500 | return False 501 | 502 | 503 | def _convert_animation_to_usd(self, gltf_node, usd_node): 504 | animations = self.gltf_loader.get_animations() 505 | if (len(animations) > 0): # only support first animation group 506 | animation = animations[0] 507 | 508 | animation_channels = animation.get_animation_channels_for_node(gltf_node) 509 | 510 | if len(animation_channels) > 0: 511 | total_max_time = -999 512 | total_min_time = 999 513 | 514 | min_max_time = self._create_usd_animation2(usd_node, gltf_node, animation_channels) 515 | 516 | total_max_time = max(total_max_time, min_max_time.max) 517 | total_min_time = min(total_min_time, min_max_time.min) 518 | 519 | self.stage.SetStartTimeCode(total_min_time * self.fps) 520 | self.stage.SetEndTimeCode(total_max_time * self.fps) 521 | self.stage.SetTimeCodesPerSecond(self.fps) 522 | 523 | def _create_keyframe_transform_node(self, gltf_node, animation_channels, input_sample): 524 | matrix = gltf_node.matrix 525 | if matrix: 526 | translation = Gf.Vec3f() 527 | rotation = Gf.Quatf() 528 | scale = Gf.Vec3h() 529 | usd_matrix = self._convert_to_usd_matrix(matrix) 530 | UsdSkel.DecomposeTransform(usd_matrix, translation, rotation, scale) 531 | else: 532 | translation = Gf.Vec3f(gltf_node.translation) 533 | rotation = Gf.Quatf(gltf_node.rotation[3], gltf_node.rotation[0], gltf_node.rotation[1], gltf_node.rotation[2]) 534 | scale = Gf.Vec3h(gltf_node.scale) 535 | 536 | for animation_channel in animation_channels: 537 | if animation_channel.target.path == 'translation': 538 | translation = animation_channel.sampler.get_interpolated_output_data(input_sample) 539 | elif animation_channel.target.path == 'rotation': 540 | rotation = animation_channel.sampler.get_interpolated_output_data(input_sample) 541 | elif animation_channel.target.path == 'scale': 542 | scale = animation_channel.sampler.get_interpolated_output_data(input_sample) 543 | elif animation_channel.target.path == 'weights': 544 | weights = animation_channel.sampler.get_output_data() 545 | 546 | return UsdSkel.MakeTransform(translation, rotation, scale) 547 | 548 | def _convert_skin_to_usd(self, gltf_node, gltf_primitive, usd_node, usd_mesh): 549 | """Converts a glTF skin to a UsdSkel 550 | 551 | Arguments: 552 | gltf_node {dict} -- glTF node 553 | node_index {int} -- index of the glTF node 554 | usd_parent_node {UsdPrim} -- parent node of the usd node 555 | usd_mesh {[type]} -- [description] 556 | """ 557 | skel_binding_api = UsdSkel.BindingAPI(usd_mesh) 558 | gltf_skin = gltf_node.get_skin() 559 | if not gltf_skin: 560 | self.logger.warning('The glTF node has joints, but no skin associated with them') 561 | else: 562 | gltf_joint_names = [GLTF2USDUtils.convert_to_usd_friendly_node_name(joint.name) for joint in gltf_skin.get_joints()] 563 | usd_joint_names = [Sdf.Path(self._get_usd_joint_hierarchy_name(joint, gltf_skin.root_joints)) for joint in gltf_skin.get_joints()] 564 | skeleton = self._create_usd_skeleton(gltf_skin, usd_node, usd_joint_names) 565 | skeleton_animation = self._create_usd_skeleton_animation(gltf_skin, skeleton, usd_joint_names) 566 | 567 | parent_path = usd_node.GetPath() 568 | 569 | bind_matrices = [] 570 | rest_matrices = [] 571 | 572 | skeleton_root = self.stage.GetPrimAtPath(parent_path) 573 | skel_binding_api = UsdSkel.BindingAPI(usd_mesh) 574 | skel_binding_api.CreateGeomBindTransformAttr(Gf.Matrix4d(((1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1)))) 575 | skel_binding_api.CreateSkeletonRel().AddTarget(skeleton.GetPath()) 576 | if skeleton_animation: 577 | skel_binding_api.CreateAnimationSourceRel().AddTarget(skeleton_animation.GetPath()) 578 | skeleton_skel_binding_api = UsdSkel.BindingAPI(skeleton) 579 | skeleton_skel_binding_api.CreateAnimationSourceRel().AddTarget(skeleton_animation.GetPath()) 580 | 581 | bind_matrices = self._compute_bind_transforms(gltf_skin) 582 | 583 | primitive_attributes = gltf_primitive.get_attributes() 584 | 585 | if 'WEIGHTS_0' in primitive_attributes and 'JOINTS_0' in primitive_attributes: 586 | total_vertex_weights = primitive_attributes['WEIGHTS_0'].get_data() 587 | total_vertex_joints = primitive_attributes['JOINTS_0'].get_data() 588 | total_joint_indices = [] 589 | total_joint_weights = [] 590 | 591 | for joint_indices, weights in zip(total_vertex_joints, total_vertex_weights): 592 | for joint_index, weight in zip(joint_indices, weights): 593 | total_joint_indices.append(joint_index) 594 | total_joint_weights.append(weight) 595 | 596 | joint_indices_attr = skel_binding_api.CreateJointIndicesPrimvar(False, 4).Set(total_joint_indices) 597 | total_joint_weights = Vt.FloatArray(total_joint_weights) 598 | UsdSkel.NormalizeWeights(total_joint_weights, 4) 599 | joint_weights_attr = skel_binding_api.CreateJointWeightsPrimvar(False, 4).Set(total_joint_weights) 600 | 601 | 602 | def _compute_bind_transforms(self, gltf_skin): 603 | """Compute the bind matrices from the skin 604 | 605 | Arguments: 606 | gltf_skin {Skin} -- glTF skin 607 | 608 | Returns: 609 | [list] -- List of bind matrices 610 | """ 611 | 612 | bind_matrices = [] 613 | inverse_bind_matrices = gltf_skin.get_inverse_bind_matrices() 614 | 615 | for matrix in inverse_bind_matrices: 616 | bind_matrix = self._convert_to_usd_matrix(matrix).GetInverse() 617 | bind_matrices.append(bind_matrix) 618 | 619 | return bind_matrices 620 | 621 | def _convert_to_usd_matrix(self, matrix): 622 | """Converts a glTF matrix to a Usd Matrix 623 | 624 | Arguments: 625 | matrix {[type]} -- [description] 626 | 627 | Returns: 628 | [type] -- [description] 629 | """ 630 | 631 | return Gf.Matrix4d( 632 | matrix[0], matrix[1], matrix[2], matrix[3], 633 | matrix[4], matrix[5], matrix[6], matrix[7], 634 | matrix[8], matrix[9], matrix[10], matrix[11], 635 | matrix[12], matrix[13], matrix[14], matrix[15] 636 | ) 637 | 638 | def _compute_rest_matrix(self, gltf_node): 639 | """ 640 | Compute the rest matrix from a glTF node. 641 | The translation, rotation and scale are combined into a transformation matrix 642 | 643 | Returns: 644 | Matrix4d -- USD matrix 645 | """ 646 | 647 | xform_matrix = None 648 | matrix = gltf_node.matrix 649 | if matrix != None: 650 | xform_matrix = self._convert_to_usd_matrix(matrix) 651 | return xform_matrix 652 | else: 653 | usd_scale = Gf.Vec3h(1,1,1) 654 | usd_rotation = Gf.Quatf().GetIdentity() 655 | usd_translation = Gf.Vec3f(0,0,0) 656 | 657 | scale = gltf_node.scale 658 | usd_scale = Gf.Vec3h(scale[0], scale[1], scale[2]) 659 | 660 | rotation = gltf_node.rotation 661 | usd_rotation = Gf.Quatf(rotation[3], rotation[0], rotation[1], rotation[2]) 662 | 663 | translation = gltf_node.translation 664 | usd_translation = Gf.Vec3f(translation[0], translation[1], translation[2]) 665 | 666 | return UsdSkel.MakeTransform(usd_translation, usd_rotation, usd_scale) 667 | 668 | 669 | def _create_usd_animation(self, usd_node, animation_channel): 670 | """Converts a glTF animation to a USD animation 671 | 672 | Arguments: 673 | usd_node {[type]} -- usd node 674 | animation_channel {AnimationMap} -- map of animation target path and animation sampler indices 675 | 676 | Returns: 677 | [type] -- [description] 678 | """ 679 | 680 | sampler = animation_channel.sampler 681 | 682 | 683 | max_time = int(round(sampler.get_input_max()[0] )) 684 | min_time = int(round(sampler.get_input_min()[0] )) 685 | input_keyframes = sampler.get_input_data() 686 | output_keyframes = sampler.get_output_data() 687 | 688 | num_values = sampler.get_output_count() / sampler.get_input_count() 689 | (transform, convert_func) = self._get_keyframe_conversion_func(usd_node, animation_channel) 690 | 691 | for i, keyframe in enumerate(numpy.arange(min_time, max_time, 1./self.fps)): 692 | convert_func(transform, keyframe, output_keyframes, i, num_values) 693 | 694 | MinMaxTime = collections.namedtuple('MinMaxTime', ('max', 'min')) 695 | return MinMaxTime(max=max_time, min=min_time) 696 | 697 | def _create_usd_animation2(self, usd_node, gltf_node, animation_channels): 698 | """Converts a glTF animation to a USD animation 699 | 700 | Arguments: 701 | usd_node {[type]} -- usd node 702 | gltf_node {[type]} -- glTF node 703 | animation_channel {AnimationMap} -- map of animation target path and animation sampler indices 704 | 705 | Returns: 706 | [type] -- [description] 707 | """ 708 | max_time = -999 709 | min_time = 999 710 | for channel in animation_channels: 711 | max_time = max(max_time, channel.sampler.get_input_max()[0]) 712 | min_time = min(min_time, channel.sampler.get_input_min()[0]) 713 | 714 | 715 | transform = usd_node.AddTransformOp(opSuffix='transform') 716 | for animation_channel in animation_channels: 717 | #if animation_channel.target.path != 'weights': 718 | for i, keyframe in enumerate(numpy.arange(min_time, max_time, 1./self.fps)): 719 | transform_node = self._create_keyframe_transform_node(gltf_node, animation_channels, keyframe) 720 | transform.Set(transform_node, Usd.TimeCode(i)) 721 | 722 | MinMaxTime = collections.namedtuple('MinMaxTime', ('max', 'min')) 723 | return MinMaxTime(max=max_time, min=min_time) 724 | 725 | def _set_blendshape_weights(self, skel_anim, usd_node, gltf_node): 726 | animations = self.gltf_loader.get_animations() 727 | if (len(animations) > 0): # only support first animation group 728 | animation = animations[0] 729 | 730 | animation_channels = animation.get_animation_channels_for_node(gltf_node) 731 | for animation_channel in animation_channels: 732 | if animation_channel.target.path == 'weights': 733 | output_data = animation_channel.sampler.get_output_data() 734 | input_data = animation_channel.sampler.get_input_data() 735 | 736 | output_data_entries = [] 737 | for index in range(0, len(output_data)/2): 738 | output_data_entries.append([output_data[index * 2], output_data[index * 2 + 1]]) 739 | usd_blend_shape_weights = skel_anim.CreateBlendShapeWeightsAttr() 740 | for entry in zip(output_data_entries, input_data): 741 | usd_blend_shape_weights.Set(Vt.FloatArray(entry[0]), Usd.TimeCode(entry[1] * self.fps)) 742 | 743 | def _get_keyframe_conversion_func(self, usd_node, animation_channel): 744 | """Convert glTF key frames to USD animation key frames 745 | 746 | Arguments: 747 | usd_node {UsdPrim} -- USD node to apply animations to 748 | animation_channel {obj} -- glTF animation 749 | 750 | Raises: 751 | Exception -- [description] 752 | 753 | Returns: 754 | [func] -- USD animation conversion function 755 | """ 756 | path = animation_channel.target.path 757 | animation_sampler = animation_channel.sampler 758 | 759 | def convert_translation(transform, time, output, i, _): 760 | value = animation_sampler.get_interpolated_output_data(time) 761 | transform.Set(time=time * self.fps, value=(value[0], value[1], value[2])) 762 | 763 | def convert_scale(transform, time, output, i, _): 764 | value = animation_sampler.get_interpolated_output_data(time) 765 | transform.Set(time=time * self.fps, value=(value[0], value[1], value[2])) 766 | 767 | def convert_rotation(transform, time, output, i, _): 768 | value = animation_sampler.get_interpolated_output_data(time) 769 | if self.use_euler_rotation: 770 | value = Gf.Rotation(value).Decompose((1,0,0), (0,1,0), (0,0,1)) 771 | transform.Set(time=time * self.fps, value=value) 772 | 773 | def convert_weights(transform, time, output, i, values_per_step): 774 | start = i * values_per_step 775 | end = start + values_per_step 776 | values = output[start:end] 777 | value = list(map(lambda x: round(x, 5) + 0, values)) 778 | transform.Set(time=time * self.fps, value=value) 779 | 780 | if path == 'translation': 781 | return (usd_node.AddTranslateOp(opSuffix='translate'), convert_translation) 782 | elif path == 'rotation': 783 | if self.use_euler_rotation: 784 | return (usd_node.AddRotateXYZOp(opSuffix='rotate'), convert_rotation) 785 | else: 786 | return (usd_node.AddOrientOp(opSuffix='rotate'), convert_rotation) 787 | elif path == 'scale': 788 | return (usd_node.AddScaleOp(opSuffix='scale'), convert_scale) 789 | elif path == 'weights': 790 | prim = usd_node.GetPrim().GetChild("skeleton_root").GetChild("skel").GetChild("anim") 791 | anim_attr = prim.GetAttribute('blendShapeWeights') 792 | return (anim_attr, convert_weights) 793 | else: 794 | raise Exception('Unsupported animation target path! {}'.format(path)) 795 | 796 | 797 | def convert(self): 798 | if hasattr(self, 'gltf_loader'): 799 | self._convert_images_to_usd() 800 | self._convert_materials_to_preview_surface_new() 801 | self.convert_nodes_to_xform() 802 | 803 | def check_usd_compliance(rootLayer, arkit=False): 804 | #An API change in v18.11 changed the sytax for UsdUtils.ComplianceChecker... 805 | if Usd.GetMinorVersion() > 18 or (Usd.GetMinorVersion() == 18 and Usd.GetPatchVersion() >= 11): 806 | checker = UsdUtils.ComplianceChecker(arkit=arkit, skipARKitRootLayerCheck=False) 807 | checker.CheckCompliance(rootLayer) 808 | else: 809 | #Behavior in v18.09 810 | checker = UsdUtils.ComplianceChecker(rootLayer, arkit=arkit, skipARKitRootLayerCheck=False) 811 | 812 | errors = checker.GetErrors() 813 | failedChecks = checker.GetFailedChecks() 814 | for msg in errors + failedChecks: 815 | print(msg) 816 | return len(errors) == 0 and len(failedChecks) == 0 817 | 818 | 819 | def convert_to_usd(gltf_file, usd_file, fps, scale, arkit=False, verbose=False, use_euler_rotation=False, optimize_textures=False, generate_texture_transform_texture=True, scale_texture=False): 820 | """Converts a glTF file to USD 821 | 822 | Arguments: 823 | gltf_file {str} -- path to glTF file 824 | usd_file {str} -- path to write USD file 825 | 826 | Keyword Arguments: 827 | verbose {bool} -- [description] (default: {False}) 828 | """ 829 | temp_dir = tempfile.mkdtemp() 830 | temp_usd_file = os.path.join(temp_dir, ntpath.basename(usd_file)) 831 | try: 832 | usd = GLTF2USD(gltf_file=gltf_file, usd_file=temp_usd_file, fps=fps, scale=scale, verbose=verbose, use_euler_rotation=use_euler_rotation, optimize_textures=optimize_textures, generate_texture_transform_texture=generate_texture_transform_texture, scale_texture=scale_texture) 833 | if usd.stage: 834 | asset = usd.stage.GetRootLayer() 835 | gltf_asset = usd.gltf_loader.get_asset() 836 | if gltf_asset: 837 | gltf_metadata = {'creator': 'gltf2usd v{}'.format(__version__)} 838 | if gltf_asset.generator: 839 | gltf_metadata['gltf_generator'] = gltf_asset.generator 840 | if gltf_asset.version: 841 | gltf_metadata['gltf_version'] = gltf_asset.version 842 | if gltf_asset.minversion: 843 | gltf_metadata['gltf_minversion'] = gltf_asset.minversion 844 | if gltf_asset.copyright: 845 | gltf_metadata['gltf_copyright'] = gltf_asset.copyright 846 | if gltf_asset.extras: 847 | for key, value in gltf_asset.extras.items(): 848 | gltf_metadata['gltf_extras_{}'.format(key)] = value 849 | asset.customLayerData = gltf_metadata 850 | 851 | usd.logger.info('Conversion complete!') 852 | 853 | asset.Save() 854 | usd.logger.info('created {}'.format(asset.realPath)) 855 | if not os.path.isdir(os.path.dirname(usd_file)): 856 | os.makedirs(os.path.dirname(usd_file)) 857 | 858 | if temp_usd_file.endswith('.usdz') or temp_usd_file.endswith('.usdc'): 859 | usdc_file = '%s.%s' % (os.path.splitext(temp_usd_file)[0], 'usdc') 860 | asset.Export(usdc_file, args=dict(format='usdc')) 861 | usd.logger.info('created {}'.format(usdc_file)) 862 | 863 | if temp_usd_file.endswith('.usdz'): 864 | #change to directory of the generated usd files to avoid issues with 865 | # relative paths with CreateNewUsdzPackage 866 | os.chdir(os.path.dirname(usdc_file)) 867 | temp_usd_file = ntpath.basename(temp_usd_file) 868 | r = Ar.GetResolver() 869 | resolved_asset = r.Resolve(ntpath.basename(usdc_file)) 870 | context = r.CreateDefaultContextForAsset(resolved_asset) 871 | 872 | success = check_usd_compliance(resolved_asset, arkit=args.arkit) 873 | with Ar.ResolverContextBinder(context): 874 | if arkit and not success: 875 | usd.logger.warning('USD is not ARKit compliant') 876 | return 877 | 878 | success = UsdUtils.CreateNewUsdzPackage(resolved_asset, temp_usd_file) and success 879 | if success: 880 | shutil.copyfile(temp_usd_file, usd_file) 881 | usd.logger.info('created package {} with contents:'.format(usd_file)) 882 | zip_file = Usd.ZipFile.Open(usd_file) 883 | file_names = zip_file.GetFileNames() 884 | for file_name in file_names: 885 | usd.logger.info('\t{}'.format(file_name)) 886 | else: 887 | usd.logger.error('could not create {}'.format(usd_file)) 888 | else: 889 | # Copy textures referenced in the Usda/Usdc files from the temp directory to the target directory 890 | temp_stage = Usd.Stage.Open(temp_usd_file) 891 | usd_uv_textures = [x for x in temp_stage.Traverse() if x.IsA(UsdShade.Shader) and UsdShade.Shader(x).GetShaderId() == 'UsdUVTexture'] 892 | 893 | for usd_uv_texture in usd_uv_textures: 894 | file_name = usd_uv_texture.GetAttribute('inputs:file').Get() 895 | if file_name: 896 | file_name = str(file_name).replace('@', '') 897 | 898 | if os.path.isfile(os.path.join(temp_dir, file_name)): 899 | shutil.copyfile(os.path.join(temp_dir, file_name), os.path.join(os.path.dirname(usd_file), file_name)) 900 | shutil.copyfile(temp_usd_file, usd_file) 901 | finally: 902 | shutil.rmtree(temp_dir) 903 | 904 | 905 | if __name__ == '__main__': 906 | parser = argparse.ArgumentParser(description='Convert glTF to USD: v{}'.format(__version__)) 907 | parser.add_argument('--gltf', '-g', action='store', dest='gltf_file', help='glTF file (in .gltf format)', required=True) 908 | parser.add_argument('--fps', action='store', dest='fps', help='The frames per second for the animations', type=float, default=24.0) 909 | parser.add_argument('--output', '-o', action='store', dest='usd_file', help='destination to store generated .usda file', required=True) 910 | parser.add_argument('--verbose', '-v', action='store_true', dest='verbose', help='Enable verbose mode') 911 | parser.add_argument('--scale', '-s', action='store', dest='scale', help='Scale the resulting USDA', type=float, default=100) 912 | parser.add_argument('--arkit', action='store_true', dest='arkit', help='Check USD with ARKit compatibility before making USDZ file') 913 | parser.add_argument('--use-euler-rotation', action='store_true', dest='use_euler_rotation', help='sets euler rotations for node animations instead of quaternion rotations') 914 | parser.add_argument('--optimize-textures', action='store_true', dest='optimize_textures', default=False, help='Specifies if image file size should be optimized and reduced at the expense of longer export time') 915 | parser.add_argument('--generate_texture_transform_texture', dest='generate_texture_transform_texture', action='store_true', help='Enables texture transform texture generation') 916 | parser.add_argument('--no-generate_texture_transform_texture', dest='generate_texture_transform_texture', action='store_false', help='Disables texture transform texture generation') 917 | parser.add_argument('--scale-texture', dest='scale_texture', action='store_true', help='Multiplies metallic/roughness factors by textures', default=False) 918 | parser.set_defaults(generate_texture_transform_texture=True) 919 | 920 | args = parser.parse_args() 921 | 922 | if args.gltf_file: 923 | convert_to_usd(os.path.expanduser(args.gltf_file), os.path.abspath(os.path.expanduser(args.usd_file)), args.fps, args.scale, args.arkit, args.verbose, args.use_euler_rotation, args.optimize_textures, args.generate_texture_transform_texture, args.scale_texture) 924 | -------------------------------------------------------------------------------- /Source/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcoley/gltf2usd/c0752099313834a466acf04b89af33dfd5dc9a0b/Source/tests/__init__.py -------------------------------------------------------------------------------- /Source/tests/assets/Start_Walking/Boss_diffuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcoley/gltf2usd/c0752099313834a466acf04b89af33dfd5dc9a0b/Source/tests/assets/Start_Walking/Boss_diffuse.png -------------------------------------------------------------------------------- /Source/tests/assets/Start_Walking/buffer.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcoley/gltf2usd/c0752099313834a466acf04b89af33dfd5dc9a0b/Source/tests/assets/Start_Walking/buffer.bin -------------------------------------------------------------------------------- /Source/tests/testInitializeGLTFLoader.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import sys 4 | import os 5 | from os import path 6 | sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) ) 7 | 8 | from _gltf2usd.gltf2loader import GLTF2Loader 9 | from _gltf2usd.gltf2usdUtils import GLTF2USDUtils 10 | 11 | class TestInitializeGLTFLoader(unittest.TestCase): 12 | def setUp(self): 13 | gltf_file = os.path.join("tests", "assets", "Start_Walking", "Start_Walking.gltf") 14 | self.loader = GLTF2Loader(gltf_file) 15 | 16 | def test_get_nodes(self): 17 | nodes = self.loader.get_nodes() 18 | for node in nodes: 19 | print(node.parent) 20 | 21 | def test_get_scenes(self): 22 | scenes = self.loader.get_scenes() 23 | 24 | def test_get_main_scene(self): 25 | main_scene = self.loader.get_main_scene() 26 | 27 | def test_get_nodes_from_scene(self): 28 | main_scene = self.loader.get_main_scene() 29 | nodes = main_scene.get_nodes() 30 | 31 | def test_get_mesh(self): 32 | node = [node for node in self.loader.get_nodes() if node.get_mesh() != None][0] 33 | mesh = node.get_mesh() 34 | 35 | def test_get_mesh_primitive(self): 36 | node = [node for node in self.loader.get_nodes() if node.get_mesh() != None][0] 37 | mesh = node.get_mesh() 38 | primitive = mesh.get_primitives() 39 | 40 | 41 | def test_get_skins(self): 42 | skins = self.loader.get_skins() 43 | for skin in skins: 44 | print(skin) 45 | 46 | def test_get_skin_inverse_bind_matrices(self): 47 | skin = self.loader.get_skins()[0] 48 | inverse_bind_matrices = skin.get_inverse_bind_matrices() 49 | for ibm in inverse_bind_matrices: 50 | print(ibm) 51 | 52 | def test_get_skin_joints(self): 53 | skin = self.loader.get_skins()[0] 54 | joints = skin.get_joints() 55 | 56 | for joint in joints: 57 | print(joint.name) 58 | 59 | def test_get_root_skin_joint(self): 60 | skin = self.loader.get_skins()[0] 61 | root_joints = skin.root_joints 62 | 63 | def test_skin_get_inverse_bind_matrices(self): 64 | skin = self.loader.get_skins()[0] 65 | skin.get_inverse_bind_matrices() 66 | 67 | def test_convert_node_name_to_usd_friendly_name(self): 68 | node = self.loader.get_nodes()[0] 69 | GLTF2USDUtils.convert_to_usd_friendly_node_name(node.name) 70 | 71 | def test_convert_node_transform_to_rest_matrix(self): 72 | node = self.loader.get_nodes()[0] 73 | GLTF2USDUtils.compute_usd_transform_matrix_from_gltf_node(node) 74 | 75 | def test_get_animation_channels_for_node_index_and_path(self): 76 | animation = self.loader.get_animations()[0] 77 | node = self.loader.get_nodes()[2] 78 | animation_channel = animation.get_animation_channel_for_node_and_path(node, 'rotation') 79 | 80 | def test_get_animation_channels_for_node_index(self): 81 | animation = self.loader.get_animations()[0] 82 | node = self.loader.get_nodes()[2] 83 | animation_channels = animation.get_animation_channels_for_node(node) 84 | print('\n\n\n\n\n\n\n') 85 | print([animation_channel.target.path for animation_channel in animation_channels]) 86 | 87 | def test_get_animation_sampler_input_data(self): 88 | animation = self.loader.get_animations()[0] 89 | node = self.loader.get_nodes()[2] 90 | animation_channel = animation.get_animation_channel_for_node_and_path(node, 'rotation') 91 | input_data = animation_channel.sampler.get_input_data() 92 | 93 | print('input:\n\n\n\n\n\n\n') 94 | print(input_data) 95 | 96 | def test_get_animation_sampler_output_data(self): 97 | animation = self.loader.get_animations()[0] 98 | node = self.loader.get_nodes()[2] 99 | animation_channel = animation.get_animation_channel_for_node_and_path(node, 'rotation') 100 | output_data = animation_channel.sampler.get_output_data() 101 | print('output:\n\n\n\n\n\n\n') 102 | print(output_data) 103 | 104 | 105 | 106 | 107 | if __name__ == '__main__': 108 | suite = unittest.TestLoader().loadTestsFromTestCase(TestInitializeGLTFLoader) 109 | unittest.TextTestRunner(verbosity=2).run(suite) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pillow == 8.2.0 2 | enum34 == 1.1.6 3 | numpy == 1.15.1 4 | --------------------------------------------------------------------------------