def __gather_armature_object_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings): targets = {} if blender_object.type != "ARMATURE": return tuple() delta_rotation_detection = [False, False] # Normal / Delta for fcurve in blender_action.fcurves: object_path = get_target_object_path(fcurve.data_path) if object_path != "": continue # In some invalid files, channel hasn't any keyframes ... this channel need to be ignored if len(fcurve.keyframe_points) == 0: continue try: target_property = get_target_property_name(fcurve.data_path) except: gltf2_io_debug.print_console( "WARNING", "Invalid animation fcurve name on action {}".format( blender_action.name)) continue target = gltf2_blender_get.get_object_from_datapath( blender_object, object_path) # Detect that armature is not multiple keyed for euler and quaternion # Keep only the current rotation mode used by bone rotation, delta, rotation_modes = get_rotation_modes(target_property) # Delta rotation management if delta is False: if delta_rotation_detection[ 1] is True: # normal rotation coming, but delta is already present continue delta_rotation_detection[0] = True else: if delta_rotation_detection[ 0] is True: # delta rotation coming, but normal is already present continue delta_rotation_detection[1] = True if rotation and target.rotation_mode not in rotation_modes: 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)
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 name = __gather_name(blender_action, blender_object, export_settings) try: 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=name, samplers=__gather_samplers(blender_action, blender_object, export_settings)) except RuntimeError as error: print_console( "WARNING", "Animation '{}' could not be exported. Cause: {}".format( name, error)) return None # To allow reuse of samplers in one animation, __link_samplers(animation, export_settings) if not animation.channels: return None export_user_extensions('gather_animation_hook', export_settings, animation, blender_action, blender_object) return animation
def set_wrap_mode(tex_img, pysampler): """Set the extension on an Image Texture node from the pysampler.""" wrap_s = pysampler.wrap_s wrap_t = pysampler.wrap_t if wrap_s is None: wrap_s = TextureWrap.Repeat if wrap_t is None: wrap_t = TextureWrap.Repeat # The extension property on the Image Texture node can only handle the case # where both directions are the same and are either REPEAT or CLAMP_TO_EDGE. if (wrap_s, wrap_t) == (TextureWrap.Repeat, TextureWrap.Repeat): extension = TextureWrap.Repeat elif (wrap_s, wrap_t) == (TextureWrap.ClampToEdge, TextureWrap.ClampToEdge): extension = TextureWrap.ClampToEdge else: print_console( 'WARNING', 'texture wrap mode unsupported: (%s, %s)' % (wrap_name(wrap_s), wrap_name(wrap_t)), ) # Default to repeat extension = TextureWrap.Repeat if extension == TextureWrap.Repeat: tex_img.extension = 'REPEAT' elif extension == TextureWrap.ClampToEdge: tex_img.extension = 'EXTEND'
def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings): indices = blender_primitive.get('indices') if indices is None: return None # NOTE: Values used by some graphics APIs as "primitive restart" values are disallowed. # Specifically, the values 65535 (in UINT16) and 4294967295 (in UINT32) cannot be used as indices. # https://github.com/KhronosGroup/glTF/issues/1142 # https://github.com/KhronosGroup/glTF/pull/1476/files # Also, UINT8 mode is not supported: # https://github.com/KhronosGroup/glTF/issues/1471 max_index = indices.max() if max_index < 65535: component_type = gltf2_io_constants.ComponentType.UnsignedShort indices = indices.astype(np.uint16, copy=False) elif max_index < 4294967295: component_type = gltf2_io_constants.ComponentType.UnsignedInt indices = indices.astype(np.uint32, copy=False) else: print_console( 'ERROR', 'A mesh contains too many vertices (' + str(max_index) + ') and needs to be split before export.') return None element_type = gltf2_io_constants.DataType.Scalar binary_data = gltf2_io_binary_data.BinaryData(indices.tobytes()) return gltf2_blender_gather_accessors.gather_accessor( binary_data, component_type, len(indices), None, None, element_type, 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 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_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 name = __gather_name(blender_action, blender_object, export_settings) try: 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=name, samplers=__gather_samplers(blender_action, blender_object, export_settings) ) except RuntimeError as error: print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(name, error)) return None # To allow reuse of samplers in one animation, __link_samplers(animation, export_settings) if not animation.channels: return None return animation
def __get_image_data(sockets_or_slots, export_settings) -> gltf2_blender_image.ExportImage: # 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, export_settings) -> typing.Optional[typing.List[typing.List[float]]]: channelcache = export_settings['gltf_channelcache'] if image.name in channelcache: return channelcache[image.name] pixels = np.array(image.pixels) pixels = pixels.reshape((pixels.shape[0] // image.channels, image.channels)) channels = np.split(pixels, pixels.shape[1], axis=1) channelcache[image.name] = channels return channels if __is_socket(sockets_or_slots): results = [__get_tex_from_socket(socket, export_settings) for socket in sockets_or_slots] composed_image = None for result, socket in zip(results, sockets_or_slots): if result.shader_node.image.channels == 0: gltf2_io_debug.print_console("WARNING", "Image '{}' has no color channels and cannot be exported.".format( result.shader_node.image)) continue # rudimentarily try follow the node tree to find the correct image data. source_channel = 0 for elem in result.path: if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB): source_channel = { 'R': 0, 'G': 1, 'B': 2 }[elem.from_socket.name] image = gltf2_blender_image.ExportImage.from_blender_image(result.shader_node.image) if composed_image is None: composed_image = gltf2_blender_image.ExportImage.white_image(image.width, image.height) # Change target channel for metallic and roughness. if socket.name == 'Metallic': composed_image[2] = image[source_channel] elif socket.name == 'Roughness': composed_image[1] = image[source_channel] elif socket.name == 'Occlusion' and len(sockets_or_slots) > 2: composed_image[0] = image[source_channel] else: composed_image.update(image) return composed_image elif __is_slot(sockets_or_slots): texture = __get_tex_from_slot(sockets_or_slots[0]) image = gltf2_blender_image.ExportImage.from_blender_image(texture.image) return image else: raise NotImplementedError()
def __filter_lights_punctual(blender_lamp, export_settings) -> bool: if blender_lamp.type in ["HEMI", "AREA"]: gltf2_io_debug.print_console( "WARNING", "Unsupported light source {}".format(blender_lamp.type)) return False return True
def gather_mesh(blender_mesh: bpy.types.Mesh, library: Optional[str], blender_object: Optional[bpy.types.Object], vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], skip_filter: bool, material_names: Tuple[str], export_settings ) -> Optional[gltf2_io.Mesh]: if not skip_filter and not __filter_mesh(blender_mesh, library, vertex_groups, modifiers, export_settings): return None mesh = gltf2_io.Mesh( extensions=__gather_extensions(blender_mesh, library, vertex_groups, modifiers, export_settings), extras=__gather_extras(blender_mesh, library, vertex_groups, modifiers, export_settings), name=__gather_name(blender_mesh, library, vertex_groups, modifiers, export_settings), weights=__gather_weights(blender_mesh, library, vertex_groups, modifiers, export_settings), primitives=__gather_primitives(blender_mesh, library, blender_object, vertex_groups, modifiers, material_names, export_settings), ) if len(mesh.primitives) == 0: print_console("WARNING", "Mesh '{}' has no primitives and will be omitted.".format(mesh.name)) return None export_user_extensions('gather_mesh_hook', export_settings, mesh, blender_mesh, blender_object, vertex_groups, modifiers, skip_filter, material_names) return mesh
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 = gltf2_blender_get.get_object_from_datapath(blender_object, object_path) except ValueError as e: # 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", "Animation target {} not found".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)
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.") if not export_settings['gltf_all_vertex_influences']: 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
def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings): indices = blender_primitive['indices'] # NOTE: Values used by some graphics APIs as "primitive restart" values are disallowed. # Specifically, the values 65535 (in UINT16) and 4294967295 (in UINT32) cannot be used as indices. # https://github.com/KhronosGroup/glTF/issues/1142 # https://github.com/KhronosGroup/glTF/pull/1476/files # Also, UINT8 mode is not supported: # https://github.com/KhronosGroup/glTF/issues/1471 max_index = max(indices) if max_index < 65535: component_type = gltf2_io_constants.ComponentType.UnsignedShort elif max_index < 4294967295: component_type = gltf2_io_constants.ComponentType.UnsignedInt else: print_console('ERROR', 'A mesh contains too many vertices (' + str(max_index) + ') and needs to be split before export.') return None element_type = gltf2_io_constants.DataType.Scalar binary_data = gltf2_io_binary_data.BinaryData.from_list(indices, component_type) return gltf2_blender_gather_accessors.gather_accessor( binary_data, component_type, len(indices) // gltf2_io_constants.DataType.num_elements(element_type), None, None, element_type, export_settings )
def gather_mesh(blender_mesh: bpy.types.Mesh, vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], skip_filter: bool, export_settings) -> Optional[gltf2_io.Mesh]: if not skip_filter and 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)) if len(mesh.primitives) == 0: print_console( "WARNING", "Mesh '{}' has no primitives and will be omitted.".format( mesh.name)) return None return mesh
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)
def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], 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] # Sampling is forced if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: return True # Sampling due to unsupported interpolation 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 keyframes with 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 if blender_object_if_armature is not None: animation_target = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) if isinstance(animation_target, bpy.types.PoseBone): if len(animation_target.constraints) != 0: # Constraints such as IK act on the bone -> can not be represented in glTF atm gltf2_io_debug.print_console("WARNING", "Baking animation because of unsupported constraints acting on the bone") return True return False
def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], 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] # Sampling is forced if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: return True # Sampling due to unsupported interpolation 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 keyframes with 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_equal(list(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 if blender_object_if_armature is not None: animation_target = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) if isinstance(animation_target, bpy.types.PoseBone): if len(animation_target.constraints) != 0: # Constraints such as IK act on the bone -> can not be represented in glTF atm gltf2_io_debug.print_console("WARNING", "Baking animation because of unsupported constraints acting on the bone") return True return False
def gather_animations(blender_object: bpy.types.Object, tracks: typing.Dict[str, typing.List[int]], offset: int, export_settings) -> typing.Tuple[typing.List[gltf2_io.Animation], typing.Dict[str, typing.List[int]]]: """ Gather all animations which contribute to the objects property, and corresponding track names :param blender_object: The blender object which is animated :param export_settings: :return: A list of glTF2 animations and tracks """ 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_settings) # save the current active action of the object, if any # We will restore it after export current_action = None if blender_object.animation_data and blender_object.animation_data.action: current_action = blender_object.animation_data.action # Export all collected actions. for blender_action, track_name in blender_actions: # Set action as active, to be able to bake if needed if blender_object.animation_data: # Not for shapekeys! if blender_object.animation_data.action is None \ or (blender_object.animation_data.action.name != blender_action.name): if blender_object.animation_data.is_property_readonly('action'): # NLA stuff: some track are on readonly mode, we can't change action error = "Action is readonly. Please check NLA editor" print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(blender_action.name, error)) continue try: blender_object.animation_data.action = blender_action except: error = "Action is readonly. Please check NLA editor" print_console("WARNING", "Animation '{}' could not be exported. Cause: {}".format(blender_action.name, error)) continue animation = __gather_animation(blender_action, blender_object, export_settings) if animation is not None: animations.append(animation) # Store data for merging animation later if track_name is not None: # Do not take into account animation not in NLA # Do not take into account default NLA track names if not (track_name.startswith("NlaTrack") or track_name.startswith("[Action Stash]")): if track_name not in tracks.keys(): tracks[track_name] = [] tracks[track_name].append(offset + len(animations)-1) # Store index of animation in animations # Restore current action if blender_object.animation_data: if blender_object.animation_data.action is not None and current_action is not None and blender_object.animation_data.action.name != current_action.name: blender_object.animation_data.action = current_action return animations, tracks
def __get_image_data(sockets_or_slots, export_settings) -> ExportImage: # For shared resources, 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 # resources. if __is_socket(sockets_or_slots): results = [__get_tex_from_socket(socket, export_settings) for socket in sockets_or_slots] composed_image = ExportImage() for result, socket in zip(results, sockets_or_slots): if result.shader_node.image.channels == 0: gltf2_io_debug.print_console("WARNING", "Image '{}' has no color channels and cannot be exported.".format( result.shader_node.image)) continue # rudimentarily try follow the node tree to find the correct image data. src_chan = Channel.R for elem in result.path: if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB): src_chan = { 'R': Channel.R, 'G': Channel.G, 'B': Channel.B, }[elem.from_socket.name] if elem.from_socket.name == 'Alpha': src_chan = Channel.A dst_chan = None # some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes) if socket.name == 'Metallic': dst_chan = Channel.B elif socket.name == 'Roughness': dst_chan = Channel.G elif socket.name == 'Occlusion' and len(sockets_or_slots) > 1 and sockets_or_slots[1] is not None: dst_chan = Channel.R elif socket.name == 'Alpha' and len(sockets_or_slots) > 1 and sockets_or_slots[1] is not None: dst_chan = Channel.A if dst_chan is not None: composed_image.fill_image(result.shader_node.image, dst_chan, src_chan) # Since metal/roughness are always used together, make sure # the other channel is filled. if socket.name == 'Metallic' and not composed_image.is_filled(Channel.G): composed_image.fill_white(Channel.G) elif socket.name == 'Roughness' and not composed_image.is_filled(Channel.B): composed_image.fill_white(Channel.B) else: # copy full image...eventually following sockets might overwrite things composed_image = ExportImage.from_blender_image(result.shader_node.image) return composed_image elif __is_slot(sockets_or_slots): texture = __get_tex_from_slot(sockets_or_slots[0]) image = ExportImage.from_blender_image(texture.image) return image else: raise NotImplementedError()
def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings): targets = {} for fcurve in blender_action.fcurves: # In some invalid files, channel hasn't any keyframes ... this channel need to be ignored if len(fcurve.keyframe_points) == 0: continue try: target_property = get_target_property_name(fcurve.data_path) except: gltf2_io_debug.print_console( "WARNING", "Invalid animation fcurve name on action {}".format( blender_action.name)) continue 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 = gltf2_blender_get.get_object_from_datapath( blender_object, object_path) if blender_object.type == "MESH" and object_path.startswith( "key_blocks"): shape_key = blender_object.data.shape_keys.path_resolve( object_path) if shape_key.mute is True: continue target = blender_object.data.shape_keys except ValueError as e: # 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": shape_key = blender_object.data.shape_keys.path_resolve( object_path) if shape_key.mute is True: continue target = blender_object.data.shape_keys else: gltf2_io_debug.print_console( "WARNING", "Animation target {} not found".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)
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) # save the current active action of the object, if any # We will restore it after export current_action = None if blender_object.animation_data and blender_object.animation_data.action: current_action = blender_object.animation_data.action # Export all collected actions. for blender_action in blender_actions: # Set action as active, to be able to bake if needed if blender_object.animation_data: # Not for shapekeys! if blender_object.animation_data.action is None \ or (blender_object.animation_data.action.name != blender_action.name): if blender_object.animation_data.is_property_readonly( 'action'): # NLA stuff: some track are on readonly mode, we can't change action error = "Action is readonly. Please check NLA editor" print_console( "WARNING", "Animation '{}' could not be exported. Cause: {}". format(blender_action.name, error)) continue try: blender_object.animation_data.action = blender_action except: error = "Action is readonly. Please check NLA editor" print_console( "WARNING", "Animation '{}' could not be exported. Cause: {}". format(blender_action.name, error)) continue animation = __gather_animation(blender_action, blender_object, export_settings) if animation is not None: animations.append(animation) # Restore current action if blender_object.animation_data: if blender_object.animation_data.action is not None and current_action is not None and blender_object.animation_data.action.name != current_action.name: blender_object.animation_data.action = current_action return animations
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['gltf_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['gltf_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)
def __gather_sampler(blender_shader_sockets, export_settings): shader_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets] 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)
def __gather_trans_rot_scale(blender_object, export_settings): if blender_object.matrix_parent_inverse == Matrix.Identity(4): trans = blender_object.location if blender_object.rotation_mode in ['QUATERNION', 'AXIS_ANGLE']: rot = blender_object.rotation_quaternion else: rot = blender_object.rotation_euler.to_quaternion() sca = blender_object.scale else: # matrix_local = matrix_parent_inverse*location*rotation*scale # Decomposing matrix_local gives less accuracy, but is needed if matrix_parent_inverse is not the identity. if blender_object.matrix_local[3][3] != 0.0: trans, rot, sca = gltf2_blender_extract.decompose_transition( blender_object.matrix_local, export_settings) else: # Some really weird cases, scale is null (if parent is null when evaluation is done) print_console( 'WARNING', 'Some nodes are 0 scaled during evaluation. Result can be wrong' ) trans = blender_object.location if blender_object.rotation_mode in ['QUATERNION', 'AXIS_ANGLE']: rot = blender_object.rotation_quaternion else: rot = blender_object.rotation_euler.to_quaternion() sca = blender_object.scale # make sure the rotation is normalized rot.normalize() trans = gltf2_blender_extract.convert_swizzle_location( trans, None, None, export_settings) rot = gltf2_blender_extract.convert_swizzle_rotation(rot, export_settings) sca = gltf2_blender_extract.convert_swizzle_scale(sca, export_settings) if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection: trans -= gltf2_blender_extract.convert_swizzle_location( blender_object.instance_collection.instance_offset, None, None, export_settings) translation, rotation, scale = (None, None, None) trans[0], trans[1], trans[2] = gltf2_blender_math.round_if_near(trans[0], 0.0), gltf2_blender_math.round_if_near(trans[1], 0.0), \ gltf2_blender_math.round_if_near(trans[2], 0.0) rot[0], rot[1], rot[2], rot[3] = gltf2_blender_math.round_if_near(rot[0], 1.0), gltf2_blender_math.round_if_near(rot[1], 0.0), \ gltf2_blender_math.round_if_near(rot[2], 0.0), gltf2_blender_math.round_if_near(rot[3], 0.0) sca[0], sca[1], sca[2] = gltf2_blender_math.round_if_near(sca[0], 1.0), gltf2_blender_math.round_if_near(sca[1], 1.0), \ gltf2_blender_math.round_if_near(sca[2], 1.0) 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] != 1.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 0.0: rotation = [rot[1], rot[2], rot[3], rot[0]] 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 __get_image_data(sockets_or_slots, export_settings) -> gltf2_blender_image.ExportImage: # For shared resources, 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 # resources. if __is_socket(sockets_or_slots): results = [__get_tex_from_socket(socket, export_settings) for socket in sockets_or_slots] composed_image = None for result, socket in zip(results, sockets_or_slots): if result.shader_node.image.channels == 0: gltf2_io_debug.print_console("WARNING", "Image '{}' has no color channels and cannot be exported.".format( result.shader_node.image)) continue # rudimentarily try follow the node tree to find the correct image data. source_channel = 0 for elem in result.path: if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB): source_channel = { 'R': 0, 'G': 1, 'B': 2 }[elem.from_socket.name] image = gltf2_blender_image.ExportImage.from_blender_image(result.shader_node.image) target_channel = None # some sockets need channel rewriting (gltf pbr defines fixed channels for some attributes) if socket.name == 'Metallic': target_channel = 2 elif socket.name == 'Roughness': target_channel = 1 elif socket.name == 'Occlusion' and len(sockets_or_slots) > 1 and sockets_or_slots[1] is not None: target_channel = 0 elif socket.name == 'Alpha' and len(sockets_or_slots) > 1 and sockets_or_slots[1] is not None: composed_image.set_alpha(True) target_channel = 3 if target_channel is not None: if composed_image is None: composed_image = gltf2_blender_image.ExportImage.white_image(image.width, image.height) composed_image[target_channel] = image[source_channel] else: # copy full image...eventually following sockets might overwrite things composed_image = image return composed_image elif __is_slot(sockets_or_slots): texture = __get_tex_from_slot(sockets_or_slots[0]) image = gltf2_blender_image.ExportImage.from_blender_image(texture.image) return image else: raise NotImplementedError()
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] # Sampling is forced if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: return True # Sampling due to unsupported interpolation 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 keyframes with 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 # Baking is required when the animation targets a quaternion with bezier interpolation if channels[0].data_path == "rotation_quaternion" and interpolation == "BEZIER": gltf2_io_debug.print_console("WARNING", "Baking animation because targeting a quaternion with bezier interpolation") return True return False
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), 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 )
def __gather_skins(blender_primitive, export_settings): attributes = {} if export_settings[gltf2_blender_export_keys.SKINS]: bone_set_index = 0 joint_id = 'JOINTS_' + str(bone_set_index) weight_id = 'WEIGHTS_' + str(bone_set_index) while blender_primitive["attributes"].get( joint_id) and blender_primitive["attributes"].get(weight_id): if bone_set_index >= 1: if not export_settings['gltf_all_vertex_influences']: gltf2_io_debug.print_console( "WARNING", "There are more than 4 joint vertex influences." "The 4 with highest weight will be used (and normalized)." ) break # joints internal_joint = blender_primitive["attributes"][joint_id] component_type = gltf2_io_constants.ComponentType.UnsignedShort if max(internal_joint) < 256: component_type = gltf2_io_constants.ComponentType.UnsignedByte joint = array_to_accessor( internal_joint, component_type, data_type=gltf2_io_constants.DataType.Vec4, ) attributes[joint_id] = joint # weights internal_weight = blender_primitive["attributes"][weight_id] # normalize first 4 weights, when not exporting all influences if not export_settings['gltf_all_vertex_influences']: for idx in range(0, len(internal_weight), 4): weight_slice = internal_weight[idx:idx + 4] total = sum(weight_slice) if total > 0: factor = 1.0 / total internal_weight[idx:idx + 4] = [ w * factor for w in weight_slice ] weight = array_to_accessor( internal_weight, component_type=gltf2_io_constants.ComponentType.Float, data_type=gltf2_io_constants.DataType.Vec4, ) attributes[weight_id] = weight bone_set_index += 1 joint_id = 'JOINTS_' + str(bone_set_index) weight_id = 'WEIGHTS_' + str(bone_set_index) return attributes
def add_scene(self, scene): """ 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 :return: nothing """ if not isinstance(scene, gltf2_io.Scene): gltf2_io_debug.print_console( "ERROR", "Tried to add non scene type to glTF") return self.__traverse(scene)
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 base_color_socket is None: base_color_socket = gltf2_blender_get.get_socket_or_texture_slot_old( blender_material, "BaseColorFactor") if base_color_socket is None: base_color_socket = gltf2_blender_get.get_socket_or_texture_slot( blender_material, "Background") if not isinstance(base_color_socket, bpy.types.NodeSocket): return None if not base_color_socket.is_linked: return list(base_color_socket.default_value) for link in base_color_socket.links: if link.from_node.name == "albedo_tint": return list(link.from_node.outputs["Color"].default_value) texture_node = __get_tex_from_socket(base_color_socket) if texture_node is None: return None def is_valid_multiply_node(node): return isinstance(node, bpy.types.ShaderNodeMixRGB) and \ node.blend_type == "MULTIPLY" and \ len(node.inputs) == 3 multiply_node = next((link.from_node for link in texture_node.path if is_valid_multiply_node(link.from_node)), None) if multiply_node is None: return None def is_factor_socket(socket): return isinstance(socket, bpy.types.NodeSocketColor) and \ (not socket.is_linked or socket.links[0] not in texture_node.path) factor_socket = next( (socket for socket in multiply_node.inputs if is_factor_socket(socket)), None) if factor_socket is None: return None if factor_socket.is_linked: print_console( "WARNING", "BaseColorFactor only supports sockets without links (in Node '{}')." .format(multiply_node.name)) return None return list(factor_socket.default_value)
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: #i = [input for input in blender_material.node_tree.inputs] #o = [output for output in blender_material.node_tree.outputs] if name == "Emissive": type = bpy.types.ShaderNodeEmission name = "Color" elif name == "Background": type = bpy.types.ShaderNodeBackground name = "Color" else: type = bpy.types.ShaderNodeBsdfPrincipled nodes = [ n for n in blender_material.node_tree.nodes if isinstance(n, type) ] inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], []) if inputs: return inputs[0] else: if bpy.app.version < (2, 80, 0): if name != 'Base Color': return None gltf2_io_debug.print_console( "WARNING", "You are using texture slots, which are deprecated. In future versions" "of the glTF exporter they will not be supported any more") for blender_texture_slot in blender_material.texture_slots: if blender_texture_slot and blender_texture_slot.texture and \ blender_texture_slot.texture.type == 'IMAGE' and \ blender_texture_slot.texture.image is not None: # # Base color texture # if blender_texture_slot.use_map_color_diffuse: return blender_texture_slot else: pass return None
def __gather_orm_texture(blender_material, export_settings): # Check for the presence of Occlusion, Roughness, Metallic sharing a single image. # If not fully shared, return None, so the images will be cached and processed separately. occlusion = gltf2_blender_get.get_socket(blender_material, "Occlusion") if occlusion is None or not __has_image_node_from_socket(occlusion): occlusion = gltf2_blender_get.get_socket_old(blender_material, "Occlusion") if occlusion is None or not __has_image_node_from_socket(occlusion): return None metallic_socket = gltf2_blender_get.get_socket(blender_material, "Metallic") roughness_socket = gltf2_blender_get.get_socket(blender_material, "Roughness") hasMetal = metallic_socket is not None and __has_image_node_from_socket( metallic_socket) hasRough = roughness_socket is not None and __has_image_node_from_socket( roughness_socket) if not hasMetal and not hasRough: metallic_roughness = gltf2_blender_get.get_socket_old( blender_material, "MetallicRoughness") if metallic_roughness is None or not __has_image_node_from_socket( metallic_roughness): return None result = (occlusion, metallic_roughness) elif not hasMetal: result = (occlusion, roughness_socket) elif not hasRough: result = (occlusion, metallic_socket) else: result = (occlusion, roughness_socket, metallic_socket) if not gltf2_blender_gather_texture_info.check_same_size_images(result): print_console( "INFO", "Occlusion and metal-roughness texture will be exported separately " "(use same-sized images if you want them combined)") return None # Double-check this will past the filter in texture_info info = gltf2_blender_gather_texture_info.gather_texture_info( result[0], result, export_settings) if info is None: return None return result
def __write_file(json, buffer, export_settings): try: gltf2_io_export.save_gltf(json, 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
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 __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 resolution = __get_tex_from_socket( blender_shader_sockets_or_texture_slots[0]).shader_node.image.size if any( any(a != b for a, b in zip( __get_tex_from_socket(elem).shader_node.image.size, resolution)) for elem in blender_shader_sockets_or_texture_slots): def format_image(image_node): return "{} ({}x{})".format(image_node.image.name, image_node.image.size[0], image_node.image.size[1]) images = [ format_image(__get_tex_from_socket(elem).shader_node) for elem in blender_shader_sockets_or_texture_slots ] print_console( "ERROR", "Image sizes do not match. In order to be merged into one image file, " "images need to be of the same size. Images: {}".format( images)) return False return True
def gather_mesh(blender_mesh: bpy.types.Mesh, uuid_for_skined_data, vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], skip_filter: bool, materials: Tuple[bpy.types.Material], original_mesh: bpy.types.Mesh, export_settings) -> Optional[gltf2_io.Mesh]: if not skip_filter and 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), weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings), primitives=__gather_primitives(blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, materials, export_settings), ) if len(mesh.primitives) == 0: print_console( "WARNING", "Mesh '{}' has no primitives and will be omitted.".format( mesh.name)) return None blender_object = None if uuid_for_skined_data: blender_object = export_settings['vtree'].nodes[ uuid_for_skined_data].blender_object export_user_extensions('gather_mesh_hook', export_settings, mesh, blender_mesh, blender_object, vertex_groups, modifiers, skip_filter, materials) return mesh
def __gather_intensity(blender_lamp, _) -> Optional[float]: emission_node = __get_cycles_emission_node(blender_lamp) if emission_node is not None: if blender_lamp.type != 'SUN': # When using cycles, the strength should be influenced by a LightFalloff node result = gltf2_blender_search_node_tree.from_socket( emission_node.get("Strength"), gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeLightFalloff) ) if result: quadratic_falloff_node = result[0].shader_node emission_strength = quadratic_falloff_node.inputs["Strength"].default_value / (math.pi * 4.0) else: gltf2_io_debug.print_console('WARNING', 'No quadratic light falloff node attached to emission strength property') emission_strength = blender_lamp.energy else: emission_strength = emission_node.inputs["Strength"].default_value return emission_strength return blender_lamp.energy
def gather_mesh(blender_mesh: bpy.types.Mesh, vertex_groups: Optional[bpy.types.VertexGroups], modifiers: Optional[bpy.types.ObjectModifiers], skip_filter: bool, export_settings ) -> Optional[gltf2_io.Mesh]: if not skip_filter and 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) ) if len(mesh.primitives) == 0: print_console("WARNING", "Mesh '{}' has no primitives and will be omitted.".format(mesh.name)) return None return mesh
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 base_color_socket is None: base_color_socket = gltf2_blender_get.get_socket_or_texture_slot_old(blender_material, "BaseColorFactor") if base_color_socket is None: base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background") if not isinstance(base_color_socket, bpy.types.NodeSocket): return None if not base_color_socket.is_linked: return list(base_color_socket.default_value) texture_node = __get_tex_from_socket(base_color_socket) if texture_node is None: return None def is_valid_multiply_node(node): return isinstance(node, bpy.types.ShaderNodeMixRGB) and \ node.blend_type == "MULTIPLY" and \ len(node.inputs) == 3 multiply_node = next((link.from_node for link in texture_node.path if is_valid_multiply_node(link.from_node)), None) if multiply_node is None: return None def is_factor_socket(socket): return isinstance(socket, bpy.types.NodeSocketColor) and \ (not socket.is_linked or socket.links[0] not in texture_node.path) factor_socket = next((socket for socket in multiply_node.inputs if is_factor_socket(socket)), None) if factor_socket is None: return None if factor_socket.is_linked: print_console("WARNING", "BaseColorFactor only supports sockets without links (in Node '{}')." .format(multiply_node.name)) return None return list(factor_socket.default_value)
def get_texture_transform_from_texture_node(texture_node): if not isinstance(texture_node, bpy.types.ShaderNodeTexImage): return None mapping_socket = texture_node.inputs["Vector"] if len(mapping_socket.links) == 0: return None mapping_node = mapping_socket.links[0].from_node if not isinstance(mapping_node, bpy.types.ShaderNodeMapping): return None if mapping_node.vector_type not in ["TEXTURE", "POINT", "VECTOR"]: gltf2_io_debug.print_console("WARNING", "Skipping exporting texture transform because it had type " + mapping_node.vector_type + "; recommend using POINT instead" ) return None if mapping_node.rotation[0] or mapping_node.rotation[1]: # TODO: can we handle this? gltf2_io_debug.print_console("WARNING", "Skipping exporting texture transform because it had non-zero " "rotations in the X/Y direction; only a Z rotation can be exported!" ) return None mapping_transform = {} mapping_transform["offset"] = [mapping_node.translation[0], mapping_node.translation[1]] mapping_transform["rotation"] = mapping_node.rotation[2] mapping_transform["scale"] = [mapping_node.scale[0], mapping_node.scale[1]] if mapping_node.vector_type == "TEXTURE": # This means use the inverse of the TRS transform. def inverted(mapping_transform): offset = mapping_transform["offset"] rotation = mapping_transform["rotation"] scale = mapping_transform["scale"] # Inverse of a TRS is not always a TRS. This function will be right # at least when the following don't occur. if abs(rotation) > 1e-5 and abs(scale[0] - scale[1]) > 1e-5: return None if abs(scale[0]) < 1e-5 or abs(scale[1]) < 1e-5: return None new_offset = Matrix.Rotation(-rotation, 3, 'Z') * Vector((-offset[0], -offset[1], 1)) new_offset[0] /= scale[0]; new_offset[1] /= scale[1] return { "offset": new_offset[0:2], "rotation": -rotation, "scale": [1/scale[0], 1/scale[1]], } mapping_transform = inverted(mapping_transform) if mapping_transform is None: gltf2_io_debug.print_console("WARNING", "Skipping exporting texture transform with type TEXTURE because " "we couldn't convert it to TRS; recommend using POINT instead" ) return None elif mapping_node.vector_type == "VECTOR": # Vectors don't get translated mapping_transform["offset"] = [0, 0] texture_transform = texture_transform_blender_to_gltf(mapping_transform) if all([component == 0 for component in texture_transform["offset"]]): del(texture_transform["offset"]) if all([component == 1 for component in texture_transform["scale"]]): del(texture_transform["scale"]) if texture_transform["rotation"] == 0: del(texture_transform["rotation"]) if len(texture_transform) == 0: return None return texture_transform
def __filter_lights_punctual(blender_lamp, export_settings) -> bool: if blender_lamp.type in ["HEMI", "AREA"]: gltf2_io_debug.print_console("WARNING", "Unsupported light source {}".format(blender_lamp.type)) return False return True