class Buffer:
    ARRAY_BUFFER = 34962
    ELEMENT_ARRAY_BUFFER = 34963

    BYTE = 5120
    UNSIGNED_BYTE = 5121
    SHORT = 5122
    UNSIGNED_SHORT = 5123
    INT = 5124
    UNSIGNED_INT = 5125

    FLOAT = 5126

    MAT4 = 'MAT4'
    VEC4 = 'VEC4'
    VEC3 = 'VEC3'
    VEC2 = 'VEC2'
    SCALAR = 'SCALAR'

    class Accessor:
        __slots__ = (
            "name",
            "buffer",
            "buffer_view",
            "byte_offset",
            "byte_stride",
            "component_type",
            "count",
            "min",
            "max",
            "data_type",
            "type_size",
            "_ctype",
            "_ctype_size",
            "_buffer_data",
            )

        def __init__(self,
                     name,
                     buffer,
                     buffer_view,
                     byte_offset,
                     byte_stride,
                     component_type,
                     count,
                     data_type):
            self.name = name
            self.buffer = buffer
            self.buffer_view = buffer_view
            self.byte_offset = byte_offset
            self.byte_stride = byte_stride
            self.component_type = component_type
            self.count = count
            self.min = [math.inf for i in range(16)]
            self.max = [-math.inf for i in range(16)]
            self.data_type = data_type

            if self.data_type == Buffer.MAT4:
                self.type_size = 16
            elif self.data_type == Buffer.VEC4:
                self.type_size = 4
            elif self.data_type == Buffer.VEC3:
                self.type_size = 3
            elif self.data_type == Buffer.VEC2:
                self.type_size = 2
            else:
                self.type_size = 1

            if component_type == Buffer.BYTE:
                self._ctype = '<b'
            elif component_type == Buffer.UNSIGNED_BYTE:
                self._ctype = '<B'
            elif component_type == Buffer.SHORT:
                self._ctype = '<h'
            elif component_type == Buffer.UNSIGNED_SHORT:
                self._ctype = '<H'
            elif component_type == Buffer.INT:
                self._ctype = '<i'
            elif component_type == Buffer.UNSIGNED_INT:
                self._ctype = '<I'
            elif component_type == Buffer.FLOAT:
                self._ctype = '<f'
            else:
                raise ValueError("Bad component type")

            self._ctype_size = struct.calcsize(self._ctype)
            self._buffer_data = self.buffer.get_buffer_data(self.buffer_view)

        def __len__(self):
            return self.count

        def __getitem__(self, idx):
            if not isinstance(idx, int):
                raise TypeError("Expected an integer index")

            ptr = (
                (
                    (idx % self.type_size)
                    * self._ctype_size + idx // self.type_size * self.byte_stride
                ) + self.byte_offset
            )

            return struct.unpack_from(self._ctype, self._buffer_data, ptr)[0]

        def __setitem__(self, idx, value):
            if not isinstance(idx, int):
                raise TypeError("Expected an integer index")

            i = idx % self.type_size
            self.min[i] = value if value < self.min[i] else self.min[i]
            self.max[i] = value if value > self.max[i] else self.max[i]

            ptr = (
                (i * self._ctype_size + idx // self.type_size * self.byte_stride)
                + self.byte_offset
            )

            struct.pack_into(self._ctype, self._buffer_data, ptr, value)

    __slots__ = (
        "name",
        "bytelength",
        "buffer_views",
        "accessors",
        )

    def __init__(self, name):
        self.name = 'buffer_{}'.format(name)
        self.bytelength = 0
        self.buffer_views = collections.OrderedDict()
        self.accessors = {}

    def export_buffer(self, state):
        data = bytearray()
        for view in self.buffer_views.values():
            data.extend(view['data'])

        if state['settings']['buffers_embed_data'] and not state['settings']['gltf_export_binary']:
            uri = 'data:application/octet-stream;base64,' + base64.b64encode(data).decode('ascii')
        else:
            uri = bpy.path.clean_name(self.name) + '.bin'
            path = os.path.join(state['settings']['gltf_output_dir'], uri)
            state['files'][path] = data

        gltf = {
            'byteLength': self.bytelength,
            'name': self.name,
        }

        embedded_binary = (
            state['settings']['buffers_embed_data']
            and state['settings']['gltf_export_binary']
        )
        if not embedded_binary:
            gltf['uri'] = uri

        if state['version'] < Version('2.0'):
            gltf['type'] = 'arraybuffer'

        return gltf

    def add_view(self, bytelength, bytestride, target):
        buffer_name = 'bufferView_{}_{}'.format(self.name, len(self.buffer_views))
        self.buffer_views[buffer_name] = {
            'data': bytearray(bytelength),
            'target': target,
            'bytelength': bytelength,
            'byteoffset': self.bytelength,
            'bytestride': bytestride,
        }
        self.bytelength += bytelength
        return buffer_name

    def export_views(self, state):
        gltf_views = []

        for key, value in self.buffer_views.items():
            gltf = {
                'byteLength': value['bytelength'],
                'byteOffset': value['byteoffset'],
                'name': key,
            }

            if state['version'] >= Version('2.0') and value['bytestride'] > 0:
                gltf['byteStride'] = value['bytestride']

            gltf['buffer'] = Reference('buffers', self.name, gltf, 'buffer')
            state['references'].append(gltf['buffer'])

            if value['target'] is not None:
                gltf['target'] = value['target']

            gltf_views.append(gltf)

            state['input']['bufferViews'].append(SimpleID(key))

        return gltf_views

    def get_buffer_data(self, buffer_view):
        return self.buffer_views[buffer_view]['data']

    def add_accessor(self,
                     buffer_view,
                     byte_offset,
                     byte_stride,
                     component_type,
                     count,
                     data_type):
        accessor_name = 'accessor_{}_{}'.format(self.name, len(self.accessors))
        self.accessors[accessor_name] = self.Accessor(
            accessor_name,
            self,
            buffer_view,
            byte_offset,
            byte_stride,
            component_type,
            count,
            data_type
        )
        return self.accessors[accessor_name]

    def export_accessors(self, state):
        gltf_accessors = []

        for key, value in self.accessors.items():
            # Do not export an empty accessor
            if value.count == 0:
                continue

            gltf = {
                'byteOffset': value.byte_offset,
                'componentType': value.component_type,
                'count': value.count,
                'min': value.min[:value.type_size],
                'max': value.max[:value.type_size],
                'type': value.data_type,
                'name': value.name,
            }

            if state['version'] < Version('2.0'):
                gltf['byteStride'] = value.byte_stride

            gltf['bufferView'] = Reference('bufferViews', value.buffer_view, gltf, 'bufferView')
            state['references'].append(gltf['bufferView'])

            gltf_accessors.append(gltf)

            state['input']['accessors'].append(SimpleID(key))

        return gltf_accessors

    def combine(self, other, state):
        # Handle the simple stuff
        combined = Buffer(state['settings']['gltf_name'])
        combined.bytelength = self.bytelength + other.bytelength
        combined.accessors = {**self.accessors, **other.accessors}

        # Need to update byte offsets in buffer views
        combined.buffer_views = self.buffer_views.copy()
        other_views = other.buffer_views.copy()
        for key in other_views.keys():
            other_views[key]['byteoffset'] += self.bytelength
        combined.buffer_views.update(other_views)

        return combined class Reference:
    __slots__ = (
        "blender_type",
        "blender_name",
        "source",
        "prop",
        "value",
    )

    def __init__(self, blender_type, blender_name, source, prop):
        self.blender_type = blender_type
        self.blender_name = blender_name
        self.source = source
        self.prop = prop
        self.value = None

    def __str__(self):
        return json.dumps([self.blender_type, self.blender_name, self.value]) class SimpleID:
    def __init__(self, name, data=None):
        self.name = name
        self.data = data def get_bone_name(bone):
    return '{}_{}'.format(bone.id_data.name, bone.name) class AnimationExporter(BaseExporter):
    gltf_key = 'animations'
    blender_key = 'action_pairs'

    @classmethod
    def gather_actions(cls, state):
        armature_objects = [obj for obj in state['input']['objects'] if obj.type == 'ARMATURE']
        regular_objects = [obj for obj in state['input']['objects'] if obj.type != 'ARMATURE']

        actions = []

        def export_eligible(objects):
            for obj in objects:
                actions.extend([
                    AnimationPair(obj, action) for action in state['input']['actions']
                    if _can_object_use_action(obj, action)
                ])

        def export_active(objects):
            for obj in objects:
                if obj.animation_data and obj.animation_data.action:
                    actions.append(AnimationPair(obj, obj.animation_data.action))

        armature_setting = state['settings']['animations_armature_export']
        object_setting = state['settings']['animations_object_export']

        if armature_setting == 'ACTIVE':
            export_active(armature_objects)
        elif armature_setting == 'ELIGIBLE':
            export_eligible(armature_objects)
        else:
            print(
                'WARNING: Unrecognized setting for animations_armature_export:',
                '{}'.format(armature_setting)
            )

        if object_setting == 'ACTIVE':
            export_active(regular_objects)
        elif object_setting == 'ELIGIBLE':
            export_eligible(regular_objects)
        else:
            print(
                'WARNING: Unrecognized setting for animations_object_export:',
                '{}'.format(object_setting)
            )

        return actions

    @classmethod
    def gather_shape_key_actions(cls, state):
        shape_key_objects = [
            obj for obj in state['input']['objects']
            if obj.type == 'MESH' and obj.data.shape_keys
        ]
        shape_key_setting = state['settings']['animations_shape_key_export']

        shape_key_actions = []

        if shape_key_setting == 'ACTIVE':
            for obj in shape_key_objects:
                action = obj.data.shape_keys.animation_data.action
                shape_key_actions.append(AnimationPair(obj, action, True))
        elif shape_key_setting == 'ELIGIBLE':
            for obj in shape_key_objects:
                eligible_actions = []
                shape_keys = set([
                    block.name for block in obj.data.shape_keys.key_blocks
                    if block != block.relative_key
                ])
                for action in state['input']['actions']:
                    fcurve_keys = set([fcurve.data_path.split('"')[1] for fcurve in action.fcurves])
                    if fcurve_keys <= shape_keys:
                        eligible_actions.append(action)
                for action in eligible_actions:
                    shape_key_actions.append(AnimationPair(obj, action, True))
        else:
            print(
                'WARNING: Unrecognized setting for animations_shape_key_export:',
                '{}'.format(shape_key_setting)
            )

        return shape_key_actions

    @classmethod
    def gather(cls, state):
        actions = cls.gather_actions(state)
        actions.extend(cls.gather_shape_key_actions(state))
        return actions

    @classmethod
    def export_action(cls, state, blender_data):
        obj = blender_data.target
        action = blender_data.action
        action_name = blender_data.name

        if state['version'] < Version('2.0'):
            target_key = 'id'
        else:
            target_key = 'node'

        channels = {}
        decompose = state['decompose_fn']
        axis_mat = mathutils.Matrix.Identity(4)
        if state['settings']['nodes_global_matrix_apply']:
            axis_mat = state['settings']['nodes_global_matrix']

        sce = bpy.context.scene
        prev_frame = sce.frame_current
        clear_anim_data = False
        if obj.animation_data is None:
            obj.animation_data_create()
            clear_anim_data = True
        prev_action = obj.animation_data.action

        frame_start, frame_end = [int(x) for x in action.frame_range]
        num_frames = frame_end - frame_start + 1
        obj.animation_data.action = action

        has_location = set()
        has_rotation = set()
        has_scale = set()

        # Check action groups to see what needs to be animated
        pose_bones = {}
        for group in action.groups:
            for channel in group.channels:
                data_path = channel.data_path
                if obj.pose and 'pose.bones' in data_path:
                    target_name = data_path.split('"')[1]
                    transform = data_path.split('.')[-1]
                    pose_bones[obj.pose.bones[target_name]] = None
                else:
                    target_name = obj.name
                    transform = data_path.lower()
                    if obj.name not in channels:
                        channels[obj.name] = []

                if 'location' in transform:
                    has_location.add(target_name)
                if 'rotation' in transform:
                    has_rotation.add(target_name)
                if 'scale' in transform:
                    has_scale.add(target_name)
        channels.update({pbone.name: [] for pbone in pose_bones})

        # Iterate frames and bake animations
        for frame in range(frame_start, frame_end + 1):
            sce.frame_set(frame)

            if obj.name in channels:
                # Decompose here so we don't store a reference to the matrix
                loc, rot, scale = decompose(obj.matrix_local)
                if obj.name not in has_location:
                    loc = None
                if obj.name not in has_rotation:
                    rot = None
                if obj.name not in has_scale:
                    scale = None
                channels[obj.name].append((loc, rot, scale))

            for pbone in pose_bones:
                if pbone.parent:
                    mat = pbone.parent.matrix.inverted() * pbone.matrix
                else:
                    mat = axis_mat * pbone.matrix

                loc, rot, scale = _decompose(mat)

                if pbone.name not in has_location:
                    loc = None
                if pbone.name not in has_rotation:
                    rot = None
                if pbone.name not in has_scale:
                    scale = None
                channels[pbone.name].append((loc, rot, scale))

        gltf_channels = []
        gltf_parameters = {}
        gltf_samplers = []

        tbuf = Buffer('{}_time'.format(action_name))
        tbv = tbuf.add_view(num_frames * 1 * 4, 1 * 4, None)
        tdata = tbuf.add_accessor(tbv, 0, 1 * 4, Buffer.FLOAT, num_frames, Buffer.SCALAR)
        time = 0
        for i in range(num_frames):
            tdata[i] = time
            time += state['animation_dt']
        state['buffers'].append(tbuf)
        state['input']['buffers'].append(SimpleID(tbuf.name))
        time_parameter_name = '{}_time_parameter'.format(action_name)
        ref = Reference('accessors', tdata.name, gltf_parameters, time_parameter_name)
        gltf_parameters[time_parameter_name] = ref
        state['references'].append(ref)

        input_list = '{}_{}_samplers'.format(action_name, obj.name)
        state['input'][input_list] = []

        sampler_keys = []
        for targetid, chan in channels.items():
            buf = Buffer('{}_{}'.format(targetid, action_name))
            ldata = rdata = sdata = None
            paths = []
            if targetid in has_location:
                lbv = buf.add_view(num_frames * 3 * 4, 3 * 4, None)
                ldata = buf.add_accessor(lbv, 0, 3 * 4, Buffer.FLOAT, num_frames, Buffer.VEC3)
                paths.append('translation')
            if targetid in has_rotation:
                rbv = buf.add_view(num_frames * 4 * 4, 4 * 4, None)
                rdata = buf.add_accessor(rbv, 0, 4 * 4, Buffer.FLOAT, num_frames, Buffer.VEC4)
                paths.append('rotation')
            if targetid in has_scale:
                sbv = buf.add_view(num_frames * 3 * 4, 3 * 4, None)
                sdata = buf.add_accessor(sbv, 0, 3 * 4, Buffer.FLOAT, num_frames, Buffer.VEC3)
                paths.append('scale')

            if not paths:
                continue

            for i in range(num_frames):
                loc, rot, scale = chan[i]
                if ldata:
                    for j in range(3):
                        ldata[(i * 3) + j] = loc[j]
                if sdata:
                    for j in range(3):
                        sdata[(i * 3) + j] = scale[j]
                if rdata:
                    for j in range(4):
                        rdata[(i * 4) + j] = rot[j]

            state['buffers'].append(buf)
            state['input']['buffers'].append(SimpleID(buf.name))

            is_bone = False
            if targetid != obj.name:
                is_bone = True
                targetid = get_bone_name(bpy.data.armatures[obj.data.name].bones[targetid])

            for path in paths:
                sampler_name = '{}_{}_{}_sampler'.format(action_name, targetid, path)
                sampler_keys.append(sampler_name)
                parameter_name = '{}_{}_{}_parameter'.format(action_name, targetid, path)

                gltf_channel = {
                    'sampler': sampler_name,
                    'target': {
                        target_key: targetid,
                        'path': path,
                    }
                }
                gltf_channels.append(gltf_channel)
                id_ref = Reference(
                    'objects' if is_bone else 'objects',
                    targetid,
                    gltf_channel['target'],
                    target_key
                )
                state['references'].append(id_ref)
                state['input'][input_list].append(SimpleID(sampler_name))
                sampler_ref = Reference(input_list, sampler_name, gltf_channel, 'sampler')
                state['references'].append(sampler_ref)

                gltf_sampler = {
                    'input': None,
                    'interpolation': 'LINEAR',
                    'output': None,
                }
                gltf_samplers.append(gltf_sampler)

                accessor_name = {
                    'translation': ldata.name if ldata else None,
                    'rotation': rdata.name if rdata else None,
                    'scale': sdata.name if sdata else None,
                }[path]

                if state['version'] < Version('2.0'):
                    gltf_sampler['input'] = time_parameter_name
                    gltf_sampler['output'] = parameter_name
                    accessor_ref = Reference(
                        'accessors',
                        accessor_name,
                        gltf_parameters,
                        parameter_name
                    )
                    gltf_parameters[parameter_name] = accessor_ref
                else:
                    time_ref = Reference(
                        'accessors',
                        tdata.name,
                        gltf_sampler,
                        'input'
                    )
                    gltf_sampler['input'] = time_ref
                    state['references'].append(time_ref)
                    accessor_ref = Reference(
                        'accessors',
                        accessor_name,
                        gltf_sampler,
                        'output'
                    )
                    gltf_sampler['output'] = accessor_ref

                state['references'].append(accessor_ref)

        gltf_action = {
            'name': blender_data.action.name,
            'channels': gltf_channels,
            'samplers': gltf_samplers,
        }

        if state['version'] < Version('2.0'):
            gltf_action['samplers'] = {
                '{}_{}'.format(input_list, i[0]): i[1]
                for i in zip(sampler_keys, gltf_action['samplers'])
            }
            gltf_action['parameters'] = gltf_parameters

        sce.frame_set(prev_frame)
        obj.animation_data.action = prev_action
        if clear_anim_data:
            obj.animation_data_clear()

        return gltf_action

    @classmethod
    def export_shape_key_action(cls, state, blender_data):
        obj = blender_data.target
        action = blender_data.action
        action_name = blender_data.name

        fcurves = action.fcurves
        frame_range = action.frame_range
        frame_count = int(frame_range[1]) - int(frame_range[0])
        for fcurve in fcurves:
            fcurve.convert_to_samples(*frame_range)
        samples = {
            fcurve.data_path.split('"')[1]: [point.co[1] for point in fcurve.sampled_points]
            for fcurve in fcurves
        }
        shape_keys = [
            block for block in obj.data.shape_keys.key_blocks
            if block != block.relative_key
        ]
        empty_data = [0.0] * frame_count

        weight_data = zip(*[samples.get(key.name, empty_data) for key in shape_keys])
        weight_data = itertools.chain.from_iterable(weight_data)
        dt_data = [state['animation_dt'] * i for i in range(frame_count)]

        anim_buffer = Buffer('{}_{}'.format(obj.name, action_name))
        state['buffers'].append(anim_buffer)
        state['input']['buffers'].append(SimpleID(anim_buffer.name))

        time_view = anim_buffer.add_view(frame_count * 1 * 4, 1 * 4, None)
        time_acc = anim_buffer.add_accessor(
            time_view,
            0,
            1 * 4,
            Buffer.FLOAT,
            frame_count,
            Buffer.SCALAR
        )
        for i, dt in enumerate(dt_data):
            time_acc[i] = dt

        key_count = len(shape_keys)
        weight_view = anim_buffer.add_view(frame_count * key_count * 4, 4, None)
        weight_acc = anim_buffer.add_accessor(
            weight_view,
            0,
            1 * 4,
            Buffer.FLOAT,
            frame_count * key_count,
            Buffer.SCALAR
        )
        for i, weight in enumerate(weight_data):
            weight_acc[i] = weight

        channel = {
            'sampler': 0,
            'target': {
                'path': 'weights',
            },
        }
        channel['target']['node'] = Reference('objects', obj.name, channel['target'], 'node')
        state['references'].append(channel['target']['node'])

        sampler = {
            'interpolation': 'LINEAR',
        }
        sampler['input'] = Reference('accessors', time_acc.name, sampler, 'input')
        state['references'].append(sampler['input'])
        sampler['output'] = Reference('accessors', weight_acc.name, sampler, 'output')
        state['references'].append(sampler['output'])

        gltf_action = {
            'name': blender_data.action.name,
            'channels': [channel],
            'samplers': [sampler],
        }

        return gltf_action

    @classmethod
    def export(cls, state, blender_data):
        if blender_data.is_shape_key:
            return cls.export_shape_key_action(state, blender_data)

        return cls.export_action(state, blender_data) class CameraExporter(BaseExporter):
    gltf_key = 'cameras'
    blender_key = 'cameras'

    @classmethod
    def export(cls, state, blender_data):
        camera_gltf = {}
        if blender_data.type == 'ORTHO':
            xmag = 0.5 * blender_data.ortho_scale
            ymag = xmag * state['aspect_ratio']
            camera_gltf = {
                'orthographic': {
                    'xmag': ymag,
                    'ymag': xmag,
                    'zfar': blender_data.clip_end,
                    'znear': blender_data.clip_start,
                },
                'type': 'orthographic',
            }
        else:
            angle_y = blender_data.angle_y if blender_data.angle_y != 0.0 else 1e-6
            camera_gltf = {
                'perspective': {
                    'aspectRatio': blender_data.angle_x / angle_y,
                    'yfov': angle_y,
                    'zfar': blender_data.clip_end,
                    'znear': blender_data.clip_start,
                },
                'type': 'perspective',
            }
        camera_gltf['name'] = blender_data.name
        extras = cls.get_custom_properties(blender_data)
        if extras:
            camera_gltf['extras'] = extras
        return camera_gltf class ImageExporter(BaseExporter):
    gltf_key = 'images'
    blender_key = 'images'

    @classmethod
    def check(cls, state, blender_data):
        errors = []
        if blender_data.size[0] == 0:
            errors.append('x dimension is 0')
        if blender_data.size[1] == 0:
            errors.append('y dimension is 0')
        if blender_data.type != 'IMAGE':
            errors.append('not an image {}'.format(blender_data.type))

        if errors:
            err_list = '\n\t'.join(errors)
            print(
                'Unable to export image {} due to the following errors:\n\t{}'
                .format(blender_data.name, err_list)
            )
            return False

        return True

    @classmethod
    def default(cls, state, blender_data):
        return {
            'name': blender_data.name,
            'uri': ''
        }

    @classmethod
    def image_to_data_uri(cls, image):
        width = image.size[0]
        height = image.size[1]
        buf = bytearray([int(p * 255) for p in image.pixels])

        # reverse the vertical line order and add null bytes at the start
        width_byte_4 = width * 4
        raw_data = b''.join(b'\x00' + buf[span:span + width_byte_4]
                            for span in range((height - 1) * width_byte_4, -1, - width_byte_4))

        def png_pack(png_tag, data):
            chunk_head = png_tag + data
            return (struct.pack("!I", len(data)) +
                    chunk_head +
                    struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)))

        png_bytes = b''.join([
            b'\x89PNG\r\n\x1a\n',
            png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
            png_pack(b'IDAT', zlib.compress(raw_data, 9)),
            png_pack(b'IEND', b'')])

        return png_bytes

    @classmethod
    def export(cls, state, blender_data):
        path = ''
        data = None

        gltf = {'name': blender_data.name}

        storage_setting = state['settings']['images_data_storage']
        image_packed = blender_data.packed_file is not None
        if image_packed and storage_setting in ['COPY', 'REFERENCE']:
            if blender_data.file_format in EXT_MAP:
                # save the file to the output directory
                gltf['uri'] = '.'.join([blender_data.name, EXT_MAP[blender_data.file_format]])
                temp = blender_data.filepath
                blender_data.filepath = os.path.join(
                    state['settings']['gltf_output_dir'],
                    gltf['uri']
                )
                blender_data.save()
                with open(bpy.path.abspath(blender_data.filepath), 'rb') as fin:
                    data = fin.read()
                blender_data.filepath = temp
            else:
                # convert to png and save
                gltf['uri'] = '.'.join([blender_data.name, 'png'])
                data = cls.image_to_data_uri(blender_data)
            path = os.path.join(state['settings']['gltf_output_dir'], gltf['uri'])

        elif storage_setting == 'COPY':
            with open(bpy.path.abspath(blender_data.filepath), 'rb') as fin:
                data = fin.read()
            gltf['uri'] = bpy.path.basename(blender_data.filepath)
            path = os.path.join(state['settings']['gltf_output_dir'], gltf['uri'])
        elif storage_setting == 'REFERENCE':
            gltf['uri'] = blender_data.filepath.replace('//', '')
        elif storage_setting == 'EMBED':
            png_bytes = cls.image_to_data_uri(blender_data)
            gltf['mimeType'] = 'image/png'
            if state['settings']['gltf_export_binary']:
                buf = Buffer(blender_data.name)
                view_key = buf.add_view(len(png_bytes), 0, None)
                view = buf.buffer_views[view_key]
                view['data'] = png_bytes

                pad = 4 - len(png_bytes) % 4
                if pad not in [0, 4]:
                    buf.add_view(pad, 0, None)

                gltf['bufferView'] = Reference('bufferViews', view_key, gltf, 'bufferView')
                state['references'].append(gltf['bufferView'])

                state['buffers'].append(buf)
                state['input']['buffers'].append(SimpleID('buffer_' + blender_data.name))
            else:
                gltf['uri'] = 'data:image/png;base64,' + base64.b64encode(png_bytes).decode()
        else:
            print(
                'Encountered unknown option ({}) for images_data_storage setting'
                .format(storage_setting)
            )

        if path:
            state['files'][path] = data

        return gltf class MaterialExporter(BaseExporter):
    gltf_key = 'materials'
    blender_key = 'materials'

    @classmethod
    def export(cls, state, blender_data):
        gltf = {
            'name': blender_data.name,
        }

        if state['version'] < Version('2.0'):
            return gltf

        if hasattr(blender_data, 'pbr_export_settings'):
            gltf['doubleSided'] = not blender_data.game_settings.use_backface_culling
            pbr_settings = blender_data.pbr_export_settings
            pbr = {
                'baseColorFactor': pbr_settings.base_color_factor[:],
                'metallicFactor': pbr_settings.metallic_factor,
                'roughnessFactor': pbr_settings.roughness_factor,
            }

            gltf['alphaMode'] = pbr_settings.alpha_mode
            if gltf['alphaMode'] == 'MASK':
                gltf['alphaCutoff'] = pbr_settings.alpha_cutoff

            input_textures = [texture.name for texture in state['input']['textures']]
            base_color_text = pbr_settings.base_color_texture
            if base_color_text and base_color_text in input_textures:
                pbr['baseColorTexture'] = {
                    'texCoord': pbr_settings.base_color_text_index,
                }
                pbr['baseColorTexture']['index'] = Reference(
                    'textures',
                    pbr_settings.base_color_texture,
                    pbr['baseColorTexture'],
                    'index'
                )
                state['references'].append(pbr['baseColorTexture']['index'])

            metal_rough_text = pbr_settings.metal_roughness_texture
            if metal_rough_text and metal_rough_text in input_textures:
                pbr['metallicRoughnessTexture'] = {
                    'texCoord': pbr_settings.metal_rough_text_index,
                }
                pbr['metallicRoughnessTexture']['index'] = Reference(
                    'textures',
                    pbr_settings.metal_roughness_texture,
                    pbr['metallicRoughnessTexture'],
                    'index'
                )
                state['references'].append(pbr['metallicRoughnessTexture']['index'])

            gltf['pbrMetallicRoughness'] = pbr

            gltf['emissiveFactor'] = pbr_settings.emissive_factor[:]

            emissive_text = pbr_settings.emissive_texture
            if emissive_text and emissive_text in input_textures:
                gltf['emissiveTexture'] = {
                    'texCoord': pbr_settings.emissive_text_index,
                }
                gltf['emissiveTexture']['index'] = Reference(
                    'textures',
                    pbr_settings.emissive_texture,
                    gltf['emissiveTexture'],
                    'index'
                )
                state['references'].append(gltf['emissiveTexture']['index'])

            normal_text = pbr_settings.normal_texture
            if normal_text and normal_text in input_textures:
                gltf['normalTexture'] = {
                    'texCoord': pbr_settings.normal_text_index,
                }
                gltf['normalTexture']['index'] = Reference(
                    'textures',
                    pbr_settings.normal_texture,
                    gltf['normalTexture'],
                    'index'
                )
                state['references'].append(gltf['normalTexture']['index'])

            occlusion_text = pbr_settings.occlusion_texture
            if occlusion_text and occlusion_text in input_textures:
                gltf['occlusionTexture'] = {
                    'texCoord': pbr_settings.occlusion_text_index,
                }
                gltf['occlusionTexture']['index'] = Reference(
                    'textures',
                    pbr_settings.occlusion_texture,
                    gltf['occlusionTexture'],
                    'index'
                )
                state['references'].append(gltf['occlusionTexture']['index'])

        return gltf class MeshExporter(BaseExporter):
    gltf_key = 'meshes'
    blender_key = 'meshes'

    @classmethod
    def export(cls, state, blender_data):
        # glTF data
        mesh_name = blender_data.name
        blender_data = state['mod_meshes'].get(blender_data.name, blender_data)
        gltf_mesh = {
            'name': mesh_name,
            'primitives': [],
        }

        extras = BaseExporter.get_custom_properties(blender_data)
        if extras:
            gltf_mesh['extras'] = extras

        shape_keys = state['shape_keys'].get(mesh_name, [])

        vert_lists = [get_vert_list(blender_data, len(shape_keys) > 0)]
        vert_lists += [
            get_vert_list(key_mesh, True)
            for key_mesh in [key[1] for key in shape_keys]
        ]

        # Process mesh data and gather attributes
        buffer = Buffer(mesh_name)
        gltf_attrs = cls.export_attributes(state, buffer, mesh_name, None, vert_lists[0])

        # Process shape keys
        targets = [
            cls.export_attributes(
                state,
                Buffer(key_mesh.name),
                key_mesh.name,
                vert_lists[0],
                vert_lists[i + 1]
            )
            for i, key_mesh in enumerate([key[1] for key in shape_keys])
        ]

        if shape_keys:
            gltf_mesh['weights'] = [key[0] for key in shape_keys]

        prims = gather_primitives(state, blender_data, vert_lists)

        if requires_int_indices(vert_lists[0]):
            # Use the integer index extension
            if OES_ELEMENT_INDEX_UINT not in state['gl_extensions_used']:
                state['gl_extensions_used'].append(OES_ELEMENT_INDEX_UINT)
            itype = Buffer.UNSIGNED_INT
        else:
            itype = Buffer.UNSIGNED_SHORT

        gltf_mesh['primitives'] = [
            cls.export_primitive(state, buffer, mat, indices, itype, gltf_attrs, targets)
            for mat, indices in prims.items() if indices
        ]

        return gltf_mesh

    @classmethod
    def export_primitive(cls, state, buf, material, indices, index_type, attributes, targets):
        index_stride = 4 if index_type == Buffer.UNSIGNED_INT else 2

        # Pad index buffer if necessary to maintain a size that is a multiple of 4
        view_length = index_stride * len(indices)
        view_length = view_length + ((4 - view_length % 4) % 4)

        index_view = buf.add_view(view_length, 0, Buffer.ELEMENT_ARRAY_BUFFER)
        idata = buf.add_accessor(
            index_view,
            0,
            index_stride,
            index_type,
            len(indices),
            Buffer.SCALAR
        )

        for i, index in enumerate(indices):
            idata[i] = index

        gltf_prim = {
            'attributes': attributes,
            'mode': 4,
        }

        gltf_prim['indices'] = Reference('accessors', idata.name, gltf_prim, 'indices')
        state['references'].append(gltf_prim['indices'])

        if targets:
            gltf_prim['targets'] = targets

        # Add the material reference after checking that it is valid
        if material:
            gltf_prim['material'] = Reference('materials', material, gltf_prim, 'material')
            state['references'].append(gltf_prim['material'])

        return gltf_prim

    @classmethod
    def export_attributes(cls, state, buf, mesh_name, base_vert_list, vert_list):
        gltf_attrs = {}
        is_skinned = mesh_name in state['skinned_meshes']

        color_size = 4 if state['settings']['meshes_vertex_color_alpha'] else 3
        num_uv_layers = len(vert_list[0].uvs)
        num_col_layers = len(vert_list[0].colors)
        vertex_size = (3 + 3 + num_uv_layers * 2 + num_col_layers * color_size) * 4
        if is_skinned:
            vertex_size += (4 + 4) * 4
        num_verts = len(vert_list)

        if state['settings']['meshes_interleave_vertex_data']:
            view = buf.add_view(vertex_size * num_verts, vertex_size, Buffer.ARRAY_BUFFER)
        else:
            view = None

        offset = OffsetTracker()

        def create_attr_accessor(name, component_type, component_count):
            component_size = 1 if component_type == Buffer.UNSIGNED_BYTE else 4
            if not view:
                stride = component_size * component_count
                buffer = Buffer(mesh_name + '_' + name)
                state['buffers'].append(buffer)
                state['input']['buffers'].append(SimpleID(buffer.name))
                _view = buffer.add_view(stride * num_verts, stride, Buffer.ARRAY_BUFFER)
                interleaved = False
            else:
                _view = view
                buffer = buf
                stride = vertex_size
                interleaved = True

            data_type = [Buffer.SCALAR, Buffer.VEC2, Buffer.VEC3, Buffer.VEC4][component_count - 1]
            _offset = offset.get()
            acc = buffer.add_accessor(_view, _offset, stride, component_type, num_verts, data_type)
            if interleaved:
                offset.add(component_size * component_count)
            return acc

        def add_attribute(name, num_components, component_type=Buffer.FLOAT):
            acc = create_attr_accessor(name, component_type, num_components)
            gltf_attrs[name] = Reference('accessors', acc.name, gltf_attrs, name)
            state['references'].append(gltf_attrs[name])
            return acc

        vdata = add_attribute('POSITION', 3)
        ndata = add_attribute('NORMAL', 3)
        if not base_vert_list:
            tdata = [
                add_attribute('TEXCOORD_' + str(i), 2)
                for i in range(num_uv_layers)
            ]
            cdata = [
                add_attribute('COLOR_' + str(i), color_size)
                for i in range(num_col_layers)
            ]

        # Copy vertex data
        if base_vert_list:
            fill_data(vdata, (
                [a - b for a, b in zip(v.co, b.co)]
                for v, b in zip(vert_list, base_vert_list)
            ))
            fill_data(ndata, (
                [a - b for a, b in zip(v.normal, b.normal)]
                for v, b in zip(vert_list, base_vert_list)
            ))
        else:
            fill_data(vdata, (v.co for v in vert_list))
            fill_data(ndata, (v.normal for v in vert_list))
            for i, accessor in enumerate(cdata):
                if state['settings']['meshes_vertex_color_alpha']:
                    fill_data(accessor, (v.colors[i] for v in vert_list))
                else:
                    fill_data(accessor, (v.colors[i][:3] for v in vert_list))
            for i, accessor in enumerate(tdata):
                if state['settings']['asset_profile'] == 'WEB':
                    fill_data(accessor, ((v.uvs[i][0], 1.0 - v.uvs[i][1]) for v in vert_list))
                else:
                    fill_data(accessor, (v.uvs[i] for v in vert_list))
            for i, vtx in enumerate(vert_list):
                vtx.index = i

        state['buffers'].append(buf)
        state['input']['buffers'].append(SimpleID(buf.name))

        if is_skinned:
            if state['version'] < Version('2.0'):
                joint_key = 'JOINT'
                weight_key = 'WEIGHT'
            else:
                joint_key = 'JOINTS_0'
                weight_key = 'WEIGHTS_0'

            jdata = add_attribute(joint_key, 4, Buffer.UNSIGNED_BYTE)
            fill_data(jdata, (v.joint_indexes for v in vert_list))
            wdata = add_attribute(weight_key, 4)
            fill_data(wdata, (v.weights for v in vert_list))

        return gltf_attrs class NodeExporter(BaseExporter):
    gltf_key = 'nodes'
    blender_key = 'objects'

    @classmethod
    def export(cls, state, blender_data):
        node = {
            'name': blender_data.name,
        }

        obj_children = [
            child for child in blender_data.children
            if child in state['input']['objects'] and not child.parent_bone
        ]
        if obj_children:
            node['children'] = []
        for i, child in enumerate(obj_children):
            node['children'].append(Reference('objects', child.name, node['children'], i))
            state['references'].append(node['children'][-1])

        extras = cls.get_custom_properties(blender_data)
        extras.update({
            prop.name: prop.value for prop in blender_data.game.properties.values()
        })
        if extras:
            node['extras'] = extras

        decompose = state['decompose_fn']

        if blender_data.parent and blender_data.parent_bone:
            parent_bone = blender_data.parent.data.bones[blender_data.parent_bone]
            bone_name = get_bone_name(parent_bone)
            if bone_name not in state['bone_children']:
                state['bone_children'][bone_name] = []
            state['bone_children'][bone_name].append(blender_data.name)

        if blender_data.type == 'MESH':
            decompose = state['decompose_mesh_fn']
            mesh = state['mod_meshes_obj'].get(blender_data.name, blender_data.data)
            mesh_name = mesh.name
            mesh = state['mod_meshes'].get(mesh.name, mesh)
            if state['version'] < Version('2.0'):
                node['meshes'] = []
                node['meshes'].append(Reference('meshes', mesh_name, node['meshes'], 0))
                state['references'].append(node['meshes'][0])
            else:
                node['mesh'] = Reference('meshes', mesh_name, node, 'mesh')
                state['references'].append(node['mesh'])
            armature = blender_data.find_armature()
            if armature:
                state['input']['skins'].append(blender_data)
                state['skinned_meshes'].add(mesh_name)
                node['skin'] = Reference('skins', blender_data.name, node, 'skin')
                state['references'].append(node['skin'])
                if state['version'] < Version('2.0'):
                    bone_names = [
                        get_bone_name(b) for b in armature.data.bones if b.parent is None
                    ]
                    node['skeletons'] = []
                    node['skeletons'].extend([
                        Reference('objects', bone, node['skeletons'], i)
                        for i, bone in enumerate(bone_names)
                    ])
                    for ref in node['skeletons']:
                        state['references'].append(ref)
        elif blender_data.type == 'CAMERA':
            node['camera'] = Reference('cameras', blender_data.data.name, node, 'camera')
            state['references'].append(node['camera'])
        elif blender_data.type == 'EMPTY' and blender_data.dupli_group is not None:
            node['children'] = node.get('children', [])
            node['children'].append(cls.export_dupli_group(state, blender_data.dupli_group))
            node['children'][-1].source = node['children']
            node['children'][-1].prop = len(node['children']) - 1
        elif blender_data.type == 'ARMATURE':
            decompose = state['decompose_mesh_fn']
            for i, bone in enumerate(blender_data.data.bones):
                state['input']['bones'].append(SimpleID(get_bone_name(bone), bone))
            if 'children' not in node:
                node['children'] = []
            offset = len(node['children'])
            root_bones = [bone for bone in blender_data.data.bones if bone.parent is None]
            root_refs = [
                Reference('objects', get_bone_name(b), node['children'], i + offset)
                for i, b in enumerate(root_bones)
            ]

            for bone in root_refs:
                state['references'].append(bone)
            node['children'].extend(root_refs)

        (
            node['translation'],
            node['rotation'],
            node['scale']
        ) = decompose(blender_data.matrix_local)

        return node

    @classmethod
    def export_dupli_group(cls, state, dupli_group):
        group_sid = SimpleID(
            'dupli_group_{}.{}'.format(dupli_group.name, len(state['dupli_nodes']))
        )
        state['input']['dupli_ids'].append(group_sid)

        group_node = {
            'name': group_sid.name,
        }
        state['dupli_nodes'].append(group_node)

        dupli_nodes = [group_node]
        children = []

        for dupli_obj in dupli_group.objects:
            obj_node = cls.export(state, dupli_obj)
            state['dupli_nodes'].append(obj_node)

            obj_sid = SimpleID(
                'dupli_node_{}.{}'.format(dupli_obj.name, len(state['dupli_nodes']))
            )
            state['input']['dupli_ids'].append(obj_sid)

            obj_node['name'] = obj_sid.name
            if dupli_obj.dupli_group is None and 'children' in obj_node:
                del obj_node['children']

            obj_ref = Reference('objects', obj_sid.name, children, len(children))
            children.append(obj_ref)
            state['references'].append(obj_ref)

            dupli_nodes.append(obj_node)

        group_node['children'] = children

        group_ref = Reference('objects', group_sid.name, None, None)
        state['references'].append(group_ref)
        return group_ref class SceneExporter(BaseExporter):
    gltf_key = 'scenes'
    blender_key = 'scenes'

    @classmethod
    def export(cls, state, blender_data):
        bg_color = blender_data.world.horizon_color[:] if blender_data.world else [0.05]*3
        result = {
            'extras': {
                'background_color': bg_color,
                'frames_per_second': blender_data.render.fps,
            },
            'name': blender_data.name,
        }

        if blender_data.camera and blender_data.camera.data in state['input']['cameras']:
            result['extras']['active_camera'] = Reference(
                'cameras',
                blender_data.camera.name,
                result['extras'],
                'active_camera'
            )
            state['references'].append(result['extras']['active_camera'])

        extras = BaseExporter.get_custom_properties(blender_data)
        if extras:
            result['extras'].update(BaseExporter.get_custom_properties(blender_data))

        result['nodes'] = [
            Reference('objects', ob.name, None, None)
            for ob in blender_data.objects
            if ob in state['input']['objects'] and ob.parent is None and ob.is_visible(blender_data)
        ]
        for i, ref in enumerate(result['nodes']):
            ref.source = result['nodes']
            ref.prop = i
        state['references'].extend(result['nodes'])

        hidden_nodes = [
            Reference('objects', ob.name, None, None)
            for ob in blender_data.objects
            if ob in state['input']['objects'] and not ob.is_visible(blender_data)
        ]

        if hidden_nodes:
            result['extras']['hidden_nodes'] = hidden_nodes
            for i, ref in enumerate(hidden_nodes):
                ref.source = result['extras']['hidden_nodes']
                ref.prop = i
            state['references'].extend(result['extras']['hidden_nodes'])

        return result class SkinExporter(BaseExporter):
    gltf_key = 'skins'
    blender_key = 'skins'

    @classmethod
    def export(cls, state, blender_data):
        if state['version'] < Version('2.0'):
            joints_key = 'jointNames'
        else:
            joints_key = 'joints'

        arm = blender_data.find_armature()

        axis_mat = mathutils.Matrix.Identity(4)
        if state['settings']['nodes_global_matrix_apply']:
            axis_mat = state['settings']['nodes_global_matrix']

        bind_shape_mat = (
            axis_mat
            * arm.matrix_world.inverted()
            * blender_data.matrix_world
            * axis_mat.inverted()
        )

        bone_groups = [
            group for group in blender_data.vertex_groups if group.name in arm.data.bones
        ]

        gltf_skin = {
            'name': blender_data.name,
        }
        gltf_skin[joints_key] = [
            Reference('objects', get_bone_name(arm.data.bones[group.name]), None, None)
            for group in bone_groups
        ]
        for i, ref in enumerate(gltf_skin[joints_key]):
            ref.source = gltf_skin[joints_key]
            ref.prop = i
            state['references'].append(ref)

        if state['version'] < Version('2.0'):
            gltf_skin['bindShapeMatrix'] = togl(mathutils.Matrix.Identity(4))
        else:
            bone_names = [get_bone_name(b) for b in arm.data.bones if b.parent is None]
            if len(bone_names) > 1:
                print('Warning: Armature {} has no root node'.format(arm.data.name))
            gltf_skin['skeleton'] = Reference('objects', bone_names[0], gltf_skin, 'skeleton')
            state['references'].append(gltf_skin['skeleton'])

        element_size = 16 * 4
        num_elements = len(bone_groups)
        buf = Buffer('IBM_{}_skin'.format(blender_data.name))
        buf_view = buf.add_view(element_size * num_elements, element_size, None)
        idata = buf.add_accessor(buf_view, 0, element_size, Buffer.FLOAT, num_elements, Buffer.MAT4)

        for i, group in enumerate(bone_groups):
            bone = arm.data.bones[group.name]
            mat = togl((axis_mat * bone.matrix_local).inverted() * bind_shape_mat)
            for j in range(16):
                idata[(i * 16) + j] = mat[j]

        gltf_skin['inverseBindMatrices'] = Reference(
            'accessors',
            idata.name,
            gltf_skin,
            'inverseBindMatrices'
        )
        state['references'].append(gltf_skin['inverseBindMatrices'])
        state['buffers'].append(buf)
        state['input']['buffers'].append(SimpleID(buf.name))

        return gltf_skin class TextureExporter(BaseExporter):
    gltf_key = 'textures'
    blender_key = 'textures'

    @classmethod
    def check(cls, state, blender_data):
        errors = []
        if not isinstance(blender_data, bpy.types.ImageTexture):
            errors.append('is not an ImageTexture')
        elif blender_data.image is None:
            errors.append('has no image reference')
        elif blender_data.image.channels not in [3, 4]:
            errors.append(
                'points to {}-channel image (must be 3 or 4)'
                .format(blender_data.image.channels)
            )

        if errors:
            err_list = '\n\t'.join(errors)
            print(
                'Unable to export texture {} due to the following errors:\n\t{}'
                .format(blender_data.name, err_list)
            )
            return False

        return True

    @classmethod
    def export_sampler(cls, blender_data):
        gltf_sampler = {
            'name': blender_data.name,
        }

        # Handle wrapS and wrapT
        if blender_data.extension in ('REPEAT', 'CHECKER', 'EXTEND'):
            if blender_data.use_mirror_x:
                gltf_sampler['wrapS'] = GL_MIRRORED_REPEAT
            else:
                gltf_sampler['wrapS'] = GL_REPEAT

            if blender_data.use_mirror_y:
                gltf_sampler['wrapT'] = GL_MIRRORED_REPEAT
            else:
                gltf_sampler['wrapT'] = GL_REPEAT
        elif blender_data.extension in ('CLIP', 'CLIP_CUBE'):
            gltf_sampler['wrapS'] = GL_CLAMP_TO_EDGE
            gltf_sampler['wrapT'] = GL_CLAMP_TO_EDGE
        else:
            print('Warning: Unknown texture extension option:', blender_data.extension)

        # Handle minFilter and magFilter
        if blender_data.use_mipmap:
            gltf_sampler['minFilter'] = GL_LINEAR_MIPMAP_LINEAR
            gltf_sampler['magFilter'] = GL_LINEAR
        else:
            gltf_sampler['minFilter'] = GL_NEAREST
            gltf_sampler['magFilter'] = GL_NEAREST

        return gltf_sampler

    @classmethod
    def export(cls, state, blender_data):
        gltf_texture = {
            'name': blender_data.name,
        }

        gltf_sampler = cls.export_sampler(blender_data)
        state['input']['samplers'].append(SimpleID(blender_data.name))
        state['samplers'].append(gltf_sampler)

        gltf_texture['sampler'] = Reference('samplers', blender_data.name, gltf_texture, 'sampler')
        state['references'].append(gltf_texture['sampler'])

        gltf_texture['source'] = Reference(
            'images',
            blender_data.image.name,
            gltf_texture,
            'source'
        )
        state['references'].append(gltf_texture['source'])

        if state['version'] < Version('2.0'):
            tformat = None
            channels = blender_data.image.channels
            image_is_srgb = blender_data.image.colorspace_settings.name == 'sRGB'
            use_srgb = state['settings']['images_allow_srgb'] and image_is_srgb

            if channels == 3:
                if use_srgb:
                    tformat = GL_SRGB
                else:
                    tformat = GL_RGB
            elif channels == 4:
                if use_srgb:
                    tformat = GL_SRGB_ALPHA
                else:
                    tformat = GL_RGBA

            gltf_texture['format'] = gltf_texture['internalFormat'] = tformat

        return gltf_texture
from distutils.version import StrictVersion as Version
from .exporters import (
    Buffer,
    Reference,
    SimpleID,
    get_bone_name,

    AnimationExporter,
    CameraExporter,
    ImageExporter,
    MaterialExporter,
    MeshExporter,
    NodeExporter,
    SceneExporter,
    SkinExporter,
    TextureExporter,
)
import functools
import itertools
import json
import struct
import bpy
import mathutils




__all__ = ['export_gltf']


DEFAULT_SETTINGS = {
    'gltf_output_dir': '',
    'gltf_name': 'gltf',
    'gltf_export_binary': False,
    'buffers_embed_data': True,
    'buffers_combine_data': False,
    'nodes_export_hidden': False,
    'nodes_global_matrix': mathutils.Matrix.Identity(4),
    'nodes_global_matrix_apply': True,
    'nodes_selected_only': False,
    'blocks_prune_unused': True,
    'meshes_apply_modifiers': True,
    'meshes_interleave_vertex_data': True,
    'meshes_vertex_color_alpha': True,
    'images_data_storage': 'COPY',
    'asset_version': '2.0',
    'asset_copyright': '',
    'asset_profile': 'WEB',
    'images_allow_srgb': False,
    'extension_exporters': [],
    'animations_object_export': 'ACTIVE',
    'animations_armature_export': 'ELIGIBLE',
    'animations_shape_key_export': 'ELIGIBLE',
    'hacks_streaming': False,
}


PROFILE_MAP = {
    'WEB': {'api': 'WebGL', 'version': '1.0'},
    'DESKTOP': {'api': 'OpenGL', 'version': '3.0'}
}


def togl(matrix):
    return [i for col in matrix.col for i in col]


def _decompose(matrix):
    loc, rot, scale = matrix.decompose()
    loc = loc.to_tuple()
    rot = (rot.x, rot.y, rot.z, rot.w)
    scale = scale.to_tuple()

    return loc, rot, scale


def export_joint(state, bone):
    axis_mat = mathutils.Matrix.Identity(4)
    if state['settings']['nodes_global_matrix_apply']:
        axis_mat = state['settings']['nodes_global_matrix']

    matrix = axis_mat * bone.matrix_local
    if bone.parent:
        matrix = bone.parent.matrix_local.inverted() * bone.matrix_local


    bone_name = get_bone_name(bone)