Skip to content
Snippets Groups Projects
io_export_pc2.py 7.10 KiB
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_info = {
    "name": "Export Pointcache Format(.pc2)",
    "author": "Florian Meyer (tstscr)",
    "version": (1, 1, 2),
    "blender": (2, 80, 0),
    "location": "File > Export > Pointcache (.pc2)",
    "description": "Export mesh Pointcache data (.pc2)",
    "warning": "",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/pc2.html",
    "category": "Import-Export",
}

"""
Related links:
https://developer.blender.org/T34456
https://developer.blender.org/T25408

Usage Notes:

in Maya Mel:
cacheFile -pc2 1 -pcf "<insert filepath of source>" -f "<insert target filename w/o extension>" -dir "<insert directory path for target>" -format "OneFile";

"""

import bpy
from bpy.props import BoolProperty, IntProperty, EnumProperty
import mathutils
from bpy_extras.io_utils import ExportHelper

from os import remove
import time
import math
import struct


def get_sampled_frames(start, end, sampling):
    return [math.modf(start + x * sampling) for x in range(int((end - start) / sampling) + 1)]


def do_export(context, props, filepath):
    mat_x90 = mathutils.Matrix.Rotation(-math.pi/2, 4, 'X')
    ob = context.active_object
    sc = context.scene
    start = props.range_start
    end = props.range_end
    sampling = float(props.sampling)
    apply_modifiers = props.apply_modifiers
    depsgraph = None
    if apply_modifiers:
        depsgraph = context.evaluated_depsgraph_get()
        me = ob.evaluated_get(depsgraph).to_mesh()
    else:
        me = ob.to_mesh()
    vertCount = len(me.vertices)
    sampletimes = get_sampled_frames(start, end, sampling)
    sampleCount = len(sampletimes)

    # Create the header
    headerFormat = '<12siiffi'
    headerStr = struct.pack(headerFormat, b'POINTCACHE2\0',
                            1, vertCount, start, sampling, sampleCount)

    file = open(filepath, "wb")
    file.write(headerStr)

    for frame in sampletimes:
        # stupid modf() gives decimal part first!
        sc.frame_set(int(frame[1]), subframe=frame[0])
        if apply_modifiers:
            me = ob.evaluated_get(depsgraph).to_mesh()
        else:
            me = ob.to_mesh()

        if len(me.vertices) != vertCount:
            bpy.data.meshes.remove(me, do_unlink=True)
            file.close()
            try:
                remove(filepath)
            except:
                empty = open(filepath, 'w')
                empty.write('DUMMIFILE - export failed\n')
                empty.close()
            print('Export failed. Vertexcount of Object is not constant')
            return False

        if props.world_space:
            me.transform(ob.matrix_world)
        if props.rot_x90:
            me.transform(mat_x90)

        for v in me.vertices:
            thisVertex = struct.pack('<fff', float(v.co[0]),
                                     float(v.co[1]),
                                     float(v.co[2]))
            file.write(thisVertex)

    if apply_modifiers:
        ob.evaluated_get(depsgraph).to_mesh_clear()
    else:
        me = ob.to_mesh_clear()

    file.flush()
    file.close()
    return True


# EXPORT OPERATOR
class Export_pc2(bpy.types.Operator, ExportHelper):
    """Export the active Object as a .pc2 Pointcache file"""
    bl_idname = "export_shape.pc2"
    bl_label = "Export Pointcache (.pc2)"

    filename_ext = ".pc2"

    rot_x90: BoolProperty(
        name="Convert to Y-up",
        description="Rotate 90 degrees around X to convert to y-up",
        default=True,)
    world_space: BoolProperty(
        name="Export into Worldspace",
        description="Transform the Vertexcoordinates into Worldspace",
        default=False,)
    apply_modifiers: BoolProperty(
        name="Apply Modifiers",
        description="Applies the Modifiers",
        default=True,)
    range_start: IntProperty(
        name='Start Frame',
        description='First frame to use for Export',
        default=1,)
    range_end: IntProperty(
        name='End Frame',
        description='Last frame to use for Export',
        default=250,)
    sampling: EnumProperty(
        name='Sampling',
        description='Sampling --> frames per sample (0.1 yields 10 samples per frame)',
        items=(('0.01', '0.01', ''),
               ('0.05', '0.05', ''),
               ('0.1', '0.1', ''),
               ('0.2', '0.2', ''),
               ('0.25', '0.25', ''),
               ('0.5', '0.5', ''),
               ('1', '1', ''),
               ('2', '2', ''),
               ('3', '3', ''),
               ('4', '4', ''),
               ('5', '5', ''),
               ('10', '10', ''),
               ),
        default='1',
                            )

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return (
            obj is not None
            and obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT'}
        )

    def execute(self, context):
        start_time = time.time()
        print('\n_____START_____')
        props = self.properties
        filepath = self.filepath
        filepath = bpy.path.ensure_ext(filepath, self.filename_ext)

        exported = do_export(context, props, filepath)

        if exported:
            print('finished export in %s seconds' %
                  ((time.time() - start_time)))
            print(filepath)

        return {'FINISHED'}

    def invoke(self, context, event):
        wm = context.window_manager

        if True:
            # File selector
            wm.fileselect_add(self)  # will run self.execute()
            return {'RUNNING_MODAL'}
        elif True:
            # search the enum
            wm.invoke_search_popup(self)
            return {'RUNNING_MODAL'}
        elif False:
            # Redo popup
            return wm.invoke_props_popup(self, event)
        elif False:
            return self.execute(context)


def menu_func_export_button(self, context):
    self.layout.operator(Export_pc2.bl_idname, text="Pointcache (.pc2)")


classes = [
    Export_pc2,
]


def register():
    for cls in classes:
        bpy.utils.register_class(cls)

    bpy.types.TOPBAR_MT_file_export.append(menu_func_export_button)
    #bpy.types.VIEW3D_PT_tools_objectmode.prepend(menu_func_export_button)


def unregister():
    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_button)
    #bpy.types.VIEW3D_PT_tools_objectmode.remove(menu_func_export_button)
    for cls in classes:
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()