├── 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 |
--------------------------------------------------------------------------------