├── LICENSE ├── README.md ├── __init__.py ├── auto_load.py ├── exporter ├── __init__.py ├── com │ ├── gltf2_blender_conversion.py │ ├── gltf2_blender_data_path.py │ ├── gltf2_blender_extras.py │ ├── gltf2_blender_json.py │ ├── gltf2_blender_material_helpers.py │ ├── gltf2_blender_math.py │ ├── gltf2_io.py │ ├── gltf2_io_color_management.py │ ├── gltf2_io_constants.py │ ├── gltf2_io_debug.py │ ├── gltf2_io_extensions.py │ └── gltf2_io_lights_punctual.py └── exp │ ├── __init__.py │ ├── gltf2_blender_batch_export.py │ ├── gltf2_blender_batch_gather.py │ ├── gltf2_blender_export.py │ ├── gltf2_blender_export_keys.py │ ├── gltf2_blender_extract.py │ ├── gltf2_blender_gather.py │ ├── gltf2_blender_gather_accessors.py │ ├── gltf2_blender_gather_animation_channel_target.py │ ├── gltf2_blender_gather_animation_channels.py │ ├── gltf2_blender_gather_animation_sampler_keyframes.py │ ├── gltf2_blender_gather_animation_samplers.py │ ├── gltf2_blender_gather_animations.py │ ├── gltf2_blender_gather_cache.py │ ├── gltf2_blender_gather_cameras.py │ ├── gltf2_blender_gather_drivers.py │ ├── gltf2_blender_gather_image.py │ ├── gltf2_blender_gather_joints.py │ ├── gltf2_blender_gather_light_spots.py │ ├── gltf2_blender_gather_lights.py │ ├── gltf2_blender_gather_material_normal_texture_info_class.py │ ├── gltf2_blender_gather_material_occlusion_texture_info_class.py │ ├── gltf2_blender_gather_materials.py │ ├── gltf2_blender_gather_materials_pbr_metallic_roughness.py │ ├── gltf2_blender_gather_mesh.py │ ├── gltf2_blender_gather_nodes.py │ ├── gltf2_blender_gather_primitive_attributes.py │ ├── gltf2_blender_gather_primitives.py │ ├── gltf2_blender_gather_sampler.py │ ├── gltf2_blender_gather_skins.py │ ├── gltf2_blender_gather_texture.py │ ├── gltf2_blender_gather_texture_info.py │ ├── gltf2_blender_get.py │ ├── gltf2_blender_gltf2_exporter.py │ ├── gltf2_blender_image.py │ ├── gltf2_blender_search_node_tree.py │ ├── gltf2_blender_utils.py │ ├── gltf2_io_binary_data.py │ ├── gltf2_io_buffer.py │ ├── gltf2_io_draco_compression_extension.py │ ├── gltf2_io_export.py │ ├── gltf2_io_image_data.py │ ├── gltf2_io_user_extensions.py │ └── msfs_xml_export.py ├── extensions ├── __init__.py └── ext_master.py ├── func_behavior.py ├── func_material.py ├── func_properties.py ├── func_xml.py ├── li_behavior.py ├── li_material.py ├── li_properties.py ├── ui_materials.py └── ui_properties.py /__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | 19 | bl_info = { 20 | "name" : "MSFSToolkit", 21 | "author" : "Otmar Nitsche", 22 | "description" : "This toolkit prepares your 3D assets to be used for Microsoft Flight Simulator. Copyright (c) 2020 Otmar Nitsche", 23 | "blender" : (2, 82, 3), 24 | "version" : (0, 41, 4), 25 | "location" : "View3D", 26 | "warning" : "This version of the addon is work-in-progress. Don't use it in your active development cycle, as it adds variables and objects to the scene that may cause issues further down the line.", 27 | "category" : "3D View", 28 | "wiki_url": "https://www.fsdeveloper.com/wiki/index.php?title=Blender2MSFS" 29 | } 30 | 31 | from . import auto_load 32 | 33 | from . func_behavior import * 34 | from . func_xml import * 35 | from . func_properties import * 36 | 37 | from . li_material import * 38 | from . li_properties import * 39 | 40 | from . ui_materials import * 41 | #from . ui_properties import * 42 | 43 | ################################################################################## 44 | # Load custom glTF exporter and activate Asobo extensions: 45 | from . exporter import * 46 | from . extensions import * 47 | ################################################################################## 48 | 49 | auto_load.init() 50 | 51 | ## class to add the preference settings 52 | class addSettingsPanel(bpy.types.AddonPreferences): 53 | bl_idname = __package__ 54 | 55 | export_texture_dir: bpy.props.StringProperty ( 56 | name = "Default Texture Location", 57 | description = "Default Texture Location", 58 | default = "" 59 | ) 60 | 61 | export_copyright: bpy.props.StringProperty ( 62 | name = "Default Copyright Name", 63 | description = "Default Copyright Name", 64 | default = "" 65 | ) 66 | 67 | ## draw the panel in the addon preferences 68 | def draw(self, context): 69 | layout = self.layout 70 | 71 | row = layout.row() 72 | row.label(text="Optional - You can set here the default values. This will be used in the export window", icon='INFO') 73 | 74 | box = layout.box() 75 | col = box.column(align = False) 76 | 77 | ## texture default location 78 | col.prop(self, 'export_texture_dir', expand=False) 79 | 80 | ## default copyright 81 | col.prop(self, 'export_copyright', expand=False) 82 | 83 | 84 | def register(): 85 | auto_load.register() 86 | from .extensions import register 87 | register() 88 | from .exporter import register 89 | register() 90 | bpy.utils.register_class(addSettingsPanel) 91 | 92 | #removed by request of scenery designers. 93 | #bpy.types.Scene.msfs_guid = bpy.props.StringProperty(name="GUID",default="") 94 | 95 | def register_panel(): 96 | from .extensions import register_panel 97 | register_panel() 98 | 99 | def unregister(): 100 | #from .extensions import unregister 101 | #unregister() 102 | #from .exporter import unregister 103 | #unregister() 104 | auto_load.unregister() 105 | bpy.utils.unregister_class(addSettingsPanel) 106 | 107 | def unregister_panel(): 108 | from .extensions import unregister_panel 109 | unregister_panel() 110 | -------------------------------------------------------------------------------- /auto_load.py: -------------------------------------------------------------------------------- 1 | import os 2 | import bpy 3 | import sys 4 | import typing 5 | import inspect 6 | import pkgutil 7 | import importlib 8 | from pathlib import Path 9 | 10 | __all__ = ( 11 | "init", 12 | "register", 13 | "unregister", 14 | ) 15 | 16 | modules = None 17 | ordered_classes = None 18 | 19 | def init(): 20 | global modules 21 | global ordered_classes 22 | 23 | modules = get_all_submodules(Path(__file__).parent) 24 | ordered_classes = get_ordered_classes_to_register(modules) 25 | 26 | def register(): 27 | for cls in ordered_classes: 28 | bpy.utils.register_class(cls) 29 | 30 | for module in modules: 31 | if module.__name__ == __name__: 32 | continue 33 | if hasattr(module, "register"): 34 | module.register() 35 | 36 | def unregister(): 37 | for cls in reversed(ordered_classes): 38 | bpy.utils.unregister_class(cls) 39 | 40 | for module in modules: 41 | if module.__name__ == __name__: 42 | continue 43 | if hasattr(module, "unregister"): 44 | module.unregister() 45 | 46 | 47 | # Import modules 48 | ################################################# 49 | 50 | def get_all_submodules(directory): 51 | return list(iter_submodules(directory, directory.name)) 52 | 53 | def iter_submodules(path, package_name): 54 | for name in sorted(iter_submodule_names(path)): 55 | yield importlib.import_module("." + name, package_name) 56 | 57 | def iter_submodule_names(path, root=""): 58 | for _, module_name, is_package in pkgutil.iter_modules([str(path)]): 59 | if is_package: 60 | sub_path = path / module_name 61 | sub_root = root + module_name + "." 62 | yield from iter_submodule_names(sub_path, sub_root) 63 | else: 64 | yield root + module_name 65 | 66 | 67 | # Find classes to register 68 | ################################################# 69 | 70 | def get_ordered_classes_to_register(modules): 71 | return toposort(get_register_deps_dict(modules)) 72 | 73 | def get_register_deps_dict(modules): 74 | deps_dict = {} 75 | classes_to_register = set(iter_classes_to_register(modules)) 76 | for cls in classes_to_register: 77 | deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register)) 78 | return deps_dict 79 | 80 | def iter_own_register_deps(cls, own_classes): 81 | yield from (dep for dep in iter_register_deps(cls) if dep in own_classes) 82 | 83 | def iter_register_deps(cls): 84 | for value in typing.get_type_hints(cls, {}, {}).values(): 85 | dependency = get_dependency_from_annotation(value) 86 | if dependency is not None: 87 | yield dependency 88 | 89 | def get_dependency_from_annotation(value): 90 | if isinstance(value, tuple) and len(value) == 2: 91 | if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty): 92 | return value[1]["type"] 93 | return None 94 | 95 | def iter_classes_to_register(modules): 96 | base_types = get_register_base_types() 97 | for cls in get_classes_in_modules(modules): 98 | if any(base in base_types for base in cls.__bases__): 99 | if not getattr(cls, "is_registered", False): 100 | yield cls 101 | 102 | def get_classes_in_modules(modules): 103 | classes = set() 104 | for module in modules: 105 | for cls in iter_classes_in_module(module): 106 | classes.add(cls) 107 | return classes 108 | 109 | def iter_classes_in_module(module): 110 | for value in module.__dict__.values(): 111 | if inspect.isclass(value): 112 | yield value 113 | 114 | def get_register_base_types(): 115 | return set(getattr(bpy.types, name) for name in [ 116 | "Panel", "Operator", "PropertyGroup", 117 | "AddonPreferences", "Header", "Menu", 118 | "Node", "NodeSocket", "NodeTree", 119 | "UIList", "RenderEngine" 120 | ]) 121 | 122 | 123 | # Find order to register to solve dependencies 124 | ################################################# 125 | 126 | def toposort(deps_dict): 127 | sorted_list = [] 128 | sorted_values = set() 129 | while len(deps_dict) > 0: 130 | unsorted = [] 131 | for value, deps in deps_dict.items(): 132 | if len(deps) == 0: 133 | sorted_list.append(value) 134 | sorted_values.add(value) 135 | else: 136 | unsorted.append(value) 137 | deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted} 138 | return sorted_list -------------------------------------------------------------------------------- /exporter/com/gltf2_blender_conversion.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from math import sin, cos 16 | 17 | def texture_transform_blender_to_gltf(mapping_transform): 18 | """ 19 | Converts the offset/rotation/scale from a Mapping node applied in Blender's 20 | UV space to the equivalent KHR_texture_transform. 21 | """ 22 | offset = mapping_transform.get('offset', [0, 0]) 23 | rotation = mapping_transform.get('rotation', 0) 24 | scale = mapping_transform.get('scale', [1, 1]) 25 | return { 26 | 'offset': [ 27 | offset[0] - scale[1] * sin(rotation), 28 | 1 - offset[1] - scale[1] * cos(rotation), 29 | ], 30 | 'rotation': rotation, 31 | 'scale': [scale[0], scale[1]], 32 | } 33 | 34 | def texture_transform_gltf_to_blender(texture_transform): 35 | """ 36 | Converts a KHR_texture_transform into the equivalent offset/rotation/scale 37 | for a Mapping node applied in Blender's UV space. 38 | """ 39 | offset = texture_transform.get('offset', [0, 0]) 40 | rotation = texture_transform.get('rotation', 0) 41 | scale = texture_transform.get('scale', [1, 1]) 42 | return { 43 | 'offset': [ 44 | offset[0] + scale[1] * sin(rotation), 45 | 1 - offset[1] - scale[1] * cos(rotation), 46 | ], 47 | 'rotation': rotation, 48 | 'scale': [scale[0], scale[1]], 49 | } 50 | 51 | def get_target(property): 52 | return { 53 | "delta_location": "translation", 54 | "delta_rotation_euler": "rotation", 55 | "location": "translation", 56 | "rotation_axis_angle": "rotation", 57 | "rotation_euler": "rotation", 58 | "rotation_quaternion": "rotation", 59 | "scale": "scale", 60 | "value": "weights" 61 | }.get(property) 62 | -------------------------------------------------------------------------------- /exporter/com/gltf2_blender_data_path.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | def get_target_property_name(data_path: str) -> str: 17 | """Retrieve target property.""" 18 | return data_path.rsplit('.', 1)[-1] 19 | 20 | 21 | def get_target_object_path(data_path: str) -> str: 22 | """Retrieve target object data path without property""" 23 | path_split = data_path.rsplit('.', 1) 24 | self_targeting = len(path_split) < 2 25 | if self_targeting: 26 | return "" 27 | return path_split[0] 28 | 29 | def get_rotation_modes(target_property: str) -> str: 30 | """Retrieve rotation modes based on target_property""" 31 | if target_property in ["rotation_euler", "delta_rotation_euler"]: 32 | return True, ["XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX"] 33 | elif target_property in ["rotation_quaternion", "delta_rotation_quaternion"]: 34 | return True, ["QUATERNION"] 35 | elif target_property in ["rotation_axis_angle"]: 36 | return True, ["AXIS_ANGLE"] 37 | else: 38 | return False, [] 39 | -------------------------------------------------------------------------------- /exporter/com/gltf2_blender_extras.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import bpy 17 | from .gltf2_blender_json import is_json_convertible 18 | 19 | 20 | # Custom properties, which are in most cases present and should not be imported/exported. 21 | BLACK_LIST = ['cycles', 'cycles_visibility', 'cycles_curves', '_RNA_UI'] 22 | 23 | 24 | def generate_extras(blender_element): 25 | """Filter and create a custom property, which is stored in the glTF extra field.""" 26 | if not blender_element: 27 | return None 28 | 29 | extras = {} 30 | 31 | for custom_property in blender_element.keys(): 32 | if custom_property in BLACK_LIST: 33 | continue 34 | 35 | value = __to_json_compatible(blender_element[custom_property]) 36 | 37 | if value is not None: 38 | extras[custom_property] = value 39 | 40 | if not extras: 41 | return None 42 | 43 | return extras 44 | 45 | 46 | def __to_json_compatible(value): 47 | """Make a value (usually a custom property) compatible with json""" 48 | 49 | if isinstance(value, bpy.types.ID): 50 | return value 51 | 52 | elif isinstance(value, str): 53 | return value 54 | 55 | elif isinstance(value, (int, float)): 56 | return value 57 | 58 | # for list classes 59 | elif isinstance(value, list): 60 | value = list(value) 61 | # make sure contents are json-compatible too 62 | for index in range(len(value)): 63 | value[index] = __to_json_compatible(value[index]) 64 | return value 65 | 66 | # for IDPropertyArray classes 67 | elif hasattr(value, "to_list"): 68 | value = value.to_list() 69 | return value 70 | 71 | elif hasattr(value, "to_dict"): 72 | value = value.to_dict() 73 | if is_json_convertible(value): 74 | return value 75 | 76 | return None 77 | 78 | 79 | def set_extras(blender_element, extras, exclude=[]): 80 | """Copy extras onto a Blender object.""" 81 | if not extras or not isinstance(extras, dict): 82 | return 83 | 84 | for custom_property, value in extras.items(): 85 | if custom_property in BLACK_LIST: 86 | continue 87 | if custom_property in exclude: 88 | continue 89 | 90 | try: 91 | blender_element[custom_property] = value 92 | except TypeError: 93 | print('Error setting property %s to value of type %s' % (custom_property, type(value))) 94 | -------------------------------------------------------------------------------- /exporter/com/gltf2_blender_json.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import bpy 17 | 18 | 19 | class BlenderJSONEncoder(json.JSONEncoder): 20 | """Blender JSON Encoder.""" 21 | 22 | def default(self, obj): 23 | if isinstance(obj, bpy.types.ID): 24 | return dict( 25 | name=obj.name, 26 | type=obj.__class__.__name__ 27 | ) 28 | return super(BlenderJSONEncoder, self).default(obj) 29 | 30 | 31 | def is_json_convertible(data): 32 | """Test, if a data set can be expressed as JSON.""" 33 | try: 34 | json.dumps(data, cls=BlenderJSONEncoder) 35 | return True 36 | except: 37 | return False 38 | -------------------------------------------------------------------------------- /exporter/com/gltf2_blender_material_helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | def get_gltf_node_name(): 17 | return "glTF Settings" 18 | -------------------------------------------------------------------------------- /exporter/com/gltf2_blender_math.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import typing 16 | import math 17 | from mathutils import Matrix, Vector, Quaternion, Euler 18 | 19 | from ..com.gltf2_blender_data_path import get_target_property_name 20 | 21 | 22 | def multiply(a, b): 23 | """Multiplication.""" 24 | return a @ b 25 | 26 | 27 | def list_to_mathutils(values: typing.List[float], data_path: str) -> typing.Union[Vector, Quaternion, Euler]: 28 | """Transform a list to blender py object.""" 29 | target = get_target_property_name(data_path) 30 | 31 | if target == 'delta_location': 32 | return Vector(values) # TODO Should be Vector(values) - Vector(something)? 33 | elif target == 'delta_rotation_euler': 34 | return Euler(values).to_quaternion() # TODO Should be multiply(Euler(values).to_quaternion(), something)? 35 | elif target == 'location': 36 | return Vector(values) 37 | elif target == 'rotation_axis_angle': 38 | angle = values[0] 39 | axis = values[1:] 40 | return Quaternion(axis, math.radians(angle)) 41 | elif target == 'rotation_euler': 42 | return Euler(values).to_quaternion() 43 | elif target == 'rotation_quaternion': 44 | return Quaternion(values) 45 | elif target == 'scale': 46 | return Vector(values) 47 | elif target == 'value': 48 | return Vector(values) 49 | 50 | return values 51 | 52 | 53 | def mathutils_to_gltf(x: typing.Union[Vector, Quaternion]) -> typing.List[float]: 54 | """Transform a py object to glTF list.""" 55 | if isinstance(x, Vector): 56 | return list(x) 57 | if isinstance(x, Quaternion): 58 | # Blender has w-first quaternion notation 59 | return [x[1], x[2], x[3], x[0]] 60 | else: 61 | return list(x) 62 | 63 | 64 | def to_yup() -> Matrix: 65 | """Transform to Yup.""" 66 | return Matrix( 67 | ((1.0, 0.0, 0.0, 0.0), 68 | (0.0, 0.0, 1.0, 0.0), 69 | (0.0, -1.0, 0.0, 0.0), 70 | (0.0, 0.0, 0.0, 1.0)) 71 | ) 72 | 73 | 74 | to_zup = to_yup 75 | 76 | 77 | def swizzle_yup(v: typing.Union[Vector, Quaternion], data_path: str) -> typing.Union[Vector, Quaternion]: 78 | """Manage Yup.""" 79 | target = get_target_property_name(data_path) 80 | swizzle_func = { 81 | "delta_location": swizzle_yup_location, 82 | "delta_rotation_euler": swizzle_yup_rotation, 83 | "location": swizzle_yup_location, 84 | "rotation_axis_angle": swizzle_yup_rotation, 85 | "rotation_euler": swizzle_yup_rotation, 86 | "rotation_quaternion": swizzle_yup_rotation, 87 | "scale": swizzle_yup_scale, 88 | "value": swizzle_yup_value 89 | }.get(target) 90 | 91 | if swizzle_func is None: 92 | raise RuntimeError("Cannot transform values at {}".format(data_path)) 93 | 94 | return swizzle_func(v) 95 | 96 | 97 | def swizzle_yup_location(loc: Vector) -> Vector: 98 | """Manage Yup location.""" 99 | return Vector((loc[0], loc[2], -loc[1])) 100 | 101 | 102 | def swizzle_yup_rotation(rot: Quaternion) -> Quaternion: 103 | """Manage Yup rotation.""" 104 | return Quaternion((rot[0], rot[1], rot[3], -rot[2])) 105 | 106 | 107 | def swizzle_yup_scale(scale: Vector) -> Vector: 108 | """Manage Yup scale.""" 109 | return Vector((scale[0], scale[2], scale[1])) 110 | 111 | 112 | def swizzle_yup_value(value: typing.Any) -> typing.Any: 113 | """Manage Yup value.""" 114 | return value 115 | 116 | 117 | def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4)) -> typing \ 118 | .Union[Vector, Quaternion]: 119 | """Manage transformations.""" 120 | target = get_target_property_name(data_path) 121 | transform_func = { 122 | "delta_location": transform_location, 123 | "delta_rotation_euler": transform_rotation, 124 | "location": transform_location, 125 | "rotation_axis_angle": transform_rotation, 126 | "rotation_euler": transform_rotation, 127 | "rotation_quaternion": transform_rotation, 128 | "scale": transform_scale, 129 | "value": transform_value 130 | }.get(target) 131 | 132 | if transform_func is None: 133 | raise RuntimeError("Cannot transform values at {}".format(data_path)) 134 | 135 | return transform_func(v, transform) 136 | 137 | 138 | def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector: 139 | """Transform location.""" 140 | m = Matrix.Translation(location) 141 | m = multiply(transform, m) 142 | return m.to_translation() 143 | 144 | 145 | def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4)) -> Quaternion: 146 | """Transform rotation.""" 147 | rotation.normalize() 148 | m = rotation.to_matrix().to_4x4() 149 | m = multiply(transform, m) 150 | return m.to_quaternion() 151 | 152 | 153 | def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector: 154 | """Transform scale.""" 155 | m = Matrix.Identity(4) 156 | m[0][0] = scale.x 157 | m[1][1] = scale.y 158 | m[2][2] = scale.z 159 | m = multiply(transform, m) 160 | 161 | return m.to_scale() 162 | 163 | 164 | def transform_value(value: Vector, _: Matrix = Matrix.Identity(4)) -> Vector: 165 | """Transform value.""" 166 | return value 167 | 168 | 169 | def round_if_near(value: float, target: float) -> float: 170 | """If value is very close to target, round to target.""" 171 | return value if abs(value - target) > 2.0e-6 else target 172 | 173 | 174 | def scale_rot_swap_matrix(rot): 175 | """Returns a matrix m st. Scale[s] Rot[rot] = Rot[rot] Scale[m s]. 176 | If rot.to_matrix() is a signed permutation matrix, works for any s. 177 | Otherwise works only if s is a uniform scaling. 178 | """ 179 | m = nearby_signed_perm_matrix(rot) # snap to signed perm matrix 180 | m.transpose() # invert permutation 181 | for i in range(3): 182 | for j in range(3): 183 | m[i][j] = abs(m[i][j]) # discard sign 184 | return m 185 | 186 | 187 | def nearby_signed_perm_matrix(rot): 188 | """Returns a signed permutation matrix close to rot.to_matrix(). 189 | (A signed permutation matrix is like a permutation matrix, except 190 | the non-zero entries can be ±1.) 191 | """ 192 | m = rot.to_matrix() 193 | x, y, z = m[0], m[1], m[2] 194 | 195 | # Set the largest entry in the first row to ±1 196 | a, b, c = abs(x[0]), abs(x[1]), abs(x[2]) 197 | i = 0 if a >= b and a >= c else 1 if b >= c else 2 198 | x[i] = 1 if x[i] > 0 else -1 199 | x[(i+1) % 3] = 0 200 | x[(i+2) % 3] = 0 201 | 202 | # Same for second row: only two columns to consider now. 203 | a, b = abs(y[(i+1) % 3]), abs(y[(i+2) % 3]) 204 | j = (i+1) % 3 if a >= b else (i+2) % 3 205 | y[j] = 1 if y[j] > 0 else -1 206 | y[(j+1) % 3] = 0 207 | y[(j+2) % 3] = 0 208 | 209 | # Same for third row: only one column left 210 | k = (0 + 1 + 2) - i - j 211 | z[k] = 1 if z[k] > 0 else -1 212 | z[(k+1) % 3] = 0 213 | z[(k+2) % 3] = 0 214 | 215 | return m 216 | -------------------------------------------------------------------------------- /exporter/com/gltf2_io_color_management.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | def color_srgb_to_scene_linear(c): 17 | """ 18 | Convert from sRGB to scene linear color space. 19 | 20 | Source: Cycles addon implementation, node_color.h. 21 | """ 22 | if c < 0.04045: 23 | return 0.0 if c < 0.0 else c * (1.0 / 12.92) 24 | else: 25 | return ((c + 0.055) * (1.0 / 1.055)) ** 2.4 26 | 27 | def color_linear_to_srgb(c): 28 | """ 29 | Convert from linear to sRGB color space. 30 | 31 | Source: Cycles addon implementation, node_color.h. 32 | """ 33 | if c < 0.0031308: 34 | return 0.0 if c < 0.0 else c * 12.92 35 | else: 36 | return 1.055 * c ** (1.0 / 2.4) - 0.055 37 | -------------------------------------------------------------------------------- /exporter/com/gltf2_io_constants.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from enum import IntEnum 16 | 17 | 18 | class ComponentType(IntEnum): 19 | Byte = 5120 20 | UnsignedByte = 5121 21 | Short = 5122 22 | UnsignedShort = 5123 23 | UnsignedInt = 5125 24 | Float = 5126 25 | 26 | @classmethod 27 | def to_type_code(cls, component_type): 28 | return { 29 | ComponentType.Byte: 'b', 30 | ComponentType.UnsignedByte: 'B', 31 | ComponentType.Short: 'h', 32 | ComponentType.UnsignedShort: 'H', 33 | ComponentType.UnsignedInt: 'I', 34 | ComponentType.Float: 'f' 35 | }[component_type] 36 | 37 | @classmethod 38 | def from_legacy_define(cls, type_define): 39 | return { 40 | GLTF_COMPONENT_TYPE_BYTE: ComponentType.Byte, 41 | GLTF_COMPONENT_TYPE_UNSIGNED_BYTE: ComponentType.UnsignedByte, 42 | GLTF_COMPONENT_TYPE_SHORT: ComponentType.Short, 43 | GLTF_COMPONENT_TYPE_UNSIGNED_SHORT: ComponentType.UnsignedShort, 44 | GLTF_COMPONENT_TYPE_UNSIGNED_INT: ComponentType.UnsignedInt, 45 | GLTF_COMPONENT_TYPE_FLOAT: ComponentType.Float 46 | }[type_define] 47 | 48 | @classmethod 49 | def get_size(cls, component_type): 50 | return { 51 | ComponentType.Byte: 1, 52 | ComponentType.UnsignedByte: 1, 53 | ComponentType.Short: 2, 54 | ComponentType.UnsignedShort: 2, 55 | ComponentType.UnsignedInt: 4, 56 | ComponentType.Float: 4 57 | }[component_type] 58 | 59 | 60 | class DataType: 61 | Scalar = "SCALAR" 62 | Vec2 = "VEC2" 63 | Vec3 = "VEC3" 64 | Vec4 = "VEC4" 65 | Mat2 = "MAT2" 66 | Mat3 = "MAT3" 67 | Mat4 = "MAT4" 68 | 69 | def __new__(cls, *args, **kwargs): 70 | raise RuntimeError("{} should not be instantiated".format(cls.__name__)) 71 | 72 | @classmethod 73 | def num_elements(cls, data_type): 74 | return { 75 | DataType.Scalar: 1, 76 | DataType.Vec2: 2, 77 | DataType.Vec3: 3, 78 | DataType.Vec4: 4, 79 | DataType.Mat2: 4, 80 | DataType.Mat3: 9, 81 | DataType.Mat4: 16 82 | }[data_type] 83 | 84 | @classmethod 85 | def vec_type_from_num(cls, num_elems): 86 | if not (0 < num_elems < 5): 87 | raise ValueError("No vector type with {} elements".format(num_elems)) 88 | return { 89 | 1: DataType.Scalar, 90 | 2: DataType.Vec2, 91 | 3: DataType.Vec3, 92 | 4: DataType.Vec4 93 | }[num_elems] 94 | 95 | @classmethod 96 | def mat_type_from_num(cls, num_elems): 97 | if not (4 <= num_elems <= 16): 98 | raise ValueError("No matrix type with {} elements".format(num_elems)) 99 | return { 100 | 4: DataType.Mat2, 101 | 9: DataType.Mat3, 102 | 16: DataType.Mat4 103 | }[num_elems] 104 | 105 | 106 | class TextureFilter(IntEnum): 107 | Nearest = 9728 108 | Linear = 9729 109 | NearestMipmapNearest = 9984 110 | LinearMipmapNearest = 9985 111 | NearestMipmapLinear = 9986 112 | LinearMipmapLinear = 9987 113 | 114 | 115 | class TextureWrap(IntEnum): 116 | ClampToEdge = 33071 117 | MirroredRepeat = 33648 118 | Repeat = 10497 119 | 120 | 121 | ################# 122 | # LEGACY DEFINES 123 | 124 | GLTF_VERSION = "2.0" 125 | 126 | # 127 | # Component Types 128 | # 129 | GLTF_COMPONENT_TYPE_BYTE = "BYTE" 130 | GLTF_COMPONENT_TYPE_UNSIGNED_BYTE = "UNSIGNED_BYTE" 131 | GLTF_COMPONENT_TYPE_SHORT = "SHORT" 132 | GLTF_COMPONENT_TYPE_UNSIGNED_SHORT = "UNSIGNED_SHORT" 133 | GLTF_COMPONENT_TYPE_UNSIGNED_INT = "UNSIGNED_INT" 134 | GLTF_COMPONENT_TYPE_FLOAT = "FLOAT" 135 | 136 | 137 | # 138 | # Data types 139 | # 140 | GLTF_DATA_TYPE_SCALAR = "SCALAR" 141 | GLTF_DATA_TYPE_VEC2 = "VEC2" 142 | GLTF_DATA_TYPE_VEC3 = "VEC3" 143 | GLTF_DATA_TYPE_VEC4 = "VEC4" 144 | GLTF_DATA_TYPE_MAT2 = "MAT2" 145 | GLTF_DATA_TYPE_MAT3 = "MAT3" 146 | GLTF_DATA_TYPE_MAT4 = "MAT4" 147 | -------------------------------------------------------------------------------- /exporter/com/gltf2_io_debug.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # 16 | # Imports 17 | # 18 | 19 | import time 20 | import logging 21 | 22 | # 23 | # Globals 24 | # 25 | 26 | OUTPUT_LEVELS = ['ERROR', 'WARNING', 'INFO', 'PROFILE', 'DEBUG', 'VERBOSE'] 27 | 28 | g_current_output_level = 'DEBUG' 29 | g_profile_started = False 30 | g_profile_start = 0.0 31 | g_profile_end = 0.0 32 | g_profile_delta = 0.0 33 | 34 | # 35 | # Functions 36 | # 37 | 38 | 39 | def set_output_level(level): 40 | """Set an output debug level.""" 41 | global g_current_output_level 42 | 43 | if OUTPUT_LEVELS.index(level) < 0: 44 | return 45 | 46 | g_current_output_level = level 47 | 48 | 49 | def print_console(level, output): 50 | """Print to Blender console with a given header and output.""" 51 | global OUTPUT_LEVELS 52 | global g_current_output_level 53 | 54 | if OUTPUT_LEVELS.index(level) > OUTPUT_LEVELS.index(g_current_output_level): 55 | return 56 | 57 | print(get_timestamp() + " | " + level + ': ' + output) 58 | 59 | 60 | def print_newline(): 61 | """Print a new line to Blender console.""" 62 | print() 63 | 64 | 65 | def get_timestamp(): 66 | current_time = time.gmtime() 67 | return time.strftime("%H:%M:%S", current_time) 68 | 69 | 70 | def print_timestamp(label=None): 71 | """Print a timestamp to Blender console.""" 72 | output = 'Timestamp: ' + get_timestamp() 73 | 74 | if label is not None: 75 | output = output + ' (' + label + ')' 76 | 77 | print_console('PROFILE', output) 78 | 79 | 80 | def profile_start(): 81 | """Start profiling by storing the current time.""" 82 | global g_profile_start 83 | global g_profile_started 84 | 85 | if g_profile_started: 86 | print_console('ERROR', 'Profiling already started') 87 | return 88 | 89 | g_profile_started = True 90 | 91 | g_profile_start = time.time() 92 | 93 | 94 | def profile_end(label=None): 95 | """Stop profiling and printing out the delta time since profile start.""" 96 | global g_profile_end 97 | global g_profile_delta 98 | global g_profile_started 99 | 100 | if not g_profile_started: 101 | print_console('ERROR', 'Profiling not started') 102 | return 103 | 104 | g_profile_started = False 105 | 106 | g_profile_end = time.time() 107 | g_profile_delta = g_profile_end - g_profile_start 108 | 109 | output = 'Delta time: ' + str(g_profile_delta) 110 | 111 | if label is not None: 112 | output = output + ' (' + label + ')' 113 | 114 | print_console('PROFILE', output) 115 | 116 | 117 | # TODO: need to have a unique system for logging importer/exporter 118 | # TODO: this logger is used for importer, but in io and in blender part, but is written here in a _io_ file 119 | class Log: 120 | def __init__(self, loglevel): 121 | self.logger = logging.getLogger('glTFImporter') 122 | self.hdlr = logging.StreamHandler() 123 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 124 | self.hdlr.setFormatter(formatter) 125 | self.logger.addHandler(self.hdlr) 126 | self.logger.setLevel(int(loglevel)) 127 | -------------------------------------------------------------------------------- /exporter/com/gltf2_io_extensions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List, Dict, Any 16 | 17 | 18 | class Extension: 19 | """Container for extensions. Allows to specify requiredness""" 20 | def __init__(self, name: str, extension: Dict[str, Any], required: bool = True): 21 | self.name = name 22 | self.extension = extension 23 | self.required = required 24 | 25 | 26 | class ChildOfRootExtension(Extension): 27 | """Container object for extensions that should be appended to the root extensions""" 28 | def __init__(self, path: List[str], name: str, extension: Dict[str, Any], required: bool = True): 29 | """ 30 | Wrap a local extension entity into an object that will later be inserted into a root extension and converted 31 | to a reference. 32 | :param path: The path of the extension object in the root extension. E.g. ['lights'] for 33 | KHR_lights_punctual. Must be a path to a list in the extensions dict. 34 | :param extension: The data that should be placed into the extension list 35 | """ 36 | self.path = path 37 | super().__init__(name, extension, required) 38 | -------------------------------------------------------------------------------- /exporter/com/gltf2_io_lights_punctual.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .gltf2_io import from_dict, from_union, from_none, from_float, from_str, from_list 16 | from .gltf2_io import to_float, to_class 17 | 18 | 19 | class LightSpot: 20 | """light/spot""" 21 | def __init__(self, inner_cone_angle, outer_cone_angle): 22 | self.inner_cone_angle = inner_cone_angle 23 | self.outer_cone_angle = outer_cone_angle 24 | 25 | @staticmethod 26 | def from_dict(obj): 27 | assert isinstance(obj, dict) 28 | inner_cone_angle = from_union([from_float, from_none], obj.get("innerConeAngle")) 29 | outer_cone_angle = from_union([from_float, from_none], obj.get("outerConeAngle")) 30 | return LightSpot(inner_cone_angle, outer_cone_angle) 31 | 32 | def to_dict(self): 33 | result = {} 34 | result["innerConeAngle"] = from_union([from_float, from_none], self.inner_cone_angle) 35 | result["outerConeAngle"] = from_union([from_float, from_none], self.outer_cone_angle) 36 | return result 37 | 38 | 39 | class Light: 40 | """defines a set of lights for use with glTF 2.0. Lights define light sources within a scene""" 41 | def __init__(self, color, intensity, spot, type, range, name, extensions, extras): 42 | self.color = color 43 | self.intensity = intensity 44 | self.spot = spot 45 | self.type = type 46 | self.range = range 47 | self.name = name 48 | self.extensions = extensions 49 | self.extras = extras 50 | 51 | @staticmethod 52 | def from_dict(obj): 53 | assert isinstance(obj, dict) 54 | color = from_union([lambda x: from_list(from_float, x), from_none], obj.get("color")) 55 | intensity = from_union([from_float, from_none], obj.get("intensity")) 56 | spot = LightSpot.from_dict(obj.get("spot")) 57 | type = from_str(obj.get("type")) 58 | range = from_union([from_float, from_none], obj.get("range")) 59 | name = from_union([from_str, from_none], obj.get("name")) 60 | extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], 61 | obj.get("extensions")) 62 | extras = obj.get("extras") 63 | return Light(color, intensity, spot, type, range, name, extensions, extras) 64 | 65 | def to_dict(self): 66 | result = {} 67 | result["color"] = from_union([lambda x: from_list(to_float, x), from_none], self.color) 68 | result["intensity"] = from_union([from_float, from_none], self.intensity) 69 | result["spot"] = from_union([lambda x: to_class(LightSpot, x), from_none], self.spot) 70 | result["type"] = from_str(self.type) 71 | result["range"] = from_union([from_float, from_none], self.range) 72 | result["name"] = from_union([from_str, from_none], self.name) 73 | result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none], 74 | self.extensions) 75 | result["extras"] = self.extras 76 | return result 77 | -------------------------------------------------------------------------------- /exporter/exp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tml1024/Blender2MSFS2/9db00ad88de4c5bbe29f518adfcb397a86f2254d/exporter/exp/__init__.py -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_batch_export.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | # 19 | # This is the modified exporter for the Blender2MSFS addon. 20 | # The only purpose of the modification is to allow for extensions 21 | # in the "asset" section of the glTF file. 22 | # 23 | ################################################################################################### 24 | 25 | import bpy 26 | import time 27 | import re 28 | import os 29 | from ..com.gltf2_io_debug import print_console, print_newline 30 | from .gltf2_blender_gltf2_exporter import GlTF2Exporter 31 | from . import gltf2_io_draco_compression_extension 32 | from .gltf2_io_user_extensions import export_user_extensions 33 | 34 | def save_ext_gltf(context, export_settings): 35 | """Go through the collections and find the lods, export them one by one.""" 36 | 37 | from . import gltf2_blender_export 38 | 39 | lods = [] 40 | lod_pattern = re.compile("^x(\d+)", re.IGNORECASE) 41 | filename_base, extension = os.path.splitext(export_settings['gltf_filepath']) 42 | filename, extension = os.path.splitext(os.path.basename(export_settings['gltf_filepath'])) 43 | 44 | lod_model_export_settings = export_settings.copy() 45 | 46 | for collection in bpy.data.collections: 47 | match = lod_pattern.match(collection.name) 48 | 49 | if match: 50 | #save collection name in export settings: 51 | lod_model_export_settings['gltf_current_collection'] = collection.name 52 | 53 | lod_id = "_LOD" + match.group(1) 54 | lod_filename = filename_base+lod_id+extension 55 | lods.append(lod_filename) 56 | 57 | lod_model_export_settings['gltf_filepath'] = lod_filename 58 | lod_model_export_settings['gltf_binaryfilename'] = filename+lod_id+'.bin' 59 | 60 | 61 | # Begin export process: 62 | original_frame = bpy.context.scene.frame_current 63 | if not lod_model_export_settings['gltf_current_frame']: 64 | bpy.context.scene.frame_set(0) 65 | 66 | gltf2_blender_export.__notify_start_ext_gltf(context) 67 | start_time = time.time() 68 | pre_export_callbacks = lod_model_export_settings["pre_export_callbacks"] 69 | for callback in pre_export_callbacks: 70 | callback(lod_model_export_settings) 71 | 72 | json, buffer = __export_ext_gltf(lod_model_export_settings) 73 | 74 | post_export_callbacks = lod_model_export_settings["post_export_callbacks"] 75 | for callback in post_export_callbacks: 76 | callback(lod_model_export_settings) 77 | gltf2_blender_export.__write_file_ext_gltf(json, buffer, lod_model_export_settings) 78 | 79 | end_time = time.time() 80 | gltf2_blender_export.__notify_end_ext_gltf(context, end_time - start_time) 81 | 82 | if not lod_model_export_settings['gltf_current_frame']: 83 | bpy.context.scene.frame_set(original_frame) 84 | 85 | #save XML file if required: 86 | if export_settings['gltf_msfs_xml'] == True: 87 | from .msfs_xml_export import save_xml 88 | save_xml(context,export_settings,lods) 89 | 90 | if len(lods) == 1: 91 | msg = "Exported one lod model." 92 | print(msg) 93 | for filename in lods: 94 | print("<%s>"%filename) 95 | return{'FINISHED'} 96 | elif len(lods) > 1: 97 | msg = "Exported %i lod models."%len(lods) 98 | print(msg) 99 | for filename in lods: 100 | print("<%s>"%filename) 101 | return{'FINISHED'} 102 | else: 103 | msg = "ERROR: Could not find LODs in the scene. Collection names should be: 'X00','X01', 'X02', and so on." 104 | print(msg) 105 | return{'CANCELLED'} 106 | 107 | 108 | def __export_ext_gltf(export_settings): 109 | from . import gltf2_blender_export 110 | 111 | exporter = GlTF2Exporter(export_settings) 112 | __gather_ext_gltf(exporter, export_settings) 113 | buffer = gltf2_blender_export.__create_buffer_ext_gltf(exporter, export_settings) 114 | exporter.finalize_images() 115 | json = gltf2_blender_export.__fix_json_ext_gltf(exporter.glTF.to_dict()) 116 | 117 | return json, buffer 118 | 119 | def __gather_ext_gltf(exporter, export_settings): 120 | from . import gltf2_blender_batch_gather 121 | active_scene_idx, scenes, animations = gltf2_blender_batch_gather.gather_gltf2(export_settings) 122 | #active_scene_idx, scenes, animations = gltf2_blender_gather.gather_gltf2(export_settings) 123 | 124 | plan = {'active_scene_idx': active_scene_idx, 'scenes': scenes, 'animations': animations} 125 | export_user_extensions('gather_gltf_hook', export_settings, plan) 126 | active_scene_idx, scenes, animations = plan['active_scene_idx'], plan['scenes'], plan['animations'] 127 | 128 | if export_settings['gltf_draco_mesh_compression']: 129 | gltf2_io_draco_compression_extension.compress_scene_primitives(scenes, export_settings) 130 | exporter.add_draco_extension() 131 | 132 | for idx, scene in enumerate(scenes): 133 | exporter.add_scene(scene, idx==active_scene_idx) 134 | for animation in animations: 135 | exporter.add_animation(animation) 136 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_batch_gather.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | # 19 | # This is the modified exporter for the Blender2MSFS addon. 20 | # The only purpose of the modification is to allow for extensions 21 | # in the "asset" section of the glTF file. 22 | # 23 | ################################################################################################### 24 | 25 | import bpy 26 | 27 | from ..com import gltf2_io 28 | from ..com.gltf2_io_debug import print_console 29 | from . import gltf2_blender_gather_nodes 30 | from . import gltf2_blender_gather_animations 31 | from .gltf2_blender_gather_cache import cached 32 | from ..com.gltf2_blender_extras import generate_extras 33 | from . import gltf2_blender_export_keys 34 | from .gltf2_io_user_extensions import export_user_extensions 35 | 36 | 37 | def gather_gltf2(export_settings): 38 | """ 39 | Gather glTF properties from the current state of blender. 40 | 41 | :return: list of scene graphs to be added to the glTF export 42 | """ 43 | scenes = [] 44 | animations = [] # unfortunately animations in gltf2 are just as 'root' as scenes. 45 | active_scene = None 46 | for blender_scene in bpy.data.scenes: 47 | scenes.append(__gather_scene(blender_scene, export_settings)) 48 | if export_settings[gltf2_blender_export_keys.ANIMATIONS]: 49 | animations += __gather_animations(blender_scene, export_settings) 50 | if bpy.context.scene.name == blender_scene.name: 51 | active_scene = len(scenes) -1 52 | return active_scene, scenes, animations 53 | 54 | 55 | #@cached - removed to ensure the function is executed on every call. 56 | def __gather_scene(blender_scene, export_settings): 57 | scene = gltf2_io.Scene( 58 | extensions=None, 59 | extras=__gather_extras(blender_scene, export_settings), 60 | name=blender_scene.name, 61 | nodes=[] 62 | ) 63 | 64 | _active_collection = None 65 | 66 | for collection in bpy.data.collections: 67 | if collection.name == export_settings['gltf_current_collection']: 68 | _active_collection = collection 69 | 70 | if _active_collection == None: 71 | return #might need some more error catching? 72 | 73 | for _blender_object in [obj for obj in _active_collection.all_objects if obj.proxy is None]: 74 | if _blender_object.parent is None: 75 | blender_object = _blender_object.proxy if _blender_object.proxy else _blender_object 76 | node = gltf2_blender_gather_nodes.gather_node( 77 | blender_object, 78 | blender_object.library.name if blender_object.library else None, 79 | blender_scene, None, export_settings) 80 | if node is not None: 81 | scene.nodes.append(node) 82 | 83 | export_user_extensions('gather_scene_hook', export_settings, scene, blender_scene) 84 | 85 | return scene 86 | 87 | 88 | def __gather_animations(blender_scene, export_settings): 89 | animations = [] 90 | merged_tracks = {} 91 | 92 | for _blender_object in blender_scene.objects: 93 | 94 | blender_object = _blender_object.proxy if _blender_object.proxy else _blender_object 95 | 96 | # First check if this object is exported or not. Do not export animation of not exported object 97 | obj_node = gltf2_blender_gather_nodes.gather_node(blender_object, 98 | blender_object.library.name if blender_object.library else None, 99 | blender_scene, None, export_settings) 100 | if obj_node is not None: 101 | # Check was done on armature, but use here the _proxy object, because this is where the animation is 102 | animations_, merged_tracks = gltf2_blender_gather_animations.gather_animations(_blender_object, merged_tracks, len(animations), export_settings) 103 | animations += animations_ 104 | 105 | if export_settings['gltf_nla_strips'] is False: 106 | # Fake an animation with all animations of the scene 107 | merged_tracks = {} 108 | merged_tracks['Animation'] = [] 109 | for idx, animation in enumerate(animations): 110 | merged_tracks['Animation'].append(idx) 111 | 112 | 113 | to_delete_idx = [] 114 | for merged_anim_track in merged_tracks.keys(): 115 | if len(merged_tracks[merged_anim_track]) < 2: 116 | 117 | # There is only 1 animation in the track 118 | # If name of the track is not a default name, use this name for action 119 | if len(merged_tracks[merged_anim_track]) != 0: 120 | animations[merged_tracks[merged_anim_track][0]].name = merged_anim_track 121 | 122 | continue 123 | 124 | base_animation_idx = None 125 | offset_sampler = 0 126 | 127 | for idx, anim_idx in enumerate(merged_tracks[merged_anim_track]): 128 | if idx == 0: 129 | base_animation_idx = anim_idx 130 | animations[anim_idx].name = merged_anim_track 131 | already_animated = [] 132 | for channel in animations[anim_idx].channels: 133 | already_animated.append((channel.target.node, channel.target.path)) 134 | continue 135 | 136 | to_delete_idx.append(anim_idx) 137 | 138 | # Merging extras 139 | # Warning, some values can be overwritten if present in multiple merged animations 140 | if animations[anim_idx].extras is not None: 141 | for k in animations[anim_idx].extras.keys(): 142 | if animations[base_animation_idx].extras is None: 143 | animations[base_animation_idx].extras = {} 144 | animations[base_animation_idx].extras[k] = animations[anim_idx].extras[k] 145 | 146 | offset_sampler = len(animations[base_animation_idx].samplers) 147 | for sampler in animations[anim_idx].samplers: 148 | animations[base_animation_idx].samplers.append(sampler) 149 | 150 | for channel in animations[anim_idx].channels: 151 | if (channel.target.node, channel.target.path) in already_animated: 152 | print_console("WARNING", "Some strips have same channel animation ({}), on node {} !".format(channel.target.path, channel.target.node.name)) 153 | continue 154 | animations[base_animation_idx].channels.append(channel) 155 | animations[base_animation_idx].channels[-1].sampler = animations[base_animation_idx].channels[-1].sampler + offset_sampler 156 | already_animated.append((channel.target.node, channel.target.path)) 157 | 158 | new_animations = [] 159 | if len(to_delete_idx) != 0: 160 | for idx, animation in enumerate(animations): 161 | if idx in to_delete_idx: 162 | continue 163 | new_animations.append(animation) 164 | else: 165 | new_animations = animations 166 | 167 | 168 | return new_animations 169 | 170 | 171 | def __gather_extras(blender_object, export_settings): 172 | if export_settings[gltf2_blender_export_keys.EXTRAS]: 173 | return generate_extras(blender_object) 174 | return None 175 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_export.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | # 19 | # This is the modified exporter for the Blender2MSFS addon. 20 | # The only purpose of the modification is to allow for extensions 21 | # in the "asset" section of the glTF file. 22 | # 23 | ################################################################################################### 24 | 25 | import time 26 | 27 | import bpy 28 | import sys 29 | import traceback 30 | 31 | from ..com import gltf2_blender_json 32 | from . import gltf2_blender_export_keys 33 | from . import gltf2_blender_gather 34 | #from io_scene_gltf2.blender.exp import gltf2_blender_gather 35 | from .gltf2_blender_gltf2_exporter import GlTF2Exporter #replacing the original exporter here. 36 | from ..com.gltf2_io_debug import print_console, print_newline 37 | from . import gltf2_io_export 38 | from . import gltf2_io_draco_compression_extension 39 | from .gltf2_io_user_extensions import export_user_extensions 40 | 41 | 42 | def save_ext_gltf(context, export_settings): 43 | """Start the glTF 2.0 export and saves to content either to a .gltf or .glb file.""" 44 | if bpy.context.active_object is not None: 45 | if bpy.context.active_object.mode != "OBJECT": # For linked object, you can't force OBJECT mode 46 | bpy.ops.object.mode_set(mode='OBJECT') 47 | 48 | original_frame = bpy.context.scene.frame_current 49 | if not export_settings['gltf_current_frame']: 50 | bpy.context.scene.frame_set(0) 51 | 52 | __notify_start_ext_gltf(context) 53 | start_time = time.time() 54 | pre_export_callbacks = export_settings["pre_export_callbacks"] 55 | for callback in pre_export_callbacks: 56 | callback(export_settings) 57 | 58 | json, buffer = __export_ext_gltf(export_settings) 59 | 60 | post_export_callbacks = export_settings["post_export_callbacks"] 61 | for callback in post_export_callbacks: 62 | callback(export_settings) 63 | __write_file_ext_gltf(json, buffer, export_settings) 64 | 65 | end_time = time.time() 66 | __notify_end_ext_gltf(context, end_time - start_time) 67 | 68 | if not export_settings['gltf_current_frame']: 69 | bpy.context.scene.frame_set(original_frame) 70 | 71 | #save XML file if required: 72 | if export_settings['gltf_msfs_xml'] == True: 73 | from .msfs_xml_export import save_xml 74 | save_xml(context,export_settings) 75 | 76 | return {'FINISHED'} 77 | 78 | 79 | def __export_ext_gltf(export_settings): 80 | exporter = GlTF2Exporter(export_settings) 81 | __gather_ext_gltf(exporter, export_settings) 82 | buffer = __create_buffer_ext_gltf(exporter, export_settings) 83 | exporter.finalize_images() 84 | json = __fix_json_ext_gltf(exporter.glTF.to_dict()) 85 | 86 | return json, buffer 87 | 88 | 89 | def __gather_ext_gltf(exporter, export_settings): 90 | active_scene_idx, scenes, animations = gltf2_blender_gather.gather_gltf2(export_settings) 91 | 92 | plan = {'active_scene_idx': active_scene_idx, 'scenes': scenes, 'animations': animations} 93 | export_user_extensions('gather_gltf_hook', export_settings, plan) 94 | active_scene_idx, scenes, animations = plan['active_scene_idx'], plan['scenes'], plan['animations'] 95 | 96 | if export_settings['gltf_draco_mesh_compression']: 97 | gltf2_io_draco_compression_extension.compress_scene_primitives(scenes, export_settings) 98 | exporter.add_draco_extension() 99 | 100 | for idx, scene in enumerate(scenes): 101 | exporter.add_scene(scene, idx==active_scene_idx) 102 | for animation in animations: 103 | exporter.add_animation(animation) 104 | 105 | 106 | def __create_buffer_ext_gltf(exporter, export_settings): 107 | buffer = bytes() 108 | if export_settings[gltf2_blender_export_keys.FORMAT] == 'GLB': 109 | buffer = exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY], is_glb=True) 110 | else: 111 | if export_settings[gltf2_blender_export_keys.FORMAT] == 'GLTF_EMBEDDED': 112 | exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY]) 113 | else: 114 | exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY], 115 | export_settings[gltf2_blender_export_keys.BINARY_FILENAME]) 116 | 117 | return buffer 118 | 119 | 120 | def __fix_json_ext_gltf(obj): 121 | # TODO: move to custom JSON encoder 122 | fixed = obj 123 | if isinstance(obj, dict): 124 | fixed = {} 125 | for key, value in obj.items(): 126 | if not __should_include_json_value_ext_gltf(key, value): 127 | continue 128 | fixed[key] = __fix_json_ext_gltf(value) 129 | elif isinstance(obj, list): 130 | fixed = [] 131 | for value in obj: 132 | fixed.append(__fix_json_ext_gltf(value)) 133 | elif isinstance(obj, float): 134 | # force floats to int, if they are integers (prevent INTEGER_WRITTEN_AS_FLOAT validator warnings) 135 | if int(obj) == obj: 136 | return int(obj) 137 | return fixed 138 | 139 | 140 | def __should_include_json_value_ext_gltf(key, value): 141 | allowed_empty_collections = ["KHR_materials_unlit"] 142 | 143 | if value is None: 144 | return False 145 | elif __is_empty_collection_ext_gltf(value) and key not in allowed_empty_collections: 146 | return False 147 | return True 148 | 149 | 150 | def __is_empty_collection_ext_gltf(value): 151 | return (isinstance(value, dict) or isinstance(value, list)) and len(value) == 0 152 | 153 | 154 | def __write_file_ext_gltf(json, buffer, export_settings): 155 | try: 156 | gltf2_io_export.save_gltf( 157 | json, 158 | export_settings, 159 | gltf2_blender_json.BlenderJSONEncoder, 160 | buffer) 161 | except AssertionError as e: 162 | _, _, tb = sys.exc_info() 163 | traceback.print_tb(tb) # Fixed format 164 | tb_info = traceback.extract_tb(tb) 165 | for tbi in tb_info: 166 | filename, line, func, text = tbi 167 | print_console('ERROR', 'An error occurred on line {} in statement {}'.format(line, text)) 168 | print_console('ERROR', str(e)) 169 | raise e 170 | 171 | 172 | def __notify_start_ext_gltf(context): 173 | print_console('INFO', 'Starting glTF 2.0 export') 174 | context.window_manager.progress_begin(0, 100) 175 | context.window_manager.progress_update(0) 176 | 177 | 178 | def __notify_end_ext_gltf(context, elapsed): 179 | print_console('INFO', 'Finished glTF 2.0 export in {} s'.format(elapsed)) 180 | context.window_manager.progress_end() 181 | print_newline() 182 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_export_keys.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FILTERED_VERTEX_GROUPS = 'filtered_vertex_groups' 16 | FILTERED_MESHES = 'filtered_meshes' 17 | FILTERED_IMAGES = 'filtered_images' 18 | FILTERED_IMAGES_USE_ALPHA = 'filtered_images_use_alpha' 19 | FILTERED_MERGED_IMAGES = 'filtered_merged_images' 20 | FILTERED_TEXTURES = 'filtered_textures' 21 | FILTERED_MATERIALS = 'filtered_materials' 22 | FILTERED_LIGHTS = 'filtered_lights' 23 | TEMPORARY_MESHES = 'temporary_meshes' 24 | FILTERED_OBJECTS = 'filtered_objects' 25 | FILTERED_CAMERAS = 'filtered_cameras' 26 | 27 | APPLY = 'gltf_apply' 28 | SELECTED = 'gltf_selected' 29 | VISIBLE = 'gltf_visible' 30 | RENDERABLE = 'gltf_renderable' 31 | ACTIVE_COLLECTION = 'gltf_active_collection' 32 | SKINS = 'gltf_skins' 33 | DISPLACEMENT = 'gltf_displacement' 34 | FORCE_SAMPLING = 'gltf_force_sampling' 35 | FRAME_RANGE = 'gltf_frame_range' 36 | FRAME_STEP = 'gltf_frame_step' 37 | JOINT_CACHE = 'gltf_joint_cache' 38 | COPYRIGHT = 'gltf_copyright' 39 | FORMAT = 'gltf_format' 40 | FILE_DIRECTORY = 'gltf_filedirectory' 41 | TEXTURE_DIRECTORY = 'gltf_texturedirectory' 42 | BINARY_FILENAME = 'gltf_binaryfilename' 43 | YUP = 'gltf_yup' 44 | MORPH = 'gltf_morph' 45 | TEX_COORDS = 'gltf_texcoords' 46 | COLORS = 'gltf_colors' 47 | NORMALS = 'gltf_normals' 48 | TANGENTS = 'gltf_tangents' 49 | MORPH_TANGENT = 'gltf_morph_tangent' 50 | MORPH_NORMAL = 'gltf_morph_normal' 51 | MATERIALS = 'gltf_materials' 52 | EXTRAS = 'gltf_extras' 53 | CAMERAS = 'gltf_cameras' 54 | LIGHTS = 'gltf_lights' 55 | ANIMATIONS = 'gltf_animations' 56 | EMBED_IMAGES = 'gltf_embed_images' 57 | BINARY = 'gltf_binary' 58 | EMBED_BUFFERS = 'gltf_embed_buffers' 59 | USE_NO_COLOR = 'gltf_use_no_color' 60 | 61 | METALLIC_ROUGHNESS_IMAGE = "metallic_roughness_image" 62 | GROUP_INDEX = 'group_index' 63 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | 17 | from ..com import gltf2_io 18 | from ..com.gltf2_io_debug import print_console 19 | from . import gltf2_blender_gather_nodes 20 | #from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes 21 | from . import gltf2_blender_gather_animations 22 | from .gltf2_blender_gather_cache import cached 23 | from ..com.gltf2_blender_extras import generate_extras 24 | from . import gltf2_blender_export_keys 25 | from .gltf2_io_user_extensions import export_user_extensions 26 | 27 | 28 | def gather_gltf2(export_settings): 29 | """ 30 | Gather glTF properties from the current state of blender. 31 | 32 | :return: list of scene graphs to be added to the glTF export 33 | """ 34 | scenes = [] 35 | animations = [] # unfortunately animations in gltf2 are just as 'root' as scenes. 36 | active_scene = None 37 | for blender_scene in bpy.data.scenes: 38 | scenes.append(__gather_scene(blender_scene, export_settings)) 39 | if export_settings[gltf2_blender_export_keys.ANIMATIONS]: 40 | animations += __gather_animations(blender_scene, export_settings) 41 | if bpy.context.scene.name == blender_scene.name: 42 | active_scene = len(scenes) -1 43 | return active_scene, scenes, animations 44 | 45 | 46 | @cached 47 | def __gather_scene(blender_scene, export_settings): 48 | scene = gltf2_io.Scene( 49 | extensions=None, 50 | extras=__gather_extras(blender_scene, export_settings), 51 | name=blender_scene.name, 52 | nodes=[] 53 | ) 54 | 55 | for _blender_object in [obj for obj in blender_scene.objects if obj.proxy is None]: 56 | if _blender_object.parent is None: 57 | blender_object = _blender_object.proxy if _blender_object.proxy else _blender_object 58 | node = gltf2_blender_gather_nodes.gather_node( 59 | blender_object, 60 | blender_object.library.name if blender_object.library else None, 61 | blender_scene, None, export_settings) 62 | if node is not None: 63 | scene.nodes.append(node) 64 | 65 | export_user_extensions('gather_scene_hook', export_settings, scene, blender_scene) 66 | 67 | return scene 68 | 69 | 70 | def __gather_animations(blender_scene, export_settings): 71 | animations = [] 72 | merged_tracks = {} 73 | 74 | for _blender_object in blender_scene.objects: 75 | 76 | blender_object = _blender_object.proxy if _blender_object.proxy else _blender_object 77 | 78 | # First check if this object is exported or not. Do not export animation of not exported object 79 | obj_node = gltf2_blender_gather_nodes.gather_node(blender_object, 80 | blender_object.library.name if blender_object.library else None, 81 | blender_scene, None, export_settings) 82 | if obj_node is not None: 83 | # Check was done on armature, but use here the _proxy object, because this is where the animation is 84 | animations_, merged_tracks = gltf2_blender_gather_animations.gather_animations(_blender_object, merged_tracks, len(animations), export_settings) 85 | animations += animations_ 86 | 87 | if export_settings['gltf_nla_strips'] is False: 88 | # Fake an animation with all animations of the scene 89 | merged_tracks = {} 90 | merged_tracks['Animation'] = [] 91 | for idx, animation in enumerate(animations): 92 | merged_tracks['Animation'].append(idx) 93 | 94 | 95 | to_delete_idx = [] 96 | for merged_anim_track in merged_tracks.keys(): 97 | if len(merged_tracks[merged_anim_track]) < 2: 98 | 99 | # There is only 1 animation in the track 100 | # If name of the track is not a default name, use this name for action 101 | animations[merged_tracks[merged_anim_track][0]].name = merged_anim_track 102 | 103 | continue 104 | 105 | base_animation_idx = None 106 | offset_sampler = 0 107 | 108 | for idx, anim_idx in enumerate(merged_tracks[merged_anim_track]): 109 | if idx == 0: 110 | base_animation_idx = anim_idx 111 | animations[anim_idx].name = merged_anim_track 112 | already_animated = [] 113 | for channel in animations[anim_idx].channels: 114 | already_animated.append((channel.target.node, channel.target.path)) 115 | continue 116 | 117 | to_delete_idx.append(anim_idx) 118 | 119 | # Merging extras 120 | # Warning, some values can be overwritten if present in multiple merged animations 121 | if animations[anim_idx].extras is not None: 122 | for k in animations[anim_idx].extras.keys(): 123 | if animations[base_animation_idx].extras is None: 124 | animations[base_animation_idx].extras = {} 125 | animations[base_animation_idx].extras[k] = animations[anim_idx].extras[k] 126 | 127 | offset_sampler = len(animations[base_animation_idx].samplers) 128 | for sampler in animations[anim_idx].samplers: 129 | animations[base_animation_idx].samplers.append(sampler) 130 | 131 | for channel in animations[anim_idx].channels: 132 | if (channel.target.node, channel.target.path) in already_animated: 133 | print_console("WARNING", "Some strips have same channel animation ({}), on node {} !".format(channel.target.path, channel.target.node.name)) 134 | continue 135 | animations[base_animation_idx].channels.append(channel) 136 | animations[base_animation_idx].channels[-1].sampler = animations[base_animation_idx].channels[-1].sampler + offset_sampler 137 | already_animated.append((channel.target.node, channel.target.path)) 138 | 139 | new_animations = [] 140 | if len(to_delete_idx) != 0: 141 | for idx, animation in enumerate(animations): 142 | if idx in to_delete_idx: 143 | continue 144 | new_animations.append(animation) 145 | else: 146 | new_animations = animations 147 | 148 | 149 | return new_animations 150 | 151 | 152 | def __gather_extras(blender_object, export_settings): 153 | if export_settings[gltf2_blender_export_keys.EXTRAS]: 154 | return generate_extras(blender_object) 155 | return None 156 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_accessors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from .gltf2_blender_gather_cache import cached 17 | from ..com import gltf2_io 18 | from ..com import gltf2_io_constants 19 | from . import gltf2_io_binary_data 20 | 21 | 22 | #@cached 23 | #For batch LOD exports, this decorator should not be applied in order to 24 | #force one interation for each LOD. "PhysicsTeacher" 2021-08-05 25 | def gather_accessor(buffer_view: gltf2_io_binary_data.BinaryData, 26 | component_type: gltf2_io_constants.ComponentType, 27 | count, 28 | max, 29 | min, 30 | type: gltf2_io_constants.DataType, 31 | export_settings) -> gltf2_io.Accessor: 32 | return gltf2_io.Accessor( 33 | buffer_view=buffer_view, 34 | byte_offset=None, 35 | component_type=component_type, 36 | count=count, 37 | extensions=None, 38 | extras=None, 39 | max=list(max) if max is not None else None, 40 | min=list(min) if min is not None else None, 41 | name=None, 42 | normalized=None, 43 | sparse=None, 44 | type=type 45 | ) 46 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_animation_channel_target.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import bpy 17 | import typing 18 | from ..com import gltf2_io 19 | from .gltf2_blender_gather_cache import cached 20 | from . import gltf2_blender_gather_nodes 21 | from . import gltf2_blender_gather_joints 22 | from . import gltf2_blender_gather_skins 23 | from .gltf2_io_user_extensions import export_user_extensions 24 | 25 | @cached 26 | def gather_animation_channel_target(channels: typing.Tuple[bpy.types.FCurve], 27 | blender_object: bpy.types.Object, 28 | bake_bone: typing.Union[str, None], 29 | bake_channel: typing.Union[str, None], 30 | driver_obj, 31 | export_settings 32 | ) -> gltf2_io.AnimationChannelTarget: 33 | 34 | animation_channel_target = gltf2_io.AnimationChannelTarget( 35 | extensions=__gather_extensions(channels, blender_object, export_settings, bake_bone), 36 | extras=__gather_extras(channels, blender_object, export_settings, bake_bone), 37 | node=__gather_node(channels, blender_object, export_settings, bake_bone, driver_obj), 38 | path=__gather_path(channels, blender_object, export_settings, bake_bone, bake_channel) 39 | ) 40 | 41 | export_user_extensions('gather_animation_channel_target_hook', 42 | export_settings, 43 | animation_channel_target, 44 | channels, 45 | blender_object, 46 | bake_bone, 47 | bake_channel) 48 | 49 | return animation_channel_target 50 | 51 | def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve], 52 | blender_object: bpy.types.Object, 53 | export_settings, 54 | bake_bone: typing.Union[str, None] 55 | ) -> typing.Any: 56 | return None 57 | 58 | 59 | def __gather_extras(channels: typing.Tuple[bpy.types.FCurve], 60 | blender_object: bpy.types.Object, 61 | export_settings, 62 | bake_bone: typing.Union[str, None] 63 | ) -> typing.Any: 64 | return None 65 | 66 | 67 | def __gather_node(channels: typing.Tuple[bpy.types.FCurve], 68 | blender_object: bpy.types.Object, 69 | export_settings, 70 | bake_bone: typing.Union[str, None], 71 | driver_obj 72 | ) -> gltf2_io.Node: 73 | 74 | if driver_obj is not None: 75 | return gltf2_blender_gather_nodes.gather_node(driver_obj, 76 | driver_obj.library.name if driver_obj.library else None, 77 | None, None, export_settings) 78 | 79 | if blender_object.type == "ARMATURE": 80 | # TODO: get joint from fcurve data_path and gather_joint 81 | 82 | if bake_bone is not None: 83 | blender_bone = blender_object.pose.bones[bake_bone] 84 | else: 85 | blender_bone = blender_object.path_resolve(channels[0].data_path.rsplit('.', 1)[0]) 86 | 87 | if isinstance(blender_bone, bpy.types.PoseBone): 88 | if export_settings["gltf_def_bones"] is False: 89 | obj = blender_object.proxy if blender_object.proxy else blender_object 90 | return gltf2_blender_gather_joints.gather_joint(obj, blender_bone, export_settings) 91 | else: 92 | bones, _, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_object) 93 | if blender_bone.name in [b.name for b in bones]: 94 | obj = blender_object.proxy if blender_object.proxy else blender_object 95 | return gltf2_blender_gather_joints.gather_joint(obj, blender_bone, export_settings) 96 | 97 | return gltf2_blender_gather_nodes.gather_node(blender_object, 98 | blender_object.library.name if blender_object.library else None, 99 | None, None, export_settings) 100 | 101 | 102 | def __gather_path(channels: typing.Tuple[bpy.types.FCurve], 103 | blender_object: bpy.types.Object, 104 | export_settings, 105 | bake_bone: typing.Union[str, None], 106 | bake_channel: typing.Union[str, None] 107 | ) -> str: 108 | if bake_channel is None: 109 | # Note: channels has some None items only for SK if some SK are not animated 110 | target = [c for c in channels if c is not None][0].data_path.split('.')[-1] 111 | else: 112 | target = bake_channel 113 | path = { 114 | "delta_location": "translation", 115 | "delta_rotation_euler": "rotation", 116 | "location": "translation", 117 | "rotation_axis_angle": "rotation", 118 | "rotation_euler": "rotation", 119 | "rotation_quaternion": "rotation", 120 | "scale": "scale", 121 | "value": "weights" 122 | }.get(target) 123 | 124 | if target is None: 125 | return None 126 | 127 | return path 128 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_cache.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import functools 16 | import bpy 17 | from . import gltf2_blender_get 18 | 19 | 20 | def cached(func): 21 | """ 22 | Decorate the cache gather functions results. 23 | 24 | The gather function is only executed if its result isn't in the cache yet 25 | :param func: the function to be decorated. It will have a static __cache member afterwards 26 | :return: 27 | """ 28 | @functools.wraps(func) 29 | def wrapper_cached(*args, **kwargs): 30 | assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function" 31 | cache_key_args = args 32 | # make a shallow copy of the keyword arguments so that 'export_settings' can be removed 33 | cache_key_kwargs = dict(kwargs) 34 | if kwargs.get("export_settings"): 35 | export_settings = kwargs["export_settings"] 36 | # 'export_settings' should not be cached 37 | del cache_key_kwargs["export_settings"] 38 | else: 39 | export_settings = args[-1] 40 | cache_key_args = args[:-1] 41 | 42 | __by_name = [bpy.types.Object, bpy.types.Scene, bpy.types.Material, bpy.types.Action, bpy.types.Mesh, bpy.types.PoseBone] 43 | 44 | # we make a tuple from the function arguments so that they can be used as a key to the cache 45 | cache_key = () 46 | for i in cache_key_args: 47 | if type(i) in __by_name: 48 | cache_key += (i.name,) 49 | else: 50 | cache_key += (i,) 51 | for i in cache_key_kwargs.values(): 52 | if type(i) in __by_name: 53 | cache_key += (i.name,) 54 | else: 55 | cache_key += (i,) 56 | 57 | # invalidate cache if export settings have changed 58 | if not hasattr(func, "__export_settings") or export_settings != func.__export_settings: 59 | func.__cache = {} 60 | func.__export_settings = export_settings 61 | # use or fill cache 62 | if cache_key in func.__cache: 63 | return func.__cache[cache_key] 64 | else: 65 | result = func(*args) 66 | func.__cache[cache_key] = result 67 | return result 68 | return wrapper_cached 69 | 70 | def bonecache(func): 71 | 72 | def reset_cache_bonecache(): 73 | func.__current_action_name = None 74 | func.__current_armature_name = None 75 | func.__bonecache = {} 76 | 77 | func.reset_cache = reset_cache_bonecache 78 | 79 | @functools.wraps(func) 80 | def wrapper_bonecache(*args, **kwargs): 81 | if args[2] is None: 82 | pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(args[0], 83 | args[1][0].data_path) 84 | else: 85 | pose_bone_if_armature = args[0].pose.bones[args[2]] 86 | 87 | if not hasattr(func, "__current_action_name"): 88 | func.reset_cache() 89 | if args[6] != func.__current_action_name or args[0] != func.__current_armature_name: 90 | result = func(*args) 91 | func.__bonecache = result 92 | func.__current_action_name = args[6] 93 | func.__current_armature_name = args[0] 94 | return result[args[7]][pose_bone_if_armature.name] 95 | else: 96 | return func.__bonecache[args[7]][pose_bone_if_armature.name] 97 | return wrapper_bonecache 98 | 99 | # TODO: replace "cached" with "unique" in all cases where the caching is functional and not only for performance reasons 100 | call_or_fetch = cached 101 | unique = cached 102 | 103 | def skdriverdiscovercache(func): 104 | 105 | def reset_cache_skdriverdiscovercache(): 106 | func.__current_armature_name = None 107 | func.__skdriverdiscover = {} 108 | 109 | func.reset_cache = reset_cache_skdriverdiscovercache 110 | 111 | @functools.wraps(func) 112 | def wrapper_skdriverdiscover(*args, **kwargs): 113 | if not hasattr(func, "__current_armature_name") or func.__current_armature_name is None: 114 | func.reset_cache() 115 | 116 | if args[0] != func.__current_armature_name: 117 | result = func(*args) 118 | func.__skdriverdiscover[args[0]] = result 119 | func.__current_armature_name = args[0] 120 | return result 121 | else: 122 | return func.__skdriverdiscover[args[0]] 123 | return wrapper_skdriverdiscover 124 | 125 | def skdrivervalues(func): 126 | 127 | def reset_cache_skdrivervalues(): 128 | func.__skdrivervalues = {} 129 | 130 | func.reset_cache = reset_cache_skdrivervalues 131 | 132 | @functools.wraps(func) 133 | def wrapper_skdrivervalues(*args, **kwargs): 134 | if not hasattr(func, "__skdrivervalues") or func.__skdrivervalues is None: 135 | func.reset_cache() 136 | 137 | if args[0].name not in func.__skdrivervalues.keys(): 138 | func.__skdrivervalues[args[0].name] = {} 139 | if args[1] not in func.__skdrivervalues[args[0].name]: 140 | vals = func(*args) 141 | func.__skdrivervalues[args[0].name][args[1]] = vals 142 | return vals 143 | else: 144 | return func.__skdrivervalues[args[0].name][args[1]] 145 | return wrapper_skdrivervalues 146 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_cameras.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .gltf2_blender_gather_cache import cached 16 | from ..com.gltf2_blender_extras import generate_extras 17 | from ..com import gltf2_io 18 | from .gltf2_io_user_extensions import export_user_extensions 19 | 20 | import bpy 21 | import math 22 | 23 | 24 | @cached 25 | def gather_camera(blender_camera, export_settings): 26 | if not __filter_camera(blender_camera, export_settings): 27 | return None 28 | 29 | camera = gltf2_io.Camera( 30 | extensions=__gather_extensions(blender_camera, export_settings), 31 | extras=__gather_extras(blender_camera, export_settings), 32 | name=__gather_name(blender_camera, export_settings), 33 | orthographic=__gather_orthographic(blender_camera, export_settings), 34 | perspective=__gather_perspective(blender_camera, export_settings), 35 | type=__gather_type(blender_camera, export_settings) 36 | ) 37 | 38 | export_user_extensions('gather_camera_hook', export_settings, camera, blender_camera) 39 | 40 | return camera 41 | 42 | 43 | def __filter_camera(blender_camera, export_settings): 44 | return bool(__gather_type(blender_camera, export_settings)) 45 | 46 | 47 | def __gather_extensions(blender_camera, export_settings): 48 | return None 49 | 50 | 51 | def __gather_extras(blender_camera, export_settings): 52 | if export_settings['gltf_extras']: 53 | return generate_extras(blender_camera) 54 | return None 55 | 56 | 57 | def __gather_name(blender_camera, export_settings): 58 | return blender_camera.name 59 | 60 | 61 | def __gather_orthographic(blender_camera, export_settings): 62 | if __gather_type(blender_camera, export_settings) == "orthographic": 63 | orthographic = gltf2_io.CameraOrthographic( 64 | extensions=None, 65 | extras=None, 66 | xmag=None, 67 | ymag=None, 68 | zfar=None, 69 | znear=None 70 | ) 71 | 72 | orthographic.xmag = blender_camera.ortho_scale 73 | orthographic.ymag = blender_camera.ortho_scale 74 | 75 | orthographic.znear = blender_camera.clip_start 76 | orthographic.zfar = blender_camera.clip_end 77 | 78 | return orthographic 79 | return None 80 | 81 | 82 | def __gather_perspective(blender_camera, export_settings): 83 | if __gather_type(blender_camera, export_settings) == "perspective": 84 | perspective = gltf2_io.CameraPerspective( 85 | aspect_ratio=None, 86 | extensions=None, 87 | extras=None, 88 | yfov=None, 89 | zfar=None, 90 | znear=None 91 | ) 92 | 93 | width = bpy.context.scene.render.pixel_aspect_x * bpy.context.scene.render.resolution_x 94 | height = bpy.context.scene.render.pixel_aspect_y * bpy.context.scene.render.resolution_y 95 | perspective.aspectRatio = width / height 96 | 97 | if width >= height: 98 | if blender_camera.sensor_fit != 'VERTICAL': 99 | perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio) 100 | else: 101 | perspective.yfov = blender_camera.angle 102 | else: 103 | if blender_camera.sensor_fit != 'HORIZONTAL': 104 | perspective.yfov = blender_camera.angle 105 | else: 106 | perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio) 107 | 108 | perspective.znear = blender_camera.clip_start 109 | perspective.zfar = blender_camera.clip_end 110 | 111 | return perspective 112 | return None 113 | 114 | 115 | def __gather_type(blender_camera, export_settings): 116 | if blender_camera.type == 'PERSP': 117 | return "perspective" 118 | elif blender_camera.type == 'ORTHO': 119 | return "orthographic" 120 | return None 121 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_drivers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from .gltf2_blender_gather_cache import skdriverdiscovercache, skdrivervalues 17 | from ..com.gltf2_blender_data_path import get_target_object_path 18 | 19 | 20 | @skdriverdiscovercache 21 | def get_sk_drivers(blender_armature): 22 | 23 | drivers = [] 24 | 25 | for child in blender_armature.children: 26 | if not child.data: 27 | continue 28 | # child.data can be an armature - which has no shapekeys 29 | if not hasattr(child.data, 'shape_keys'): 30 | continue 31 | if not child.data.shape_keys: 32 | continue 33 | if not child.data.shape_keys.animation_data: 34 | continue 35 | if not child.data.shape_keys.animation_data.drivers: 36 | continue 37 | if len(child.data.shape_keys.animation_data.drivers) <= 0: 38 | continue 39 | 40 | shapekeys_idx = {} 41 | cpt_sk = 0 42 | for sk in child.data.shape_keys.key_blocks: 43 | if sk == sk.relative_key: 44 | continue 45 | if sk.mute is True: 46 | continue 47 | shapekeys_idx[sk.name] = cpt_sk 48 | cpt_sk += 1 49 | 50 | # Note: channels will have some None items only for SK if some SK are not animated 51 | idx_channel_mapping = [] 52 | all_sorted_channels = [] 53 | for sk_c in child.data.shape_keys.animation_data.drivers: 54 | sk_name = child.data.shape_keys.path_resolve(get_target_object_path(sk_c.data_path)).name 55 | idx = shapekeys_idx[sk_name] 56 | idx_channel_mapping.append((shapekeys_idx[sk_name], sk_c)) 57 | existing_idx = dict(idx_channel_mapping) 58 | for i in range(0, cpt_sk): 59 | if i not in existing_idx.keys(): 60 | all_sorted_channels.append(None) 61 | else: 62 | all_sorted_channels.append(existing_idx[i]) 63 | 64 | drivers.append((child, tuple(all_sorted_channels))) 65 | 66 | return tuple(drivers) 67 | 68 | @skdrivervalues 69 | def get_sk_driver_values(blender_object, frame, fcurves): 70 | sk_values = [] 71 | for f in [f for f in fcurves if f is not None]: 72 | sk_values.append(blender_object.data.shape_keys.path_resolve(get_target_object_path(f.data_path)).value) 73 | 74 | return tuple(sk_values) 75 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_joints.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import mathutils 16 | 17 | from . import gltf2_blender_export_keys 18 | from .gltf2_blender_gather_cache import cached 19 | from ..com import gltf2_io 20 | from . import gltf2_blender_extract 21 | from ..com import gltf2_blender_math 22 | from . import gltf2_blender_gather_skins 23 | from ..com.gltf2_blender_extras import generate_extras 24 | 25 | @cached 26 | def gather_joint(blender_object, blender_bone, export_settings): 27 | """ 28 | Generate a glTF2 node from a blender bone, as joints in glTF2 are simply nodes. 29 | 30 | :param blender_bone: a blender PoseBone 31 | :param export_settings: the settings for this export 32 | :return: a glTF2 node (acting as a joint) 33 | """ 34 | axis_basis_change = mathutils.Matrix.Identity(4) 35 | if export_settings[gltf2_blender_export_keys.YUP]: 36 | axis_basis_change = mathutils.Matrix( 37 | ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) 38 | 39 | # extract bone transform 40 | if blender_bone.parent is None: 41 | correction_matrix_local = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local) 42 | else: 43 | correction_matrix_local = gltf2_blender_math.multiply( 44 | blender_bone.parent.bone.matrix_local.inverted(), blender_bone.bone.matrix_local) 45 | matrix_basis = blender_bone.matrix_basis 46 | trans, rot, sca = gltf2_blender_extract.decompose_transition( 47 | gltf2_blender_math.multiply(correction_matrix_local, matrix_basis), export_settings) 48 | translation, rotation, scale = (None, None, None) 49 | if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0: 50 | translation = [trans[0], trans[1], trans[2]] 51 | if rot[0] != 1.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 0.0: 52 | rotation = [rot[1], rot[2], rot[3], rot[0]] 53 | if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0: 54 | scale = [sca[0], sca[1], sca[2]] 55 | 56 | # traverse into children 57 | children = [] 58 | 59 | if export_settings["gltf_def_bones"] is False: 60 | for bone in blender_bone.children: 61 | children.append(gather_joint(blender_object, bone, export_settings)) 62 | else: 63 | _, children_, _ = gltf2_blender_gather_skins.get_bone_tree(None, blender_bone.id_data) 64 | if blender_bone.name in children_.keys(): 65 | for bone in children_[blender_bone.name]: 66 | children.append(gather_joint(blender_object, blender_bone.id_data.pose.bones[bone], export_settings)) 67 | 68 | # finally add to the joints array containing all the joints in the hierarchy 69 | return gltf2_io.Node( 70 | camera=None, 71 | children=children, 72 | extensions=None, 73 | extras=__gather_extras(blender_bone, export_settings), 74 | matrix=None, 75 | mesh=None, 76 | name=blender_bone.name, 77 | rotation=rotation, 78 | scale=scale, 79 | skin=None, 80 | translation=translation, 81 | weights=None 82 | ) 83 | 84 | def __gather_extras(blender_bone, export_settings): 85 | if export_settings['gltf_extras']: 86 | return generate_extras(blender_bone.bone) 87 | return None 88 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_light_spots.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Optional 16 | from ..com import gltf2_io_lights_punctual 17 | 18 | 19 | def gather_light_spot(blender_lamp, export_settings) -> Optional[gltf2_io_lights_punctual.LightSpot]: 20 | 21 | if not __filter_light_spot(blender_lamp, export_settings): 22 | return None 23 | 24 | spot = gltf2_io_lights_punctual.LightSpot( 25 | inner_cone_angle=__gather_inner_cone_angle(blender_lamp, export_settings), 26 | outer_cone_angle=__gather_outer_cone_angle(blender_lamp, export_settings) 27 | ) 28 | return spot 29 | 30 | 31 | def __filter_light_spot(blender_lamp, _) -> bool: 32 | if blender_lamp.type != "SPOT": 33 | return False 34 | 35 | return True 36 | 37 | 38 | def __gather_inner_cone_angle(blender_lamp, _) -> Optional[float]: 39 | angle = blender_lamp.spot_size * 0.5 40 | return angle - angle * blender_lamp.spot_blend 41 | 42 | 43 | def __gather_outer_cone_angle(blender_lamp, _) -> Optional[float]: 44 | return blender_lamp.spot_size * 0.5 45 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_lights.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | import math 17 | from typing import Optional, List, Dict, Any 18 | 19 | from .gltf2_blender_gather_cache import cached 20 | from ..com.gltf2_blender_extras import generate_extras 21 | 22 | from ..com import gltf2_io_lights_punctual 23 | from ..com import gltf2_io_debug 24 | 25 | from . import gltf2_blender_gather_light_spots 26 | from . import gltf2_blender_search_node_tree 27 | 28 | 29 | @cached 30 | def gather_lights_punctual(blender_lamp, export_settings) -> Optional[Dict[str, Any]]: 31 | if not __filter_lights_punctual(blender_lamp, export_settings): 32 | return None 33 | 34 | light = gltf2_io_lights_punctual.Light( 35 | color=__gather_color(blender_lamp, export_settings), 36 | intensity=__gather_intensity(blender_lamp, export_settings), 37 | spot=__gather_spot(blender_lamp, export_settings), 38 | type=__gather_type(blender_lamp, export_settings), 39 | range=__gather_range(blender_lamp, export_settings), 40 | name=__gather_name(blender_lamp, export_settings), 41 | extensions=__gather_extensions(blender_lamp, export_settings), 42 | extras=__gather_extras(blender_lamp, export_settings) 43 | ) 44 | 45 | return light.to_dict() 46 | 47 | 48 | def __filter_lights_punctual(blender_lamp, export_settings) -> bool: 49 | if blender_lamp.type in ["HEMI", "AREA"]: 50 | gltf2_io_debug.print_console("WARNING", "Unsupported light source {}".format(blender_lamp.type)) 51 | return False 52 | 53 | return True 54 | 55 | 56 | def __gather_color(blender_lamp, export_settings) -> Optional[List[float]]: 57 | emission_node = __get_cycles_emission_node(blender_lamp) 58 | if emission_node is not None: 59 | return list(emission_node.inputs["Color"].default_value)[:3] 60 | 61 | return list(blender_lamp.color) 62 | 63 | 64 | def __gather_intensity(blender_lamp, _) -> Optional[float]: 65 | emission_node = __get_cycles_emission_node(blender_lamp) 66 | if emission_node is not None: 67 | if blender_lamp.type != 'SUN': 68 | # When using cycles, the strength should be influenced by a LightFalloff node 69 | result = gltf2_blender_search_node_tree.from_socket( 70 | emission_node.inputs.get("Strength"), 71 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeLightFalloff) 72 | ) 73 | if result: 74 | quadratic_falloff_node = result[0].shader_node 75 | emission_strength = quadratic_falloff_node.inputs["Strength"].default_value / (math.pi * 4.0) 76 | else: 77 | gltf2_io_debug.print_console('WARNING', 78 | 'No quadratic light falloff node attached to emission strength property') 79 | emission_strength = blender_lamp.energy 80 | else: 81 | emission_strength = emission_node.inputs["Strength"].default_value 82 | return emission_strength 83 | 84 | return blender_lamp.energy 85 | 86 | 87 | def __gather_spot(blender_lamp, export_settings) -> Optional[gltf2_io_lights_punctual.LightSpot]: 88 | if blender_lamp.type == "SPOT": 89 | return gltf2_blender_gather_light_spots.gather_light_spot(blender_lamp, export_settings) 90 | return None 91 | 92 | 93 | def __gather_type(blender_lamp, _) -> str: 94 | return { 95 | "POINT": "point", 96 | "SUN": "directional", 97 | "SPOT": "spot" 98 | }[blender_lamp.type] 99 | 100 | 101 | def __gather_range(blender_lamp, export_settings) -> Optional[float]: 102 | if blender_lamp.use_custom_distance: 103 | return blender_lamp.cutoff_distance 104 | return None 105 | 106 | 107 | def __gather_name(blender_lamp, export_settings) -> Optional[str]: 108 | return blender_lamp.name 109 | 110 | 111 | def __gather_extensions(blender_lamp, export_settings) -> Optional[dict]: 112 | return None 113 | 114 | 115 | def __gather_extras(blender_lamp, export_settings) -> Optional[Any]: 116 | if export_settings['gltf_extras']: 117 | return generate_extras(blender_lamp) 118 | return None 119 | 120 | 121 | def __get_cycles_emission_node(blender_lamp) -> Optional[bpy.types.ShaderNodeEmission]: 122 | if blender_lamp.use_nodes and blender_lamp.node_tree: 123 | for currentNode in blender_lamp.node_tree.nodes: 124 | is_shadernode_output = isinstance(currentNode, bpy.types.ShaderNodeOutputLight) 125 | if is_shadernode_output: 126 | if not currentNode.is_active_output: 127 | continue 128 | result = gltf2_blender_search_node_tree.from_socket( 129 | currentNode.inputs.get("Surface"), 130 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeEmission) 131 | ) 132 | if not result: 133 | continue 134 | return result[0].shader_node 135 | return None 136 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_material_normal_texture_info_class.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | import typing 17 | from .gltf2_blender_gather_cache import cached 18 | from ..com import gltf2_io 19 | from . import gltf2_blender_gather_texture 20 | from . import gltf2_blender_search_node_tree 21 | from . import gltf2_blender_get 22 | from ..com.gltf2_io_extensions import Extension 23 | from .gltf2_io_user_extensions import export_user_extensions 24 | 25 | 26 | @cached 27 | def gather_material_normal_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[ 28 | typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]], 29 | export_settings): 30 | if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): 31 | return None 32 | 33 | texture_info = gltf2_io.MaterialNormalTextureInfoClass( 34 | extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), 35 | extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), 36 | scale=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings), 37 | index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings), 38 | tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings) 39 | ) 40 | 41 | if texture_info.index is None: 42 | return None 43 | 44 | export_user_extensions('gather_material_normal_texture_info_class_hook', 45 | export_settings, 46 | texture_info, 47 | blender_shader_sockets_or_texture_slots) 48 | 49 | return texture_info 50 | 51 | 52 | def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): 53 | if not blender_shader_sockets_or_texture_slots: 54 | return False 55 | if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]): 56 | return False 57 | if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): 58 | if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]): 59 | # sockets do not lead to a texture --> discard 60 | return False 61 | return True 62 | 63 | 64 | def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings): 65 | if not hasattr(blender_shader_sockets_or_texture_slots[0], 'links'): 66 | return None 67 | 68 | tex_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets_or_texture_slots] 69 | texture_node = tex_nodes[0] if (tex_nodes is not None and len(tex_nodes) > 0) else None 70 | if texture_node is None: 71 | return None 72 | texture_transform = gltf2_blender_get.get_texture_transform_from_texture_node(texture_node) 73 | if texture_transform is None: 74 | return None 75 | 76 | extension = Extension("KHR_texture_transform", texture_transform) 77 | return {"KHR_texture_transform": extension} 78 | 79 | 80 | def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings): 81 | return None 82 | 83 | 84 | def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings): 85 | if __is_socket(blender_shader_sockets_or_texture_slots): 86 | result = gltf2_blender_search_node_tree.from_socket( 87 | blender_shader_sockets_or_texture_slots[0], 88 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeNormalMap)) 89 | if not result: 90 | return None 91 | strengthInput = result[0].shader_node.inputs['Strength'] 92 | if not strengthInput.is_linked and strengthInput.default_value != 1: 93 | return strengthInput.default_value 94 | return None 95 | 96 | 97 | def __gather_index(blender_shader_sockets_or_texture_slots, export_settings): 98 | # We just put the actual shader into the 'index' member 99 | return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings) 100 | 101 | 102 | def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings): 103 | if __is_socket(blender_shader_sockets_or_texture_slots): 104 | blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node 105 | if len(blender_shader_node.inputs['Vector'].links) == 0: 106 | return 0 107 | 108 | input_node = blender_shader_node.inputs['Vector'].links[0].from_node 109 | 110 | if isinstance(input_node, bpy.types.ShaderNodeMapping): 111 | 112 | if len(input_node.inputs['Vector'].links) == 0: 113 | return 0 114 | 115 | input_node = input_node.inputs['Vector'].links[0].from_node 116 | 117 | if not isinstance(input_node, bpy.types.ShaderNodeUVMap): 118 | return 0 119 | 120 | if input_node.uv_map == '': 121 | return 0 122 | 123 | # Try to gather map index. 124 | for blender_mesh in bpy.data.meshes: 125 | texCoordIndex = blender_mesh.uv_layers.find(input_node.uv_map) 126 | if texCoordIndex >= 0: 127 | return texCoordIndex 128 | 129 | return 0 130 | else: 131 | raise NotImplementedError() 132 | 133 | 134 | def __is_socket(sockets_or_slots): 135 | return isinstance(sockets_or_slots[0], bpy.types.NodeSocket) 136 | 137 | 138 | def __get_tex_from_socket(socket): 139 | result = gltf2_blender_search_node_tree.from_socket( 140 | socket, 141 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) 142 | if not result: 143 | return None 144 | if result[0].shader_node.image is None: 145 | return None 146 | return result[0] 147 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | import typing 17 | from .gltf2_blender_gather_cache import cached 18 | from ..com import gltf2_io 19 | from . import gltf2_blender_gather_texture 20 | from . import gltf2_blender_search_node_tree 21 | from . import gltf2_blender_get 22 | from ..com.gltf2_io_extensions import Extension 23 | from .gltf2_io_user_extensions import export_user_extensions 24 | 25 | 26 | @cached 27 | def gather_material_occlusion_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[ 28 | typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]], 29 | export_settings): 30 | if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): 31 | return None 32 | 33 | texture_info = gltf2_io.MaterialOcclusionTextureInfoClass( 34 | extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), 35 | extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), 36 | strength=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings), 37 | index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings), 38 | tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings) 39 | ) 40 | 41 | if texture_info.index is None: 42 | return None 43 | 44 | export_user_extensions('gather_material_occlusion_texture_info_class_hook', 45 | export_settings, 46 | texture_info, 47 | blender_shader_sockets_or_texture_slots) 48 | 49 | return texture_info 50 | 51 | 52 | def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): 53 | if not blender_shader_sockets_or_texture_slots: 54 | return False 55 | if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]): 56 | return False 57 | if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): 58 | if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]): 59 | # sockets do not lead to a texture --> discard 60 | return False 61 | return True 62 | 63 | 64 | def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings): 65 | if not hasattr(blender_shader_sockets_or_texture_slots[0], 'links'): 66 | return None 67 | 68 | tex_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets_or_texture_slots] 69 | texture_node = tex_nodes[0] if (tex_nodes is not None and len(tex_nodes) > 0) else None 70 | if texture_node is None: 71 | return None 72 | texture_transform = gltf2_blender_get.get_texture_transform_from_texture_node(texture_node) 73 | if texture_transform is None: 74 | return None 75 | 76 | extension = Extension("KHR_texture_transform", texture_transform) 77 | return {"KHR_texture_transform": extension} 78 | 79 | 80 | def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings): 81 | return None 82 | 83 | 84 | def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings): 85 | return None 86 | 87 | 88 | def __gather_index(blender_shader_sockets_or_texture_slots, export_settings): 89 | # We just put the actual shader into the 'index' member 90 | return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings) 91 | 92 | 93 | def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings): 94 | if __is_socket(blender_shader_sockets_or_texture_slots): 95 | blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node 96 | if len(blender_shader_node.inputs['Vector'].links) == 0: 97 | return 0 98 | 99 | input_node = blender_shader_node.inputs['Vector'].links[0].from_node 100 | 101 | if isinstance(input_node, bpy.types.ShaderNodeMapping): 102 | 103 | if len(input_node.inputs['Vector'].links) == 0: 104 | return 0 105 | 106 | input_node = input_node.inputs['Vector'].links[0].from_node 107 | 108 | if not isinstance(input_node, bpy.types.ShaderNodeUVMap): 109 | return 0 110 | 111 | if input_node.uv_map == '': 112 | return 0 113 | 114 | # Try to gather map index. 115 | for blender_mesh in bpy.data.meshes: 116 | texCoordIndex = blender_mesh.uv_layers.find(input_node.uv_map) 117 | if texCoordIndex >= 0: 118 | return texCoordIndex 119 | 120 | return 0 121 | else: 122 | raise NotImplementedError() 123 | 124 | 125 | def __is_socket(sockets_or_slots): 126 | return isinstance(sockets_or_slots[0], bpy.types.NodeSocket) 127 | 128 | 129 | def __get_tex_from_socket(socket): 130 | result = gltf2_blender_search_node_tree.from_socket( 131 | socket, 132 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) 133 | if not result: 134 | return None 135 | if result[0].shader_node.image is None: 136 | return None 137 | return result[0] 138 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | 17 | from ..com import gltf2_io 18 | from . import gltf2_blender_gather_texture_info, gltf2_blender_search_node_tree 19 | from . import gltf2_blender_get 20 | from .gltf2_blender_gather_cache import cached 21 | from ..com.gltf2_io_debug import print_console 22 | from .gltf2_io_user_extensions import export_user_extensions 23 | 24 | 25 | @cached 26 | def gather_material_pbr_metallic_roughness(blender_material, orm_texture, export_settings): 27 | if not __filter_pbr_material(blender_material, export_settings): 28 | return None 29 | 30 | material = gltf2_io.MaterialPBRMetallicRoughness( 31 | base_color_factor=__gather_base_color_factor(blender_material, export_settings), 32 | base_color_texture=__gather_base_color_texture(blender_material, export_settings), 33 | extensions=__gather_extensions(blender_material, export_settings), 34 | extras=__gather_extras(blender_material, export_settings), 35 | metallic_factor=__gather_metallic_factor(blender_material, export_settings), 36 | metallic_roughness_texture=__gather_metallic_roughness_texture(blender_material, orm_texture, export_settings), 37 | roughness_factor=__gather_roughness_factor(blender_material, export_settings) 38 | ) 39 | 40 | export_user_extensions('gather_material_pbr_metallic_roughness_hook', export_settings, material, blender_material, orm_texture) 41 | 42 | return material 43 | 44 | 45 | def __filter_pbr_material(blender_material, export_settings): 46 | return True 47 | 48 | 49 | def __gather_base_color_factor(blender_material, export_settings): 50 | base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color") 51 | if base_color_socket is None: 52 | base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor") 53 | if base_color_socket is None: 54 | base_color_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "BaseColorFactor") 55 | if base_color_socket is None: 56 | base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background") 57 | if not isinstance(base_color_socket, bpy.types.NodeSocket): 58 | return None 59 | if not base_color_socket.is_linked: 60 | return list(base_color_socket.default_value) 61 | 62 | texture_node = __get_tex_from_socket(base_color_socket) 63 | if texture_node is None: 64 | return None 65 | 66 | def is_valid_multiply_node(node): 67 | return isinstance(node, bpy.types.ShaderNodeMixRGB) and \ 68 | node.blend_type == "MULTIPLY" and \ 69 | len(node.inputs) == 3 70 | 71 | multiply_node = next((link.from_node for link in texture_node.path if is_valid_multiply_node(link.from_node)), None) 72 | if multiply_node is None: 73 | return None 74 | 75 | def is_factor_socket(socket): 76 | return isinstance(socket, bpy.types.NodeSocketColor) and \ 77 | (not socket.is_linked or socket.links[0] not in texture_node.path) 78 | 79 | factor_socket = next((socket for socket in multiply_node.inputs if is_factor_socket(socket)), None) 80 | if factor_socket is None: 81 | return None 82 | 83 | if factor_socket.is_linked: 84 | print_console("WARNING", "BaseColorFactor only supports sockets without links (in Node '{}')." 85 | .format(multiply_node.name)) 86 | return None 87 | 88 | return list(factor_socket.default_value) 89 | 90 | 91 | def __gather_base_color_texture(blender_material, export_settings): 92 | base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color") 93 | if base_color_socket is None: 94 | base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor") 95 | if base_color_socket is None: 96 | base_color_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "BaseColor") 97 | if base_color_socket is None: 98 | base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background") 99 | 100 | alpha_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Alpha") 101 | if alpha_socket is not None and alpha_socket.is_linked: 102 | inputs = (base_color_socket, alpha_socket, ) 103 | else: 104 | inputs = (base_color_socket,) 105 | 106 | return gltf2_blender_gather_texture_info.gather_texture_info(inputs, export_settings) 107 | 108 | 109 | def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket): 110 | result = gltf2_blender_search_node_tree.from_socket( 111 | blender_shader_socket, 112 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) 113 | if not result: 114 | return None 115 | return result[0] 116 | 117 | 118 | def __gather_extensions(blender_material, export_settings): 119 | return None 120 | 121 | 122 | def __gather_extras(blender_material, export_settings): 123 | return None 124 | 125 | 126 | def __gather_metallic_factor(blender_material, export_settings): 127 | metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic") 128 | if metallic_socket is None: 129 | metallic_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "MetallicFactor") 130 | if isinstance(metallic_socket, bpy.types.NodeSocket) and not metallic_socket.is_linked: 131 | return metallic_socket.default_value 132 | return None 133 | 134 | 135 | def __gather_metallic_roughness_texture(blender_material, orm_texture, export_settings): 136 | if orm_texture is not None: 137 | texture_input = orm_texture 138 | else: 139 | metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic") 140 | roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness") 141 | 142 | hasMetal = metallic_socket is not None and __has_image_node_from_socket(metallic_socket) 143 | hasRough = roughness_socket is not None and __has_image_node_from_socket(roughness_socket) 144 | 145 | if not hasMetal and not hasRough: 146 | metallic_roughness = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "MetallicRoughness") 147 | if metallic_roughness is None or not __has_image_node_from_socket(metallic_roughness): 148 | return None 149 | texture_input = (metallic_roughness,) 150 | elif not hasMetal: 151 | texture_input = (roughness_socket,) 152 | elif not hasRough: 153 | texture_input = (metallic_socket,) 154 | else: 155 | texture_input = (metallic_socket, roughness_socket) 156 | 157 | return gltf2_blender_gather_texture_info.gather_texture_info(texture_input, export_settings) 158 | 159 | 160 | def __gather_roughness_factor(blender_material, export_settings): 161 | roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness") 162 | if roughness_socket is None: 163 | roughness_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "RoughnessFactor") 164 | if isinstance(roughness_socket, bpy.types.NodeSocket) and not roughness_socket.is_linked: 165 | return roughness_socket.default_value 166 | return None 167 | 168 | def __has_image_node_from_socket(socket): 169 | result = gltf2_blender_search_node_tree.from_socket( 170 | socket, 171 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) 172 | if not result: 173 | return False 174 | return True 175 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_mesh.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | from typing import Optional, Dict, List, Any, Tuple 17 | from .gltf2_blender_export_keys import MORPH 18 | from .gltf2_blender_gather_cache import cached 19 | from ..com import gltf2_io 20 | from . import gltf2_blender_gather_primitives 21 | from ..com.gltf2_blender_extras import generate_extras 22 | from ..com.gltf2_io_debug import print_console 23 | from .gltf2_io_user_extensions import export_user_extensions 24 | 25 | 26 | @cached 27 | def gather_mesh(blender_mesh: bpy.types.Mesh, 28 | library: Optional[str], 29 | blender_object: Optional[bpy.types.Object], 30 | vertex_groups: Optional[bpy.types.VertexGroups], 31 | modifiers: Optional[bpy.types.ObjectModifiers], 32 | skip_filter: bool, 33 | material_names: Tuple[str], 34 | export_settings 35 | ) -> Optional[gltf2_io.Mesh]: 36 | if not skip_filter and not __filter_mesh(blender_mesh, library, vertex_groups, modifiers, export_settings): 37 | return None 38 | 39 | mesh = gltf2_io.Mesh( 40 | extensions=__gather_extensions(blender_mesh, library, vertex_groups, modifiers, export_settings), 41 | extras=__gather_extras(blender_mesh, library, vertex_groups, modifiers, export_settings), 42 | name=__gather_name(blender_mesh, library, vertex_groups, modifiers, export_settings), 43 | weights=__gather_weights(blender_mesh, library, vertex_groups, modifiers, export_settings), 44 | primitives=__gather_primitives(blender_mesh, library, blender_object, vertex_groups, modifiers, material_names, export_settings), 45 | ) 46 | 47 | if len(mesh.primitives) == 0: 48 | print_console("WARNING", "Mesh '{}' has no primitives and will be omitted.".format(mesh.name)) 49 | return None 50 | 51 | export_user_extensions('gather_mesh_hook', 52 | export_settings, 53 | mesh, 54 | blender_mesh, 55 | blender_object, 56 | vertex_groups, 57 | modifiers, 58 | skip_filter, 59 | material_names) 60 | 61 | return mesh 62 | 63 | 64 | def __filter_mesh(blender_mesh: bpy.types.Mesh, 65 | library: Optional[str], 66 | vertex_groups: Optional[bpy.types.VertexGroups], 67 | modifiers: Optional[bpy.types.ObjectModifiers], 68 | export_settings 69 | ) -> bool: 70 | 71 | if blender_mesh.users == 0: 72 | return False 73 | return True 74 | 75 | 76 | def __gather_extensions(blender_mesh: bpy.types.Mesh, 77 | library: Optional[str], 78 | vertex_groups: Optional[bpy.types.VertexGroups], 79 | modifiers: Optional[bpy.types.ObjectModifiers], 80 | export_settings 81 | ) -> Any: 82 | return None 83 | 84 | 85 | def __gather_extras(blender_mesh: bpy.types.Mesh, 86 | library: Optional[str], 87 | vertex_groups: Optional[bpy.types.VertexGroups], 88 | modifiers: Optional[bpy.types.ObjectModifiers], 89 | export_settings 90 | ) -> Optional[Dict[Any, Any]]: 91 | 92 | extras = {} 93 | 94 | if export_settings['gltf_extras']: 95 | extras = generate_extras(blender_mesh) or {} 96 | 97 | if export_settings[MORPH] and blender_mesh.shape_keys: 98 | morph_max = len(blender_mesh.shape_keys.key_blocks) - 1 99 | if morph_max > 0: 100 | target_names = [] 101 | for blender_shape_key in blender_mesh.shape_keys.key_blocks: 102 | if blender_shape_key != blender_shape_key.relative_key: 103 | if blender_shape_key.mute is False: 104 | target_names.append(blender_shape_key.name) 105 | extras['targetNames'] = target_names 106 | 107 | if extras: 108 | return extras 109 | 110 | return None 111 | 112 | 113 | def __gather_name(blender_mesh: bpy.types.Mesh, 114 | library: Optional[str], 115 | vertex_groups: Optional[bpy.types.VertexGroups], 116 | modifiers: Optional[bpy.types.ObjectModifiers], 117 | export_settings 118 | ) -> str: 119 | return blender_mesh.name 120 | 121 | 122 | def __gather_primitives(blender_mesh: bpy.types.Mesh, 123 | library: Optional[str], 124 | blender_object: Optional[bpy.types.Object], 125 | vertex_groups: Optional[bpy.types.VertexGroups], 126 | modifiers: Optional[bpy.types.ObjectModifiers], 127 | material_names: Tuple[str], 128 | export_settings 129 | ) -> List[gltf2_io.MeshPrimitive]: 130 | return gltf2_blender_gather_primitives.gather_primitives(blender_mesh, 131 | library, 132 | blender_object, 133 | vertex_groups, 134 | modifiers, 135 | material_names, 136 | export_settings) 137 | 138 | 139 | def __gather_weights(blender_mesh: bpy.types.Mesh, 140 | library: Optional[str], 141 | vertex_groups: Optional[bpy.types.VertexGroups], 142 | modifiers: Optional[bpy.types.ObjectModifiers], 143 | export_settings 144 | ) -> Optional[List[float]]: 145 | if not export_settings[MORPH] or not blender_mesh.shape_keys: 146 | return None 147 | 148 | morph_max = len(blender_mesh.shape_keys.key_blocks) - 1 149 | if morph_max <= 0: 150 | return None 151 | 152 | weights = [] 153 | 154 | for blender_shape_key in blender_mesh.shape_keys.key_blocks: 155 | if blender_shape_key != blender_shape_key.relative_key: 156 | if blender_shape_key.mute is False: 157 | weights.append(blender_shape_key.value) 158 | 159 | return weights 160 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_sampler.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | from ..com import gltf2_io 17 | from .gltf2_blender_gather_cache import cached 18 | from .gltf2_io_user_extensions import export_user_extensions 19 | 20 | 21 | @cached 22 | def gather_sampler(blender_shader_node: bpy.types.Node, export_settings): 23 | if not __filter_sampler(blender_shader_node, export_settings): 24 | return None 25 | 26 | sampler = gltf2_io.Sampler( 27 | extensions=__gather_extensions(blender_shader_node, export_settings), 28 | extras=__gather_extras(blender_shader_node, export_settings), 29 | mag_filter=__gather_mag_filter(blender_shader_node, export_settings), 30 | min_filter=__gather_min_filter(blender_shader_node, export_settings), 31 | name=__gather_name(blender_shader_node, export_settings), 32 | wrap_s=__gather_wrap_s(blender_shader_node, export_settings), 33 | wrap_t=__gather_wrap_t(blender_shader_node, export_settings) 34 | ) 35 | 36 | export_user_extensions('gather_sampler_hook', export_settings, sampler, blender_shader_node) 37 | 38 | return sampler 39 | 40 | 41 | def __filter_sampler(blender_shader_node, export_settings): 42 | if not blender_shader_node.interpolation == 'Closest' and not blender_shader_node.extension == 'CLIP': 43 | return False 44 | return True 45 | 46 | 47 | def __gather_extensions(blender_shader_node, export_settings): 48 | return None 49 | 50 | 51 | def __gather_extras(blender_shader_node, export_settings): 52 | return None 53 | 54 | 55 | def __gather_mag_filter(blender_shader_node, export_settings): 56 | if blender_shader_node.interpolation == 'Closest': 57 | return 9728 # NEAREST 58 | return 9729 # LINEAR 59 | 60 | 61 | def __gather_min_filter(blender_shader_node, export_settings): 62 | if blender_shader_node.interpolation == 'Closest': 63 | return 9984 # NEAREST_MIPMAP_NEAREST 64 | return 9986 # NEAREST_MIPMAP_LINEAR 65 | 66 | 67 | def __gather_name(blender_shader_node, export_settings): 68 | return None 69 | 70 | 71 | def __gather_wrap_s(blender_shader_node, export_settings): 72 | if blender_shader_node.extension == 'EXTEND': 73 | return 33071 74 | return None 75 | 76 | 77 | def __gather_wrap_t(blender_shader_node, export_settings): 78 | if blender_shader_node.extension == 'EXTEND': 79 | return 33071 80 | return None 81 | 82 | 83 | @cached 84 | def gather_sampler_from_texture_slot(blender_texture: bpy.types.TextureSlot, export_settings): 85 | magFilter = 9729 86 | wrap = 10497 87 | if blender_texture.texture.extension == 'EXTEND': 88 | wrap = 33071 89 | 90 | minFilter = 9986 91 | if magFilter == 9728: 92 | minFilter = 9984 93 | 94 | return gltf2_io.Sampler( 95 | extensions=None, 96 | extras=None, 97 | mag_filter=magFilter, 98 | min_filter=minFilter, 99 | name=None, 100 | wrap_s=wrap, 101 | wrap_t=wrap 102 | ) 103 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_skins.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import mathutils 16 | from . import gltf2_blender_export_keys 17 | from .gltf2_blender_gather_cache import cached 18 | from ..com import gltf2_io 19 | from . import gltf2_io_binary_data 20 | from ..com import gltf2_io_constants 21 | from . import gltf2_blender_gather_accessors 22 | from . import gltf2_blender_gather_joints 23 | from ..com import gltf2_blender_math 24 | from .gltf2_io_user_extensions import export_user_extensions 25 | 26 | 27 | @cached 28 | def gather_skin(blender_object, export_settings): 29 | """ 30 | Gather armatures, bones etc into a glTF2 skin object. 31 | 32 | :param blender_object: the object which may contain a skin 33 | :param export_settings: 34 | :return: a glTF2 skin object 35 | """ 36 | if not __filter_skin(blender_object, export_settings): 37 | return None 38 | 39 | skin = gltf2_io.Skin( 40 | extensions=__gather_extensions(blender_object, export_settings), 41 | extras=__gather_extras(blender_object, export_settings), 42 | inverse_bind_matrices=__gather_inverse_bind_matrices(blender_object, export_settings), 43 | joints=__gather_joints(blender_object, export_settings), 44 | name=__gather_name(blender_object, export_settings), 45 | skeleton=__gather_skeleton(blender_object, export_settings) 46 | ) 47 | 48 | export_user_extensions('gather_skin_hook', export_settings, skin, blender_object) 49 | 50 | return skin 51 | 52 | 53 | def __filter_skin(blender_object, export_settings): 54 | if not export_settings[gltf2_blender_export_keys.SKINS]: 55 | return False 56 | if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0: 57 | return False 58 | 59 | return True 60 | 61 | 62 | def __gather_extensions(blender_object, export_settings): 63 | return None 64 | 65 | 66 | def __gather_extras(blender_object, export_settings): 67 | return None 68 | 69 | def __gather_inverse_bind_matrices(blender_object, export_settings): 70 | axis_basis_change = mathutils.Matrix.Identity(4) 71 | if export_settings[gltf2_blender_export_keys.YUP]: 72 | axis_basis_change = mathutils.Matrix( 73 | ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) 74 | 75 | if export_settings['gltf_def_bones'] is False: 76 | # build the hierarchy of nodes out of the bones 77 | root_bones = [] 78 | for blender_bone in blender_object.pose.bones: 79 | if not blender_bone.parent: 80 | root_bones.append(blender_bone) 81 | else: 82 | _, children_, root_bones = get_bone_tree(None, blender_object) 83 | 84 | matrices = [] 85 | 86 | # traverse the matrices in the same order as the joints and compute the inverse bind matrix 87 | def __collect_matrices(bone): 88 | inverse_bind_matrix = gltf2_blender_math.multiply( 89 | axis_basis_change, 90 | gltf2_blender_math.multiply( 91 | blender_object.matrix_world, 92 | bone.bone.matrix_local 93 | ) 94 | ).inverted() 95 | matrices.append(inverse_bind_matrix) 96 | 97 | if export_settings['gltf_def_bones'] is False: 98 | for child in bone.children: 99 | __collect_matrices(child) 100 | else: 101 | if bone.name in children_.keys(): 102 | for child in children_[bone.name]: 103 | __collect_matrices(blender_object.pose.bones[child]) 104 | 105 | # start with the "root" bones and recurse into children, in the same ordering as the how joints are gathered 106 | for root_bone in root_bones: 107 | __collect_matrices(root_bone) 108 | 109 | # flatten the matrices 110 | inverse_matrices = [] 111 | for matrix in matrices: 112 | for column in range(0, 4): 113 | for row in range(0, 4): 114 | inverse_matrices.append(matrix[row][column]) 115 | 116 | binary_data = gltf2_io_binary_data.BinaryData.from_list(inverse_matrices, gltf2_io_constants.ComponentType.Float) 117 | return gltf2_blender_gather_accessors.gather_accessor( 118 | binary_data, 119 | gltf2_io_constants.ComponentType.Float, 120 | len(inverse_matrices) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Mat4), 121 | None, 122 | None, 123 | gltf2_io_constants.DataType.Mat4, 124 | export_settings 125 | ) 126 | 127 | 128 | def __gather_joints(blender_object, export_settings): 129 | root_joints = [] 130 | if export_settings['gltf_def_bones'] is False: 131 | # build the hierarchy of nodes out of the bones 132 | for blender_bone in blender_object.pose.bones: 133 | if not blender_bone.parent: 134 | root_joints.append(gltf2_blender_gather_joints.gather_joint(blender_object, blender_bone, export_settings)) 135 | else: 136 | _, children_, root_joints = get_bone_tree(None, blender_object) 137 | root_joints = [gltf2_blender_gather_joints.gather_joint(blender_object, i, export_settings) for i in root_joints] 138 | 139 | # joints is a flat list containing all nodes belonging to the skin 140 | joints = [] 141 | 142 | def __collect_joints(node): 143 | joints.append(node) 144 | if export_settings['gltf_def_bones'] is False: 145 | for child in node.children: 146 | __collect_joints(child) 147 | else: 148 | if node.name in children_.keys(): 149 | for child in children_[node.name]: 150 | __collect_joints(gltf2_blender_gather_joints.gather_joint(blender_object, blender_object.pose.bones[child], export_settings)) 151 | 152 | for joint in root_joints: 153 | __collect_joints(joint) 154 | 155 | return joints 156 | 157 | 158 | def __gather_name(blender_object, export_settings): 159 | return blender_object.name 160 | 161 | 162 | def __gather_skeleton(blender_object, export_settings): 163 | # In the future support the result of https://github.com/KhronosGroup/glTF/pull/1195 164 | return None # gltf2_blender_gather_nodes.gather_node(blender_object, blender_scene, export_settings) 165 | 166 | @cached 167 | def get_bone_tree(blender_dummy, blender_object): 168 | 169 | bones = [] 170 | children = {} 171 | root_bones = [] 172 | 173 | def get_parent(bone): 174 | bones.append(bone.name) 175 | if bone.parent is not None: 176 | if bone.parent.name not in children.keys(): 177 | children[bone.parent.name] = [] 178 | children[bone.parent.name].append(bone.name) 179 | get_parent(bone.parent) 180 | else: 181 | root_bones.append(bone.name) 182 | 183 | for bone in [b for b in blender_object.data.bones if b.use_deform is True]: 184 | get_parent(bone) 185 | 186 | # remove duplicates 187 | for k, v in children.items(): 188 | children[k] = list(set(v)) 189 | list_ = list(set(bones)) 190 | root_ = list(set(root_bones)) 191 | return [blender_object.data.bones[b] for b in list_], children, [blender_object.pose.bones[b] for b in root_] 192 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_texture.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import typing 16 | import bpy 17 | from .gltf2_blender_gather_cache import cached 18 | 19 | from ..com import gltf2_io 20 | from . import gltf2_blender_gather_sampler 21 | from . import gltf2_blender_search_node_tree 22 | from . import gltf2_blender_gather_image 23 | from ..com import gltf2_io_debug 24 | from .gltf2_io_user_extensions import export_user_extensions 25 | 26 | 27 | @cached 28 | def gather_texture( 29 | blender_shader_sockets_or_texture_slots: typing.Union[ 30 | typing.Tuple[bpy.types.NodeSocket], typing.Tuple[typing.Any]], 31 | export_settings): 32 | """ 33 | Gather texture sampling information and image channels from a blender shader texture attached to a shader socket. 34 | 35 | :param blender_shader_sockets: The sockets of the material which should contribute to the texture 36 | :param export_settings: configuration of the export 37 | :return: a glTF 2.0 texture with sampler and source embedded (will be converted to references by the exporter) 38 | """ 39 | # TODO: extend to texture slots 40 | if not __filter_texture(blender_shader_sockets_or_texture_slots, export_settings): 41 | return None 42 | 43 | texture = gltf2_io.Texture( 44 | extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), 45 | extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), 46 | name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings), 47 | sampler=__gather_sampler(blender_shader_sockets_or_texture_slots, export_settings), 48 | source=__gather_source(blender_shader_sockets_or_texture_slots, export_settings) 49 | ) 50 | 51 | # although valid, most viewers can't handle missing source properties 52 | if texture.source is None: 53 | return None 54 | 55 | export_user_extensions('gather_texture_hook', export_settings, texture, blender_shader_sockets_or_texture_slots) 56 | 57 | return texture 58 | 59 | 60 | def __filter_texture(blender_shader_sockets_or_texture_slots, export_settings): 61 | return True 62 | 63 | 64 | def __gather_extensions(blender_shader_sockets, export_settings): 65 | return None 66 | 67 | 68 | def __gather_extras(blender_shader_sockets, export_settings): 69 | return None 70 | 71 | 72 | def __gather_name(blender_shader_sockets, export_settings): 73 | return None 74 | 75 | 76 | def __gather_sampler(blender_shader_sockets_or_texture_slots, export_settings): 77 | if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): 78 | shader_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets_or_texture_slots] 79 | if len(shader_nodes) > 1: 80 | gltf2_io_debug.print_console("WARNING", 81 | "More than one shader node tex image used for a texture. " 82 | "The resulting glTF sampler will behave like the first shader node tex image.") 83 | return gltf2_blender_gather_sampler.gather_sampler( 84 | shader_nodes[0], 85 | export_settings) 86 | elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot): 87 | return gltf2_blender_gather_sampler.gather_sampler_from_texture_slot( 88 | blender_shader_sockets_or_texture_slots[0], 89 | export_settings 90 | ) 91 | else: 92 | # TODO: implement texture slot sampler 93 | raise NotImplementedError() 94 | 95 | 96 | def __gather_source(blender_shader_sockets_or_texture_slots, export_settings): 97 | return gltf2_blender_gather_image.gather_image(blender_shader_sockets_or_texture_slots, export_settings) 98 | 99 | # Helpers 100 | 101 | 102 | def __get_tex_from_socket(socket): 103 | result = gltf2_blender_search_node_tree.from_socket( 104 | socket, 105 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) 106 | if not result: 107 | return None 108 | return result[0] 109 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_gather_texture_info.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | import typing 17 | from .gltf2_blender_gather_cache import cached 18 | from ..com import gltf2_io 19 | from . import gltf2_blender_gather_texture 20 | from . import gltf2_blender_search_node_tree 21 | from . import gltf2_blender_get 22 | from ..com.gltf2_io_debug import print_console 23 | from ..com.gltf2_io_extensions import Extension 24 | from .gltf2_io_user_extensions import export_user_extensions 25 | 26 | 27 | @cached 28 | def gather_texture_info(blender_shader_sockets_or_texture_slots: typing.Union[ 29 | typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]], 30 | export_settings): 31 | if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): 32 | return None 33 | 34 | texture_info = gltf2_io.TextureInfo( 35 | extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings), 36 | extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings), 37 | index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings), 38 | tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings) 39 | ) 40 | 41 | if texture_info.index is None: 42 | return None 43 | 44 | export_user_extensions('gather_texture_info_hook', export_settings, texture_info, blender_shader_sockets_or_texture_slots) 45 | 46 | return texture_info 47 | 48 | 49 | def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings): 50 | if not blender_shader_sockets_or_texture_slots: 51 | return False 52 | if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]): 53 | return False 54 | if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): 55 | if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]): 56 | # sockets do not lead to a texture --> discard 57 | return False 58 | 59 | resolution = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node.image.size 60 | if any(any(a != b for a, b in zip(__get_tex_from_socket(elem).shader_node.image.size, resolution)) 61 | for elem in blender_shader_sockets_or_texture_slots): 62 | def format_image(image_node): 63 | return "{} ({}x{})".format(image_node.image.name, image_node.image.size[0], image_node.image.size[1]) 64 | 65 | images = [format_image(__get_tex_from_socket(elem).shader_node) for elem in 66 | blender_shader_sockets_or_texture_slots] 67 | 68 | print_console("ERROR", "Image sizes do not match. In order to be merged into one image file, " 69 | "images need to be of the same size. Images: {}".format(images)) 70 | return False 71 | 72 | return True 73 | 74 | 75 | def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings): 76 | if not hasattr(blender_shader_sockets_or_texture_slots[0], 'links'): 77 | return None 78 | 79 | tex_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets_or_texture_slots] 80 | texture_node = tex_nodes[0] if (tex_nodes is not None and len(tex_nodes) > 0) else None 81 | if texture_node is None: 82 | return None 83 | texture_transform = gltf2_blender_get.get_texture_transform_from_texture_node(texture_node) 84 | if texture_transform is None: 85 | return None 86 | 87 | extension = Extension("KHR_texture_transform", texture_transform) 88 | return {"KHR_texture_transform": extension} 89 | 90 | 91 | def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings): 92 | return None 93 | 94 | 95 | def __gather_index(blender_shader_sockets_or_texture_slots, export_settings): 96 | # We just put the actual shader into the 'index' member 97 | return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings) 98 | 99 | 100 | def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings): 101 | if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket): 102 | blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node 103 | if len(blender_shader_node.inputs['Vector'].links) == 0: 104 | return 0 105 | 106 | input_node = blender_shader_node.inputs['Vector'].links[0].from_node 107 | 108 | if isinstance(input_node, bpy.types.ShaderNodeMapping): 109 | 110 | if len(input_node.inputs['Vector'].links) == 0: 111 | return 0 112 | 113 | input_node = input_node.inputs['Vector'].links[0].from_node 114 | 115 | if not isinstance(input_node, bpy.types.ShaderNodeUVMap): 116 | return 0 117 | 118 | if input_node.uv_map == '': 119 | return 0 120 | 121 | # Try to gather map index. 122 | for blender_mesh in bpy.data.meshes: 123 | texCoordIndex = blender_mesh.uv_layers.find(input_node.uv_map) 124 | if texCoordIndex >= 0: 125 | return texCoordIndex 126 | 127 | return 0 128 | elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot): 129 | # TODO: implement for texture slots 130 | return 0 131 | else: 132 | raise NotImplementedError() 133 | 134 | 135 | def __get_tex_from_socket(socket): 136 | result = gltf2_blender_search_node_tree.from_socket( 137 | socket, 138 | gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage)) 139 | if not result: 140 | return None 141 | if result[0].shader_node.image is None: 142 | return None 143 | return result[0] 144 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_get.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | from mathutils import Vector, Matrix 17 | 18 | from ..com.gltf2_blender_material_helpers import get_gltf_node_name 19 | from ..com.gltf2_blender_conversion import texture_transform_blender_to_gltf 20 | from ..com import gltf2_io_debug 21 | 22 | 23 | def get_animation_target(action_group: bpy.types.ActionGroup): 24 | return action_group.channels[0].data_path.split('.')[-1] 25 | 26 | 27 | def get_object_from_datapath(blender_object, data_path: str): 28 | if "." in data_path: 29 | # gives us: ('modifiers["Subsurf"]', 'levels') 30 | path_prop, path_attr = data_path.rsplit(".", 1) 31 | 32 | # same as: prop = obj.modifiers["Subsurf"] 33 | if path_attr in ["rotation", "scale", "location", 34 | "rotation_axis_angle", "rotation_euler", "rotation_quaternion"]: 35 | prop = blender_object.path_resolve(path_prop) 36 | else: 37 | prop = blender_object.path_resolve(data_path) 38 | else: 39 | prop = blender_object 40 | # single attribute such as name, location... etc 41 | # path_attr = data_path 42 | 43 | return prop 44 | 45 | 46 | def get_socket_or_texture_slot(blender_material: bpy.types.Material, name: str): 47 | """ 48 | For a given material input name, retrieve the corresponding node tree socket or blender render texture slot. 49 | 50 | :param blender_material: a blender material for which to get the socket/slot 51 | :param name: the name of the socket/slot 52 | :return: either a blender NodeSocket, if the material is a node tree or a blender Texture otherwise 53 | """ 54 | if blender_material.node_tree and blender_material.use_nodes: 55 | #i = [input for input in blender_material.node_tree.inputs] 56 | #o = [output for output in blender_material.node_tree.outputs] 57 | if name == "Emissive": 58 | # Check for a dedicated Emission node first, it must supersede the newer built-in one 59 | # because the newer one is always present in all Principled BSDF materials. 60 | type = bpy.types.ShaderNodeEmission 61 | name = "Color" 62 | nodes = [n for n in blender_material.node_tree.nodes if isinstance(n, type)] 63 | inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], []) 64 | if inputs: 65 | return inputs[0] 66 | # If a dedicated Emission node was not found, fall back to the Principled BSDF Emission socket. 67 | name = "Emission" 68 | type = bpy.types.ShaderNodeBsdfPrincipled 69 | elif name == "Background": 70 | type = bpy.types.ShaderNodeBackground 71 | name = "Color" 72 | else: 73 | type = bpy.types.ShaderNodeBsdfPrincipled 74 | nodes = [n for n in blender_material.node_tree.nodes if isinstance(n, type)] 75 | inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], []) 76 | if inputs: 77 | return inputs[0] 78 | 79 | return None 80 | 81 | 82 | def get_socket_or_texture_slot_old(blender_material: bpy.types.Material, name: str): 83 | """ 84 | For a given material input name, retrieve the corresponding node tree socket in the special glTF node group. 85 | 86 | :param blender_material: a blender material for which to get the socket/slot 87 | :param name: the name of the socket/slot 88 | :return: either a blender NodeSocket, if the material is a node tree or a blender Texture otherwise 89 | """ 90 | gltf_node_group_name = get_gltf_node_name().lower() 91 | if blender_material.node_tree and blender_material.use_nodes: 92 | nodes = [n for n in blender_material.node_tree.nodes if \ 93 | isinstance(n, bpy.types.ShaderNodeGroup) and \ 94 | (n.node_tree.name.startswith('glTF Metallic Roughness') or n.node_tree.name.lower() == gltf_node_group_name)] 95 | inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], []) 96 | if inputs: 97 | return inputs[0] 98 | 99 | return None 100 | 101 | 102 | def find_shader_image_from_shader_socket(shader_socket, max_hops=10): 103 | """Find any ShaderNodeTexImage in the path from the socket.""" 104 | if shader_socket is None: 105 | return None 106 | 107 | if max_hops <= 0: 108 | return None 109 | 110 | for link in shader_socket.links: 111 | if isinstance(link.from_node, bpy.types.ShaderNodeTexImage): 112 | return link.from_node 113 | 114 | for socket in link.from_node.inputs.values(): 115 | image = find_shader_image_from_shader_socket(shader_socket=socket, max_hops=max_hops - 1) 116 | if image is not None: 117 | return image 118 | 119 | return None 120 | 121 | 122 | def get_texture_transform_from_texture_node(texture_node): 123 | if not isinstance(texture_node, bpy.types.ShaderNodeTexImage): 124 | return None 125 | 126 | mapping_socket = texture_node.inputs["Vector"] 127 | if len(mapping_socket.links) == 0: 128 | return None 129 | 130 | mapping_node = mapping_socket.links[0].from_node 131 | if not isinstance(mapping_node, bpy.types.ShaderNodeMapping): 132 | return None 133 | 134 | if mapping_node.vector_type not in ["TEXTURE", "POINT", "VECTOR"]: 135 | gltf2_io_debug.print_console("WARNING", 136 | "Skipping exporting texture transform because it had type " + 137 | mapping_node.vector_type + "; recommend using POINT instead" 138 | ) 139 | return None 140 | 141 | 142 | rotation_0, rotation_1 = mapping_node.inputs['Rotation'].default_value[0], mapping_node.inputs['Rotation'].default_value[1] 143 | if rotation_0 or rotation_1: 144 | # TODO: can we handle this? 145 | gltf2_io_debug.print_console("WARNING", 146 | "Skipping exporting texture transform because it had non-zero " 147 | "rotations in the X/Y direction; only a Z rotation can be exported!" 148 | ) 149 | return None 150 | 151 | mapping_transform = {} 152 | mapping_transform["offset"] = [mapping_node.inputs['Location'].default_value[0], mapping_node.inputs['Location'].default_value[1]] 153 | mapping_transform["rotation"] = mapping_node.inputs['Rotation'].default_value[2] 154 | mapping_transform["scale"] = [mapping_node.inputs['Scale'].default_value[0], mapping_node.inputs['Scale'].default_value[1]] 155 | 156 | if mapping_node.vector_type == "TEXTURE": 157 | # This means use the inverse of the TRS transform. 158 | def inverted(mapping_transform): 159 | offset = mapping_transform["offset"] 160 | rotation = mapping_transform["rotation"] 161 | scale = mapping_transform["scale"] 162 | 163 | # Inverse of a TRS is not always a TRS. This function will be right 164 | # at least when the following don't occur. 165 | if abs(rotation) > 1e-5 and abs(scale[0] - scale[1]) > 1e-5: 166 | return None 167 | if abs(scale[0]) < 1e-5 or abs(scale[1]) < 1e-5: 168 | return None 169 | 170 | new_offset = Matrix.Rotation(-rotation, 3, 'Z') @ Vector((-offset[0], -offset[1], 1)) 171 | new_offset[0] /= scale[0]; new_offset[1] /= scale[1] 172 | return { 173 | "offset": new_offset[0:2], 174 | "rotation": -rotation, 175 | "scale": [1/scale[0], 1/scale[1]], 176 | } 177 | 178 | mapping_transform = inverted(mapping_transform) 179 | if mapping_transform is None: 180 | gltf2_io_debug.print_console("WARNING", 181 | "Skipping exporting texture transform with type TEXTURE because " 182 | "we couldn't convert it to TRS; recommend using POINT instead" 183 | ) 184 | return None 185 | 186 | elif mapping_node.vector_type == "VECTOR": 187 | # Vectors don't get translated 188 | mapping_transform["offset"] = [0, 0] 189 | 190 | texture_transform = texture_transform_blender_to_gltf(mapping_transform) 191 | 192 | if all([component == 0 for component in texture_transform["offset"]]): 193 | del(texture_transform["offset"]) 194 | if all([component == 1 for component in texture_transform["scale"]]): 195 | del(texture_transform["scale"]) 196 | if texture_transform["rotation"] == 0: 197 | del(texture_transform["rotation"]) 198 | 199 | if len(texture_transform) == 0: 200 | return None 201 | 202 | return texture_transform 203 | 204 | 205 | def get_node(data_path): 206 | """Return Blender node on a given Blender data path.""" 207 | if data_path is None: 208 | return None 209 | 210 | index = data_path.find("[\"") 211 | if (index == -1): 212 | return None 213 | 214 | node_name = data_path[(index + 2):] 215 | 216 | index = node_name.find("\"") 217 | if (index == -1): 218 | return None 219 | 220 | return node_name[:(index)] 221 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_image.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import bpy 16 | import os 17 | from typing import Optional, Tuple 18 | import numpy as np 19 | import tempfile 20 | import enum 21 | 22 | 23 | class Channel(enum.IntEnum): 24 | R = 0 25 | G = 1 26 | B = 2 27 | A = 3 28 | 29 | # These describe how an ExportImage's channels should be filled. 30 | 31 | class FillImage: 32 | """Fills a channel with the channel src_chan from a Blender image.""" 33 | def __init__(self, image: bpy.types.Image, src_chan: Channel): 34 | self.image = image 35 | self.src_chan = src_chan 36 | 37 | class FillWhite: 38 | """Fills a channel with all ones (1.0).""" 39 | pass 40 | 41 | 42 | class ExportImage: 43 | """Custom image class. 44 | 45 | An image is represented by giving a description of how to fill its red, 46 | green, blue, and alpha channels. For example: 47 | 48 | self.fills = { 49 | Channel.R: FillImage(image=bpy.data.images['Im1'], src_chan=Channel.B), 50 | Channel.G: FillWhite(), 51 | } 52 | 53 | This says that the ExportImage's R channel should be filled with the B 54 | channel of the Blender image 'Im1', and the ExportImage's G channel 55 | should be filled with all 1.0s. Undefined channels mean we don't care 56 | what values that channel has. 57 | 58 | This is flexible enough to handle the case where eg. the user used the R 59 | channel of one image as the metallic value and the G channel of another 60 | image as the roughness, and we need to synthesize an ExportImage that 61 | packs those into the B and G channels for glTF. 62 | 63 | Storing this description (instead of raw pixels) lets us make more 64 | intelligent decisions about how to encode the image. 65 | """ 66 | 67 | def __init__(self): 68 | self.fills = {} 69 | 70 | @staticmethod 71 | def from_blender_image(image: bpy.types.Image): 72 | export_image = ExportImage() 73 | for chan in range(image.channels): 74 | export_image.fill_image(image, dst_chan=chan, src_chan=chan) 75 | return export_image 76 | 77 | def fill_image(self, image: bpy.types.Image, dst_chan: Channel, src_chan: Channel): 78 | self.fills[dst_chan] = FillImage(image, src_chan) 79 | 80 | def fill_white(self, dst_chan: Channel): 81 | self.fills[dst_chan] = FillWhite() 82 | 83 | def is_filled(self, chan: Channel) -> bool: 84 | return chan in self.fills 85 | 86 | def empty(self) -> bool: 87 | return not self.fills 88 | 89 | def blender_image(self) -> Optional[bpy.types.Image]: 90 | """If there's an existing Blender image we can use, 91 | returns it. Otherwise (if channels need packing), 92 | returns None. 93 | """ 94 | if self.__on_happy_path(): 95 | for fill in self.fills.values(): 96 | return fill.image 97 | return None 98 | 99 | def __on_happy_path(self) -> bool: 100 | # All src_chans match their dst_chan and come from the same image 101 | return ( 102 | all(isinstance(fill, FillImage) for fill in self.fills.values()) and 103 | all(dst_chan == fill.src_chan for dst_chan, fill in self.fills.items()) and 104 | len(set(fill.image.name for fill in self.fills.values())) == 1 105 | ) 106 | 107 | def encode(self, mime_type: Optional[str]) -> bytes: 108 | self.file_format = { 109 | "image/jpeg": "JPEG", 110 | "image/png": "PNG" 111 | }.get(mime_type, "PNG") 112 | 113 | # Happy path = we can just use an existing Blender image 114 | if self.__on_happy_path(): 115 | return self.__encode_happy() 116 | 117 | # Unhappy path = we need to create the image self.fills describes. 118 | return self.__encode_unhappy() 119 | 120 | def __encode_happy(self) -> bytes: 121 | return self.__encode_from_image(self.blender_image()) 122 | 123 | def __encode_unhappy(self) -> bytes: 124 | # We need to assemble the image out of channels. 125 | # Do it with numpy and image.pixels. 126 | result = None 127 | 128 | img_fills = { 129 | chan: fill 130 | for chan, fill in self.fills.items() 131 | if isinstance(fill, FillImage) 132 | } 133 | # Loop over images instead of dst_chans; ensures we only decode each 134 | # image once even if it's used in multiple channels. 135 | image_names = list(set(fill.image.name for fill in img_fills.values())) 136 | for image_name in image_names: 137 | image = bpy.data.images[image_name] 138 | 139 | if result is None: 140 | dim = (image.size[0], image.size[1]) 141 | result = np.ones(dim[0] * dim[1] * 4, np.float32) 142 | tmp_buf = np.empty(dim[0] * dim[1] * 4, np.float32) 143 | # Images should all be the same size (should be guaranteed by 144 | # gather_texture_info). 145 | assert (image.size[0], image.size[1]) == dim 146 | 147 | image.pixels.foreach_get(tmp_buf) 148 | 149 | for dst_chan, img_fill in img_fills.items(): 150 | if img_fill.image == image: 151 | result[int(dst_chan)::4] = tmp_buf[int(img_fill.src_chan)::4] 152 | 153 | tmp_buf = None # GC this 154 | 155 | if result is None: 156 | # No ImageFills; use a 1x1 white pixel 157 | dim = (1, 1) 158 | result = np.array([1.0, 1.0, 1.0, 1.0]) 159 | 160 | return self.__encode_from_numpy_array(result, dim) 161 | 162 | def __encode_from_numpy_array(self, pixels: np.ndarray, dim: Tuple[int, int]) -> bytes: 163 | tmp_image = None 164 | try: 165 | tmp_image = bpy.data.images.new( 166 | "##gltf-export:tmp-image##", 167 | width=dim[0], 168 | height=dim[1], 169 | alpha=Channel.A in self.fills, 170 | ) 171 | assert tmp_image.channels == 4 # 4 regardless of the alpha argument above. 172 | 173 | tmp_image.pixels.foreach_set(pixels) 174 | 175 | return _encode_temp_image(tmp_image, self.file_format) 176 | 177 | finally: 178 | if tmp_image is not None: 179 | bpy.data.images.remove(tmp_image, do_unlink=True) 180 | 181 | def __encode_from_image(self, image: bpy.types.Image) -> bytes: 182 | # See if there is an existing file we can use. 183 | data = None 184 | if image.source == 'FILE' and image.file_format == self.file_format and \ 185 | not image.is_dirty: 186 | if image.packed_file is not None: 187 | data = image.packed_file.data 188 | else: 189 | src_path = bpy.path.abspath(image.filepath_raw) 190 | if os.path.isfile(src_path): 191 | with open(src_path, 'rb') as f: 192 | data = f.read() 193 | # Check magic number is right 194 | if data: 195 | if self.file_format == 'PNG': 196 | if data.startswith(b'\x89PNG'): 197 | return data 198 | elif self.file_format == 'JPEG': 199 | if data.startswith(b'\xff\xd8\xff'): 200 | return data 201 | 202 | # Copy to a temp image and save. 203 | tmp_image = None 204 | try: 205 | tmp_image = image.copy() 206 | tmp_image.update() 207 | 208 | if image.is_dirty: 209 | # Copy the pixels to get the changes 210 | tmp_buf = np.empty(image.size[0] * image.size[1] * 4, np.float32) 211 | image.pixels.foreach_get(tmp_buf) 212 | tmp_image.pixels.foreach_set(tmp_buf) 213 | tmp_buf = None # GC this 214 | 215 | return _encode_temp_image(tmp_image, self.file_format) 216 | finally: 217 | if tmp_image is not None: 218 | bpy.data.images.remove(tmp_image, do_unlink=True) 219 | 220 | 221 | def _encode_temp_image(tmp_image: bpy.types.Image, file_format: str) -> bytes: 222 | with tempfile.TemporaryDirectory() as tmpdirname: 223 | tmpfilename = tmpdirname + '/img' 224 | tmp_image.filepath_raw = tmpfilename 225 | 226 | tmp_image.file_format = file_format 227 | 228 | tmp_image.save() 229 | 230 | with open(tmpfilename, "rb") as f: 231 | return f.read() 232 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_search_node_tree.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # 16 | # Imports 17 | # 18 | 19 | import bpy 20 | import typing 21 | 22 | 23 | class Filter: 24 | """Base class for all node tree filter operations.""" 25 | 26 | def __init__(self): 27 | pass 28 | 29 | def __call__(self, shader_node): 30 | return True 31 | 32 | 33 | class FilterByName(Filter): 34 | """ 35 | Filter the material node tree by name. 36 | 37 | example usage: 38 | find_from_socket(start_socket, ShaderNodeFilterByName("Normal")) 39 | """ 40 | 41 | def __init__(self, name): 42 | self.name = name 43 | super(FilterByName, self).__init__() 44 | 45 | def __call__(self, shader_node): 46 | return shader_node.name == self.name 47 | 48 | 49 | class FilterByType(Filter): 50 | """Filter the material node tree by type.""" 51 | 52 | def __init__(self, type): 53 | self.type = type 54 | super(FilterByType, self).__init__() 55 | 56 | def __call__(self, shader_node): 57 | return isinstance(shader_node, self.type) 58 | 59 | 60 | class NodeTreeSearchResult: 61 | def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink]): 62 | self.shader_node = shader_node 63 | self.path = path 64 | 65 | 66 | # TODO: cache these searches 67 | def from_socket(start_socket: bpy.types.NodeSocket, 68 | shader_node_filter: typing.Union[Filter, typing.Callable]) -> typing.List[NodeTreeSearchResult]: 69 | """ 70 | Find shader nodes where the filter expression is true. 71 | 72 | :param start_socket: the beginning of the traversal 73 | :param shader_node_filter: should be a function(x: shader_node) -> bool 74 | :return: a list of shader nodes for which filter is true 75 | """ 76 | # hide implementation (especially the search path) 77 | def __search_from_socket(start_socket: bpy.types.NodeSocket, 78 | shader_node_filter: typing.Union[Filter, typing.Callable], 79 | search_path: typing.List[bpy.types.NodeLink]) -> typing.List[NodeTreeSearchResult]: 80 | results = [] 81 | 82 | for link in start_socket.links: 83 | # follow the link to a shader node 84 | linked_node = link.from_node 85 | # check if the node matches the filter 86 | if shader_node_filter(linked_node): 87 | results.append(NodeTreeSearchResult(linked_node, search_path + [link])) 88 | # traverse into inputs of the node 89 | for input_socket in linked_node.inputs: 90 | linked_results = __search_from_socket(input_socket, shader_node_filter, search_path + [link]) 91 | if linked_results: 92 | # add the link to the current path 93 | search_path.append(link) 94 | results += linked_results 95 | 96 | return results 97 | 98 | if start_socket is None: 99 | return [] 100 | 101 | return __search_from_socket(start_socket, shader_node_filter, []) 102 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_blender_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import math 16 | from ..com import gltf2_io_constants 17 | 18 | 19 | # TODO: we could apply functional programming to these problems (currently we only have a single use case) 20 | 21 | def split_list_by_data_type(l: list, data_type: gltf2_io_constants.DataType): 22 | """ 23 | Split a flat list of components by their data type. 24 | 25 | E.g.: A list [0,1,2,3,4,5] of data type Vec3 would be split to [[0,1,2], [3,4,5]] 26 | :param l: the flat list 27 | :param data_type: the data type of the list 28 | :return: a list of lists, where each element list contains the components of the data type 29 | """ 30 | if not (len(l) % gltf2_io_constants.DataType.num_elements(data_type) == 0): 31 | raise ValueError("List length does not match specified data type") 32 | num_elements = gltf2_io_constants.DataType.num_elements(data_type) 33 | return [l[i:i + num_elements] for i in range(0, len(l), num_elements)] 34 | 35 | 36 | def max_components(l: list, data_type: gltf2_io_constants.DataType) -> list: 37 | """ 38 | Find the maximum components in a flat list. 39 | 40 | This is required, for example, for the glTF2.0 accessor min and max properties 41 | :param l: the flat list of components 42 | :param data_type: the data type of the list (determines the length of the result) 43 | :return: a list with length num_elements(data_type) containing the maximum per component along the list 44 | """ 45 | components_lists = split_list_by_data_type(l, data_type) 46 | result = [-math.inf] * gltf2_io_constants.DataType.num_elements(data_type) 47 | for components in components_lists: 48 | for i, c in enumerate(components): 49 | result[i] = max(result[i], c) 50 | return result 51 | 52 | 53 | def min_components(l: list, data_type: gltf2_io_constants.DataType) -> list: 54 | """ 55 | Find the minimum components in a flat list. 56 | 57 | This is required, for example, for the glTF2.0 accessor min and max properties 58 | :param l: the flat list of components 59 | :param data_type: the data type of the list (determines the length of the result) 60 | :return: a list with length num_elements(data_type) containing the minimum per component along the list 61 | """ 62 | components_lists = split_list_by_data_type(l, data_type) 63 | result = [math.inf] * gltf2_io_constants.DataType.num_elements(data_type) 64 | for components in components_lists: 65 | for i, c in enumerate(components): 66 | result[i] = min(result[i], c) 67 | return result 68 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_io_binary_data.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import typing 16 | import array 17 | from ..com import gltf2_io_constants 18 | 19 | 20 | class BinaryData: 21 | """Store for gltf binary data that can later be stored in a buffer.""" 22 | 23 | def __init__(self, data: bytes): 24 | if not isinstance(data, bytes): 25 | raise TypeError("Data is not a bytes array") 26 | self.data = data 27 | 28 | def __eq__(self, other): 29 | return self.data == other.data 30 | 31 | def __hash__(self): 32 | return hash(self.data) 33 | 34 | @classmethod 35 | def from_list(cls, lst: typing.List[typing.Any], gltf_component_type: gltf2_io_constants.ComponentType): 36 | format_char = gltf2_io_constants.ComponentType.to_type_code(gltf_component_type) 37 | return BinaryData(array.array(format_char, lst).tobytes()) 38 | 39 | @property 40 | def byte_length(self): 41 | return len(self.data) 42 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_io_buffer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import base64 16 | 17 | from ..com import gltf2_io 18 | from . import gltf2_io_binary_data 19 | 20 | 21 | class Buffer: 22 | """Class representing binary data for use in a glTF file as 'buffer' property.""" 23 | 24 | def __init__(self, buffer_index=0): 25 | self.__data = b"" 26 | self.__buffer_index = buffer_index 27 | 28 | def add_and_get_view(self, binary_data: gltf2_io_binary_data.BinaryData) -> gltf2_io.BufferView: 29 | """Add binary data to the buffer. Return a glTF BufferView.""" 30 | offset = len(self.__data) 31 | self.__data += binary_data.data 32 | 33 | # offsets should be a multiple of 4 --> therefore add padding if necessary 34 | padding = (4 - (binary_data.byte_length % 4)) % 4 35 | self.__data += b"\x00" * padding 36 | 37 | buffer_view = gltf2_io.BufferView( 38 | buffer=self.__buffer_index, 39 | byte_length=binary_data.byte_length, 40 | byte_offset=offset, 41 | byte_stride=None, 42 | extensions=None, 43 | extras=None, 44 | name=None, 45 | target=None 46 | ) 47 | return buffer_view 48 | 49 | @property 50 | def byte_length(self): 51 | return len(self.__data) 52 | 53 | def to_bytes(self): 54 | return self.__data 55 | 56 | def to_embed_string(self): 57 | return 'data:application/octet-stream;base64,' + base64.b64encode(self.__data).decode('ascii') 58 | 59 | def clear(self): 60 | self.__data = b"" 61 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_io_export.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # 16 | # Imports 17 | # 18 | 19 | import json 20 | import struct 21 | 22 | # 23 | # Globals 24 | # 25 | 26 | # 27 | # Functions 28 | # 29 | from collections import OrderedDict 30 | 31 | 32 | def save_gltf(gltf, export_settings, encoder, glb_buffer): 33 | indent = None 34 | separators = (',', ':') 35 | 36 | if export_settings['gltf_format'] != 'GLB': 37 | indent = 4 38 | # The comma is typically followed by a newline, so no trailing whitespace is needed on it. 39 | separators = (',', ' : ') 40 | 41 | sort_order = [ 42 | "asset", 43 | "extensionsUsed", 44 | "extensionsRequired", 45 | "extensions", 46 | "extras", 47 | "scene", 48 | "scenes", 49 | "nodes", 50 | "cameras", 51 | "animations", 52 | "materials", 53 | "meshes", 54 | "textures", 55 | "images", 56 | "skins", 57 | "accessors", 58 | "bufferViews", 59 | "samplers", 60 | "buffers" 61 | ] 62 | gltf_ordered = OrderedDict(sorted(gltf.items(), key=lambda item: sort_order.index(item[0]))) 63 | gltf_encoded = json.dumps(gltf_ordered, indent=indent, separators=separators, cls=encoder, allow_nan=False) 64 | 65 | # 66 | 67 | if export_settings['gltf_format'] != 'GLB': 68 | file = open(export_settings['gltf_filepath'], "w", encoding="utf8", newline="\n") 69 | file.write(gltf_encoded) 70 | file.write("\n") 71 | file.close() 72 | 73 | binary = export_settings['gltf_binary'] 74 | if len(binary) > 0 and not export_settings['gltf_embed_buffers']: 75 | file = open(export_settings['gltf_filedirectory'] + export_settings['gltf_binaryfilename'], "wb") 76 | file.write(binary) 77 | file.close() 78 | 79 | else: 80 | file = open(export_settings['gltf_filepath'], "wb") 81 | 82 | gltf_data = gltf_encoded.encode() 83 | binary = glb_buffer 84 | 85 | length_gltf = len(gltf_data) 86 | spaces_gltf = (4 - (length_gltf & 3)) & 3 87 | length_gltf += spaces_gltf 88 | 89 | length_bin = len(binary) 90 | zeros_bin = (4 - (length_bin & 3)) & 3 91 | length_bin += zeros_bin 92 | 93 | length = 12 + 8 + length_gltf 94 | if length_bin > 0: 95 | length += 8 + length_bin 96 | 97 | # Header (Version 2) 98 | file.write('glTF'.encode()) 99 | file.write(struct.pack("I", 2)) 100 | file.write(struct.pack("I", length)) 101 | 102 | # Chunk 0 (JSON) 103 | file.write(struct.pack("I", length_gltf)) 104 | file.write('JSON'.encode()) 105 | file.write(gltf_data) 106 | file.write(b' ' * spaces_gltf) 107 | 108 | # Chunk 1 (BIN) 109 | if length_bin > 0: 110 | file.write(struct.pack("I", length_bin)) 111 | file.write('BIN\0'.encode()) 112 | file.write(binary) 113 | file.write(b'\0' * zeros_bin) 114 | 115 | file.close() 116 | 117 | return True 118 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_io_image_data.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import re 15 | 16 | 17 | class ImageData: 18 | """Contains encoded images""" 19 | # FUTURE_WORK: as a method to allow the node graph to be better supported, we could model some of 20 | # the node graph elements with numpy functions 21 | 22 | def __init__(self, data: bytes, mime_type: str, name: str): 23 | self._data = data 24 | self._mime_type = mime_type 25 | self._name = name 26 | 27 | def __eq__(self, other): 28 | return self._data == other.data 29 | 30 | def __hash__(self): 31 | return hash(self._data) 32 | 33 | def adjusted_name(self): 34 | regex_dot = re.compile("\.") 35 | adjusted_name = re.sub(regex_dot, "_", self.name) 36 | new_name = "".join([char for char in adjusted_name if char not in "!#$&'()*+,/:;<>?@[\]^`{|}~"]) 37 | return new_name 38 | 39 | @property 40 | def data(self): 41 | return self._data 42 | 43 | @property 44 | def name(self): 45 | return self._name 46 | 47 | @property 48 | def file_extension(self): 49 | if self._mime_type == "image/jpeg": 50 | return ".jpg" 51 | return ".png" 52 | 53 | @property 54 | def byte_length(self): 55 | return len(self._data) 56 | -------------------------------------------------------------------------------- /exporter/exp/gltf2_io_user_extensions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The glTF-Blender-IO authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | def export_user_extensions(hook_name, export_settings, *args): 16 | if args and hasattr(args[0], "extensions"): 17 | if args[0].extensions is None: 18 | args[0].extensions = {} 19 | 20 | for extension in export_settings['gltf_user_extensions']: 21 | hook = getattr(extension, hook_name, None) 22 | if hook is not None: 23 | try: 24 | hook(*args, export_settings) 25 | except Exception as e: 26 | print(hook_name, "fails on", extension) 27 | print(str(e)) 28 | -------------------------------------------------------------------------------- /exporter/exp/msfs_xml_export.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | # 19 | # This is the modified exporter for the Blender2MSFS addon. 20 | # The only purpose of the modification is to allow for extensions 21 | # in the "asset" section of the glTF file. 22 | # 23 | ################################################################################################### 24 | 25 | import bpy 26 | import xml.etree.ElementTree as etree 27 | from xml.dom import minidom #to make things pretty 28 | from xml.dom.minidom import parse, parseString 29 | 30 | import os.path 31 | from os import urandom 32 | import re 33 | import itertools 34 | 35 | def generate_guid(): 36 | b = bytearray(urandom(16)) 37 | b[6] = (b[6]&0xf)|0x40 38 | return "{"+str('%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % tuple(b))+"}" 39 | 40 | def pretty_xml_given_root(root): 41 | """ 42 | Useful for when you are editing xml data on the fly 43 | """ 44 | xml_string = minidom.parseString(etree.tostring(root)).toprettyxml() 45 | return xml_string 46 | 47 | def save_xml(context, export_settings, lods=[]): 48 | """Creates/Appends the XML file for the MSFS model(s)""" 49 | 50 | xml_file = export_settings['gltf_filedirectory']+bpy.path.ensure_ext(export_settings['gltf_msfs_xml_file'],'.xml') 51 | 52 | root = None 53 | 54 | if os.path.exists(xml_file): 55 | with open(xml_file) as f: 56 | try: 57 | xml_string = f.read() 58 | except: 59 | msg= "Couldn't read the XML file under: <%s>"%xml_file 60 | print(msg) 61 | return 0 62 | 63 | #We need to remove the xml node to continue. 64 | xml_string = re.sub('<[?]xml.*[?]>', '', xml_string) 65 | 66 | #Since Asobo doesn't stick to conventions, we need to add a root-node to the file: 67 | xml_string = ''+xml_string+'' 68 | 69 | # Remove formatting prior to loading to avoid newline issues 70 | root = etree.fromstring(re.sub('\s+(?=<)', '', xml_string)) 71 | 72 | msfs_guid = "" 73 | 74 | #check the ModelInfo 75 | ModelInfo_node = root.find('ModelInfo') 76 | if ModelInfo_node == None: 77 | ModelInfo_node = etree.SubElement(root, "ModelInfo") 78 | ModelInfo_node.set('version', "1.1") 79 | if export_settings['gltf_msfs_generate_guid'] == True: 80 | msfs_guid = generate_guid() 81 | ModelInfo_node.set('guid',msfs_guid) 82 | else: 83 | if export_settings['gltf_msfs_generate_guid'] == True: 84 | if ('guid' in ModelInfo_node.attrib and 'guid' in ModelInfo_node.attrib != ""): 85 | msfs_guid = ModelInfo_node.attrib['guid'] 86 | else: 87 | msfs_guid = generate_guid() 88 | 89 | ModelInfo_node.set('guid',msfs_guid) 90 | 91 | if len(lods) > 0: 92 | LODS_node = ModelInfo_node.find('LODS') 93 | if LODS_node == None: 94 | LODS_node = etree.SubElement(ModelInfo_node, "LODS") 95 | else: 96 | nodes_to_remove = [] 97 | for lod in LODS_node: 98 | #Delete all lod models: 99 | if lod.tag == "LOD": 100 | nodes_to_remove.append(lod) 101 | for node in reversed(nodes_to_remove): 102 | LODS_node.remove(node) 103 | 104 | #re-generate the LODs: 105 | lod_size = [] 106 | current_size = 0 107 | for model in reversed(lods): 108 | lod_size.insert(0,current_size) 109 | if current_size == 0: 110 | current_size = 4 111 | else: 112 | current_size = current_size * 2 113 | 114 | for (size, model) in zip(lod_size,lods): 115 | my_lod = etree.SubElement(LODS_node,"LOD") 116 | 117 | my_lod.set('ModelFile',os.path.basename(os.path.realpath(model))) 118 | if size != 0: 119 | my_lod.set('minSize',str(size)) 120 | 121 | else: 122 | root = etree.Element("root") 123 | ModelInfo_node = etree.SubElement(root, "ModelInfo") 124 | ModelInfo_node.set('version', "1.1") 125 | if export_settings['gltf_msfs_generate_guid'] == True: 126 | msfs_guid = generate_guid() 127 | ModelInfo_node.set('guid',msfs_guid) 128 | if len(lods) > 0: 129 | lod_size = [] 130 | current_size = 0 131 | for model in reversed(lods): 132 | lod_size.insert(0,current_size) 133 | if current_size == 0: 134 | current_size = 4 135 | else: 136 | current_size = current_size * 2 137 | 138 | LODS_node = etree.SubElement(ModelInfo_node, "LODS") 139 | for (size, model) in zip(lod_size,lods): 140 | my_lod = etree.SubElement(LODS_node,"LOD") 141 | 142 | my_lod.set('ModelFile',os.path.basename(os.path.realpath(model))) 143 | if size != 0: 144 | my_lod.set('minSize',str(size)) 145 | 146 | # Create a string, make it pretty: 147 | xml_string = pretty_xml_given_root(root) 148 | 149 | # Remove declaration and root node, and step back indent by one level 150 | xml_string = xml_string.replace("\n\t", "\n").replace("\n", "").replace("\n", "").replace("\n", "") 151 | 152 | #Write to file: 153 | with open(xml_file, "w") as f: 154 | f.write(xml_string) 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /extensions/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | 19 | import bpy 20 | 21 | from . ext_master import * 22 | 23 | def register(): 24 | try: 25 | bpy.utils.register_class(ExtAsoboProperties) 26 | except Exception: 27 | pass 28 | bpy.types.Scene.msfs_extAsoboProperties = bpy.props.PointerProperty(type=ExtAsoboProperties) 29 | 30 | def register_panel(): 31 | # Register the panel on demand, we need to be sure to only register it once 32 | # This is necessary because the panel is a child of the extensions panel, 33 | # which may not be registered when we try to register this extension 34 | try: 35 | bpy.utils.register_class(GLTF_PT_AsoboExtensionPanel) 36 | except Exception: 37 | pass 38 | 39 | # If the glTF exporter is disabled, we need to unregister the extension panel 40 | # Just return a function to the exporter so it can unregister the panel 41 | return unregister_panel 42 | 43 | 44 | def unregister_panel(): 45 | # Since panel is registered on demand, it is possible it is not registered 46 | try: 47 | bpy.utils.unregister_class(GLTF_PT_AsoboExtensionPanel) 48 | except Exception: 49 | pass 50 | 51 | 52 | def unregister(): 53 | unregister_panel() 54 | try: 55 | bpy.utils.unregister_class(ExtAsoboProperties) 56 | except Exception: 57 | pass 58 | del bpy.types.Scene.msfs_extAsoboProperties -------------------------------------------------------------------------------- /func_behavior.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | # 19 | # This file contains classes and functions to attach behaviors from an XML file to 20 | # objects of the scene. 21 | # 22 | ################################################################################################### 23 | 24 | import bpy 25 | import os 26 | 27 | from bpy_extras.io_utils import ImportHelper 28 | from bpy.props import IntProperty, BoolProperty, StringProperty, FloatProperty, EnumProperty, FloatVectorProperty, PointerProperty 29 | 30 | from . func_xml import parse_xml_behavior 31 | 32 | class CodebaseAdd(bpy.types.Operator, ImportHelper): 33 | bl_label = "Add file" 34 | bl_idname = "msfs.codebase_add" 35 | 36 | filter_glob: StringProperty( default='*.xml', options={'HIDDEN'} ) 37 | 38 | def execute(self,context): 39 | path, filename = os.path.split(self.filepath) 40 | 41 | #check if the file is already in the list and skip if it is: 42 | for file in context.scene.msfs_codebase: 43 | if file.full_path == self.filepath: 44 | msg="File already in codebase. Use reload if you want to reload the file." 45 | self.report({'ERROR'},msg) 46 | return{'FINISHED'} 47 | 48 | # Try to parse the file and populate the behavior list: 49 | appended_behaviors = parse_xml_behavior(self.filepath) 50 | 51 | if appended_behaviors == -1: 52 | self.report({'ERROR'}, "Couldn't parse the file <%s>."%self.filepath) 53 | return {'CANCELLED'} 54 | 55 | self.report({'INFO'}, "Appended %i new behaviors."%appended_behaviors) 56 | context.scene.msfs_active_codebase = 0 57 | 58 | item = bpy.context.scene.msfs_codebase.add() 59 | item.name = filename 60 | item.full_path = self.filepath 61 | 62 | return {'FINISHED'} 63 | 64 | class CodebaseRemove(bpy.types.Operator): 65 | bl_label = "Remove file" 66 | bl_idname = "msfs.codebase_remove" 67 | 68 | def execute(self,context): 69 | #Some failsafes: 70 | if len(context.scene.msfs_codebase) < 1: 71 | self.report({'INFO'},"Codebase is empty. Nothing has been removed.") 72 | return{'CANCELLED'} 73 | if context.scene.msfs_active_codebase >= len(context.scene.msfs_codebase): 74 | self.report({'INFO'},"Please select the XML file you want to remove from the index.") 75 | return{'CANCELLED'} 76 | 77 | 78 | #let's first remove all behavior templates from the list: 79 | remove_behavior_list = [] 80 | number_of_behavior_found = 0 81 | for behavior in context.scene.msfs_behavior: 82 | if behavior.source_file == context.scene.msfs_codebase[context.scene.msfs_active_codebase].full_path: 83 | number_of_behavior_found += 1 84 | remove_behavior_list.append(behavior) 85 | 86 | for element in remove_behavior_list: 87 | context.scene.msfs_behavior.remove(context.scene.msfs_behavior.find(element.name)) 88 | 89 | self.report({'INFO'},"%i behaviors removed."%number_of_behavior_found) 90 | 91 | # Now, we can delete the entry in the codebase: 92 | context.scene.msfs_codebase.remove(context.scene.msfs_active_codebase) 93 | 94 | context.scene.msfs_active_codebase = 0 95 | 96 | return {'FINISHED'} 97 | 98 | class ReloadBehaviorFile(bpy.types.Operator): 99 | bl_label = "Reload behavior" 100 | bl_idname = "msfs.reload_behavior_file" 101 | 102 | def execute(self, context): 103 | #Check that there's a file active: 104 | if len(context.scene.msfs_codebase) < 1: 105 | self.report({'INFO'},"Codebase is empty. Nothing has been reloaded.") 106 | return{'CANCELLED'} 107 | if context.scene.msfs_active_codebase >= len(context.scene.msfs_codebase): 108 | self.report({'INFO'},"Please select the XML file you want to reload.") 109 | return{'CANCELLED'} 110 | 111 | #The easiest way is toreload is to remove all of the related behaviors and then load the file again. 112 | filepath = context.scene.msfs_codebase[context.scene.msfs_active_codebase].full_path 113 | 114 | remove_behavior_list = [] 115 | number_of_behavior_removed = 0 116 | for behavior in context.scene.msfs_behavior: 117 | if behavior.source_file == filepath: 118 | number_of_behavior_removed += 1 119 | remove_behavior_list.append(behavior) 120 | 121 | for element in remove_behavior_list: 122 | context.scene.msfs_behavior.remove(context.scene.msfs_behavior.find(element.name)) 123 | 124 | #Now we can re-parse the file: 125 | appended_behaviors = parse_xml_behavior(filepath) 126 | 127 | if appended_behaviors == -1: 128 | self.report({'ERROR'}, "Couldn't parse the file <%s>."%filepath) 129 | return {'CANCELLED'} 130 | 131 | delta_behavior = appended_behaviors - number_of_behavior_removed 132 | if delta_behavior == 0: 133 | self.report({'INFO'}, "Reloaded behavior file. No new behaviors found.") 134 | elif delta_behavior < 0: 135 | self.report({'INFO'}, "Reloaded behavior file. %i behaviors have been removed."%delta_behavior) 136 | else: 137 | self.report({'INFO'}, "Reloaded behavior file. Found %i new behaviors."%delta_behavior) 138 | context.scene.msfs_active_codebase = 0 139 | 140 | 141 | 142 | print("Reloading behavior file.") 143 | return{'FINISHED'} 144 | 145 | 146 | class AssignBehavior(bpy.types.Operator): 147 | #Assign the tag to the object here. 148 | 149 | bl_label = "Assign behavior" 150 | bl_idname = "msfs.behavior_assign" 151 | 152 | def execute(self, context): 153 | #check that the selection is valid: 154 | if context.scene.msfs_active_behavior >= len(context.scene.msfs_selected_behavior): 155 | self.report({'ERROR'},"Invalid behavior selection.") 156 | return {'CANCELLED'} 157 | 158 | 159 | for ob in bpy.context.selected_objects: 160 | #check if the tag already exists: 161 | found = False 162 | for behavior in ob.msfs_behavior: 163 | if behavior.name == context.scene.msfs_behavior[context.scene.msfs_active_behavior].name: 164 | self.report({'INFO'},"The tag '%s' already exists in this object."%context.scene.msfs_behavior[context.scene.msfs_active_behavior].name) 165 | found=True 166 | 167 | if found == False: 168 | path, filename = os.path.split(context.scene.msfs_behavior[context.scene.msfs_active_behavior].source_file) 169 | 170 | behavior = ob.msfs_behavior.add() 171 | behavior.name = context.scene.msfs_behavior[context.scene.msfs_active_behavior].name 172 | behavior.source_file = context.scene.msfs_behavior[context.scene.msfs_active_behavior].source_file 173 | behavior.source_filename = filename 174 | behavior.kf_start = context.scene.msfs_behavior_start 175 | behavior.kf_end = context.scene.msfs_behavior_end 176 | 177 | self.report({'INFO'},"Behavior '%s' added to the selected object(s)."%context.scene.msfs_behavior[context.scene.msfs_active_behavior].name) 178 | return {'FINISHED'} 179 | 180 | 181 | class AssignManualBehavior(bpy.types.Operator): 182 | #Assign the tag to the object here. 183 | 184 | bl_label = "Manually assign a behavior" 185 | bl_idname = "msfs.behavior_assign_manually" 186 | 187 | def execute(self, context): 188 | #Check the string: 189 | if context.scene.msfs_manual_behavior == "": 190 | self.report({'ERROR'},"The behavior tag must not be empty.") 191 | return {'CANCELLED'} 192 | 193 | 194 | for ob in bpy.context.selected_objects: 195 | #check if the tag already exists: 196 | found = False 197 | for behavior in ob.msfs_behavior: 198 | if behavior.name == context.scene.msfs_manual_behavior: 199 | self.report({'INFO'},"The tag '%s' already exists in this object."%context.scene.msfs_manual_behavior) 200 | found=True 201 | 202 | if found == False: 203 | behavior = ob.msfs_behavior.add() 204 | behavior.name = context.scene.msfs_manual_behavior 205 | behavior.source_file = "" 206 | behavior.kf_start = context.scene.msfs_manual_behavior_start 207 | behavior.kf_end = context.scene.msfs_manual_behavior_end 208 | 209 | self.report({'INFO'},"Behavior '%s' added to the selected object(s)."%context.scene.msfs_manual_behavior) 210 | return {'FINISHED'} 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /func_properties.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | 19 | import bpy 20 | 21 | class MSFS_OT_RemoveSelectedBehaviorFromObject(bpy.types.Operator): 22 | #Remove the selected tag from the selected object here 23 | 24 | bl_label = "Remove selected behavior" 25 | bl_idname = "msfs.behavior_remove_selected_from_object" 26 | 27 | def execute(self, context): 28 | #failsafes: 29 | if context.object.msfs_active_behavior >= len(context.object.msfs_behavior): 30 | self.report({'ERROR'},"Invalid behavior index.") 31 | return {'CANCELLED'} 32 | 33 | 34 | active = context.object.msfs_active_behavior 35 | behavior = context.object.msfs_behavior[context.object.msfs_active_behavior].name 36 | 37 | context.object.msfs_behavior.remove(active) 38 | 39 | if len(context.object.msfs_behavior) <= active: 40 | context.object.msfs_active_behavior = len(context.object.msfs_behavior)-1 41 | 42 | self.report({'INFO'},"Behavior %s removed from object."%behavior) 43 | 44 | return {'FINISHED'} -------------------------------------------------------------------------------- /func_xml.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | 19 | import bpy 20 | 21 | import xml.etree.ElementTree as etree 22 | import re 23 | 24 | def parse_xml_behavior(filename): 25 | print("Parsing <%s>..."%filename) 26 | number_of_behaviors = 0 27 | 28 | # parser = etree.XMLParser(encoding="utf-8") 29 | 30 | # try: 31 | # tree = etree.parse(filename, parser) 32 | # except FileNotFoundError: 33 | # msg = "Couldn't read the XML file under: <%s>"%filename 34 | # return -1 35 | # root = tree.getroot() 36 | 37 | with open(filename) as f: 38 | try: 39 | xml = f.read() 40 | except: 41 | msg= "Couldn't read the XML file under: <%s>"%filename 42 | print(msg) 43 | return 0 44 | 45 | #We need to remove the xml node to continue. 46 | xml = re.sub('<[?]xml.*[?]>', '', xml) 47 | 48 | #Since Asobo doesn't stick to conventions, we need to add a root-node to the file: 49 | xml = ''+xml+'' 50 | 51 | root = etree.fromstring(xml.lstrip()) 52 | 53 | for element in root: 54 | if element.tag == "Template": 55 | if 'Name' in element.attrib: 56 | behavior = bpy.context.scene.msfs_behavior.add() 57 | behavior.name = element.attrib['Name'] 58 | behavior.source_file = filename 59 | number_of_behaviors += 1 60 | elif element.tag == "ModelInfo": 61 | for animation in element: 62 | if animation.tag == "Animation": 63 | if 'name' in animation.attrib: 64 | behavior = bpy.context.scene.msfs_behavior.add() 65 | behavior.name = animation.attrib['name'] 66 | if 'length' in animation.attrib: 67 | behavior.anim_length = int(animation.attrib['length']) 68 | behavior.source_file = filename 69 | number_of_behaviors += 1 70 | elif element.tag == "ModelBehaviors": 71 | for component in element: 72 | if component.tag == "Component": 73 | for template in component: 74 | if template.tag == "UseTemplate": 75 | if 'ANIM_NAME' in template.attrib: 76 | behavior = bpy.context.scene.msfs_behavior.add() 77 | behavior.name = animation.attrib['ANIM_NAME'] 78 | behavior.source_file = filename 79 | number_of_behaviors += 1 80 | 81 | 82 | msg = "Appended %i behaviors."%number_of_behaviors 83 | print(msg) 84 | 85 | return number_of_behaviors 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /li_behavior.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | 19 | import bpy 20 | 21 | from bpy.props import IntProperty, BoolProperty, StringProperty, FloatProperty, EnumProperty, FloatVectorProperty, PointerProperty 22 | 23 | class MSFS_codebase(bpy.types.PropertyGroup): 24 | name: bpy.props.StringProperty(name = "File", subtype='FILE_NAME', default = "") 25 | full_path: bpy.props.StringProperty(name = "Filepath", subtype='FILE_PATH', default = "") 26 | 27 | bpy.utils.register_class(MSFS_codebase) 28 | 29 | class MSFS_behavior(bpy.types.PropertyGroup): 30 | name: bpy.props.StringProperty(name = "name", default = "") 31 | source_file: bpy.props.StringProperty(name="in file", subtype='FILE_PATH', default="") 32 | anim_length: bpy.props.IntProperty(name="Length", default=0) 33 | 34 | bpy.utils.register_class(MSFS_behavior) 35 | 36 | class MSFS_LI_material(): 37 | def update_codebase(self, context): 38 | context.scene.msfs_selected_behavior.clear() 39 | #avoid a warning message: 40 | if context.scene.msfs_active_codebase >= len(context.scene.msfs_codebase): 41 | context.scene.msfs_active_behavior = 0 42 | return 43 | 44 | # go through the list of behaviors and match the filename 45 | for behavior in context.scene.msfs_behavior: 46 | if behavior.source_file == context.scene.msfs_codebase[context.scene.msfs_active_codebase].full_path: 47 | item = context.scene.msfs_selected_behavior.add() 48 | item.name = behavior.name 49 | item.full_path = behavior.source_file 50 | context.scene.msfs_active_behavior = 0 51 | 52 | # Collection of all assigned XML behavior files 53 | bpy.types.Scene.msfs_codebase = bpy.props.CollectionProperty(type = MSFS_codebase) 54 | bpy.types.Scene.msfs_active_codebase = bpy.props.IntProperty(default=0,min=0,update=update_codebase) 55 | 56 | # Collection of all found behavior tags: 57 | bpy.types.Scene.msfs_behavior = bpy.props.CollectionProperty(type = MSFS_behavior) 58 | 59 | # Collection of all behavior tags contained in the selected XML file: 60 | bpy.types.Scene.msfs_selected_behavior = bpy.props.CollectionProperty(type = MSFS_behavior) 61 | bpy.types.Scene.msfs_active_behavior = bpy.props.IntProperty(default=0,min=0) 62 | #Start/End keyframe of the selected animation 63 | bpy.types.Scene.msfs_behavior_start = bpy.props.IntProperty(name="KF start",default=0,min=0) 64 | bpy.types.Scene.msfs_behavior_end = bpy.props.IntProperty(name="KF end",default=1,min=0) 65 | 66 | # To manually assign a behavior tag, this string property is being used: 67 | bpy.types.Scene.msfs_manual_behavior = bpy.props.StringProperty(name="Tag",default="") 68 | #Start/End keyframe of the selected animation 69 | bpy.types.Scene.msfs_manual_behavior_start = bpy.props.IntProperty(name="KF start",default=0,min=0) 70 | bpy.types.Scene.msfs_manual_behavior_end = bpy.props.IntProperty(name="KF end",default=1,min=0) 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /li_properties.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | 19 | import bpy 20 | 21 | from bpy.props import IntProperty, BoolProperty, StringProperty, FloatProperty, EnumProperty, FloatVectorProperty, PointerProperty 22 | 23 | class MSFS_attached_behavior(bpy.types.PropertyGroup): 24 | name: bpy.props.StringProperty(name = "behavior", default = "") 25 | source_file: bpy.props.StringProperty(name = "filepath", subtype='FILE_NAME', default = "") 26 | source_filename: bpy.props.StringProperty(name = "filename", subtype='FILE_NAME', default = "") 27 | kf_start: bpy.props.IntProperty(name = "kf_start", min=0, default = 0) 28 | kf_end: bpy.props.IntProperty(name = "kf_end", min=0, default = 1) 29 | 30 | bpy.utils.register_class(MSFS_attached_behavior) 31 | 32 | class MSFS_LI_object_properties(): 33 | bpy.types.Object.msfs_behavior = bpy.props.CollectionProperty(type = MSFS_attached_behavior) 34 | bpy.types.Object.msfs_active_behavior = bpy.props.IntProperty(name="active_behavior",min=0,default=0) 35 | 36 | bpy.types.Object.msfs_light_has_symmetry = bpy.props.BoolProperty(name='has symmetry',default=False) 37 | bpy.types.Object.msfs_light_flash_frequency = bpy.props.FloatProperty(name='flash frequency',min=0.0,default=0.0) 38 | bpy.types.Object.msfs_light_flash_duration = bpy.props.FloatProperty(name='flash duration',min=0.0,default=0.0) 39 | bpy.types.Object.msfs_light_flash_phase = bpy.props.FloatProperty(name='flash phase',default=0.0) 40 | bpy.types.Object.msfs_light_rotation_speed = bpy.props.FloatProperty(name='rotation speed',default=0.0) 41 | bpy.types.Object.msfs_light_day_night_cycle = bpy.props.BoolProperty(name='Day/Night cycle',default=False,description="Set this value to 'true' if you want the light to be visible at night only.") 42 | 43 | -------------------------------------------------------------------------------- /ui_properties.py: -------------------------------------------------------------------------------- 1 | ################################################################################################### 2 | # 3 | # Copyright 2020 Otmar Nitsche 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ################################################################################################### 18 | 19 | import bpy 20 | import os 21 | 22 | from . func_properties import * 23 | 24 | class MSFS_UL_ObjectBehaviorListItem(bpy.types.UIList): 25 | bl_idname = "MSFS_UL_object_behaviorListItem" 26 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname): 27 | split = layout.split(factor=0.65) 28 | split.label(text=item.name) 29 | split.label(text="(kf:%i-%i)"%(item.kf_start,item.kf_end)) 30 | 31 | class MSFS_PT_BoneProperties(bpy.types.Panel): 32 | bl_label = "MSFS Properties" 33 | bl_idname = "BONE_PT_msfs_properties" 34 | bl_space_type = 'PROPERTIES' 35 | bl_region_type = 'WINDOW' 36 | bl_context = 'bone' 37 | 38 | def draw(self, context): 39 | layout = self.layout 40 | box=layout.box() 41 | box.label(text = "Behavior list", icon = 'ANIM') 42 | box.template_list('MSFS_UL_object_behaviorListItem', "", context.object, 'msfs_behavior', context.object, 'msfs_active_behavior') 43 | 44 | if len(context.object.msfs_behavior) > context.object.msfs_active_behavior: 45 | behavior = context.object.msfs_behavior[context.object.msfs_active_behavior] 46 | 47 | subbox=box.box() 48 | subbox.label(text=behavior.name,icon='OUTLINER_DATA_GP_LAYER') 49 | if behavior.source_file != "": 50 | subbox.label(text="XML: %s"%behavior.source_filename,icon='FILE') 51 | split=subbox.split(factor=0.75) 52 | split.label(text="Keyframes start: %i"%behavior.kf_start,icon='DECORATE_KEYFRAME') 53 | split.label(text="end: %i"%behavior.kf_end) 54 | subbox.operator('msfs.behavior_remove_selected_from_object',text="Remove selected behavior",icon='TRASH') 55 | 56 | class MSFS_PT_ObjectProperties(bpy.types.Panel): 57 | bl_label = "MSFS Properties" 58 | bl_idname = "OBJECT_PT_msfs_properties" 59 | bl_space_type = 'PROPERTIES' 60 | bl_region_type = 'WINDOW' 61 | bl_context = "object" 62 | 63 | def draw(self, context): 64 | layout = self.layout 65 | 66 | if bpy.context.active_object.type == 'LIGHT': 67 | box = layout.box() 68 | box.label(text = "MSFS Light parameters", icon='LIGHT') 69 | box.prop(bpy.context.active_object, 'msfs_light_has_symmetry') 70 | box.prop(bpy.context.active_object, 'msfs_light_flash_frequency') 71 | box.prop(bpy.context.active_object, 'msfs_light_flash_duration') 72 | box.prop(bpy.context.active_object, 'msfs_light_flash_phase') 73 | box.prop(bpy.context.active_object, 'msfs_light_rotation_speed') 74 | box.prop(bpy.context.active_object, 'msfs_light_day_night_cycle') 75 | 76 | 77 | #if bpy.context.active_object.type == 'ARMATURE': 78 | # box=layout.box() 79 | # box.label(text = "Behavior tags are stored in individual bones.", icon = 'ANIM') 80 | #else: 81 | # box=layout.box() 82 | # box.label(text = "Behavior list", icon = 'ANIM') 83 | # box.template_list('OBJECTBEHAVIOR_UL_listItem', "", context.object, 'msfs_behavior', context.object, 'msfs_active_behavior') 84 | 85 | # if len(context.object.msfs_behavior) > context.object.msfs_active_behavior: 86 | # behavior = context.object.msfs_behavior[context.object.msfs_active_behavior] 87 | 88 | # subbox=box.box() 89 | # subbox.label(text=behavior.name,icon='OUTLINER_DATA_GP_LAYER') 90 | # if behavior.source_file != "": 91 | # subbox.label(text="XML: %s"%behavior.source_filename,icon='FILE') 92 | # split=subbox.split(factor=0.75) 93 | # split.label(text="Keyframes start: %i"%behavior.kf_start,icon='DECORATE_KEYFRAME') 94 | # split.label(text="end: %i"%behavior.kf_end) 95 | # subbox.operator('msfs.behavior_remove_selected_from_object',text="Remove selected behavior",icon='TRASH') 96 | 97 | 98 | 99 | --------------------------------------------------------------------------------