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)
Exemple #2
0
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'
Exemple #4
0
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)
Exemple #5
0
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()
Exemple #8
0
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
Exemple #9
0
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)
Exemple #11
0
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
Exemple #14
0
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
Exemple #18
0
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()
Exemple #19
0
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
Exemple #21
0
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)
Exemple #23
0
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)
Exemple #29
0
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)
Exemple #30
0
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()
Exemple #34
0
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_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_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