Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Duroure <julien.duroure@gmail.com>2018-11-24 18:28:33 +0300
committerJulien Duroure <julien.duroure@gmail.com>2018-11-24 18:28:33 +0300
commitb1f2133fa2849da272e9d8404f371220226ddaf1 (patch)
tree25db56e0f2211bd1059fe0e04e78430a6156e021 /io_scene_gltf2/blender/exp
parent8959f1798cfc86924493347304118c61bd5c7f7a (diff)
Initial commit of glTF 2.0 importer/exporter
Official Khronos Group Blender glTF 2.0 importer and exporter. glTF specification: https://github.com/KhronosGroup/glTF The upstream repository can be found here: https://github.com/KhronosGroup/glTF-Blender-IO Reviewed By: Bastien, Campbell Differential Revision: https://developer.blender.org/D3929
Diffstat (limited to 'io_scene_gltf2/blender/exp')
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_animate.py638
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_export.py100
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_export_keys.py66
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_extract.py1117
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_filter.py455
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather.py63
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py82
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py131
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py198
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py166
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py169
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py60
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py124
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_image.py166
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py80
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py113
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py113
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py138
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py93
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py90
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py148
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py217
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py200
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py98
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py150
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py100
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py107
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py64
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_get.py376
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py263
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py97
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_search_scene.py89
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_utils.py68
33 files changed, 6139 insertions, 0 deletions
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_animate.py b/io_scene_gltf2/blender/exp/gltf2_blender_animate.py
new file mode 100755
index 00000000..e4b11487
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_animate.py
@@ -0,0 +1,638 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import bpy
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_extract
+from mathutils import Matrix, Quaternion, Euler
+
+
+#
+# Globals
+#
+
+JOINT_NODE = 'JOINT'
+
+NEEDS_CONVERSION = 'CONVERSION_NEEDED'
+CUBIC_INTERPOLATION = 'CUBICSPLINE'
+LINEAR_INTERPOLATION = 'LINEAR'
+STEP_INTERPOLATION = 'STEP'
+BEZIER_INTERPOLATION = 'BEZIER'
+CONSTANT_INTERPOLATION = 'CONSTANT'
+
+
+#
+# Functions
+#
+
+def animate_get_interpolation(export_settings, blender_fcurve_list):
+ """
+ Retrieve the glTF interpolation, depending on a fcurve list.
+
+ Blender allows mixing and more variations of interpolations.
+ In such a case, a conversion is needed.
+ """
+ if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]:
+ return NEEDS_CONVERSION
+
+ #
+
+ interpolation = None
+
+ keyframe_count = None
+
+ for blender_fcurve in blender_fcurve_list:
+ if blender_fcurve is None:
+ continue
+
+ #
+
+ current_keyframe_count = len(blender_fcurve.keyframe_points)
+
+ if keyframe_count is None:
+ keyframe_count = current_keyframe_count
+
+ if current_keyframe_count > 0 > blender_fcurve.keyframe_points[0].co[0]:
+ return NEEDS_CONVERSION
+
+ if keyframe_count != current_keyframe_count:
+ return NEEDS_CONVERSION
+
+ #
+
+ for blender_keyframe in blender_fcurve.keyframe_points:
+ is_bezier = blender_keyframe.interpolation == BEZIER_INTERPOLATION
+ is_linear = blender_keyframe.interpolation == LINEAR_INTERPOLATION
+ is_constant = blender_keyframe.interpolation == CONSTANT_INTERPOLATION
+
+ if interpolation is None:
+ if is_bezier:
+ interpolation = CUBIC_INTERPOLATION
+ elif is_linear:
+ interpolation = LINEAR_INTERPOLATION
+ elif is_constant:
+ interpolation = STEP_INTERPOLATION
+ else:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+ else:
+ if is_bezier and interpolation != CUBIC_INTERPOLATION:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+ elif is_linear and interpolation != LINEAR_INTERPOLATION:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+ elif is_constant and interpolation != STEP_INTERPOLATION:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+ elif not is_bezier and not is_linear and not is_constant:
+ interpolation = NEEDS_CONVERSION
+ return interpolation
+
+ if interpolation is None:
+ interpolation = NEEDS_CONVERSION
+
+ return interpolation
+
+
+def animate_convert_rotation_axis_angle(axis_angle):
+ """Convert an axis angle to a quaternion rotation."""
+ q = Quaternion((axis_angle[1], axis_angle[2], axis_angle[3]), axis_angle[0])
+
+ return [q.x, q.y, q.z, q.w]
+
+
+def animate_convert_rotation_euler(euler, rotation_mode):
+ """Convert an euler angle to a quaternion rotation."""
+ rotation = Euler((euler[0], euler[1], euler[2]), rotation_mode).to_quaternion()
+
+ return [rotation.x, rotation.y, rotation.z, rotation.w]
+
+
+def animate_convert_keys(key_list):
+ """Convert Blender key frames to glTF time keys depending on the applied frames per second."""
+ times = []
+
+ for key in key_list:
+ times.append(key / bpy.context.scene.render.fps)
+
+ return times
+
+
+def animate_gather_keys(export_settings, fcurve_list, interpolation):
+ """
+ Merge and sort several key frames to one set.
+
+ If an interpolation conversion is needed, the sample key frames are created as well.
+ """
+ keys = []
+
+ frame_start = bpy.context.scene.frame_start
+ frame_end = bpy.context.scene.frame_end
+
+ if interpolation == NEEDS_CONVERSION:
+ start = None
+ end = None
+
+ for blender_fcurve in fcurve_list:
+ if blender_fcurve is None:
+ continue
+
+ if start is None:
+ start = blender_fcurve.range()[0]
+ else:
+ start = min(start, blender_fcurve.range()[0])
+
+ if end is None:
+ end = blender_fcurve.range()[1]
+ else:
+ end = max(end, blender_fcurve.range()[1])
+
+ #
+
+ add_epsilon_keyframe = False
+ for blender_keyframe in blender_fcurve.keyframe_points:
+ if add_epsilon_keyframe:
+ key = blender_keyframe.co[0] - 0.001
+
+ if key not in keys:
+ keys.append(key)
+
+ add_epsilon_keyframe = False
+
+ if blender_keyframe.interpolation == CONSTANT_INTERPOLATION:
+ add_epsilon_keyframe = True
+
+ if add_epsilon_keyframe:
+ key = end - 0.001
+
+ if key not in keys:
+ keys.append(key)
+
+ key = start
+ while key <= end:
+ if not export_settings[gltf2_blender_export_keys.FRAME_RANGE] or (frame_start <= key <= frame_end):
+ keys.append(key)
+ key += export_settings[gltf2_blender_export_keys.FRAME_STEP]
+
+ keys.sort()
+
+ else:
+ for blender_fcurve in fcurve_list:
+ if blender_fcurve is None:
+ continue
+
+ for blender_keyframe in blender_fcurve.keyframe_points:
+ key = blender_keyframe.co[0]
+ if not export_settings[gltf2_blender_export_keys.FRAME_RANGE] or (frame_start <= key <= frame_end):
+ if key not in keys:
+ keys.append(key)
+
+ keys.sort()
+
+ return keys
+
+
+def animate_location(export_settings, location, interpolation, node_type, node_name, action_name, matrix_correction,
+ matrix_basis):
+ """Calculate/gather the key value pairs for location transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, location, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+ result_in_tangent = {}
+ result_out_tangent = {}
+
+ keyframe_index = 0
+ for timeIndex, time in enumerate(times):
+ translation = [0.0, 0.0, 0.0]
+ in_tangent = [0.0, 0.0, 0.0]
+ out_tangent = [0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ translation, tmp_rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ translation, tmp_rotation, tmp_scale = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [translation, tmp_rotation, tmp_scale]
+ else:
+ channel_index = 0
+ for blender_fcurve in location:
+
+ if blender_fcurve is not None:
+
+ if interpolation == CUBIC_INTERPOLATION:
+ blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+ translation[channel_index] = blender_key_frame.co[1]
+
+ if timeIndex == 0:
+ in_tangent_value = 0.0
+ else:
+ factor = 3.0 / (time - times[timeIndex - 1])
+ in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+ if timeIndex == len(times) - 1:
+ out_tangent_value = 0.0
+ else:
+ factor = 3.0 / (times[timeIndex + 1] - time)
+ out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+ in_tangent[channel_index] = in_tangent_value
+ out_tangent[channel_index] = out_tangent_value
+ else:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ translation[channel_index] = value
+
+ channel_index += 1
+
+ # handle parent inverse
+ matrix = Matrix.Translation(translation)
+ matrix = matrix_correction * matrix
+ translation = matrix.to_translation()
+
+ translation = gltf2_blender_extract.convert_swizzle_location(translation, export_settings)
+ in_tangent = gltf2_blender_extract.convert_swizzle_location(in_tangent, export_settings)
+ out_tangent = gltf2_blender_extract.convert_swizzle_location(out_tangent, export_settings)
+
+ result[time] = translation
+ result_in_tangent[time] = in_tangent
+ result_out_tangent[time] = out_tangent
+
+ keyframe_index += 1
+
+ return result, result_in_tangent, result_out_tangent
+
+
+def animate_rotation_axis_angle(export_settings, rotation_axis_angle, interpolation, node_type, node_name, action_name,
+ matrix_correction, matrix_basis):
+ """Calculate/gather the key value pairs for axis angle transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, rotation_axis_angle, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+
+ keyframe_index = 0
+ for time in times:
+ axis_angle_rotation = [1.0, 0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ tmp_location, rotation, tmp_scale = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+ else:
+ channel_index = 0
+ for blender_fcurve in rotation_axis_angle:
+ if blender_fcurve is not None:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ axis_angle_rotation[channel_index] = value
+
+ channel_index += 1
+
+ rotation = animate_convert_rotation_axis_angle(axis_angle_rotation)
+
+ # handle parent inverse
+ rotation = Quaternion((rotation[3], rotation[0], rotation[1], rotation[2]))
+ matrix = rotation.to_matrix().to_4x4()
+ matrix = matrix_correction * matrix
+ rotation = matrix.to_quaternion()
+
+ # Bring back to internal Quaternion notation.
+ rotation = gltf2_blender_extract.convert_swizzle_rotation(
+ [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+ # Bring back to glTF Quaternion notation.
+ rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+
+ result[time] = rotation
+
+ keyframe_index += 1
+
+ return result
+
+
+def animate_rotation_euler(export_settings, rotation_euler, rotation_mode, interpolation, node_type, node_name,
+ action_name, matrix_correction, matrix_basis):
+ """Calculate/gather the key value pairs for euler angle transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, rotation_euler, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+
+ keyframe_index = 0
+ for time in times:
+ euler_rotation = [0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ tmp_location, rotation, tmp_scale = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+ else:
+ channel_index = 0
+ for blender_fcurve in rotation_euler:
+ if blender_fcurve is not None:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ euler_rotation[channel_index] = value
+
+ channel_index += 1
+
+ rotation = animate_convert_rotation_euler(euler_rotation, rotation_mode)
+
+ # handle parent inverse
+ rotation = Quaternion((rotation[3], rotation[0], rotation[1], rotation[2]))
+ matrix = rotation.to_matrix().to_4x4()
+ matrix = matrix_correction * matrix
+ rotation = matrix.to_quaternion()
+
+ # Bring back to internal Quaternion notation.
+ rotation = gltf2_blender_extract.convert_swizzle_rotation(
+ [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+ # Bring back to glTF Quaternion notation.
+ rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+
+ result[time] = rotation
+
+ keyframe_index += 1
+
+ return result
+
+
+def animate_rotation_quaternion(export_settings, rotation_quaternion, interpolation, node_type, node_name, action_name,
+ matrix_correction, matrix_basis):
+ """Calculate/gather the key value pairs for quaternion transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, rotation_quaternion, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+ result_in_tangent = {}
+ result_out_tangent = {}
+
+ keyframe_index = 0
+ for timeIndex, time in enumerate(times):
+ rotation = [1.0, 0.0, 0.0, 0.0]
+ in_tangent = [1.0, 0.0, 0.0, 0.0]
+ out_tangent = [1.0, 0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ tmp_location, rotation, tmp_scale = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+ else:
+ channel_index = 0
+ for blender_fcurve in rotation_quaternion:
+
+ if blender_fcurve is not None:
+ if interpolation == CUBIC_INTERPOLATION:
+ blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+ rotation[channel_index] = blender_key_frame.co[1]
+
+ if timeIndex == 0:
+ in_tangent_value = 0.0
+ else:
+ factor = 3.0 / (time - times[timeIndex - 1])
+ in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+ if timeIndex == len(times) - 1:
+ out_tangent_value = 0.0
+ else:
+ factor = 3.0 / (times[timeIndex + 1] - time)
+ out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+ in_tangent[channel_index] = in_tangent_value
+ out_tangent[channel_index] = out_tangent_value
+ else:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ rotation[channel_index] = value
+
+ channel_index += 1
+
+ rotation = Quaternion((rotation[0], rotation[1], rotation[2], rotation[3]))
+ in_tangent = gltf2_blender_extract.convert_swizzle_rotation(in_tangent, export_settings)
+ out_tangent = gltf2_blender_extract.convert_swizzle_rotation(out_tangent, export_settings)
+
+ # handle parent inverse
+ matrix = rotation.to_matrix().to_4x4()
+ matrix = matrix_correction * matrix
+ rotation = matrix.to_quaternion()
+
+ # Bring back to internal Quaternion notation.
+ rotation = gltf2_blender_extract.convert_swizzle_rotation(
+ [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+ # Bring to glTF Quaternion notation.
+ rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+ in_tangent = [in_tangent[1], in_tangent[2], in_tangent[3], in_tangent[0]]
+ out_tangent = [out_tangent[1], out_tangent[2], out_tangent[3], out_tangent[0]]
+
+ result[time] = rotation
+ result_in_tangent[time] = in_tangent
+ result_out_tangent[time] = out_tangent
+
+ keyframe_index += 1
+
+ return result, result_in_tangent, result_out_tangent
+
+
+def animate_scale(export_settings, scale, interpolation, node_type, node_name, action_name, matrix_correction,
+ matrix_basis):
+ """Calculate/gather the key value pairs for scale transformations."""
+ joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+ if not joint_cache.get(node_name):
+ joint_cache[node_name] = {}
+
+ keys = animate_gather_keys(export_settings, scale, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+ result_in_tangent = {}
+ result_out_tangent = {}
+
+ keyframe_index = 0
+ for timeIndex, time in enumerate(times):
+ scale_data = [1.0, 1.0, 1.0]
+ in_tangent = [0.0, 0.0, 0.0]
+ out_tangent = [0.0, 0.0, 0.0]
+
+ if node_type == JOINT_NODE:
+ if joint_cache[node_name].get(keys[keyframe_index]):
+ tmp_location, tmp_rotation, scale_data = joint_cache[node_name][keys[keyframe_index]]
+ else:
+ bpy.context.scene.frame_set(keys[keyframe_index])
+
+ matrix = matrix_correction * matrix_basis
+
+ tmp_location, tmp_rotation, scale_data = matrix.decompose()
+
+ joint_cache[node_name][keys[keyframe_index]] = [tmp_location, tmp_rotation, scale_data]
+ else:
+ channel_index = 0
+ for blender_fcurve in scale:
+
+ if blender_fcurve is not None:
+ if interpolation == CUBIC_INTERPOLATION:
+ blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+ scale_data[channel_index] = blender_key_frame.co[1]
+
+ if timeIndex == 0:
+ in_tangent_value = 0.0
+ else:
+ factor = 3.0 / (time - times[timeIndex - 1])
+ in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+ if timeIndex == len(times) - 1:
+ out_tangent_value = 0.0
+ else:
+ factor = 3.0 / (times[timeIndex + 1] - time)
+ out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+ in_tangent[channel_index] = in_tangent_value
+ out_tangent[channel_index] = out_tangent_value
+ else:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ scale_data[channel_index] = value
+
+ channel_index += 1
+
+ scale_data = gltf2_blender_extract.convert_swizzle_scale(scale_data, export_settings)
+ in_tangent = gltf2_blender_extract.convert_swizzle_scale(in_tangent, export_settings)
+ out_tangent = gltf2_blender_extract.convert_swizzle_scale(out_tangent, export_settings)
+
+ # handle parent inverse
+ matrix = Matrix()
+ matrix[0][0] = scale_data.x
+ matrix[1][1] = scale_data.y
+ matrix[2][2] = scale_data.z
+ matrix = matrix_correction * matrix
+ scale_data = matrix.to_scale()
+
+ result[time] = scale_data
+ result_in_tangent[time] = in_tangent
+ result_out_tangent[time] = out_tangent
+
+ keyframe_index += 1
+
+ return result, result_in_tangent, result_out_tangent
+
+
+def animate_value(export_settings, value_parameter, interpolation,
+ node_type, node_name, matrix_correction, matrix_basis):
+ """Calculate/gather the key value pairs for scalar anaimations."""
+ keys = animate_gather_keys(export_settings, value_parameter, interpolation)
+
+ times = animate_convert_keys(keys)
+
+ result = {}
+ result_in_tangent = {}
+ result_out_tangent = {}
+
+ keyframe_index = 0
+ for timeIndex, time in enumerate(times):
+ value_data = []
+ in_tangent = []
+ out_tangent = []
+
+ for blender_fcurve in value_parameter:
+
+ if blender_fcurve is not None:
+ if interpolation == CUBIC_INTERPOLATION:
+ blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+ value_data.append(blender_key_frame.co[1])
+
+ if timeIndex == 0:
+ in_tangent_value = 0.0
+ else:
+ factor = 3.0 / (time - times[timeIndex - 1])
+ in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+ if timeIndex == len(times) - 1:
+ out_tangent_value = 0.0
+ else:
+ factor = 3.0 / (times[timeIndex + 1] - time)
+ out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+ in_tangent.append(in_tangent_value)
+ out_tangent.append(out_tangent_value)
+ else:
+ value = blender_fcurve.evaluate(keys[keyframe_index])
+
+ value_data.append(value)
+
+ result[time] = value_data
+ result_in_tangent[time] = in_tangent
+ result_out_tangent[time] = out_tangent
+
+ keyframe_index += 1
+
+ return result, result_in_tangent, result_out_tangent
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export.py b/io_scene_gltf2/blender/exp/gltf2_blender_export.py
new file mode 100755
index 00000000..ae2db26b
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_export.py
@@ -0,0 +1,100 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import sys
+import traceback
+
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_gather
+from .gltf2_blender_gltf2_exporter import GlTF2Exporter
+from ..com import gltf2_blender_json
+from ...io.exp import gltf2_io_export
+from ...io.com.gltf2_io_debug import print_console, print_newline
+
+
+def save(operator,
+ context,
+ export_settings):
+ """Start the glTF 2.0 export and saves to content either to a .gltf or .glb file."""
+ print_console('INFO', 'Starting glTF 2.0 export')
+ context.window_manager.progress_begin(0, 100)
+ context.window_manager.progress_update(0)
+
+ if not export_settings[gltf2_blender_export_keys.COPYRIGHT]:
+ export_settings[gltf2_blender_export_keys.COPYRIGHT] = None
+
+ scenes, animations = gltf2_blender_gather.gather_gltf2(export_settings)
+ exporter = GlTF2Exporter(copyright=export_settings[gltf2_blender_export_keys.COPYRIGHT])
+ for scene in scenes:
+ exporter.add_scene(scene)
+ for animation in animations:
+ exporter.add_animation(animation)
+
+ buffer = bytes()
+ if export_settings[gltf2_blender_export_keys.FORMAT] == 'ASCII':
+ # .gltf
+ if export_settings[gltf2_blender_export_keys.EMBED_BUFFERS]:
+ exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY])
+ else:
+ exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY],
+ export_settings[gltf2_blender_export_keys.BINARY_FILENAME])
+ else:
+ # .glb
+ buffer = exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY], is_glb=True)
+ exporter.finalize_images(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY])
+ glTF = exporter.glTF
+
+ #
+
+ # TODO: move to custom JSON encoder
+ def dict_strip(obj):
+ o = obj
+ if isinstance(obj, dict):
+ o = {}
+ for k, v in obj.items():
+ if v is None:
+ continue
+ elif isinstance(v, list) and len(v) == 0:
+ continue
+ o[k] = dict_strip(v)
+ elif isinstance(obj, list):
+ o = []
+ for v in obj:
+ o.append(dict_strip(v))
+ elif isinstance(obj, float):
+ # force floats to int, if they are integers (prevent INTEGER_WRITTEN_AS_FLOAT validator warnings)
+ if int(obj) == obj:
+ return int(obj)
+ return o
+
+ try:
+ gltf2_io_export.save_gltf(dict_strip(glTF.to_dict()), export_settings, gltf2_blender_json.BlenderJSONEncoder,
+ buffer)
+ except AssertionError as e:
+ _, _, tb = sys.exc_info()
+ traceback.print_tb(tb) # Fixed format
+ tb_info = traceback.extract_tb(tb)
+ for tbi in tb_info:
+ filename, line, func, text = tbi
+ print_console('ERROR', 'An error occurred on line {} in statement {}'.format(line, text))
+ print_console('ERROR', str(e))
+ raise e
+
+ print_console('INFO', 'Finished glTF 2.0 export')
+ context.window_manager.progress_end()
+ print_newline()
+
+ return {'FINISHED'}
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py
new file mode 100755
index 00000000..9e47645a
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py
@@ -0,0 +1,66 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FILTERED_VERTEX_GROUPS = 'filtered_vertex_groups'
+FILTERED_MESHES = 'filtered_meshes'
+FILTERED_IMAGES = 'filtered_images'
+FILTERED_IMAGES_USE_ALPHA = 'filtered_images_use_alpha'
+FILTERED_MERGED_IMAGES = 'filtered_merged_images'
+FILTERED_TEXTURES = 'filtered_textures'
+FILTERED_MATERIALS = 'filtered_materials'
+FILTERED_LIGHTS = 'filtered_lights'
+TEMPORARY_MESHES = 'temporary_meshes'
+FILTERED_OBJECTS = 'filtered_objects'
+FILTERED_CAMERAS = 'filtered_cameras'
+
+APPLY = 'gltf_apply'
+LAYERS = 'gltf_layers'
+SELECTED = 'gltf_selected'
+SKINS = 'gltf_skins'
+DISPLACEMENT = 'gltf_displacement'
+FORCE_SAMPLING = 'gltf_force_sampling'
+FRAME_RANGE = 'gltf_frame_range'
+FRAME_STEP = 'gltf_frame_step'
+JOINT_CACHE = 'gltf_joint_cache'
+COPYRIGHT = 'gltf_copyright'
+FORMAT = 'gltf_format'
+FILE_DIRECTORY = 'gltf_filedirectory'
+BINARY_FILENAME = 'gltf_binaryfilename'
+YUP = 'gltf_yup'
+MORPH = 'gltf_morph'
+INDICES = 'gltf_indices'
+CAMERA_INFINITE = 'gltf_camera_infinite'
+BAKE_SKINS = 'gltf_bake_skins'
+TEX_COORDS = 'gltf_texcoords'
+COLORS = 'gltf_colors'
+NORMALS = 'gltf_normals'
+TANGENTS = 'gltf_tangents'
+FORCE_INDICES = 'gltf_force_indices'
+MORPH_TANGENT = 'gltf_morph_tangent'
+MORPH_NORMAL = 'gltf_morph_normal'
+MOVE_KEYFRAMES = 'gltf_move_keyframes'
+MATERIALS = 'gltf_materials'
+EXTRAS = 'gltf_extras'
+CAMERAS = 'gltf_cameras'
+LIGHTS = 'gltf_lights'
+ANIMATIONS = 'gltf_animations'
+EMBED_IMAGES = 'gltf_embed_images'
+BINARY = 'gltf_binary'
+EMBED_BUFFERS = 'gltf_embed_buffers'
+TEXTURE_TRANSFORM = 'gltf_texture_transform'
+USE_NO_COLOR = 'gltf_use_no_color'
+
+METALLIC_ROUGHNESS_IMAGE = "metallic_roughness_image"
+GROUP_INDEX = 'group_index'
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
new file mode 100755
index 00000000..a5220129
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
@@ -0,0 +1,1117 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+from mathutils import Vector, Quaternion
+from mathutils.geometry import tessellate_polygon
+
+from . import gltf2_blender_export_keys
+from ...io.com.gltf2_io_debug import print_console
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
+
+#
+# Globals
+#
+
+INDICES_ID = 'indices'
+MATERIAL_ID = 'material'
+ATTRIBUTES_ID = 'attributes'
+
+COLOR_PREFIX = 'COLOR_'
+MORPH_TANGENT_PREFIX = 'MORPH_TANGENT_'
+MORPH_NORMAL_PREFIX = 'MORPH_NORMAL_'
+MORPH_POSITION_PREFIX = 'MORPH_POSITION_'
+TEXCOORD_PREFIX = 'TEXCOORD_'
+WEIGHTS_PREFIX = 'WEIGHTS_'
+JOINTS_PREFIX = 'JOINTS_'
+
+TANGENT_ATTRIBUTE = 'TANGENT'
+NORMAL_ATTRIBUTE = 'NORMAL'
+POSITION_ATTRIBUTE = 'POSITION'
+
+GLTF_MAX_COLORS = 2
+
+
+#
+# Classes
+#
+
+class ShapeKey:
+ def __init__(self, shape_key, vertex_normals, polygon_normals):
+ self.shape_key = shape_key
+ self.vertex_normals = vertex_normals
+ self.polygon_normals = polygon_normals
+
+
+#
+# Functions
+#
+
+def convert_swizzle_location(loc, export_settings):
+ """Convert a location from Blender coordinate system to glTF coordinate system."""
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ return Vector((loc[0], loc[2], -loc[1]))
+ else:
+ return Vector((loc[0], loc[1], loc[2]))
+
+
+def convert_swizzle_tangent(tan, export_settings):
+ """Convert a tangent from Blender coordinate system to glTF coordinate system."""
+ if tan[0] == 0.0 and tan[1] == 0.0 and tan[2] == 0.0:
+ print_console('WARNING', 'Tangent has zero length.')
+
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ return Vector((tan[0], tan[2], -tan[1], 1.0))
+ else:
+ return Vector((tan[0], tan[1], tan[2], 1.0))
+
+
+def convert_swizzle_rotation(rot, export_settings):
+ """
+ Convert a quaternion rotation from Blender coordinate system to glTF coordinate system.
+
+ 'w' is still at first position.
+ """
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ return Quaternion((rot[0], rot[1], rot[3], -rot[2]))
+ else:
+ return Quaternion((rot[0], rot[1], rot[2], rot[3]))
+
+
+def convert_swizzle_scale(scale, export_settings):
+ """Convert a scale from Blender coordinate system to glTF coordinate system."""
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ return Vector((scale[0], scale[2], scale[1]))
+ else:
+ return Vector((scale[0], scale[1], scale[2]))
+
+
+def decompose_transition(matrix, context, export_settings):
+ translation, rotation, scale = matrix.decompose()
+ """Decompose a matrix depending if it is associated to a joint or node."""
+ if context == 'NODE':
+ translation = convert_swizzle_location(translation, export_settings)
+ rotation = convert_swizzle_rotation(rotation, export_settings)
+ scale = convert_swizzle_scale(scale, export_settings)
+
+ # Put w at the end.
+ rotation = Quaternion((rotation[1], rotation[2], rotation[3], rotation[0]))
+
+ return translation, rotation, scale
+
+
+def color_srgb_to_scene_linear(c):
+ """
+ Convert from sRGB to scene linear color space.
+
+ Source: Cycles addon implementation, node_color.h.
+ """
+ if c < 0.04045:
+ return 0.0 if c < 0.0 else c * (1.0 / 12.92)
+ else:
+ return pow((c + 0.055) * (1.0 / 1.055), 2.4)
+
+
+def extract_primitive_floor(a, indices, use_tangents):
+ """Shift indices, that the first one starts with 0. It is assumed, that the indices are packed."""
+ attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ attributes[TANGENT_ATTRIBUTE] = []
+
+ result_primitive = {
+ MATERIAL_ID: a[MATERIAL_ID],
+ INDICES_ID: [],
+ ATTRIBUTES_ID: attributes
+ }
+
+ source_attributes = a[ATTRIBUTES_ID]
+
+ #
+
+ tex_coord_index = 0
+ process_tex_coord = True
+ while process_tex_coord:
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+ if source_attributes.get(tex_coord_id) is not None:
+ attributes[tex_coord_id] = []
+ tex_coord_index += 1
+ else:
+ process_tex_coord = False
+
+ tex_coord_max = tex_coord_index
+
+ #
+
+ color_index = 0
+ process_color = True
+ while process_color:
+ color_id = COLOR_PREFIX + str(color_index)
+
+ if source_attributes.get(color_id) is not None:
+ attributes[color_id] = []
+ color_index += 1
+ else:
+ process_color = False
+
+ color_max = color_index
+
+ #
+
+ bone_index = 0
+ process_bone = True
+ while process_bone:
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+ if source_attributes.get(joint_id) is not None:
+ attributes[joint_id] = []
+ attributes[weight_id] = []
+ bone_index += 1
+ else:
+ process_bone = False
+
+ bone_max = bone_index
+
+ #
+
+ morph_index = 0
+ process_morph = True
+ while process_morph:
+ morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+ if source_attributes.get(morph_position_id) is not None:
+ attributes[morph_position_id] = []
+ attributes[morph_normal_id] = []
+ if use_tangents:
+ attributes[morph_tangent_id] = []
+ morph_index += 1
+ else:
+ process_morph = False
+
+ morph_max = morph_index
+
+ #
+
+ min_index = min(indices)
+ max_index = max(indices)
+
+ for old_index in indices:
+ result_primitive[INDICES_ID].append(old_index - min_index)
+
+ for old_index in range(min_index, max_index + 1):
+ for vi in range(0, 3):
+ attributes[POSITION_ATTRIBUTE].append(source_attributes[POSITION_ATTRIBUTE][old_index * 3 + vi])
+ attributes[NORMAL_ATTRIBUTE].append(source_attributes[NORMAL_ATTRIBUTE][old_index * 3 + vi])
+
+ if use_tangents:
+ for vi in range(0, 4):
+ attributes[TANGENT_ATTRIBUTE].append(source_attributes[TANGENT_ATTRIBUTE][old_index * 4 + vi])
+
+ for tex_coord_index in range(0, tex_coord_max):
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+ for vi in range(0, 2):
+ attributes[tex_coord_id].append(source_attributes[tex_coord_id][old_index * 2 + vi])
+
+ for color_index in range(0, color_max):
+ color_id = COLOR_PREFIX + str(color_index)
+ for vi in range(0, 4):
+ attributes[color_id].append(source_attributes[color_id][old_index * 4 + vi])
+
+ for bone_index in range(0, bone_max):
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+ for vi in range(0, 4):
+ attributes[joint_id].append(source_attributes[joint_id][old_index * 4 + vi])
+ attributes[weight_id].append(source_attributes[weight_id][old_index * 4 + vi])
+
+ for morph_index in range(0, morph_max):
+ morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+ for vi in range(0, 3):
+ attributes[morph_position_id].append(source_attributes[morph_position_id][old_index * 3 + vi])
+ attributes[morph_normal_id].append(source_attributes[morph_normal_id][old_index * 3 + vi])
+ if use_tangents:
+ for vi in range(0, 4):
+ attributes[morph_tangent_id].append(source_attributes[morph_tangent_id][old_index * 4 + vi])
+
+ return result_primitive
+
+
+def extract_primitive_pack(a, indices, use_tangents):
+ """Pack indices, that the first one starts with 0. Current indices can have gaps."""
+ attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ attributes[TANGENT_ATTRIBUTE] = []
+
+ result_primitive = {
+ MATERIAL_ID: a[MATERIAL_ID],
+ INDICES_ID: [],
+ ATTRIBUTES_ID: attributes
+ }
+
+ source_attributes = a[ATTRIBUTES_ID]
+
+ #
+
+ tex_coord_index = 0
+ process_tex_coord = True
+ while process_tex_coord:
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+ if source_attributes.get(tex_coord_id) is not None:
+ attributes[tex_coord_id] = []
+ tex_coord_index += 1
+ else:
+ process_tex_coord = False
+
+ tex_coord_max = tex_coord_index
+
+ #
+
+ color_index = 0
+ process_color = True
+ while process_color:
+ color_id = COLOR_PREFIX + str(color_index)
+
+ if source_attributes.get(color_id) is not None:
+ attributes[color_id] = []
+ color_index += 1
+ else:
+ process_color = False
+
+ color_max = color_index
+
+ #
+
+ bone_index = 0
+ process_bone = True
+ while process_bone:
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+ if source_attributes.get(joint_id) is not None:
+ attributes[joint_id] = []
+ attributes[weight_id] = []
+ bone_index += 1
+ else:
+ process_bone = False
+
+ bone_max = bone_index
+
+ #
+
+ morph_index = 0
+ process_morph = True
+ while process_morph:
+ morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+ if source_attributes.get(morph_position_id) is not None:
+ attributes[morph_position_id] = []
+ attributes[morph_normal_id] = []
+ if use_tangents:
+ attributes[morph_tangent_id] = []
+ morph_index += 1
+ else:
+ process_morph = False
+
+ morph_max = morph_index
+
+ #
+
+ old_to_new_indices = {}
+ new_to_old_indices = {}
+
+ new_index = 0
+ for old_index in indices:
+ if old_to_new_indices.get(old_index) is None:
+ old_to_new_indices[old_index] = new_index
+ new_to_old_indices[new_index] = old_index
+ new_index += 1
+
+ result_primitive[INDICES_ID].append(old_to_new_indices[old_index])
+
+ end_new_index = new_index
+
+ for new_index in range(0, end_new_index):
+ old_index = new_to_old_indices[new_index]
+
+ for vi in range(0, 3):
+ attributes[POSITION_ATTRIBUTE].append(source_attributes[POSITION_ATTRIBUTE][old_index * 3 + vi])
+ attributes[NORMAL_ATTRIBUTE].append(source_attributes[NORMAL_ATTRIBUTE][old_index * 3 + vi])
+
+ if use_tangents:
+ for vi in range(0, 4):
+ attributes[TANGENT_ATTRIBUTE].append(source_attributes[TANGENT_ATTRIBUTE][old_index * 4 + vi])
+
+ for tex_coord_index in range(0, tex_coord_max):
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+ for vi in range(0, 2):
+ attributes[tex_coord_id].append(source_attributes[tex_coord_id][old_index * 2 + vi])
+
+ for color_index in range(0, color_max):
+ color_id = COLOR_PREFIX + str(color_index)
+ for vi in range(0, 4):
+ attributes[color_id].append(source_attributes[color_id][old_index * 4 + vi])
+
+ for bone_index in range(0, bone_max):
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+ for vi in range(0, 4):
+ attributes[joint_id].append(source_attributes[joint_id][old_index * 4 + vi])
+ attributes[weight_id].append(source_attributes[weight_id][old_index * 4 + vi])
+
+ for morph_index in range(0, morph_max):
+ morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+ for vi in range(0, 3):
+ attributes[morph_position_id].append(source_attributes[morph_position_id][old_index * 3 + vi])
+ attributes[morph_normal_id].append(source_attributes[morph_normal_id][old_index * 3 + vi])
+ if use_tangents:
+ for vi in range(0, 4):
+ attributes[morph_tangent_id].append(source_attributes[morph_tangent_id][old_index * 4 + vi])
+
+ return result_primitive
+
+
+def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, export_settings):
+ """
+ Extract primitives from a mesh. Polygons are triangulated and sorted by material.
+
+ Furthermore, primitives are split up, if the indices range is exceeded.
+ Finally, triangles are also split up/duplicated, if face normals are used instead of vertex normals.
+ """
+ print_console('INFO', 'Extracting primitive')
+
+ use_tangents = False
+ if blender_mesh.uv_layers.active and len(blender_mesh.uv_layers) > 0:
+ try:
+ blender_mesh.calc_tangents()
+ use_tangents = True
+ except Exception:
+ print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.')
+
+ #
+
+ material_map = {}
+
+ #
+ # Gathering position, normal and tex_coords.
+ #
+ no_material_attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ no_material_attributes[TANGENT_ATTRIBUTE] = []
+
+ #
+ # Directory of materials with its primitive.
+ #
+ no_material_primitives = {
+ MATERIAL_ID: '',
+ INDICES_ID: [],
+ ATTRIBUTES_ID: no_material_attributes
+ }
+
+ material_name_to_primitives = {'': no_material_primitives}
+
+ #
+
+ vertex_index_to_new_indices = {}
+
+ material_map[''] = vertex_index_to_new_indices
+
+ #
+ # Create primitive for each material.
+ #
+ for blender_material in blender_mesh.materials:
+ if blender_material is None:
+ continue
+
+ attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ attributes[TANGENT_ATTRIBUTE] = []
+
+ primitive = {
+ MATERIAL_ID: blender_material.name,
+ INDICES_ID: [],
+ ATTRIBUTES_ID: attributes
+ }
+
+ material_name_to_primitives[blender_material.name] = primitive
+
+ #
+
+ vertex_index_to_new_indices = {}
+
+ material_map[blender_material.name] = vertex_index_to_new_indices
+
+ tex_coord_max = 0
+ if blender_mesh.uv_layers.active:
+ tex_coord_max = len(blender_mesh.uv_layers)
+
+ #
+
+ vertex_colors = {}
+
+ color_index = 0
+ for vertex_color in blender_mesh.vertex_colors:
+ vertex_color_name = COLOR_PREFIX + str(color_index)
+ vertex_colors[vertex_color_name] = vertex_color
+
+ color_index += 1
+ if color_index >= GLTF_MAX_COLORS:
+ break
+ color_max = color_index
+
+ #
+
+ bone_max = 0
+ for blender_polygon in blender_mesh.polygons:
+ for loop_index in blender_polygon.loop_indices:
+ vertex_index = blender_mesh.loops[loop_index].vertex_index
+ bones_count = len(blender_mesh.vertices[vertex_index].groups)
+ if bones_count > 0:
+ if bones_count % 4 == 0:
+ bones_count -= 1
+ bone_max = max(bone_max, bones_count // 4 + 1)
+
+ #
+
+ morph_max = 0
+
+ blender_shape_keys = []
+
+ if blender_mesh.shape_keys is not None:
+ morph_max = len(blender_mesh.shape_keys.key_blocks) - 1
+
+ for blender_shape_key in blender_mesh.shape_keys.key_blocks:
+ if blender_shape_key != blender_shape_key.relative_key:
+ blender_shape_keys.append(ShapeKey(
+ blender_shape_key,
+ blender_shape_key.normals_vertex_get(), # calculate vertex normals for this shape key
+ blender_shape_key.normals_polygon_get())) # calculate polygon normals for this shape key
+
+ #
+ # Convert polygon to primitive indices and eliminate invalid ones. Assign to material.
+ #
+ for blender_polygon in blender_mesh.polygons:
+ export_color = True
+
+ #
+
+ if blender_polygon.material_index < 0 or blender_polygon.material_index >= len(blender_mesh.materials) or \
+ blender_mesh.materials[blender_polygon.material_index] is None:
+ primitive = material_name_to_primitives['']
+ vertex_index_to_new_indices = material_map['']
+ else:
+ primitive = material_name_to_primitives[blender_mesh.materials[blender_polygon.material_index].name]
+ vertex_index_to_new_indices = material_map[blender_mesh.materials[blender_polygon.material_index].name]
+ #
+
+ attributes = primitive[ATTRIBUTES_ID]
+
+ face_normal = blender_polygon.normal
+ face_tangent = Vector((0.0, 0.0, 0.0))
+ face_bitangent = Vector((0.0, 0.0, 0.0))
+ if use_tangents:
+ for loop_index in blender_polygon.loop_indices:
+ temp_vertex = blender_mesh.loops[loop_index]
+ face_tangent += temp_vertex.tangent
+ face_bitangent += temp_vertex.bitangent
+
+ face_tangent.normalize()
+ face_bitangent.normalize()
+
+ #
+
+ indices = primitive[INDICES_ID]
+
+ loop_index_list = []
+
+ if len(blender_polygon.loop_indices) == 3:
+ loop_index_list.extend(blender_polygon.loop_indices)
+ elif len(blender_polygon.loop_indices) > 3:
+ # Triangulation of polygon. Using internal function, as non-convex polygons could exist.
+ polyline = []
+
+ for loop_index in blender_polygon.loop_indices:
+ vertex_index = blender_mesh.loops[loop_index].vertex_index
+ v = blender_mesh.vertices[vertex_index].co
+ polyline.append(Vector((v[0], v[1], v[2])))
+
+ triangles = tessellate_polygon((polyline,))
+
+ for triangle in triangles:
+ loop_index_list.append(blender_polygon.loop_indices[triangle[0]])
+ loop_index_list.append(blender_polygon.loop_indices[triangle[2]])
+ loop_index_list.append(blender_polygon.loop_indices[triangle[1]])
+ else:
+ continue
+
+ for loop_index in loop_index_list:
+ vertex_index = blender_mesh.loops[loop_index].vertex_index
+
+ if vertex_index_to_new_indices.get(vertex_index) is None:
+ vertex_index_to_new_indices[vertex_index] = []
+
+ #
+
+ v = None
+ n = None
+ t = None
+ b = None
+ uvs = []
+ colors = []
+ joints = []
+ weights = []
+
+ target_positions = []
+ target_normals = []
+ target_tangents = []
+
+ vertex = blender_mesh.vertices[vertex_index]
+
+ v = convert_swizzle_location(vertex.co, export_settings)
+ if blender_polygon.use_smooth:
+ n = convert_swizzle_location(vertex.normal, export_settings)
+ if use_tangents:
+ t = convert_swizzle_tangent(blender_mesh.loops[loop_index].tangent, export_settings)
+ b = convert_swizzle_location(blender_mesh.loops[loop_index].bitangent, export_settings)
+ else:
+ n = convert_swizzle_location(face_normal, export_settings)
+ if use_tangents:
+ t = convert_swizzle_tangent(face_tangent, export_settings)
+ b = convert_swizzle_location(face_bitangent, export_settings)
+
+ if use_tangents:
+ tv = Vector((t[0], t[1], t[2]))
+ bv = Vector((b[0], b[1], b[2]))
+ nv = Vector((n[0], n[1], n[2]))
+
+ if (nv.cross(tv)).dot(bv) < 0.0:
+ t[3] = -1.0
+
+ if blender_mesh.uv_layers.active:
+ for tex_coord_index in range(0, tex_coord_max):
+ uv = blender_mesh.uv_layers[tex_coord_index].data[loop_index].uv
+ uvs.append([uv.x, 1.0 - uv.y])
+
+ #
+
+ if color_max > 0 and export_color:
+ for color_index in range(0, color_max):
+ color_name = COLOR_PREFIX + str(color_index)
+ color = vertex_colors[color_name].data[loop_index].color
+ colors.append([
+ color_srgb_to_scene_linear(color[0]),
+ color_srgb_to_scene_linear(color[1]),
+ color_srgb_to_scene_linear(color[2]),
+ 1.0
+ ])
+
+ #
+
+ bone_count = 0
+
+ if vertex.groups is not None and len(vertex.groups) > 0 and export_settings[gltf2_blender_export_keys.SKINS]:
+ joint = []
+ weight = []
+ for group_element in vertex.groups:
+
+ if len(joint) == 4:
+ bone_count += 1
+ joints.append(joint)
+ weights.append(weight)
+ joint = []
+ weight = []
+
+ #
+
+ vertex_group_index = group_element.group
+ vertex_group_name = blender_vertex_groups[vertex_group_index].name
+
+ #
+
+ joint_index = 0
+ modifiers_dict = {m.type: m for m in modifiers}
+ if "ARMATURE" in modifiers_dict:
+ armature = modifiers_dict["ARMATURE"].object
+ skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings)
+ for index, j in enumerate(skin.joints):
+ if j.name == vertex_group_name:
+ joint_index = index
+
+ joint_weight = group_element.weight
+
+ #
+ joint.append(joint_index)
+ weight.append(joint_weight)
+
+ if len(joint) > 0:
+ bone_count += 1
+
+ for fill in range(0, 4 - len(joint)):
+ joint.append(0)
+ weight.append(0.0)
+
+ joints.append(joint)
+ weights.append(weight)
+
+ for fill in range(0, bone_max - bone_count):
+ joints.append([0, 0, 0, 0])
+ weights.append([0.0, 0.0, 0.0, 0.0])
+
+ #
+
+ if morph_max > 0 and export_settings[gltf2_blender_export_keys.MORPH]:
+ for morph_index in range(0, morph_max):
+ blender_shape_key = blender_shape_keys[morph_index]
+
+ v_morph = convert_swizzle_location(blender_shape_key.shape_key.data[vertex_index].co,
+ export_settings)
+
+ # Store delta.
+ v_morph -= v
+
+ target_positions.append(v_morph)
+
+ #
+
+ n_morph = None
+
+ if blender_polygon.use_smooth:
+ temp_normals = blender_shape_key.vertex_normals
+ n_morph = (temp_normals[vertex_index * 3 + 0], temp_normals[vertex_index * 3 + 1],
+ temp_normals[vertex_index * 3 + 2])
+ else:
+ temp_normals = blender_shape_key.polygon_normals
+ n_morph = (
+ temp_normals[blender_polygon.index * 3 + 0], temp_normals[blender_polygon.index * 3 + 1],
+ temp_normals[blender_polygon.index * 3 + 2])
+
+ n_morph = convert_swizzle_location(n_morph, export_settings)
+
+ # Store delta.
+ n_morph -= n
+
+ target_normals.append(n_morph)
+
+ #
+
+ if use_tangents:
+ rotation = n_morph.rotation_difference(n)
+
+ t_morph = Vector((t[0], t[1], t[2]))
+
+ t_morph.rotate(rotation)
+
+ target_tangents.append(t_morph)
+
+ #
+ #
+
+ create = True
+
+ for current_new_index in vertex_index_to_new_indices[vertex_index]:
+ found = True
+
+ for i in range(0, 3):
+ if attributes[POSITION_ATTRIBUTE][current_new_index * 3 + i] != v[i]:
+ found = False
+ break
+
+ if attributes[NORMAL_ATTRIBUTE][current_new_index * 3 + i] != n[i]:
+ found = False
+ break
+
+ if use_tangents:
+ for i in range(0, 4):
+ if attributes[TANGENT_ATTRIBUTE][current_new_index * 4 + i] != t[i]:
+ found = False
+ break
+
+ if not found:
+ continue
+
+ for tex_coord_index in range(0, tex_coord_max):
+ uv = uvs[tex_coord_index]
+
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+ for i in range(0, 2):
+ if attributes[tex_coord_id][current_new_index * 2 + i] != uv[i]:
+ found = False
+ break
+
+ if export_color:
+ for color_index in range(0, color_max):
+ color = colors[color_index]
+
+ color_id = COLOR_PREFIX + str(color_index)
+ for i in range(0, 3):
+ # Alpha is always 1.0 - see above.
+ current_color = attributes[color_id][current_new_index * 4 + i]
+ if color_srgb_to_scene_linear(current_color) != color[i]:
+ found = False
+ break
+
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ for bone_index in range(0, bone_max):
+ joint = joints[bone_index]
+ weight = weights[bone_index]
+
+ joint_id = JOINTS_PREFIX + str(bone_index)
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+ for i in range(0, 4):
+ if attributes[joint_id][current_new_index * 4 + i] != joint[i]:
+ found = False
+ break
+ if attributes[weight_id][current_new_index * 4 + i] != weight[i]:
+ found = False
+ break
+
+ if export_settings[gltf2_blender_export_keys.MORPH]:
+ for morph_index in range(0, morph_max):
+ target_position = target_positions[morph_index]
+ target_normal = target_normals[morph_index]
+ if use_tangents:
+ target_tangent = target_tangents[morph_index]
+
+ target_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+ target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+ target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+ for i in range(0, 3):
+ if attributes[target_position_id][current_new_index * 3 + i] != target_position[i]:
+ found = False
+ break
+ if attributes[target_normal_id][current_new_index * 3 + i] != target_normal[i]:
+ found = False
+ break
+ if use_tangents:
+ if attributes[target_tangent_id][current_new_index * 3 + i] != target_tangent[i]:
+ found = False
+ break
+
+ if found:
+ indices.append(current_new_index)
+
+ create = False
+ break
+
+ if not create:
+ continue
+
+ new_index = 0
+
+ if primitive.get('max_index') is not None:
+ new_index = primitive['max_index'] + 1
+
+ primitive['max_index'] = new_index
+
+ vertex_index_to_new_indices[vertex_index].append(new_index)
+
+ #
+ #
+
+ indices.append(new_index)
+
+ #
+
+ attributes[POSITION_ATTRIBUTE].extend(v)
+ attributes[NORMAL_ATTRIBUTE].extend(n)
+ if use_tangents:
+ attributes[TANGENT_ATTRIBUTE].extend(t)
+
+ if blender_mesh.uv_layers.active:
+ for tex_coord_index in range(0, tex_coord_max):
+ tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+ if attributes.get(tex_coord_id) is None:
+ attributes[tex_coord_id] = []
+
+ attributes[tex_coord_id].extend(uvs[tex_coord_index])
+
+ if export_color:
+ for color_index in range(0, color_max):
+ color_id = COLOR_PREFIX + str(color_index)
+
+ if attributes.get(color_id) is None:
+ attributes[color_id] = []
+
+ attributes[color_id].extend(colors[color_index])
+
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ for bone_index in range(0, bone_max):
+ joint_id = JOINTS_PREFIX + str(bone_index)
+
+ if attributes.get(joint_id) is None:
+ attributes[joint_id] = []
+
+ attributes[joint_id].extend(joints[bone_index])
+
+ weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+ if attributes.get(weight_id) is None:
+ attributes[weight_id] = []
+
+ attributes[weight_id].extend(weights[bone_index])
+
+ if export_settings[gltf2_blender_export_keys.MORPH]:
+ for morph_index in range(0, morph_max):
+ target_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+
+ if attributes.get(target_position_id) is None:
+ attributes[target_position_id] = []
+
+ attributes[target_position_id].extend(target_positions[morph_index])
+
+ target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+
+ if attributes.get(target_normal_id) is None:
+ attributes[target_normal_id] = []
+
+ attributes[target_normal_id].extend(target_normals[morph_index])
+
+ if use_tangents:
+ target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+ if attributes.get(target_tangent_id) is None:
+ attributes[target_tangent_id] = []
+
+ attributes[target_tangent_id].extend(target_tangents[morph_index])
+
+ #
+ # Add primitive plus split them if needed.
+ #
+
+ result_primitives = []
+
+ for material_name, primitive in material_name_to_primitives.items():
+ export_color = True
+
+ #
+
+ indices = primitive[INDICES_ID]
+
+ if len(indices) == 0:
+ continue
+
+ position = primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]
+ normal = primitive[ATTRIBUTES_ID][NORMAL_ATTRIBUTE]
+ if use_tangents:
+ tangent = primitive[ATTRIBUTES_ID][TANGENT_ATTRIBUTE]
+ tex_coords = []
+ for tex_coord_index in range(0, tex_coord_max):
+ tex_coords.append(primitive[ATTRIBUTES_ID][TEXCOORD_PREFIX + str(tex_coord_index)])
+ colors = []
+ if export_color:
+ for color_index in range(0, color_max):
+ tex_coords.append(primitive[ATTRIBUTES_ID][COLOR_PREFIX + str(color_index)])
+ joints = []
+ weights = []
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ for bone_index in range(0, bone_max):
+ joints.append(primitive[ATTRIBUTES_ID][JOINTS_PREFIX + str(bone_index)])
+ weights.append(primitive[ATTRIBUTES_ID][WEIGHTS_PREFIX + str(bone_index)])
+
+ target_positions = []
+ target_normals = []
+ target_tangents = []
+ if export_settings[gltf2_blender_export_keys.MORPH]:
+ for morph_index in range(0, morph_max):
+ target_positions.append(primitive[ATTRIBUTES_ID][MORPH_POSITION_PREFIX + str(morph_index)])
+ target_normals.append(primitive[ATTRIBUTES_ID][MORPH_NORMAL_PREFIX + str(morph_index)])
+ if use_tangents:
+ target_tangents.append(primitive[ATTRIBUTES_ID][MORPH_TANGENT_PREFIX + str(morph_index)])
+
+ #
+
+ count = len(indices)
+
+ if count == 0:
+ continue
+
+ max_index = max(indices)
+
+ #
+
+ range_indices = 65536
+ if export_settings[gltf2_blender_export_keys.INDICES] == 'UNSIGNED_BYTE':
+ range_indices = 256
+ elif export_settings[gltf2_blender_export_keys.INDICES] == 'UNSIGNED_INT':
+ range_indices = 4294967296
+
+ #
+
+ if max_index >= range_indices:
+ #
+ # Spliting result_primitives.
+ #
+
+ # At start, all indicees are pending.
+ pending_attributes = {
+ POSITION_ATTRIBUTE: [],
+ NORMAL_ATTRIBUTE: []
+ }
+
+ if use_tangents:
+ pending_attributes[TANGENT_ATTRIBUTE] = []
+
+ pending_primitive = {
+ MATERIAL_ID: material_name,
+ INDICES_ID: [],
+ ATTRIBUTES_ID: pending_attributes
+ }
+
+ pending_primitive[INDICES_ID].extend(indices)
+
+ pending_attributes[POSITION_ATTRIBUTE].extend(position)
+ pending_attributes[NORMAL_ATTRIBUTE].extend(normal)
+ if use_tangents:
+ pending_attributes[TANGENT_ATTRIBUTE].extend(tangent)
+ tex_coord_index = 0
+ for tex_coord in tex_coords:
+ pending_attributes[TEXCOORD_PREFIX + str(tex_coord_index)] = tex_coord
+ tex_coord_index += 1
+ if export_color:
+ color_index = 0
+ for color in colors:
+ pending_attributes[COLOR_PREFIX + str(color_index)] = color
+ color_index += 1
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ joint_index = 0
+ for joint in joints:
+ pending_attributes[JOINTS_PREFIX + str(joint_index)] = joint
+ joint_index += 1
+ weight_index = 0
+ for weight in weights:
+ pending_attributes[WEIGHTS_PREFIX + str(weight_index)] = weight
+ weight_index += 1
+ if export_settings[gltf2_blender_export_keys.MORPH]:
+ morph_index = 0
+ for target_position in target_positions:
+ pending_attributes[MORPH_POSITION_PREFIX + str(morph_index)] = target_position
+ morph_index += 1
+ morph_index = 0
+ for target_normal in target_normals:
+ pending_attributes[MORPH_NORMAL_PREFIX + str(morph_index)] = target_normal
+ morph_index += 1
+ if use_tangents:
+ morph_index = 0
+ for target_tangent in target_tangents:
+ pending_attributes[MORPH_TANGENT_PREFIX + str(morph_index)] = target_tangent
+ morph_index += 1
+
+ pending_indices = pending_primitive[INDICES_ID]
+
+ # Continue until all are processed.
+ while len(pending_indices) > 0:
+
+ process_indices = pending_primitive[INDICES_ID]
+
+ pending_indices = []
+
+ #
+ #
+
+ all_local_indices = []
+
+ for i in range(0, (max(process_indices) // range_indices) + 1):
+ all_local_indices.append([])
+
+ #
+ #
+
+ # For all faces ...
+ for face_index in range(0, len(process_indices), 3):
+
+ written = False
+
+ face_min_index = min(process_indices[face_index + 0], process_indices[face_index + 1],
+ process_indices[face_index + 2])
+ face_max_index = max(process_indices[face_index + 0], process_indices[face_index + 1],
+ process_indices[face_index + 2])
+
+ # ... check if it can be but in a range of maximum indices.
+ for i in range(0, (max(process_indices) // range_indices) + 1):
+ offset = i * range_indices
+
+ # Yes, so store the primitive with its indices.
+ if face_min_index >= offset and face_max_index < offset + range_indices:
+ all_local_indices[i].extend(
+ [process_indices[face_index + 0], process_indices[face_index + 1],
+ process_indices[face_index + 2]])
+
+ written = True
+ break
+
+ # If not written, the triangel face has indices from different ranges.
+ if not written:
+ pending_indices.extend([process_indices[face_index + 0], process_indices[face_index + 1],
+ process_indices[face_index + 2]])
+
+ # Only add result_primitives, which do have indices in it.
+ for local_indices in all_local_indices:
+ if len(local_indices) > 0:
+ current_primitive = extract_primitive_floor(pending_primitive, local_indices, use_tangents)
+
+ result_primitives.append(current_primitive)
+
+ print_console('DEBUG', 'Adding primitive with splitting. Indices: ' + str(
+ len(current_primitive[INDICES_ID])) + ' Vertices: ' + str(
+ len(current_primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]) // 3))
+
+ # Process primitive faces having indices in several ranges.
+ if len(pending_indices) > 0:
+ pending_primitive = extract_primitive_pack(pending_primitive, pending_indices, use_tangents)
+
+ print_console('DEBUG', 'Creating temporary primitive for splitting')
+
+ else:
+ #
+ # No splitting needed.
+ #
+ result_primitives.append(primitive)
+
+ print_console('DEBUG', 'Adding primitive without splitting. Indices: ' + str(
+ len(primitive[INDICES_ID])) + ' Vertices: ' + str(
+ len(primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]) // 3))
+
+ print_console('INFO', 'Primitives created: ' + str(len(result_primitives)))
+
+ return result_primitives
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_filter.py b/io_scene_gltf2/blender/exp/gltf2_blender_filter.py
new file mode 100755
index 00000000..ed3ee055
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_filter.py
@@ -0,0 +1,455 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import bpy
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_get
+from ...io.com.gltf2_io_debug import print_console
+from ..com.gltf2_blender_image import create_img_from_blender_image
+from ...io.com import gltf2_io_image
+
+#
+# Globals
+#
+
+PREVIEW = 'PREVIEW'
+GLOSSINESS = 'glTF Specular Glossiness'
+ROUGHNESS = 'glTF Metallic Roughness'
+
+
+#
+# Functions
+#
+
+def filter_merge_image(export_settings, blender_image):
+ metallic_channel = gltf2_blender_get.get_image_material_usage_to_socket(blender_image, "Metallic")
+ roughness_channel = gltf2_blender_get.get_image_material_usage_to_socket(blender_image, "Roughness")
+
+ if metallic_channel < 0 and roughness_channel < 0:
+ return False
+
+ output = export_settings[gltf2_blender_export_keys.METALLIC_ROUGHNESS_IMAGE]
+ if export_settings.get(export_keys.METALLIC_ROUGHNESS_IMAGE) is None:
+ width = blender_image.image.size[0]
+ height = blender_image.image.size[1]
+ output = gltf2_io_image.create_img(width, height, r=1.0, g=1.0, b=1.0, a=1.0)
+
+ source = create_img_from_blender_image(blender_image.image)
+
+ if metallic_channel >= 0:
+ gltf2_io_image.copy_img_channel(output, dst_channel=2, src_image=source, src_channel=metallic_channel)
+ output.name = blender_image.image.name + output.name
+ if roughness_channel >= 0:
+ gltf2_io_image.copy_img_channel(output, dst_channel=1, src_image=source, src_channel=roughness_channel)
+ if metallic_channel < 0:
+ output.name = output.name + blender_image.image.name
+ return True
+
+
+def filter_used_materials():
+ """Gather and return all unfiltered, valid Blender materials."""
+ materials = []
+
+ for blender_material in bpy.data.materials:
+ if blender_material.node_tree and blender_material.use_nodes:
+ for currentNode in blender_material.node_tree.nodes:
+ if isinstance(currentNode, bpy.types.ShaderNodeGroup):
+ if currentNode.node_tree.name.startswith(ROUGHNESS):
+ materials.append(blender_material)
+ elif currentNode.node_tree.name.startswith(GLOSSINESS):
+ materials.append(blender_material)
+ elif isinstance(currentNode, bpy.types.ShaderNodeBsdfPrincipled):
+ materials.append(blender_material)
+ else:
+ materials.append(blender_material)
+
+ return materials
+
+
+def filter_apply(export_settings):
+ """
+ Gathers and filters the objects and assets to export.
+
+ Also filters out invalid, deleted and not exportable elements.
+ """
+ filtered_objects = []
+ implicit_filtered_objects = []
+
+ for blender_object in bpy.data.objects:
+
+ if blender_object.users == 0:
+ continue
+
+ if export_settings[gltf2_blender_export_keys.SELECTED] and not blender_object.select:
+ continue
+
+ if not export_settings[gltf2_blender_export_keys.LAYERS] and not blender_object.layers[0]:
+ continue
+
+ filtered_objects.append(blender_object)
+
+ if export_settings[gltf2_blender_export_keys.SELECTED] or not export_settings[gltf2_blender_export_keys.LAYERS]:
+ current_parent = blender_object.parent
+ while current_parent:
+ if current_parent not in implicit_filtered_objects:
+ implicit_filtered_objects.append(current_parent)
+
+ current_parent = current_parent.parent
+
+ export_settings[gltf2_blender_export_keys.FILTERED_OBJECTS] = filtered_objects
+
+ # Meshes
+
+ filtered_meshes = {}
+ filtered_vertex_groups = {}
+ temporary_meshes = []
+
+ for blender_mesh in bpy.data.meshes:
+
+ if blender_mesh.users == 0:
+ continue
+
+ current_blender_mesh = blender_mesh
+
+ current_blender_object = None
+
+ skip = True
+
+ for blender_object in filtered_objects:
+
+ current_blender_object = blender_object
+
+ if current_blender_object.type != 'MESH':
+ continue
+
+ if current_blender_object.data == current_blender_mesh:
+
+ skip = False
+
+ use_auto_smooth = current_blender_mesh.use_auto_smooth
+
+ if use_auto_smooth:
+
+ if current_blender_mesh.shape_keys is None:
+ current_blender_object = current_blender_object.copy()
+ else:
+ use_auto_smooth = False
+
+ print_console('WARNING',
+ 'Auto smooth and shape keys cannot be exported in parallel. '
+ 'Falling back to non auto smooth.')
+
+ if export_settings[gltf2_blender_export_keys.APPLY] or use_auto_smooth:
+ # TODO: maybe add to new exporter
+ if not export_settings[gltf2_blender_export_keys.APPLY]:
+ current_blender_object.modifiers.clear()
+
+ if use_auto_smooth:
+ blender_modifier = current_blender_object.modifiers.new('Temporary_Auto_Smooth', 'EDGE_SPLIT')
+
+ blender_modifier.split_angle = current_blender_mesh.auto_smooth_angle
+ blender_modifier.use_edge_angle = not current_blender_mesh.has_custom_normals
+
+ current_blender_mesh = current_blender_object.to_mesh(bpy.context.scene, True, PREVIEW)
+ temporary_meshes.append(current_blender_mesh)
+
+ break
+
+ if skip:
+ continue
+
+ filtered_meshes[blender_mesh.name] = current_blender_mesh
+ filtered_vertex_groups[blender_mesh.name] = current_blender_object.vertex_groups
+
+ # Curves
+
+ for blender_curve in bpy.data.curves:
+
+ if blender_curve.users == 0:
+ continue
+
+ current_blender_curve = blender_curve
+
+ current_blender_mesh = None
+
+ current_blender_object = None
+
+ skip = True
+
+ for blender_object in filtered_objects:
+
+ current_blender_object = blender_object
+
+ if current_blender_object.type not in ('CURVE', 'FONT'):
+ continue
+
+ if current_blender_object.data == current_blender_curve:
+
+ skip = False
+
+ current_blender_object = current_blender_object.copy()
+
+ if not export_settings[gltf2_blender_export_keys.APPLY]:
+ current_blender_object.modifiers.clear()
+
+ current_blender_mesh = current_blender_object.to_mesh(bpy.context.scene, True, PREVIEW)
+ temporary_meshes.append(current_blender_mesh)
+
+ break
+
+ if skip:
+ continue
+
+ filtered_meshes[blender_curve.name] = current_blender_mesh
+ filtered_vertex_groups[blender_curve.name] = current_blender_object.vertex_groups
+
+ #
+
+ export_settings[gltf2_blender_export_keys.FILTERED_MESHES] = filtered_meshes
+ export_settings[gltf2_blender_export_keys.FILTERED_VERTEX_GROUPS] = filtered_vertex_groups
+ export_settings[gltf2_blender_export_keys.TEMPORARY_MESHES] = temporary_meshes
+
+ #
+
+ filtered_materials = []
+
+ for blender_material in filter_used_materials():
+
+ if blender_material.users == 0:
+ continue
+
+ for mesh_name, blender_mesh in filtered_meshes.items():
+ for compare_blender_material in blender_mesh.materials:
+ if compare_blender_material == blender_material and blender_material not in filtered_materials:
+ filtered_materials.append(blender_material)
+
+ #
+
+ for blender_object in filtered_objects:
+ if blender_object.material_slots:
+ for blender_material_slot in blender_object.material_slots:
+ if blender_material_slot.link == 'DATA':
+ continue
+
+ if blender_material_slot.material not in filtered_materials:
+ filtered_materials.append(blender_material_slot.material)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_MATERIALS] = filtered_materials
+
+ #
+
+ filtered_textures = []
+ filtered_merged_textures = []
+
+ temp_filtered_texture_names = []
+
+ for blender_material in filtered_materials:
+ if blender_material.node_tree and blender_material.use_nodes:
+
+ per_material_textures = []
+
+ for blender_node in blender_material.node_tree.nodes:
+
+ if is_valid_node(blender_node) and blender_node not in filtered_textures:
+ add_node = False
+ add_merged_node = False
+ for blender_socket in blender_node.outputs:
+ if blender_socket.is_linked:
+ for blender_link in blender_socket.links:
+ if isinstance(blender_link.to_node, bpy.types.ShaderNodeGroup):
+ is_roughness = blender_link.to_node.node_tree.name.startswith(ROUGHNESS)
+ is_glossiness = blender_link.to_node.node_tree.name.startswith(GLOSSINESS)
+ if is_roughness or is_glossiness:
+ add_node = True
+ break
+ elif isinstance(blender_link.to_node, bpy.types.ShaderNodeBsdfPrincipled):
+ add_node = True
+ break
+ elif isinstance(blender_link.to_node, bpy.types.ShaderNodeNormalMap):
+ add_node = True
+ break
+ elif isinstance(blender_link.to_node, bpy.types.ShaderNodeSeparateRGB):
+ add_merged_node = True
+ break
+
+ if add_node or add_merged_node:
+ break
+
+ if add_node:
+ filtered_textures.append(blender_node)
+ # TODO: Add displacement texture, as not stored in node tree.
+
+ if add_merged_node:
+ if len(per_material_textures) == 0:
+ filtered_merged_textures.append(per_material_textures)
+
+ per_material_textures.append(blender_node)
+
+ else:
+
+ for blender_texture_slot in blender_material.texture_slots:
+
+ if is_valid_texture_slot(blender_texture_slot) and \
+ blender_texture_slot not in filtered_textures and \
+ blender_texture_slot.name not in temp_filtered_texture_names:
+ accept = False
+
+ if blender_texture_slot.use_map_color_diffuse:
+ accept = True
+
+ if blender_texture_slot.use_map_ambient:
+ accept = True
+ if blender_texture_slot.use_map_emit:
+ accept = True
+ if blender_texture_slot.use_map_normal:
+ accept = True
+
+ if export_settings[gltf2_blender_export_keys.DISPLACEMENT]:
+ if blender_texture_slot.use_map_displacement:
+ accept = True
+
+ if accept:
+ filtered_textures.append(blender_texture_slot)
+ temp_filtered_texture_names.append(blender_texture_slot.name)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_TEXTURES] = filtered_textures
+
+ #
+
+ filtered_images = []
+ filtered_merged_images = []
+ filtered_images_use_alpha = {}
+
+ for blender_texture in filtered_textures:
+
+ if isinstance(blender_texture, bpy.types.ShaderNodeTexImage):
+ if is_valid_image(blender_texture.image) and blender_texture.image not in filtered_images:
+ filtered_images.append(blender_texture.image)
+ alpha_socket = blender_texture.outputs.get('Alpha')
+ if alpha_socket is not None and alpha_socket.is_linked:
+ filtered_images_use_alpha[blender_texture.image.name] = True
+
+ else:
+ if is_valid_image(blender_texture.texture.image) and blender_texture.texture.image not in filtered_images:
+ filtered_images.append(blender_texture.texture.image)
+ if blender_texture.use_map_alpha:
+ filtered_images_use_alpha[blender_texture.texture.image.name] = True
+
+ #
+
+ for per_material_textures in filtered_merged_textures:
+
+ export_settings[gltf2_blender_export_keys.METALLIC_ROUGHNESS_IMAGE] = None
+
+ for blender_texture in per_material_textures:
+
+ if isinstance(blender_texture, bpy.types.ShaderNodeTexImage):
+ if is_valid_image(blender_texture.image) and blender_texture.image not in filtered_images:
+ filter_merge_image(export_settings, blender_texture)
+
+ img = export_settings.get(export_keys.METALLIC_ROUGHNESS_IMAGE)
+ if img is not None:
+ filtered_merged_images.append(img)
+ export_settings[gltf2_blender_export_keys.FILTERED_TEXTURES].append(img)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_MERGED_IMAGES] = filtered_merged_images
+ export_settings[gltf2_blender_export_keys.FILTERED_IMAGES] = filtered_images
+ export_settings[gltf2_blender_export_keys.FILTERED_IMAGES_USE_ALPHA] = filtered_images_use_alpha
+
+ #
+
+ filtered_cameras = []
+
+ for blender_camera in bpy.data.cameras:
+
+ if blender_camera.users == 0:
+ continue
+
+ if export_settings[gltf2_blender_export_keys.SELECTED]:
+ if blender_camera not in filtered_objects:
+ continue
+
+ filtered_cameras.append(blender_camera)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_CAMERAS] = filtered_cameras
+
+ #
+ #
+
+ filtered_lights = []
+
+ for blender_light in bpy.data.lamps:
+
+ if blender_light.users == 0:
+ continue
+
+ if export_settings[gltf2_blender_export_keys.SELECTED]:
+ if blender_light not in filtered_objects:
+ continue
+
+ if blender_light.type == 'HEMI':
+ continue
+
+ filtered_lights.append(blender_light)
+
+ export_settings[gltf2_blender_export_keys.FILTERED_LIGHTS] = filtered_lights
+
+ #
+ #
+
+ for implicit_object in implicit_filtered_objects:
+ if implicit_object not in filtered_objects:
+ filtered_objects.append(implicit_object)
+
+ #
+ #
+ #
+
+ group_index = {}
+
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ for blender_object in filtered_objects:
+ if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0:
+ continue
+ for blender_bone in blender_object.pose.bones:
+ group_index[blender_bone.name] = len(group_index)
+
+ export_settings[gltf2_blender_export_keys.GROUP_INDEX] = group_index
+
+
+def is_valid_node(blender_node):
+ return isinstance(blender_node, bpy.types.ShaderNodeTexImage) and is_valid_image(blender_node.image)
+
+
+def is_valid_image(image):
+ return image is not None and \
+ image.users != 0 and \
+ image.size[0] > 0 and \
+ image.size[1] > 0
+
+
+def is_valid_texture_slot(blender_texture_slot):
+ return blender_texture_slot is not None and \
+ blender_texture_slot.texture and \
+ blender_texture_slot.texture.users != 0 and \
+ blender_texture_slot.texture.type == 'IMAGE' and \
+ blender_texture_slot.texture.image is not None and \
+ blender_texture_slot.texture.image.users != 0 and \
+ blender_texture_slot.texture.image.size[0] > 0 and \
+ blender_texture_slot.texture.image.size[1] > 0
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py
new file mode 100755
index 00000000..6f4f3b1e
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py
@@ -0,0 +1,63 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animations
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+def gather_gltf2(export_settings):
+ """
+ Gather glTF properties from the current state of blender.
+
+ :return: list of scene graphs to be added to the glTF export
+ """
+ scenes = []
+ animations = [] # unfortunately animations in gltf2 are just as 'root' as scenes.
+ for blender_scene in bpy.data.scenes:
+ scenes.append(__gather_scene(blender_scene, export_settings))
+ animations += __gather_animations(blender_scene, export_settings)
+
+ return scenes, animations
+
+
+@cached
+def __gather_scene(blender_scene, export_settings):
+ scene = gltf2_io.Scene(
+ extensions=None,
+ extras=None,
+ name=blender_scene.name,
+ nodes=[]
+ )
+
+ for blender_object in blender_scene.objects:
+ if blender_object.parent is None:
+ node = gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+ if node is not None:
+ scene.nodes.append(node)
+
+ # TODO: lights
+
+ return scene
+
+
+def __gather_animations(blender_scene, export_settings):
+ animations = []
+ for blender_object in blender_scene.objects:
+ animations += gltf2_blender_gather_animations.gather_animations(blender_object, export_settings)
+ return animations
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py
new file mode 100755
index 00000000..fbe18323
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py
@@ -0,0 +1,82 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import bpy
+import typing
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+
+
+@cached
+def gather_animation_channel_target(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.AnimationChannelTarget:
+ return gltf2_io.AnimationChannelTarget(
+ extensions=__gather_extensions(channels, blender_object, export_settings),
+ extras=__gather_extras(channels, blender_object, export_settings),
+ node=__gather_node(channels, blender_object, export_settings),
+ path=__gather_path(channels, blender_object, export_settings)
+ )
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_node(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.Node:
+ if blender_object.type == "ARMATURE":
+ # TODO: get joint from fcurve data_path and gather_joint
+ blender_bone = blender_object.path_resolve(channels[0].data_path.rsplit('.', 1)[0])
+ if isinstance(blender_bone, bpy.types.PoseBone):
+ return gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings)
+
+ return gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+
+
+def __gather_path(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> str:
+ target = channels[0].data_path.split('.')[-1]
+ path = {
+ "location": "translation",
+ "rotation_axis_angle": "rotation",
+ "rotation_euler": "rotation",
+ "rotation_quaternion": "rotation",
+ "scale": "scale",
+ "value": "weights"
+ }.get(target)
+
+ if target is None:
+ raise RuntimeError("Cannot export an animation with {} target".format(target))
+
+ return path
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
new file mode 100755
index 00000000..808c970d
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
@@ -0,0 +1,131 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+
+from ..com.gltf2_blender_data_path import get_target_object_path, get_target_property_name
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_samplers
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channel_target
+
+
+@cached
+def gather_animation_channels(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.List[gltf2_io.AnimationChannel]:
+ channels = []
+
+ for channel_group in __get_channel_groups(blender_action, blender_object):
+ channel = __gather_animation_channel(channel_group, blender_object, export_settings)
+ if channel is not None:
+ channels.append(channel)
+
+ return channels
+
+
+def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Union[gltf2_io.AnimationChannel, None]:
+ if not __filter_animation_channel(channels, blender_object, export_settings):
+ return None
+
+ return gltf2_io.AnimationChannel(
+ extensions=__gather_extensions(channels, blender_object, export_settings),
+ extras=__gather_extras(channels, blender_object, export_settings),
+ sampler=__gather_sampler(channels, blender_object, export_settings),
+ target=__gather_target(channels, blender_object, export_settings)
+ )
+
+
+def __filter_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> bool:
+ return True
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.AnimationSampler:
+ return gltf2_blender_gather_animation_samplers.gather_animation_sampler(
+ channels,
+ blender_object,
+ export_settings
+ )
+
+
+def __gather_target(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.AnimationChannelTarget:
+ return gltf2_blender_gather_animation_channel_target.gather_animation_channel_target(
+ channels, blender_object, export_settings)
+
+
+def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object):
+ targets = {}
+ for fcurve in blender_action.fcurves:
+ target_property = get_target_property_name(fcurve.data_path)
+ object_path = get_target_object_path(fcurve.data_path)
+
+ # find the object affected by this action
+ if not object_path:
+ target = blender_object
+ else:
+ try:
+ target = blender_object.path_resolve(object_path)
+ except ValueError:
+ # if the object is a mesh and the action target path can not be resolved, we know that this is a morph
+ # animation.
+ if blender_object.type == "MESH":
+ # if you need the specific shape key for some reason, this is it:
+ # shape_key = blender_object.data.shape_keys.path_resolve(object_path)
+ target = blender_object.data.shape_keys
+ else:
+ gltf2_io_debug.print_console("WARNING", "Can not export animations with target {}".format(object_path))
+ continue
+
+ # group channels by target object and affected property of the target
+ target_properties = targets.get(target, {})
+ channels = target_properties.get(target_property, [])
+ channels.append(fcurve)
+ target_properties[target_property] = channels
+ targets[target] = target_properties
+
+ groups = []
+ for p in targets.values():
+ groups += list(p.values())
+
+ return map(tuple, groups)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py
new file mode 100755
index 00000000..7562d8c2
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py
@@ -0,0 +1,198 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import mathutils
+import typing
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.com import gltf2_blender_math
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io_debug
+
+
+class Keyframe:
+ def __init__(self, channels: typing.Tuple[bpy.types.FCurve], time: float):
+ self.seconds = time / bpy.context.scene.render.fps
+ self.__target = channels[0].data_path.split('.')[-1]
+ self.__indices = [c.array_index for c in channels]
+
+ # Data holders for virtual properties
+ self.__value = None
+ self.__in_tangent = None
+ self.__out_tangent = None
+
+ def __get_target_len(self):
+ length = {
+ "location": 3,
+ "rotation_axis_angle": 4,
+ "rotation_euler": 3,
+ "rotation_quaternion": 4,
+ "scale": 3,
+ "value": 1
+ }.get(self.__target)
+
+ if length is None:
+ raise RuntimeError("Unknown target type {}".format(self.__target))
+
+ return length
+
+ def __set_indexed(self, value):
+ # 'value' targets don't use keyframe.array_index
+ if self.__target == "value":
+ return value
+ # Sometimes blender animations only reference a subset of components of a data target. Keyframe should always
+ # contain a complete Vector/ Quaternion --> use the array_index value of the keyframe to set components in such
+ # structures
+ result = [0.0] * self.__get_target_len()
+ for i, v in zip(self.__indices, value):
+ result[i] = v
+ result = gltf2_blender_math.list_to_mathutils(result, self.__target)
+ return result
+
+ @property
+ def value(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+ return self.__value
+
+ @value.setter
+ def value(self, value: typing.List[float]):
+ self.__value = self.__set_indexed(value)
+
+ @property
+ def in_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+ return self.__in_tangent
+
+ @in_tangent.setter
+ def in_tangent(self, value: typing.List[float]):
+ self.__in_tangent = self.__set_indexed(value)
+
+ @property
+ def out_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+ return self.__in_tangent
+
+ @out_tangent.setter
+ def out_tangent(self, value: typing.List[float]):
+ self.__out_tangent = self.__set_indexed(value)
+
+
+# cache for performance reasons
+@cached
+def gather_keyframes(channels: typing.Tuple[bpy.types.FCurve], export_settings) \
+ -> typing.List[Keyframe]:
+ """Convert the blender action groups' fcurves to keyframes for use in glTF."""
+ # Find the start and end of the whole action group
+ ranges = [channel.range() for channel in channels]
+
+ start = min([channel.range()[0] for channel in channels])
+ end = max([channel.range()[1] for channel in channels])
+
+ keyframes = []
+ if needs_baking(channels, export_settings):
+ # Bake the animation, by evaluating it at a high frequency
+ # TODO: maybe baking can also be done with FCurve.convert_to_samples
+ time = start
+ # TODO: make user controllable
+ step = 1.0 / bpy.context.scene.render.fps
+ while time <= end:
+ key = Keyframe(channels, time)
+ key.value = [c.evaluate(time) for c in channels]
+ keyframes.append(key)
+ time += step
+ else:
+ # Just use the keyframes as they are specified in blender
+ times = [keyframe.co[0] for keyframe in channels[0].keyframe_points]
+ for i, time in enumerate(times):
+ key = Keyframe(channels, time)
+ # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels]
+ key.value = [c.evaluate(time) for c in channels]
+
+ # compute tangents for cubic spline interpolation
+ if channels[0].keyframe_points[0].interpolation == "BEZIER":
+ # Construct the in tangent
+ if time == times[0]:
+ # start in-tangent has zero length
+ key.in_tangent = [0.0 for _ in channels]
+ else:
+ # otherwise construct an in tangent from the keyframes control points
+
+ key.in_tangent = [
+ 3.0 * (c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1]
+ ) / (time - times[i - 1])
+ for c in channels
+ ]
+ # Construct the out tangent
+ if time == times[-1]:
+ # end out-tangent has zero length
+ key.out_tangent = [0.0 for _ in channels]
+ else:
+ # otherwise construct an out tangent from the keyframes control points
+ key.out_tangent = [
+ 3.0 * (c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1]
+ ) / (times[i + 1] - time)
+ for c in channels
+ ]
+ keyframes.append(key)
+
+ return keyframes
+
+
+def needs_baking(channels: typing.Tuple[bpy.types.FCurve],
+ export_settings
+ ) -> bool:
+ """
+ Check if baking is needed.
+
+ Some blender animations need to be baked as they can not directly be expressed in glTF.
+ """
+ def all_equal(lst):
+ return lst[1:] == lst[:-1]
+
+
+ if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]:
+ return True
+
+ interpolation = channels[0].keyframe_points[0].interpolation
+ if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]:
+ gltf2_io_debug.print_console("WARNING",
+ "Baking animation because of an unsupported interpolation method: {}".format(
+ interpolation)
+ )
+ return True
+
+ if any(any(k.interpolation != interpolation for k in c.keyframe_points) for c in channels):
+ # There are different interpolation methods in one action group
+ gltf2_io_debug.print_console("WARNING",
+ "Baking animation because there are different "
+ "interpolation methods in one channel"
+ )
+ return True
+
+ if not all_equal([len(c.keyframe_points) for c in channels]):
+ gltf2_io_debug.print_console("WARNING",
+ "Baking animation because the number of keyframes is not "
+ "equal for all channel tracks")
+ return True
+
+ if len(channels[0].keyframe_points) <= 1:
+ # we need to bake to 'STEP', as at least two keyframes are required to interpolate
+ return True
+
+ if not all(all_equal(key_times) for key_times in zip([[k.co[0] for k in c.keyframe_points] for c in channels])):
+ # The channels have differently located keyframes
+ gltf2_io_debug.print_console("WARNING",
+ "Baking animation because of differently located keyframes in one channel")
+ return True
+
+ return False
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py
new file mode 100755
index 00000000..c94f1528
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py
@@ -0,0 +1,166 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import bpy
+import mathutils
+import typing
+import math
+
+from . import gltf2_blender_export_keys
+from mathutils import Matrix
+from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name, get_target_object_path
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.blender.com import gltf2_blender_math
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_sampler_keyframes
+
+
+@cached
+def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.AnimationSampler:
+ return gltf2_io.AnimationSampler(
+ extensions=__gather_extensions(channels, blender_object, export_settings),
+ extras=__gather_extras(channels, blender_object, export_settings),
+ input=__gather_input(channels, blender_object, export_settings),
+ interpolation=__gather_interpolation(channels, blender_object, export_settings),
+ output=__gather_output(channels, blender_object, export_settings)
+ )
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_input(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.Accessor:
+ """Gather the key time codes."""
+ keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(channels, export_settings)
+ times = [k.seconds for k in keyframes]
+
+ return gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(times, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(times),
+ extensions=None,
+ extras=None,
+ max=[max(times)],
+ min=[min(times)],
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Scalar
+ )
+
+
+def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> str:
+ if gltf2_blender_gather_animation_sampler_keyframes.needs_baking(channels, export_settings):
+ return 'STEP'
+
+ blender_keyframe = channels[0].keyframe_points[0]
+
+ # Select the interpolation method. Any unsupported method will fallback to STEP
+ return {
+ "BEZIER": "CUBICSPLINE",
+ "LINEAR": "LINEAR",
+ "CONSTANT": "STEP"
+ }[blender_keyframe.interpolation]
+
+
+def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> gltf2_io.Accessor:
+ """Gather the data of the keyframes."""
+ keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(channels, export_settings)
+
+ target_datapath = channels[0].data_path
+
+ transform = Matrix.Identity(4)
+
+ if blender_object.type == "ARMATURE":
+ bone = blender_object.path_resolve(get_target_object_path(target_datapath))
+ if isinstance(bone, bpy.types.PoseBone):
+ transform = bone.bone.matrix_local
+ if bone.parent is not None:
+ parent_transform = bone.parent.bone.matrix_local
+ transform = gltf2_blender_math.multiply(parent_transform.inverted(), transform)
+ # if not export_settings[gltf2_blender_export_keys.YUP]:
+ # transform = gltf2_blender_math.multiply(gltf2_blender_math.to_zup(), transform)
+ else:
+ # only apply the y-up conversion to root bones, as child bones already are in the y-up space
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ transform = gltf2_blender_math.multiply(gltf2_blender_math.to_yup(), transform)
+
+ values = []
+ for keyframe in keyframes:
+ # Transform the data and extract
+ value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform)
+ if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+ value = gltf2_blender_math.swizzle_yup(value, target_datapath)
+ keyframe_value = gltf2_blender_math.mathutils_to_gltf(value)
+ if keyframe.in_tangent is not None:
+ in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform)
+ if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+ in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath)
+ keyframe_value = gltf2_blender_math.mathutils_to_gltf(in_tangent) + keyframe_value
+ if keyframe.out_tangent is not None:
+ out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform)
+ if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+ out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath)
+ keyframe_value = keyframe_value + gltf2_blender_math.mathutils_to_gltf(out_tangent)
+ values += keyframe_value
+
+ component_type = gltf2_io_constants.ComponentType.Float
+ if get_target_property_name(target_datapath) == "value":
+ # channels with 'weight' targets must have scalar accessors
+ data_type = gltf2_io_constants.DataType.Scalar
+ else:
+ data_type = gltf2_io_constants.DataType.vec_type_from_num(len(keyframes[0].value))
+
+ return gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(values, component_type),
+ byte_offset=None,
+ component_type=component_type,
+ count=len(values) // gltf2_io_constants.DataType.num_elements(data_type),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=data_type
+ )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py
new file mode 100755
index 00000000..3740fefa
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py
@@ -0,0 +1,169 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channels
+
+
+def gather_animations(blender_object: bpy.types.Object, export_settings) -> typing.List[gltf2_io.Animation]:
+ """
+ Gather all animations which contribute to the objects property.
+
+ :param blender_object: The blender object which is animated
+ :param export_settings:
+ :return: A list of glTF2 animations
+ """
+ animations = []
+
+ # Collect all 'actions' affecting this object. There is a direct mapping between blender actions and glTF animations
+ blender_actions = __get_blender_actions(blender_object)
+
+ # Export all collected actions.
+ for blender_action in blender_actions:
+ animation = __gather_animation(blender_action, blender_object, export_settings)
+ if animation is not None:
+ animations.append(animation)
+
+ return animations
+
+
+def __gather_animation(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Optional[gltf2_io.Animation]:
+ if not __filter_animation(blender_action, blender_object, export_settings):
+ return None
+
+ animation = gltf2_io.Animation(
+ channels=__gather_channels(blender_action, blender_object, export_settings),
+ extensions=__gather_extensions(blender_action, blender_object, export_settings),
+ extras=__gather_extras(blender_action, blender_object, export_settings),
+ name=__gather_name(blender_action, blender_object, export_settings),
+ samplers=__gather_samplers(blender_action, blender_object, export_settings)
+ )
+
+ # To allow reuse of samplers in one animation,
+ __link_samplers(animation, export_settings)
+
+ if not animation.channels:
+ return None
+
+ return animation
+
+
+def __filter_animation(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> bool:
+ if blender_action.users == 0:
+ return False
+
+ return True
+
+
+def __gather_channels(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.List[gltf2_io.AnimationChannel]:
+ return gltf2_blender_gather_animation_channels.gather_animation_channels(
+ blender_action, blender_object, export_settings)
+
+
+def __gather_extensions(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_extras(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Any:
+ return None
+
+
+def __gather_name(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.Optional[str]:
+ return blender_action.name
+
+
+def __gather_samplers(blender_action: bpy.types.Action,
+ blender_object: bpy.types.Object,
+ export_settings
+ ) -> typing.List[gltf2_io.AnimationSampler]:
+ # We need to gather the samplers after gathering all channels --> populate this list in __link_samplers
+ return []
+
+
+def __link_samplers(animation: gltf2_io.Animation, export_settings):
+ """
+ Move animation samplers to their own list and store their indices at their previous locations.
+
+ After gathering, samplers are stored in the channels properties of the animation and need to be moved
+ to their own list while storing an index into this list at the position where they previously were.
+ This behaviour is similar to that of the glTFExporter that traverses all nodes
+ :param animation:
+ :param export_settings:
+ :return:
+ """
+ # TODO: move this to some util module and update gltf2 exporter also
+ T = typing.TypeVar('T')
+
+ def __append_unique_and_get_index(l: typing.List[T], item: T):
+ if item in l:
+ return l.index(item)
+ else:
+ index = len(l)
+ l.append(item)
+ return index
+
+ for i, channel in enumerate(animation.channels):
+ animation.channels[i].sampler = __append_unique_and_get_index(animation.samplers, channel.sampler)
+
+
+def __get_blender_actions(blender_object: bpy.types.Object
+ ) -> typing.List[bpy.types.Action]:
+ blender_actions = []
+
+ if blender_object.animation_data is not None:
+ # Collect active action.
+ if blender_object.animation_data.action is not None:
+ blender_actions.append(blender_object.animation_data.action)
+
+ # Collect associated strips from NLA tracks.
+ for track in blender_object.animation_data.nla_tracks:
+ # Multi-strip tracks do not export correctly yet (they need to be baked),
+ # so skip them for now and only write single-strip tracks.
+ if track.strips is None or len(track.strips) != 1:
+ continue
+ for strip in track.strips:
+ blender_actions.append(strip.action)
+
+ if blender_object.type == "MESH"\
+ and blender_object.data is not None \
+ and blender_object.data.shape_keys is not None \
+ and blender_object.data.shape_keys.animation_data is not None:
+ blender_actions.append(blender_object.data.shape_keys.animation_data.action)
+
+ # Remove duplicate actions.
+ blender_actions = list(set(blender_actions))
+
+ return blender_actions
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py
new file mode 100755
index 00000000..5b00a98b
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py
@@ -0,0 +1,60 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import functools
+
+
+def cached(func):
+ """
+ Decorate the cache gather functions results.
+
+ The gather function is only executed if its result isn't in the cache yet
+ :param func: the function to be decorated. It will have a static __cache member afterwards
+ :return:
+ """
+ @functools.wraps(func)
+ def wrapper_cached(*args, **kwargs):
+ assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function"
+ cache_key_args = args
+ # make a shallow copy of the keyword arguments so that 'export_settings' can be removed
+ cache_key_kwargs = dict(kwargs)
+ if kwargs.get("export_settings"):
+ export_settings = kwargs["export_settings"]
+ # 'export_settings' should not be cached
+ del cache_key_kwargs["export_settings"]
+ else:
+ export_settings = args[-1]
+ cache_key_args = args[:-1]
+
+ # we make a tuple from the function arguments so that they can be used as a key to the cache
+ cache_key = tuple(cache_key_args + tuple(cache_key_kwargs.values()))
+
+ # invalidate cache if export settings have changed
+ if not hasattr(func, "__export_settings") or export_settings != func.__export_settings:
+ func.__cache = {}
+ func.__export_settings = export_settings
+ # use or fill cache
+ if cache_key in func.__cache:
+ return func.__cache[cache_key]
+ else:
+ result = func(*args)
+ func.__cache[cache_key] = result
+ return result
+ return wrapper_cached
+
+
+# TODO: replace "cached" with "unique" in all cases where the caching is functional and not only for performance reasons
+call_or_fetch = cached
+unique = cached
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py
new file mode 100755
index 00000000..b09092ca
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py
@@ -0,0 +1,124 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+
+import bpy
+import math
+
+
+@cached
+def gather_camera(blender_object, export_settings):
+ if not __filter_camera(blender_object, export_settings):
+ return None
+
+ return gltf2_io.Camera(
+ extensions=__gather_extensions(blender_object, export_settings),
+ extras=__gather_extras(blender_object, export_settings),
+ name=__gather_name(blender_object, export_settings),
+ orthographic=__gather_orthographic(blender_object, export_settings),
+ perspective=__gather_perspective(blender_object, export_settings),
+ type=__gather_type(blender_object, export_settings)
+ )
+
+
+def __filter_camera(blender_object, export_settings):
+ if blender_object.type != 'CAMERA':
+ return False
+ if not __gather_type(blender_object, export_settings):
+ return False
+
+ return True
+
+
+def __gather_extensions(blender_object, export_settings):
+ return None
+
+
+def __gather_extras(blender_object, export_settings):
+ return None
+
+
+def __gather_name(blender_object, export_settings):
+ return blender_object.data.name
+
+
+def __gather_orthographic(blender_object, export_settings):
+ if __gather_type(blender_object, export_settings) == "orthographic":
+ orthographic = gltf2_io.CameraOrthographic(
+ extensions=None,
+ extras=None,
+ xmag=None,
+ ymag=None,
+ zfar=None,
+ znear=None
+ )
+ blender_camera = blender_object.data
+
+ orthographic.xmag = blender_camera.ortho_scale
+ orthographic.ymag = blender_camera.ortho_scale
+
+ orthographic.znear = blender_camera.clip_start
+ orthographic.zfar = blender_camera.clip_end
+
+ return orthographic
+ return None
+
+
+def __gather_perspective(blender_object, export_settings):
+ if __gather_type(blender_object, export_settings) == "perspective":
+ perspective = gltf2_io.CameraPerspective(
+ aspect_ratio=None,
+ extensions=None,
+ extras=None,
+ yfov=None,
+ zfar=None,
+ znear=None
+ )
+ blender_camera = blender_object.data
+
+ width = bpy.context.scene.render.pixel_aspect_x * bpy.context.scene.render.resolution_x
+ height = bpy.context.scene.render.pixel_aspect_y * bpy.context.scene.render.resolution_y
+ perspective.aspectRatio = width / height
+
+ if width >= height:
+ if blender_camera.sensor_fit != 'VERTICAL':
+ perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio)
+ else:
+ perspective.yfov = blender_camera.angle
+ else:
+ if blender_camera.sensor_fit != 'HORIZONTAL':
+ perspective.yfov = blender_camera.angle
+ else:
+ perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio)
+
+ perspective.znear = blender_camera.clip_start
+
+ if not export_settings[gltf2_blender_export_keys.CAMERA_INFINITE]:
+ perspective.zfar = blender_camera.clip_end
+
+ return perspective
+ return None
+
+
+def __gather_type(blender_object, export_settings):
+ camera = blender_object.data
+ if camera.type == 'PERSP':
+ return "perspective"
+ elif camera.type == 'ORTHO':
+ return "orthographic"
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py
new file mode 100755
index 00000000..4941ffe2
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py
@@ -0,0 +1,166 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+import os
+import numpy as np
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.exp import gltf2_io_image_data
+
+
+def gather_image(
+ blender_shader_sockets_or_texture_slots: typing.Union[typing.Tuple[bpy.types.NodeSocket],
+ typing.Tuple[bpy.types.Texture]],
+ export_settings):
+ if not __filter_image(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+ image = gltf2_io.Image(
+ buffer_view=__gather_buffer_view(blender_shader_sockets_or_texture_slots, export_settings),
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ mime_type=__gather_mime_type(blender_shader_sockets_or_texture_slots, export_settings),
+ name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings),
+ uri=__gather_uri(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+ return image
+
+
+def __filter_image(sockets_or_slots, export_settings):
+ if not sockets_or_slots:
+ return False
+ return True
+
+
+def __gather_buffer_view(sockets_or_slots, export_settings):
+ if export_settings[gltf2_blender_export_keys.FORMAT] != 'ASCII':
+
+ image = __get_image_data(sockets_or_slots)
+ return gltf2_io_binary_data.BinaryData(
+ data=image.to_image_data(__gather_mime_type(sockets_or_slots, export_settings)))
+ return None
+
+
+def __gather_extensions(sockets_or_slots, export_settings):
+ return None
+
+
+def __gather_extras(sockets_or_slots, export_settings):
+ return None
+
+
+def __gather_mime_type(sockets_or_slots, export_settings):
+ return 'image/png'
+ # return 'image/jpeg'
+
+
+def __gather_name(sockets_or_slots, export_settings):
+ if __is_socket(sockets_or_slots):
+ node = __get_tex_from_socket(sockets_or_slots[0])
+ if node is not None:
+ return node.shader_node.image.name
+ elif isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot):
+ return sockets_or_slots[0].name
+ return None
+
+
+def __gather_uri(sockets_or_slots, export_settings):
+ if export_settings[gltf2_blender_export_keys.FORMAT] == 'ASCII':
+ # as usual we just store the data in place instead of already resolving the references
+ return __get_image_data(sockets_or_slots)
+ return None
+
+
+def __is_socket(sockets_or_slots):
+ return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __is_slot(sockets_or_slots):
+ return isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot)
+
+
+def __get_image_data(sockets_or_slots):
+ # For shared ressources, such as images, we just store the portion of data that is needed in the glTF property
+ # in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
+ # ressources.
+ def split_pixels_by_channels(image: bpy.types.Image) -> typing.Iterable[typing.Iterable[float]]:
+ pixels = np.array(image.pixels)
+ pixels = pixels.reshape((pixels.shape[0] // image.channels, image.channels))
+ channels = np.split(pixels, pixels.shape[1], axis=1)
+ return channels
+
+ if __is_socket(sockets_or_slots):
+ results = [__get_tex_from_socket(socket) for socket in sockets_or_slots]
+ image = None
+ for result, socket in zip(results, sockets_or_slots):
+ # rudimentarily try follow the node tree to find the correct image data.
+ channel = None
+ for elem in result.path:
+ if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB):
+ channel = {
+ 'R': 0,
+ 'G': 1,
+ 'B': 2
+ }[elem.from_socket.name]
+
+ if channel is not None:
+ pixels = [split_pixels_by_channels(result.shader_node.image)[channel]]
+ else:
+ pixels = split_pixels_by_channels(result.shader_node.image)
+
+ file_name = os.path.splitext(result.shader_node.image.name)[0]
+
+ image_data = gltf2_io_image_data.ImageData(
+ file_name,
+ result.shader_node.image.size[0],
+ result.shader_node.image.size[1],
+ pixels)
+
+ if image is None:
+ image = image_data
+ else:
+ image.add_to_image(image_data)
+
+ return image
+ elif __is_slot(sockets_or_slots):
+ texture = __get_tex_from_slot(sockets_or_slots[0])
+ pixels = texture.image.pixels
+
+ image_data = gltf2_io_image_data.ImageData(
+ texture.name,
+ texture.image.size[0],
+ texture.image.size[1],
+ pixels)
+ return image_data
+ else:
+ # Texture slots
+ raise NotImplementedError()
+
+
+def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ blender_shader_socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
+
+def __get_tex_from_slot(blender_texture_slot):
+ return blender_texture_slot.texture
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py
new file mode 100755
index 00000000..38e47031
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py
@@ -0,0 +1,80 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mathutils
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.blender.com import gltf2_blender_math
+
+
+@cached
+def gather_joint(blender_bone, export_settings):
+ """
+ Generate a glTF2 node from a blender bone, as joints in glTF2 are simply nodes.
+
+ :param blender_bone: a blender PoseBone
+ :param export_settings: the settings for this export
+ :return: a glTF2 node (acting as a joint)
+ """
+ axis_basis_change = mathutils.Matrix.Identity(4)
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ axis_basis_change = mathutils.Matrix(
+ ((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)))
+
+ # extract bone transform
+ if blender_bone.parent is None:
+ correction_matrix_local = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local)
+ else:
+ correction_matrix_local = gltf2_blender_math.multiply(
+ blender_bone.parent.bone.matrix_local.inverted(), blender_bone.bone.matrix_local)
+ matrix_basis = blender_bone.matrix_basis
+ if export_settings[gltf2_blender_export_keys.BAKE_SKINS]:
+ gltf2_io_debug.print_console("WARNING", "glTF bake skins not supported")
+ # matrix_basis = blender_object.convert_space(blender_bone, blender_bone.matrix, from_space='POSE',
+ # to_space='LOCAL')
+ trans, rot, sca = gltf2_blender_extract.decompose_transition(
+ gltf2_blender_math.multiply(correction_matrix_local, matrix_basis), 'JOINT', export_settings)
+ translation, rotation, scale = (None, None, None)
+ if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
+ translation = [trans[0], trans[1], trans[2]]
+ if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0:
+ rotation = [rot[0], rot[1], rot[2], rot[3]]
+ if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
+ scale = [sca[0], sca[1], sca[2]]
+
+ # traverse into children
+ children = []
+ for bone in blender_bone.children:
+ children.append(gather_joint(bone, export_settings))
+
+ # finally add to the joints array containing all the joints in the hierarchy
+ return gltf2_io.Node(
+ camera=None,
+ children=children,
+ extensions=None,
+ extras=None,
+ matrix=None,
+ mesh=None,
+ name=blender_bone.name,
+ rotation=rotation,
+ scale=scale,
+ skin=None,
+ translation=translation,
+ weights=None
+ )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py
new file mode 100755
index 00000000..0d314681
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py
@@ -0,0 +1,113 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_material_normal_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[
+ typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+ export_settings):
+ if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+ texture_info = gltf2_io.MaterialNormalTextureInfoClass(
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ scale=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings),
+ index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+ tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+
+ return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ if not blender_shader_sockets_or_texture_slots:
+ return False
+ if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+ return False
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+ # sockets do not lead to a texture --> discard
+ return False
+ return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+ # We just put the actual shader into the 'index' member
+ return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+ if __is_socket(blender_shader_sockets_or_texture_slots):
+ blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+ if len(blender_shader_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+ if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+ if len(input_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = input_node.inputs['Vector'].links[0].from_node
+
+ if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+ return 0
+
+ if input_node.uv_map == '':
+ return 0
+
+ # Try to gather map index.
+ for blender_mesh in bpy.data.meshes:
+ texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+ if texCoordIndex >= 0:
+ return texCoordIndex
+
+ return 0
+ else:
+ raise NotImplementedError()
+
+
+def __is_socket(sockets_or_slots):
+ return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __get_tex_from_socket(socket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py
new file mode 100755
index 00000000..af219318
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py
@@ -0,0 +1,113 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_material_occlusion_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[
+ typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+ export_settings):
+ if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+ texture_info = gltf2_io.MaterialOcclusionTextureInfoClass(
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ strength=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings),
+ index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+ tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+
+ return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ if not blender_shader_sockets_or_texture_slots:
+ return False
+ if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+ return False
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+ # sockets do not lead to a texture --> discard
+ return False
+ return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+ # We just put the actual shader into the 'index' member
+ return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+ if __is_socket(blender_shader_sockets_or_texture_slots):
+ blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+ if len(blender_shader_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+ if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+ if len(input_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = input_node.inputs['Vector'].links[0].from_node
+
+ if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+ return 0
+
+ if input_node.uv_map == '':
+ return 0
+
+ # Try to gather map index.
+ for blender_mesh in bpy.data.meshes:
+ texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+ if texCoordIndex >= 0:
+ return texCoordIndex
+
+ return 0
+ else:
+ raise NotImplementedError()
+
+
+def __is_socket(sockets_or_slots):
+ return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __get_tex_from_socket(socket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py
new file mode 100755
index 00000000..d801eca7
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py
@@ -0,0 +1,138 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_normal_texture_info_class
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_occlusion_texture_info_class
+
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_pbr_metallic_roughness
+from io_scene_gltf2.blender.exp import gltf2_blender_get
+
+
+@cached
+def gather_material(blender_material, export_settings):
+ """
+ Gather the material used by the blender primitive.
+
+ :param blender_material: the blender material used in the glTF primitive
+ :param export_settings:
+ :return: a glTF material
+ """
+ if not __filter_material(blender_material, export_settings):
+ return None
+
+ material = gltf2_io.Material(
+ alpha_cutoff=__gather_alpha_cutoff(blender_material, export_settings),
+ alpha_mode=__gather_alpha_mode(blender_material, export_settings),
+ double_sided=__gather_double_sided(blender_material, export_settings),
+ emissive_factor=__gather_emmissive_factor(blender_material, export_settings),
+ emissive_texture=__gather_emissive_texture(blender_material, export_settings),
+ extensions=__gather_extensions(blender_material, export_settings),
+ extras=__gather_extras(blender_material, export_settings),
+ name=__gather_name(blender_material, export_settings),
+ normal_texture=__gather_normal_texture(blender_material, export_settings),
+ occlusion_texture=__gather_occlusion_texture(blender_material, export_settings),
+ pbr_metallic_roughness=__gather_pbr_metallic_roughness(blender_material, export_settings)
+ )
+
+ return material
+ # material = blender_primitive['material']
+ #
+ # if get_material_requires_texcoords(glTF, material) and not export_settings['gltf_texcoords']:
+ # material = -1
+ #
+ # if get_material_requires_normals(glTF, material) and not export_settings['gltf_normals']:
+ # material = -1
+ #
+ # # Meshes/primitives without material are allowed.
+ # if material >= 0:
+ # primitive.material = material
+ # else:
+ # print_console('WARNING', 'Material ' + internal_primitive[
+ # 'material'] + ' not found. Please assign glTF 2.0 material or enable Blinn-Phong material in export.')
+
+
+def __filter_material(blender_material, export_settings):
+ # if not blender_material.use_nodes:
+ # return False
+ # if not blender_material.node_tree:
+ # return False
+ return True
+
+
+def __gather_alpha_cutoff(blender_material, export_settings):
+ return None
+
+
+def __gather_alpha_mode(blender_material, export_settings):
+ return None
+
+
+def __gather_double_sided(blender_material, export_settings):
+ return None
+
+
+def __gather_emmissive_factor(blender_material, export_settings):
+ emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive")
+ if isinstance(emissive, bpy.types.NodeSocket):
+ return emissive.default_value
+ return None
+
+
+def __gather_emissive_texture(blender_material, export_settings):
+ emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive")
+ return gltf2_blender_gather_texture_info.gather_texture_info((emissive,), export_settings)
+
+
+def __gather_extensions(blender_material, export_settings):
+ extensions = {}
+
+
+ # TODO specular glossiness extension
+
+ return extensions if extensions else None
+
+
+def __gather_extras(blender_material, export_setttings):
+ return None
+
+
+def __gather_name(blender_material, export_settings):
+
+ return None
+
+
+def __gather_normal_texture(blender_material, export_settings):
+ normal = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Normal")
+ return gltf2_blender_gather_material_normal_texture_info_class.gather_material_normal_texture_info_class(
+ (normal,),
+ export_settings)
+
+
+def __gather_occlusion_texture(blender_material, export_settings):
+ emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Occlusion")
+ return gltf2_blender_gather_material_occlusion_texture_info_class.gather_material_occlusion_texture_info_class(
+ (emissive,),
+ export_settings)
+
+
+def __gather_pbr_metallic_roughness(blender_material, export_settings):
+ return gltf2_blender_gather_materials_pbr_metallic_roughness.gather_material_pbr_metallic_roughness(
+ blender_material,
+ export_settings)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py
new file mode 100755
index 00000000..7a567bc3
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py
@@ -0,0 +1,93 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info
+from io_scene_gltf2.blender.exp import gltf2_blender_get
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+@cached
+def gather_material_pbr_metallic_roughness(blender_material, export_settings):
+ if not __filter_pbr_material(blender_material, export_settings):
+ return None
+
+ material = gltf2_io.MaterialPBRMetallicRoughness(
+ base_color_factor=__gather_base_color_factor(blender_material, export_settings),
+ base_color_texture=__gather_base_color_texture(blender_material, export_settings),
+ extensions=__gather_extensions(blender_material, export_settings),
+ extras=__gather_extras(blender_material, export_settings),
+ metallic_factor=__gather_metallic_factor(blender_material, export_settings),
+ metallic_roughness_texture=__gather_metallic_roughness_texture(blender_material, export_settings),
+ roughness_factor=__gather_roughness_factor(blender_material, export_settings)
+ )
+
+ return material
+
+
+def __filter_pbr_material(blender_material, export_settings):
+ return True
+
+
+def __gather_base_color_factor(blender_material, export_settings):
+ base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
+ if base_color_socket is None:
+ base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor")
+ if isinstance(base_color_socket, bpy.types.NodeSocket):
+ return list(base_color_socket.default_value)
+ return None
+
+def __gather_base_color_texture(blender_material, export_settings):
+ base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
+ if base_color_socket is None:
+ base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor")
+ return gltf2_blender_gather_texture_info.gather_texture_info((base_color_socket,), export_settings)
+
+
+def __gather_extensions(blender_material, export_settings):
+ return None
+
+
+def __gather_extras(blender_material, export_settings):
+ return None
+
+
+def __gather_metallic_factor(blender_material, export_settings):
+ metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic")
+ if isinstance(metallic_socket, bpy.types.NodeSocket):
+ return metallic_socket.default_value
+ return None
+
+
+def __gather_metallic_roughness_texture(blender_material, export_settings):
+ metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic")
+ roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness")
+
+ if metallic_socket is None and roughness_socket is None:
+ metallic_roughness = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "MetallicRoughness")
+ texture_input = (metallic_roughness,)
+ else:
+ texture_input = (metallic_socket, roughness_socket)
+
+ return gltf2_blender_gather_texture_info.gather_texture_info(texture_input, export_settings)
+
+
+def __gather_roughness_factor(blender_material, export_settings):
+ roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness")
+ if isinstance(roughness_socket, bpy.types.NodeSocket):
+ return roughness_socket.default_value
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py
new file mode 100755
index 00000000..57903287
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py
@@ -0,0 +1,90 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from typing import Optional, Dict, List, Any
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives
+
+
+@cached
+def gather_mesh(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> Optional[gltf2_io.Mesh]:
+ if not __filter_mesh(blender_mesh, vertex_groups, modifiers, export_settings):
+ return None
+
+ mesh = gltf2_io.Mesh(
+ extensions=__gather_extensions(blender_mesh, vertex_groups, modifiers, export_settings),
+ extras=__gather_extras(blender_mesh, vertex_groups, modifiers, export_settings),
+ name=__gather_name(blender_mesh, vertex_groups, modifiers, export_settings),
+ primitives=__gather_primitives(blender_mesh, vertex_groups, modifiers, export_settings),
+ weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings)
+ )
+
+ return mesh
+
+
+def __filter_mesh(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> bool:
+ if blender_mesh.users == 0:
+ return False
+ return True
+
+
+def __gather_extensions(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> Any:
+ return None
+
+
+def __gather_extras(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> Optional[Dict[Any, Any]]:
+ return None
+
+
+def __gather_name(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> str:
+ return blender_mesh.name
+
+
+def __gather_primitives(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> List[gltf2_io.MeshPrimitive]:
+ return gltf2_blender_gather_primitives.gather_primitives(blender_mesh, vertex_groups, modifiers, export_settings)
+
+
+def __gather_weights(blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> Optional[List[float]]:
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
new file mode 100755
index 00000000..dc7192d8
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
@@ -0,0 +1,148 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_mesh
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.io.com import gltf2_io
+
+
+@cached
+def gather_node(blender_object, export_settings):
+ if not __filter_node(blender_object, export_settings):
+ return None
+
+ node = gltf2_io.Node(
+ camera=__gather_camera(blender_object, export_settings),
+ children=__gather_children(blender_object, export_settings),
+ extensions=__gather_extensions(blender_object, export_settings),
+ extras=__gather_extras(blender_object, export_settings),
+ matrix=__gather_matrix(blender_object, export_settings),
+ mesh=__gather_mesh(blender_object, export_settings),
+ name=__gather_name(blender_object, export_settings),
+ rotation=None,
+ scale=None,
+ skin=__gather_skin(blender_object, export_settings),
+ translation=None,
+ weights=__gather_weights(blender_object, export_settings)
+ )
+ node.translation, node.rotation, node.scale = __gather_trans_rot_scale(blender_object, export_settings)
+
+ return node
+
+
+def __filter_node(blender_object, export_settings):
+ if blender_object.users == 0:
+ return False
+ if export_settings[gltf2_blender_export_keys.SELECTED] and not blender_object.select:
+ return False
+ if not export_settings[gltf2_blender_export_keys.LAYERS] and not blender_object.layers[0]:
+ return False
+ if blender_object.dupli_group is not None and not blender_object.dupli_group.layers[0]:
+ return False
+
+ return True
+
+
+def __gather_camera(blender_object, export_settings):
+ return gltf2_blender_gather_cameras.gather_camera(blender_object, export_settings)
+
+
+def __gather_children(blender_object, export_settings):
+ children = []
+ # standard children
+ for child_object in blender_object.children:
+ node = gather_node(child_object, export_settings)
+ if node is not None:
+ children.append(node)
+ # blender dupli objects
+ if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+ for dupli_object in blender_object.dupli_group.objects:
+ node = gather_node(dupli_object, export_settings)
+ if node is not None:
+ children.append(node)
+
+ # blender bones
+ if blender_object.type == "ARMATURE":
+ for blender_bone in blender_object.pose.bones:
+ if not blender_bone.parent:
+ children.append(gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings))
+
+ return children
+
+
+def __gather_extensions(blender_object, export_settings):
+ return None
+
+
+def __gather_extras(blender_object, export_settings):
+ return None
+
+
+def __gather_matrix(blender_object, export_settings):
+ # return blender_object.matrix_local
+ return []
+
+
+def __gather_mesh(blender_object, export_settings):
+ if blender_object.type == "MESH":
+ # If not using vertex group, they are irrelevant for caching --> ensure that they do not trigger a cache miss
+ vertex_groups = blender_object.vertex_groups
+ modifiers = blender_object.modifiers
+ if len(vertex_groups) == 0:
+ vertex_groups = None
+ if len(modifiers) == 0:
+ modifiers = None
+
+ return gltf2_blender_gather_mesh.gather_mesh(blender_object.data, vertex_groups, modifiers, export_settings)
+ else:
+ return None
+
+
+def __gather_name(blender_object, export_settings):
+ if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+ return "Duplication_Offset_" + blender_object.name
+ return blender_object.name
+
+
+def __gather_trans_rot_scale(blender_object, export_settings):
+ trans, rot, sca = gltf2_blender_extract.decompose_transition(blender_object.matrix_local, 'NODE', export_settings)
+ if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+ trans = -gltf2_blender_extract.convert_swizzle_location(
+ blender_object.dupli_group.dupli_offset, export_settings)
+ translation, rotation, scale = (None, None, None)
+ if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
+ translation = [trans[0], trans[1], trans[2]]
+ if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0:
+ rotation = [rot[0], rot[1], rot[2], rot[3]]
+ if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
+ scale = [sca[0], sca[1], sca[2]]
+ return translation, rotation, scale
+
+
+def __gather_skin(blender_object, export_settings):
+ modifiers = {m.type: m for m in blender_object.modifiers}
+
+ if "ARMATURE" in modifiers:
+ # Skins and meshes must be in the same glTF node, which is different from how blender handles armatures
+ return gltf2_blender_gather_skins.gather_skin(modifiers["ARMATURE"].object, export_settings)
+
+
+def __gather_weights(blender_object, export_settings):
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py
new file mode 100755
index 00000000..bd1294cd
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py
@@ -0,0 +1,217 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.blender.exp import gltf2_blender_utils
+
+
+def gather_primitive_attributes(blender_primitive, export_settings):
+ """
+ Gathers the attributes, such as POSITION, NORMAL, TANGENT from a blender primitive.
+
+ :return: a dictionary of attributes
+ """
+ attributes = {}
+ attributes.update(__gather_position(blender_primitive, export_settings))
+ attributes.update(__gather_normal(blender_primitive, export_settings))
+ attributes.update(__gather_tangent(blender_primitive, export_settings))
+ attributes.update(__gather_texcoord(blender_primitive, export_settings))
+ attributes.update(__gather_colors(blender_primitive, export_settings))
+ attributes.update(__gather_skins(blender_primitive, export_settings))
+ return attributes
+
+
+def __gather_position(blender_primitive, export_settings):
+ position = blender_primitive["attributes"]["POSITION"]
+ componentType = gltf2_io_constants.ComponentType.Float
+ return {
+ "POSITION": gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(position, componentType),
+ byte_offset=None,
+ component_type=componentType,
+ count=len(position) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=gltf2_blender_utils.max_components(position, gltf2_io_constants.DataType.Vec3),
+ min=gltf2_blender_utils.min_components(position, gltf2_io_constants.DataType.Vec3),
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+ }
+
+
+def __gather_normal(blender_primitive, export_settings):
+ if export_settings[gltf2_blender_export_keys.NORMALS]:
+ normal = blender_primitive["attributes"]['NORMAL']
+ return {
+ "NORMAL": gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(normal, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(normal) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+ }
+ return {}
+
+
+def __gather_tangent(blender_primitive, export_settings):
+ if export_settings[gltf2_blender_export_keys.TANGENTS]:
+ if blender_primitive["attributes"].get('TANGENT') is not None:
+ tangent = blender_primitive["attributes"]['TANGENT']
+ return {
+ "TANGENT": gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ tangent, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(tangent) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec4
+ )
+ }
+
+ return {}
+
+
+def __gather_texcoord(blender_primitive, export_settings):
+ attributes = {}
+ if export_settings[gltf2_blender_export_keys.TEX_COORDS]:
+ tex_coord_index = 0
+ tex_coord_id = 'TEXCOORD_' + str(tex_coord_index)
+ while blender_primitive["attributes"].get(tex_coord_id) is not None:
+ tex_coord = blender_primitive["attributes"][tex_coord_id]
+ attributes[tex_coord_id] = gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ tex_coord, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(tex_coord) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec2),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec2
+ )
+ tex_coord_index += 1
+ tex_coord_id = 'TEXCOORD_' + str(tex_coord_index)
+ return attributes
+
+
+def __gather_colors(blender_primitive, export_settings):
+ attributes = {}
+ if export_settings[gltf2_blender_export_keys.COLORS]:
+ color_index = 0
+ color_id = 'COLOR_' + str(color_index)
+ while blender_primitive["attributes"].get(color_id) is not None:
+ internal_color = blender_primitive["attributes"][color_id]
+ attributes[color_id] = gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ internal_color, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_color) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec4
+ )
+ color_index += 1
+ color_id = 'COLOR_' + str(color_index)
+ return attributes
+
+
+def __gather_skins(blender_primitive, export_settings):
+ attributes = {}
+ if export_settings[gltf2_blender_export_keys.SKINS]:
+ bone_index = 0
+ joint_id = 'JOINTS_' + str(bone_index)
+ weight_id = 'WEIGHTS_' + str(bone_index)
+ while blender_primitive["attributes"].get(joint_id) and blender_primitive["attributes"].get(weight_id):
+ if bone_index >= 4:
+ gltf2_io_debug.print_console("WARNING", "There are more than 4 joint vertex influences."
+ "Consider to apply blenders Limit Total function.")
+ # TODO: add option to stop after 4
+ # break
+
+ # joints
+ internal_joint = blender_primitive["attributes"][joint_id]
+ joint = gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ internal_joint, gltf2_io_constants.ComponentType.UnsignedShort),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.UnsignedShort,
+ count=len(internal_joint) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec4
+ )
+ attributes[joint_id] = joint
+
+ # weights
+ internal_weight = blender_primitive["attributes"][weight_id]
+ weight = gltf2_io.Accessor(
+ buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+ internal_weight, gltf2_io_constants.ComponentType.Float),
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_weight) // gltf2_io_constants.DataType.num_elements(
+ gltf2_io_constants.DataType.Vec4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec4
+ )
+ attributes[weight_id] = weight
+
+ bone_index += 1
+ joint_id = 'JOINTS_' + str(bone_index)
+ weight_id = 'WEIGHTS_' + str(bone_index)
+ return attributes
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
new file mode 100755
index 00000000..5b3e607b
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
@@ -0,0 +1,200 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from typing import List, Optional
+
+from .gltf2_blender_export_keys import INDICES, FORCE_INDICES, NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes
+from io_scene_gltf2.blender.exp import gltf2_blender_utils
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.io.com.gltf2_io_debug import print_console
+
+
+@cached
+def gather_primitives(
+ blender_mesh: bpy.types.Mesh,
+ vertex_groups: Optional[bpy.types.VertexGroups],
+ modifiers: Optional[bpy.types.ObjectModifiers],
+ export_settings
+ ) -> List[gltf2_io.MeshPrimitive]:
+ """
+ Extract the mesh primitives from a blender object
+
+ :return: a list of glTF2 primitives
+ """
+ primitives = []
+ blender_primitives = gltf2_blender_extract.extract_primitives(
+ None, blender_mesh, vertex_groups, modifiers, export_settings)
+
+ for internal_primitive in blender_primitives:
+
+ primitive = gltf2_io.MeshPrimitive(
+ attributes=__gather_attributes(internal_primitive, blender_mesh, modifiers, export_settings),
+ extensions=None,
+ extras=None,
+ indices=__gather_indices(internal_primitive, blender_mesh, modifiers, export_settings),
+ material=__gather_materials(internal_primitive, blender_mesh, modifiers, export_settings),
+ mode=None,
+ targets=__gather_targets(internal_primitive, blender_mesh, modifiers, export_settings)
+ )
+ primitives.append(primitive)
+
+ return primitives
+
+
+def __gather_materials(blender_primitive, blender_mesh, modifiers, export_settings):
+ if not blender_primitive['material']:
+ # TODO: fix 'extract_promitives' so that the value of 'material' is None and not empty string
+ return None
+ material = bpy.data.materials[blender_primitive['material']]
+ return gltf2_blender_gather_materials.gather_material(material, export_settings)
+
+
+def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings):
+ indices = blender_primitive['indices']
+
+ max_index = max(indices)
+ if max_index < (1 << 8):
+ component_type = gltf2_io_constants.ComponentType.UnsignedByte
+ elif max_index < (1 << 16):
+ component_type = gltf2_io_constants.ComponentType.UnsignedShort
+ elif max_index < (1 << 32):
+ component_type = gltf2_io_constants.ComponentType.UnsignedInt
+ else:
+ print_console('ERROR', 'Invalid max_index: ' + str(max_index))
+ return None
+
+ if export_settings[FORCE_INDICES]:
+ component_type = gltf2_io_constants.ComponentType.from_legacy_define(export_settings[INDICES])
+
+ element_type = gltf2_io_constants.DataType.Scalar
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(indices, component_type)
+ return gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=component_type,
+ count=len(indices) // gltf2_io_constants.DataType.num_elements(element_type),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=element_type
+ )
+
+
+def __gather_attributes(blender_primitive, blender_mesh, modifiers, export_settings):
+ return gltf2_blender_gather_primitive_attributes.gather_primitive_attributes(blender_primitive, export_settings)
+
+
+def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings):
+ if export_settings[MORPH]:
+ targets = []
+ if blender_mesh.shape_keys is not None:
+ morph_index = 0
+ for blender_shape_key in blender_mesh.shape_keys.key_blocks:
+ if blender_shape_key != blender_shape_key.relative_key:
+
+ target_position_id = 'MORPH_POSITION_' + str(morph_index)
+ target_normal_id = 'MORPH_NORMAL_' + str(morph_index)
+ target_tangent_id = 'MORPH_TANGENT_' + str(morph_index)
+
+ if blender_primitive["attributes"].get(target_position_id):
+ target = {}
+ internal_target_position = blender_primitive["attributes"][target_position_id]
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(
+ internal_target_position,
+ gltf2_io_constants.ComponentType.Float
+ )
+ target["POSITION"] = gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_target_position) // gltf2_io_constants.DataType.num_elements(
+ gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=gltf2_blender_utils.max_components(
+ internal_target_position, gltf2_io_constants.DataType.Vec3),
+ min=gltf2_blender_utils.min_components(
+ internal_target_position, gltf2_io_constants.DataType.Vec3),
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+
+ if export_settings[NORMALS] \
+ and export_settings[MORPH_NORMAL] \
+ and blender_primitive["attributes"].get(target_normal_id):
+
+ internal_target_normal = blender_primitive["attributes"][target_normal_id]
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(
+ internal_target_normal,
+ gltf2_io_constants.ComponentType.Float,
+ )
+ target['NORMAL'] = gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_target_normal) // gltf2_io_constants.DataType.num_elements(
+ gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+
+ if export_settings[TANGENTS] \
+ and export_settings[MORPH_TANGENT] \
+ and blender_primitive["attributes"].get(target_tangent_id):
+ internal_target_tangent = blender_primitive["attributes"][target_tangent_id]
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(
+ internal_target_tangent,
+ gltf2_io_constants.ComponentType.Float,
+ )
+ target['TANGENT'] = gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(internal_target_tangent) // gltf2_io_constants.DataType.num_elements(
+ gltf2_io_constants.DataType.Vec3),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Vec3
+ )
+ targets.append(target)
+ morph_index += 1
+ return targets
+ return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py
new file mode 100755
index 00000000..840c98f4
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py
@@ -0,0 +1,98 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+@cached
+def gather_sampler(blender_shader_node: bpy.types.Node, export_settings):
+ if not __filter_sampler(blender_shader_node, export_settings):
+ return None
+
+ return gltf2_io.Sampler(
+ extensions=__gather_extensions(blender_shader_node, export_settings),
+ extras=__gather_extras(blender_shader_node, export_settings),
+ mag_filter=__gather_mag_filter(blender_shader_node, export_settings),
+ min_filter=__gather_min_filter(blender_shader_node, export_settings),
+ name=__gather_name(blender_shader_node, export_settings),
+ wrap_s=__gather_wrap_s(blender_shader_node, export_settings),
+ wrap_t=__gather_wrap_t(blender_shader_node, export_settings)
+ )
+
+
+def __filter_sampler(blender_shader_node, export_settings):
+ if not blender_shader_node.interpolation == 'Closest' and not blender_shader_node.extension == 'CLIP':
+ return False
+ return True
+
+
+def __gather_extensions(blender_shader_node, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_node, export_settings):
+ return None
+
+
+def __gather_mag_filter(blender_shader_node, export_settings):
+ if blender_shader_node.interpolation == 'Closest':
+ return 9728 # NEAREST
+ return 9729 # LINEAR
+
+
+def __gather_min_filter(blender_shader_node, export_settings):
+ if blender_shader_node.interpolation == 'Closest':
+ return 9984 # NEAREST_MIPMAP_NEAREST
+ return 9986 # NEAREST_MIPMAP_LINEAR
+
+
+def __gather_name(blender_shader_node, export_settings):
+ return None
+
+
+def __gather_wrap_s(blender_shader_node, export_settings):
+ if blender_shader_node.extension == 'CLIP':
+ return 33071
+ return None
+
+
+def __gather_wrap_t(blender_shader_node, export_settings):
+ if blender_shader_node.extension == 'CLIP':
+ return 33071
+ return None
+
+
+@cached
+def gather_sampler_from_texture_slot(blender_texture: bpy.types.TextureSlot, export_settings):
+ magFilter = 9729
+ wrap = 10497
+ if blender_texture.texture.extension == 'CLIP':
+ wrap = 33071
+
+ minFilter = 9986
+ if magFilter == 9728:
+ minFilter = 9984
+
+ return gltf2_io.Sampler(
+ extensions=None,
+ extras=None,
+ mag_filter=magFilter,
+ min_filter=minFilter,
+ name=None,
+ wrap_s=wrap,
+ wrap_t=wrap
+ )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py
new file mode 100755
index 00000000..84703414
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py
@@ -0,0 +1,150 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mathutils
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+from io_scene_gltf2.blender.com import gltf2_blender_math
+
+
+@cached
+def gather_skin(blender_object, export_settings):
+ """
+ Gather armatures, bones etc into a glTF2 skin object.
+
+ :param blender_object: the object which may contain a skin
+ :param export_settings:
+ :return: a glTF2 skin object
+ """
+ if not __filter_skin(blender_object, export_settings):
+ return None
+
+ return gltf2_io.Skin(
+ extensions=__gather_extensions(blender_object, export_settings),
+ extras=__gather_extras(blender_object, export_settings),
+ inverse_bind_matrices=__gather_inverse_bind_matrices(blender_object, export_settings),
+ joints=__gather_joints(blender_object, export_settings),
+ name=__gather_name(blender_object, export_settings),
+ skeleton=__gather_skeleton(blender_object, export_settings)
+ )
+
+
+def __filter_skin(blender_object, export_settings):
+ if not export_settings[gltf2_blender_export_keys.SKINS]:
+ return False
+ if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0:
+ return False
+
+ return True
+
+
+def __gather_extensions(blender_object, export_settings):
+ return None
+
+
+def __gather_extras(blender_object, export_settings):
+ return None
+
+
+def __gather_inverse_bind_matrices(blender_object, export_settings):
+ inverse_matrices = []
+
+ axis_basis_change = mathutils.Matrix.Identity(4)
+ if export_settings[gltf2_blender_export_keys.YUP]:
+ axis_basis_change = mathutils.Matrix(
+ ((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)))
+
+ # # artificial torso, as needed by glTF
+ # inverse_bind_matrix = blender_object.matrix_world.inverted() * axis_basis_change.inverted()
+ # for column in range(0, 4):
+ # for row in range(0, 4):
+ # inverse_matrices.append(inverse_bind_matrix[row][column])
+
+ #
+ for blender_bone in blender_object.pose.bones:
+ inverse_bind_matrix = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local)
+ bind_shape_matrix = gltf2_blender_math.multiply(gltf2_blender_math.multiply(
+ axis_basis_change, blender_object.matrix_world.inverted()), axis_basis_change.inverted())
+
+ inverse_bind_matrix = gltf2_blender_math.multiply(inverse_bind_matrix.inverted(), bind_shape_matrix)
+ for column in range(0, 4):
+ for row in range(0, 4):
+ inverse_matrices.append(inverse_bind_matrix[row][column])
+
+ binary_data = gltf2_io_binary_data.BinaryData.from_list(inverse_matrices, gltf2_io_constants.ComponentType.Float)
+ return gltf2_io.Accessor(
+ buffer_view=binary_data,
+ byte_offset=None,
+ component_type=gltf2_io_constants.ComponentType.Float,
+ count=len(inverse_matrices) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Mat4),
+ extensions=None,
+ extras=None,
+ max=None,
+ min=None,
+ name=None,
+ normalized=None,
+ sparse=None,
+ type=gltf2_io_constants.DataType.Mat4
+ )
+
+
+def __gather_joints(blender_object, export_settings):
+ # # the skeletal hierarchy groups below a 'root' joint
+ # # TODO: add transform?
+ # torso = gltf2_io.Node(
+ # camera=None,
+ # children=[],
+ # extensions={},
+ # extras=None,
+ # matrix=[],
+ # mesh=None,
+ # name="Skeleton_" + blender_object.name,
+ # rotation=None,
+ # scale=None,
+ # skin=None,
+ # translation=None,
+ # weights=None
+ # )
+
+ root_joints = []
+ # build the hierarchy of nodes out of the bones
+ for blender_bone in blender_object.pose.bones:
+ if not blender_bone.parent:
+ root_joints.append(gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings))
+
+ # joints is a flat list containing all nodes belonging to the skin
+ joints = []
+
+ def __collect_joints(node):
+ joints.append(node)
+ for child in node.children:
+ __collect_joints(child)
+ for joint in root_joints:
+ __collect_joints(joint)
+
+ return joints
+
+
+def __gather_name(blender_object, export_settings):
+ return blender_object.name
+
+
+def __gather_skeleton(blender_object, export_settings):
+ # In the future support the result of https://github.com/KhronosGroup/glTF/pull/1195
+ return None # gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py
new file mode 100755
index 00000000..93db33f9
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py
@@ -0,0 +1,100 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import typing
+import bpy
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_sampler
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_image
+from io_scene_gltf2.io.com import gltf2_io_debug
+
+
+@cached
+def gather_texture(
+ blender_shader_sockets_or_texture_slots: typing.Union[
+ typing.Tuple[bpy.types.NodeSocket], typing.Tuple[typing.Any]],
+ export_settings):
+ """
+ Gather texture sampling information and image channels from a blender shader textu re attached to a shader socket.
+
+ :param blender_shader_sockets: The sockets of the material which should contribute to the texture
+ :param export_settings: configuration of the export
+ :return: a glTF 2.0 texture with sampler and source embedded (will be converted to references by the exporter)
+ """
+ # TODO: extend to texture slots
+ if not __filter_texture(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+ return gltf2_io.Texture(
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings),
+ sampler=__gather_sampler(blender_shader_sockets_or_texture_slots, export_settings),
+ source=__gather_source(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+
+
+def __filter_texture(blender_shader_sockets_or_texture_slots, export_settings):
+ return True
+
+
+def __gather_extensions(blender_shader_sockets, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_sockets, export_settings):
+ return None
+
+
+def __gather_name(blender_shader_sockets, export_settings):
+ return None
+
+
+def __gather_sampler(blender_shader_sockets_or_texture_slots, export_settings):
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ shader_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets_or_texture_slots]
+ if len(shader_nodes) > 1:
+ gltf2_io_debug.print_console("WARNING",
+ "More than one shader node tex image used for a texture. "
+ "The resulting glTF sampler will behave like the first shader node tex image.")
+ return gltf2_blender_gather_sampler.gather_sampler(
+ shader_nodes[0],
+ export_settings)
+ elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot):
+ return gltf2_blender_gather_sampler.gather_sampler_from_texture_slot(
+ blender_shader_sockets_or_texture_slots[0],
+ export_settings
+ )
+ else:
+ # TODO: implement texture slot sampler
+ raise NotImplementedError()
+
+
+def __gather_source(blender_shader_sockets_or_texture_slots, export_settings):
+ return gltf2_blender_gather_image.gather_image(blender_shader_sockets_or_texture_slots, export_settings)
+
+# Helpers
+
+
+def __get_tex_from_socket(socket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py
new file mode 100755
index 00000000..149a2a84
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py
@@ -0,0 +1,107 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_texture_info(blender_shader_sockets_or_texture_slots: typing.Union[
+ typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+ export_settings):
+ if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+ texture_info = gltf2_io.TextureInfo(
+ extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+ extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+ index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+ tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+ )
+
+ return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+ if not blender_shader_sockets_or_texture_slots:
+ return False
+ if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+ return False
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+ # sockets do not lead to a texture --> discard
+ return False
+ return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+ return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+ # We just put the actual shader into the 'index' member
+ return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+ if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+ blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+ if len(blender_shader_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+ if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+ if len(input_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = input_node.inputs['Vector'].links[0].from_node
+
+ if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+ return 0
+
+ if input_node.uv_map == '':
+ return 0
+
+ # Try to gather map index.
+ for blender_mesh in bpy.data.meshes:
+ texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+ if texCoordIndex >= 0:
+ return texCoordIndex
+
+ return 0
+ elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot):
+ # TODO: implement for texture slots
+ return 0
+ else:
+ raise NotImplementedError()
+
+
+def __get_tex_from_socket(socket):
+ result = gltf2_blender_search_node_tree.from_socket(
+ socket,
+ gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+ if not result:
+ return None
+ return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py b/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py
new file mode 100755
index 00000000..c26c494a
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py
@@ -0,0 +1,64 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import bpy
+from io_scene_gltf2.blender.com import gltf2_blender_json
+
+
+def generate_extras(blender_element):
+ """Filter and create a custom property, which is stored in the glTF extra field."""
+ if not blender_element:
+ return None
+
+ extras = {}
+
+ # Custom properties, which are in most cases present and should not be exported.
+ black_list = ['cycles', 'cycles_visibility', 'cycles_curves', '_RNA_UI']
+
+ count = 0
+ for custom_property in blender_element.keys():
+ if custom_property in black_list:
+ continue
+
+ value = blender_element[custom_property]
+
+ add_value = False
+
+ if isinstance(value, bpy.types.ID):
+ add_value = True
+
+ if isinstance(value, str):
+ add_value = True
+
+ if isinstance(value, (int, float)):
+ add_value = True
+
+ if hasattr(value, "to_list"):
+ value = value.to_list()
+ add_value = True
+
+ if hasattr(value, "to_dict"):
+ value = value.to_dict()
+ add_value = gltf2_blender_json.is_json_convertible(value)
+
+ if add_value:
+ extras[custom_property] = value
+ count += 1
+
+ if count == 0:
+ return None
+
+ return extras
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_get.py b/io_scene_gltf2/blender/exp/gltf2_blender_get.py
new file mode 100755
index 00000000..27135224
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_get.py
@@ -0,0 +1,376 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import bpy
+
+from . import gltf2_blender_export_keys
+from ...io.exp import gltf2_io_get
+from io_scene_gltf2.io.com import gltf2_io_debug
+#
+# Globals
+#
+
+#
+# Functions
+#
+
+
+def get_animation_target(action_group: bpy.types.ActionGroup):
+ return action_group.channels[0].data_path.split('.')[-1]
+
+
+def get_socket_or_texture_slot(blender_material: bpy.types.Material, name: str):
+ """
+ For a given material input name, retrieve the corresponding node tree socket or blender render texture slot.
+
+ :param blender_material: a blender material for which to get the socket/slot
+ :param name: the name of the socket/slot
+ :return: either a blender NodeSocket, if the material is a node tree or a blender Texture otherwise
+ """
+ if blender_material.node_tree and blender_material.use_nodes:
+ if name == "Emissive":
+ # Emissive is a special case as the input node in the 'Emission' shader node is named 'Color' and only the
+ # output is named 'Emission'
+ links = [link for link in blender_material.node_tree.links if link.from_socket.name == 'Emission']
+ if not links:
+ return None
+ return links[0].to_socket
+ i = [input for input in blender_material.node_tree.inputs]
+ o = [output for output in blender_material.node_tree.outputs]
+ nodes = [node for node in blender_material.node_tree.nodes]
+ nodes = filter(lambda n: isinstance(n, bpy.types.ShaderNodeBsdfPrincipled), nodes)
+ inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], [])
+ if not inputs:
+ return None
+ return inputs[0]
+
+
+
+ return None
+
+
+def find_shader_image_from_shader_socket(shader_socket, max_hops=10):
+ """Find any ShaderNodeTexImage in the path from the socket."""
+ if shader_socket is None:
+ return None
+
+ if max_hops <= 0:
+ return None
+
+ for link in shader_socket.links:
+ if isinstance(link.from_node, bpy.types.ShaderNodeTexImage):
+ return link.from_node
+
+ for socket in link.from_node.inputs.values():
+ image = find_shader_image_from_shader_socket(shader_socket=socket, max_hops=max_hops - 1)
+ if image is not None:
+ return image
+
+ return None
+
+
+def get_shader_add_to_shader_node(shader_node):
+
+ if shader_node is None:
+ return None
+
+ if len(shader_node.outputs['BSDF'].links) == 0:
+ return None
+
+ to_node = shader_node.outputs['BSDF'].links[0].to_node
+
+ if not isinstance(to_node, bpy.types.ShaderNodeAddShader):
+ return None
+
+ return to_node
+
+#
+
+
+def get_shader_emission_from_shader_add(shader_add):
+
+ if shader_add is None:
+ return None
+
+ if not isinstance(shader_add, bpy.types.ShaderNodeAddShader):
+ return None
+
+ from_node = None
+
+ for input in shader_add.inputs:
+
+ if len(input.links) == 0:
+ continue
+
+ from_node = input.links[0].from_node
+
+ if isinstance(from_node, bpy.types.ShaderNodeEmission):
+ break
+
+ return from_node
+
+
+def get_shader_mapping_from_shader_image(shader_image):
+
+ if shader_image is None:
+ return None
+
+ if not isinstance(shader_image, bpy.types.ShaderNodeTexImage):
+ return None
+
+ if shader_image.inputs.get('Vector') is None:
+ return None
+
+ if len(shader_image.inputs['Vector'].links) == 0:
+ return None
+
+ from_node = shader_image.inputs['Vector'].links[0].from_node
+
+ #
+
+ if not isinstance(from_node, bpy.types.ShaderNodeMapping):
+ return None
+
+ return from_node
+
+
+def get_image_material_usage_to_socket(shader_image, socket_name):
+ if shader_image is None:
+ return -1
+
+ if not isinstance(shader_image, bpy.types.ShaderNodeTexImage):
+ return -2
+
+ if shader_image.outputs.get('Color') is None:
+ return -3
+
+ if len(shader_image.outputs.get('Color').links) == 0:
+ return -4
+
+ for img_link in shader_image.outputs.get('Color').links:
+ separate_rgb = img_link.to_node
+
+ if not isinstance(separate_rgb, bpy.types.ShaderNodeSeparateRGB):
+ continue
+
+ for i, channel in enumerate("RGB"):
+ if separate_rgb.outputs.get(channel) is None:
+ continue
+ for link in separate_rgb.outputs.get(channel).links:
+ if socket_name == link.to_socket.name:
+ return i
+
+ return -6
+
+
+def get_emission_node_from_lamp_output_node(lamp_node):
+ if lamp_node is None:
+ return None
+
+ if not isinstance(lamp_node, bpy.types.ShaderNodeOutputLamp):
+ return None
+
+ if lamp_node.inputs.get('Surface') is None:
+ return None
+
+ if len(lamp_node.inputs.get('Surface').links) == 0:
+ return None
+
+ from_node = lamp_node.inputs.get('Surface').links[0].from_node
+ if isinstance(from_node, bpy.types.ShaderNodeEmission):
+ return from_node
+
+ return None
+
+
+def get_ligth_falloff_node_from_emission_node(emission_node, type):
+ if emission_node is None:
+ return None
+
+ if not isinstance(emission_node, bpy.types.ShaderNodeEmission):
+ return None
+
+ if emission_node.inputs.get('Strength') is None:
+ return None
+
+ if len(emission_node.inputs.get('Strength').links) == 0:
+ return None
+
+ from_node = emission_node.inputs.get('Strength').links[0].from_node
+ if not isinstance(from_node, bpy.types.ShaderNodeLightFalloff):
+ return None
+
+ if from_node.outputs.get(type) is None:
+ return None
+
+ if len(from_node.outputs.get(type).links) == 0:
+ return None
+
+ if emission_node != from_node.outputs.get(type).links[0].to_node:
+ return None
+
+ return from_node
+
+
+def get_shader_image_from_shader_node(name, shader_node):
+
+ if shader_node is None:
+ return None
+
+ if not isinstance(shader_node, bpy.types.ShaderNodeGroup) and \
+ not isinstance(shader_node, bpy.types.ShaderNodeBsdfPrincipled) and \
+ not isinstance(shader_node, bpy.types.ShaderNodeEmission):
+ return None
+
+ if shader_node.inputs.get(name) is None:
+ return None
+
+ if len(shader_node.inputs[name].links) == 0:
+ return None
+
+ from_node = shader_node.inputs[name].links[0].from_node
+
+ #
+
+ if isinstance(from_node, bpy.types.ShaderNodeNormalMap):
+
+ name = 'Color'
+
+ if len(from_node.inputs[name].links) == 0:
+ return None
+
+ from_node = from_node.inputs[name].links[0].from_node
+
+ #
+
+ if not isinstance(from_node, bpy.types.ShaderNodeTexImage):
+ return None
+
+ return from_node
+
+
+def get_texture_index_from_shader_node(export_settings, glTF, name, shader_node):
+ """Return the texture index in the glTF array."""
+ from_node = get_shader_image_from_shader_node(name, shader_node)
+
+ if from_node is None:
+ return -1
+
+ #
+
+ if from_node.image is None or from_node.image.size[0] == 0 or from_node.image.size[1] == 0:
+ return -1
+
+ return gltf2_io_get.get_texture_index(glTF, from_node.image.name)
+
+
+def get_texture_index_from_export_settings(export_settings, name):
+ """Return the texture index in the glTF array."""
+
+
+def get_texcoord_index_from_shader_node(glTF, name, shader_node):
+ """Return the texture coordinate index, if assigned and used."""
+ from_node = get_shader_image_from_shader_node(name, shader_node)
+
+ if from_node is None:
+ return 0
+
+ #
+
+ if len(from_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = from_node.inputs['Vector'].links[0].from_node
+
+ #
+
+ if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+ if len(input_node.inputs['Vector'].links) == 0:
+ return 0
+
+ input_node = input_node.inputs['Vector'].links[0].from_node
+
+ #
+
+ if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+ return 0
+
+ if input_node.uv_map == '':
+ return 0
+
+ #
+
+ # Try to gather map index.
+ for blender_mesh in bpy.data.meshes:
+ texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+ if texCoordIndex >= 0:
+ return texCoordIndex
+
+ return 0
+
+
+def get_image_uri(export_settings, blender_image):
+ """Return the final URI depending on a file path."""
+ file_format = get_image_format(export_settings, blender_image)
+ extension = '.jpg' if file_format == 'JPEG' else '.png'
+
+ return gltf2_io_get.get_image_name(blender_image.name) + extension
+
+
+def get_image_format(export_settings, blender_image):
+ """
+ Return the final output format of the given image.
+
+ Only PNG and JPEG are supported as outputs - all other formats must be converted.
+ """
+ if blender_image.file_format in ['PNG', 'JPEG']:
+ return blender_image.file_format
+
+ use_alpha = export_settings[gltf2_blender_export_keys.FILTERED_IMAGES_USE_ALPHA].get(blender_image.name)
+
+ return 'PNG' if use_alpha else 'JPEG'
+
+
+def get_node(data_path):
+ """Return Blender node on a given Blender data path."""
+ if data_path is None:
+ return None
+
+ index = data_path.find("[\"")
+ if (index == -1):
+ return None
+
+ node_name = data_path[(index + 2):]
+
+ index = node_name.find("\"")
+ if (index == -1):
+ return None
+
+ return node_name[:(index)]
+
+
+def get_data_path(data_path):
+ """Return Blender data path."""
+ index = data_path.rfind('.')
+
+ if index == -1:
+ return data_path
+
+ return data_path[(index + 1):]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py
new file mode 100755
index 00000000..14b62c23
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py
@@ -0,0 +1,263 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.exp import gltf2_io_image_data
+from io_scene_gltf2.io.exp import gltf2_io_buffer
+
+
+class GlTF2Exporter:
+ """
+ The glTF exporter flattens a scene graph to a glTF serializable format.
+
+ Any child properties are replaced with references where necessary
+ """
+
+ def __init__(self, copyright=None):
+ self.__finalized = False
+
+ asset = gltf2_io.Asset(
+ copyright=copyright,
+ extensions=None,
+ extras=None,
+ generator='Khronos Blender glTF 2.0 I/O',
+ min_version=None,
+ version='2.0')
+
+ self.__gltf = gltf2_io.Gltf(
+ accessors=[],
+ animations=[],
+ asset=asset,
+ buffers=[],
+ buffer_views=[],
+ cameras=[],
+ extensions={},
+ extensions_required=[],
+ extensions_used=[],
+ extras=None,
+ images=[],
+ materials=[],
+ meshes=[],
+ nodes=[],
+ samplers=[],
+ scene=-1,
+ scenes=[],
+ skins=[],
+ textures=[]
+ )
+
+ self.__buffer = gltf2_io_buffer.Buffer()
+ self.__images = []
+
+ # mapping of all glTFChildOfRootProperty types to their corresponding root level arrays
+ self.__childOfRootPropertyTypeLookup = {
+ gltf2_io.Accessor: self.__gltf.accessors,
+ gltf2_io.Animation: self.__gltf.animations,
+ gltf2_io.Buffer: self.__gltf.buffers,
+ gltf2_io.BufferView: self.__gltf.buffer_views,
+ gltf2_io.Camera: self.__gltf.cameras,
+ gltf2_io.Image: self.__gltf.images,
+ gltf2_io.Material: self.__gltf.materials,
+ gltf2_io.Mesh: self.__gltf.meshes,
+ gltf2_io.Node: self.__gltf.nodes,
+ gltf2_io.Sampler: self.__gltf.samplers,
+ gltf2_io.Scene: self.__gltf.scenes,
+ gltf2_io.Skin: self.__gltf.skins,
+ gltf2_io.Texture: self.__gltf.textures
+ }
+
+ self.__propertyTypeLookup = [
+ gltf2_io.AccessorSparseIndices,
+ gltf2_io.AccessorSparse,
+ gltf2_io.AccessorSparseValues,
+ gltf2_io.AnimationChannel,
+ gltf2_io.AnimationChannelTarget,
+ gltf2_io.AnimationSampler,
+ gltf2_io.Asset,
+ gltf2_io.CameraOrthographic,
+ gltf2_io.CameraPerspective,
+ gltf2_io.MeshPrimitive,
+ gltf2_io.TextureInfo,
+ gltf2_io.MaterialPBRMetallicRoughness,
+ gltf2_io.MaterialNormalTextureInfoClass,
+ gltf2_io.MaterialOcclusionTextureInfoClass
+ ]
+
+ @property
+ def glTF(self):
+ if not self.__finalized:
+ raise RuntimeError("glTF requested, but buffers are not finalized yet")
+ return self.__gltf
+
+ def finalize_buffer(self, output_path=None, buffer_name=None, is_glb=False):
+ """
+ Finalize the glTF and write buffers.
+
+ :param buffer_path:
+ :return:
+ """
+ if self.__finalized:
+ raise RuntimeError("Tried to finalize buffers for finalized glTF file")
+
+ if is_glb:
+ uri = None
+ elif output_path and buffer_name:
+ with open(output_path + buffer_name, 'wb') as f:
+ f.write(self.__buffer.to_bytes())
+ uri = buffer_name
+ else:
+ uri = self.__buffer.to_embed_string()
+
+ buffer = gltf2_io.Buffer(
+ byte_length=self.__buffer.byte_length,
+ extensions=None,
+ extras=None,
+ name=None,
+ uri=uri
+ )
+ self.__gltf.buffers.append(buffer)
+
+ self.__finalized = True
+
+ if is_glb:
+ return self.__buffer.to_bytes()
+
+ def finalize_images(self, output_path):
+ """
+ Write all images.
+
+ Due to a current limitation the output_path must be the same as that of the glTF file
+ :param output_path:
+ :return:
+ """
+ for image in self.__images:
+ uri = output_path + image.name + ".png"
+ with open(uri, 'wb') as f:
+ f.write(image.to_png_data())
+
+ def add_scene(self, scene: gltf2_io.Scene, active: bool = True):
+ """
+ Add a scene to the glTF.
+
+ The scene should be built up with the generated glTF classes
+ :param scene: gltf2_io.Scene type. Root node of the scene graph
+ :param active: If true, sets the glTD.scene index to the added scene
+ :return: nothing
+ """
+ if self.__finalized:
+ raise RuntimeError("Tried to add scene to finalized glTF file")
+
+ # for node in scene.nodes:
+ # self.__traverse(node)
+ scene_num = self.__traverse(scene)
+ if active:
+ self.__gltf.scene = scene_num
+
+ def add_animation(self, animation: gltf2_io.Animation):
+ """
+ Add an animation to the glTF.
+
+ :param animation: glTF animation, with python style references (names)
+ :return: nothing
+ """
+ if self.__finalized:
+ raise RuntimeError("Tried to add animation to finalized glTF file")
+
+ self.__traverse(animation)
+
+ def __to_reference(self, property):
+ """
+ Append a child of root property to its respective list and return a reference into said list.
+
+ If the property is not child of root, the property itself is returned.
+ :param property: A property type object that should be converted to a reference
+ :return: a reference or the object itself if it is not child or root
+ """
+ gltf_list = self.__childOfRootPropertyTypeLookup.get(type(property), None)
+ if gltf_list is None:
+ # The object is not of a child of root --> don't convert to reference
+ return property
+
+ return self.__append_unique_and_get_index(gltf_list, property)
+
+ @staticmethod
+ def __append_unique_and_get_index(target: list, obj):
+ if obj in target:
+ return target.index(obj)
+ else:
+ index = len(target)
+ target.append(obj)
+ return index
+
+ def __add_image(self, image: gltf2_io_image_data.ImageData):
+ self.__images.append(image)
+ # TODO: we need to know the image url at this point already --> maybe add all options to the constructor of the
+ # exporter
+ # TODO: allow embedding of images (base64)
+ return image.name + ".png"
+
+ def __traverse(self, node):
+ """
+ Recursively traverse a scene graph consisting of gltf compatible elements.
+
+ The tree is traversed downwards until a primitive is reached. Then any ChildOfRoot property
+ is stored in the according list in the glTF and replaced with a index reference in the upper level.
+ """
+ def traverse_all_members(node):
+ for member_name in [a for a in dir(node) if not a.startswith('__') and not callable(getattr(node, a))]:
+ new_value = self.__traverse(getattr(node, member_name))
+ setattr(node, member_name, new_value) # usually this is the same as before
+
+ # TODO: maybe with extensions hooks we can find a more elegant solution
+ if member_name == "extensions" and new_value is not None:
+ for extension_name in new_value.keys():
+ self.__append_unique_and_get_index(self.__gltf.extensions_used, extension_name)
+ self.__append_unique_and_get_index(self.__gltf.extensions_required, extension_name)
+ return node
+
+ # traverse nodes of a child of root property type and add them to the glTF root
+ if type(node) in self.__childOfRootPropertyTypeLookup:
+ node = traverse_all_members(node)
+ idx = self.__to_reference(node)
+ # child of root properties are only present at root level --> replace with index in upper level
+ return idx
+
+ # traverse lists, such as children and replace them with indices
+ if isinstance(node, list):
+ for i in range(len(node)):
+ node[i] = self.__traverse(node[i])
+ return node
+
+ if isinstance(node, dict):
+ for key in node.keys():
+ node[key] = self.__traverse(node[key])
+ return node
+
+ # traverse into any other property
+ if type(node) in self.__propertyTypeLookup:
+ return traverse_all_members(node)
+
+ # binary data needs to be moved to a buffer and referenced with a buffer view
+ if isinstance(node, gltf2_io_binary_data.BinaryData):
+ buffer_view = self.__buffer.add_and_get_view(node)
+ return self.__to_reference(buffer_view)
+
+ # image data needs to be saved to file
+ if isinstance(node, gltf2_io_image_data.ImageData):
+ return self.__add_image(node)
+
+ # do nothing for any type that does not match a glTF schema (primitives)
+ return node
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py
new file mode 100755
index 00000000..a9dc86cf
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py
@@ -0,0 +1,97 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import bpy
+import typing
+
+
+class Filter:
+ """Base class for all node tree filter operations."""
+
+ def __init__(self):
+ pass
+
+ def __call__(self, shader_node):
+ return True
+
+
+class FilterByName(Filter):
+ """
+ Filter the material node tree by name.
+
+ example usage:
+ find_from_socket(start_socket, ShaderNodeFilterByName("Normal"))
+ """
+
+ def __init__(self, name):
+ self.name = name
+ super(FilterByName, self).__init__()
+
+ def __call__(self, shader_node):
+ return shader_node.name == self.name
+
+
+class FilterByType(Filter):
+ """Filter the material node tree by type."""
+
+ def __init__(self, type):
+ self.type = type
+ super(FilterByType, self).__init__()
+
+ def __call__(self, shader_node):
+ return isinstance(shader_node, self.type)
+
+
+class NodeTreeSearchResult:
+ def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink]):
+ self.shader_node = shader_node
+ self.path = path
+
+
+# TODO: cache these searches
+def from_socket(start_socket: bpy.types.NodeSocket,
+ shader_node_filter: typing.Union[Filter, typing.Callable]) -> typing.List[NodeTreeSearchResult]:
+ """
+ Find shader nodes where the filter expression is true.
+
+ :param start_socket: the beginning of the traversal
+ :param shader_node_filter: should be a function(x: shader_node) -> bool
+ :return: a list of shader nodes for which filter is true
+ """
+ # hide implementation (especially the search path
+ def __search_from_socket(start_socket: bpy.types.NodeSocket,
+ shader_node_filter: typing.Union[Filter, typing.Callable],
+ search_path: typing.List[bpy.types.NodeLink]) -> typing.List[NodeTreeSearchResult]:
+ results = []
+
+ for link in start_socket.links:
+ # follow the link to a shader node
+ linked_node = link.from_node
+ # add the link to the current path
+ search_path.append(link)
+ # check if the node matches the filter
+ if shader_node_filter(linked_node):
+ results.append(NodeTreeSearchResult(linked_node, search_path))
+ # traverse into inputs of the node
+ for input_socket in linked_node.inputs:
+ results += __search_from_socket(input_socket, shader_node_filter, search_path)
+
+ return results
+
+ return __search_from_socket(start_socket, shader_node_filter, [])
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py
new file mode 100755
index 00000000..0fa7db6e
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py
@@ -0,0 +1,89 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+
+
+class Filter:
+ """Base class for all node tree filter operations."""
+
+ def __call__(self, obj: bpy.types.Object):
+ return True
+
+
+class ByName(Filter):
+ """
+ Filter the objects by name.
+
+ example usage:
+ find_objects(FilterByName("Cube"))
+ """
+
+ def __init__(self, name):
+ self.name = name
+
+ def __call__(self, obj: bpy.types.Object):
+ return obj.name == self.name
+
+
+class ByDataType(Filter):
+ """Filter the scene objects by their data type."""
+
+ def __init__(self, data_type: str):
+ self.type = data_type
+
+ def __call__(self, obj: bpy.types.Object):
+ return obj.type == self.type
+
+
+class ByDataInstance(Filter):
+ """Filter the scene objects by a specific ID instance."""
+
+ def __init__(self, data_instance: bpy.types.ID):
+ self.data = data_instance
+
+ def __call__(self, obj: bpy.types.Object):
+ return self.data == obj.data
+
+
+def find_objects(object_filter: typing.Union[Filter, typing.Callable]):
+ """
+ Find objects in the scene where the filter expression is true.
+
+ :param object_filter: should be a function(x: object) -> bool
+ :return: a list of shader nodes for which filter is true
+ """
+ results = []
+ for obj in bpy.context.scene.objects:
+ if object_filter(obj):
+ results.append(obj)
+ return results
+
+
+def find_objects_from(obj: bpy.types.Object, object_filter: typing.Union[Filter, typing.Callable]):
+ """
+ Search for objects matching a filter function below a specified object.
+
+ :param obj: the starting point of the search
+ :param object_filter: a function(x: object) -> bool
+ :return: a list of objects which passed the filter
+ """
+ results = []
+ if object_filter(obj):
+ results.append(obj)
+ for child in obj.children:
+ results += find_objects_from(child, object_filter)
+ return results
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_utils.py b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py
new file mode 100755
index 00000000..c3e0d6ee
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py
@@ -0,0 +1,68 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import math
+from io_scene_gltf2.io.com import gltf2_io_constants
+
+
+# TODO: we could apply functional programming to these problems (currently we only have a single use case)
+
+def split_list_by_data_type(l: list, data_type: gltf2_io_constants.DataType):
+ """
+ Split a flat list of components by their data type.
+
+ E.g.: A list [0,1,2,3,4,5] of data type Vec3 would be split to [[0,1,2], [3,4,5]]
+ :param l: the flat list
+ :param data_type: the data type of the list
+ :return: a list of lists, where each element list contains the components of the data type
+ """
+ if not (len(l) % gltf2_io_constants.DataType.num_elements(data_type) == 0):
+ raise ValueError("List length does not match specified data type")
+ num_elements = gltf2_io_constants.DataType.num_elements(data_type)
+ return [l[i:i + num_elements] for i in range(0, len(l), num_elements)]
+
+
+def max_components(l: list, data_type: gltf2_io_constants.DataType) -> list:
+ """
+ Find the maximum components in a flat list.
+
+ This is required, for example, for the glTF2.0 accessor min and max properties
+ :param l: the flat list of components
+ :param data_type: the data type of the list (determines the length of the result)
+ :return: a list with length num_elements(data_type) containing the maximum per component along the list
+ """
+ components_lists = split_list_by_data_type(l, data_type)
+ result = [-math.inf] * gltf2_io_constants.DataType.num_elements(data_type)
+ for components in components_lists:
+ for i, c in enumerate(components):
+ result[i] = max(result[i], c)
+ return result
+
+
+def min_components(l: list, data_type: gltf2_io_constants.DataType) -> list:
+ """
+ Find the minimum components in a flat list.
+
+ This is required, for example, for the glTF2.0 accessor min and max properties
+ :param l: the flat list of components
+ :param data_type: the data type of the list (determines the length of the result)
+ :return: a list with length num_elements(data_type) containing the minimum per component along the list
+ """
+ components_lists = split_list_by_data_type(l, data_type)
+ result = [math.inf] * gltf2_io_constants.DataType.num_elements(data_type)
+ for components in components_lists:
+ for i, c in enumerate(components):
+ result[i] = min(result[i], c)
+ return result
+