import bpy import bmesh import os import base64 import math import mathutils import mathutils.geometry import struct import re import subprocess from bpy.props import (StringProperty) from bpy_extras.io_utils import (ExportHelper) bl_info = { "name": "Stem3D exporter", "author": "Alex Diener", "blender": (2, 78, 0), "location": "File > Export", "description": "Export as Stem3D", "category": "Import-Export" } def escape_string(string): return re.sub(r'"', r'\"', string) def swizzle_position(vector): return mathutils.Vector((vector[0], vector[2], -vector[1])) def swizzle_scale(vector): return mathutils.Vector((vector[0], vector[2], vector[1])) def swizzle_tangent(vector): return mathutils.Vector((vector[0], vector[2], -vector[1], 1.0)) def swizzle_rotation(quaternion): return mathutils.Quaternion((quaternion[0], quaternion[1], quaternion[3], -quaternion[2])) def get_primitives(blender_mesh, blender_vertex_groups, armature_object): vertices = [] indexes = [] bone_name_map = {} if armature_object is not None: for bone in armature_object.data.bones: bone_name_map[bone.name] = len(bone_name_map) if blender_mesh.uv_layers.active: blender_mesh.calc_tangents() index = 0 for blender_polygon in blender_mesh.polygons: loop_index_list = [] if len(blender_polygon.loop_indices) == 3: loop_index_list.extend(blender_polygon.loop_indices) elif len(blender_polygon.loop_indices) > 3: polyline = [] for loop_index in blender_polygon.loop_indices: vertex_index = blender_mesh.loops[loop_index].vertex_index vertex = blender_mesh.vertices[vertex_index].co polyline.append(mathutils.Vector((vertex[0], vertex[1], vertex[2]))) triangles = mathutils.geometry.tessellate_polygon((polyline,)) for triangle in triangles: loop_index_list.append(blender_polygon.loop_indices[triangle[0]]) loop_index_list.append(blender_polygon.loop_indices[triangle[2]]) loop_index_list.append(blender_polygon.loop_indices[triangle[1]]) else: continue face_normal = blender_polygon.normal face_tangent = mathutils.Vector((0.0, 0.0, 0.0)) for loop_index in blender_polygon.loop_indices: vertex = blender_mesh.loops[loop_index] face_tangent += vertex.tangent face_tangent.normalize() flip_tangent = False for loop_index in blender_polygon.loop_indices: vertex_index = blender_mesh.loops[loop_index].vertex_index vertex = blender_mesh.vertices[vertex_index].co if vertex[0] < 0.0: flip_tangent = True break for loop_index in loop_index_list: vertex_index = blender_mesh.loops[loop_index].vertex_index vertex = blender_mesh.vertices[vertex_index] position = None normal = None tangent = None tex_coord = None color = None bones = None weights = None position = swizzle_position(vertex.co) if blender_polygon.use_smooth: normal = swizzle_position(vertex.normal) tangent = swizzle_tangent(blender_mesh.loops[loop_index].tangent) else: normal = swizzle_position(face_normal) tangent = swizzle_tangent(face_tangent) if flip_tangent: tangent[3] = -tangent[3] if blender_mesh.uv_layers.active: tex_coord = blender_mesh.uv_layers.active.data[loop_index].uv else: tex_coord = (0.0, 0.0) color = (1.0, 1.0, 1.0, 1.0) bones = [] weights = [] if vertex.groups is not None and armature_object is not None: for vertex_group in vertex.groups: if bone_name_map.get(blender_vertex_groups[vertex_group.group].name) is not None: bones.append(bone_name_map[blender_vertex_groups[vertex_group.group].name]) weights.append(vertex_group.weight) if len(bones) == 0: bones.append(0) weights.append(1.0) while len(bones) < 4: bones.append(0) weights.append(0.0) vertices.append([position, tex_coord, normal, tangent, color, bones, weights]) indexes.append(index) index += 1 return vertices, indexes def pack_vertices(vertices, write_ptnxcbw): vertices_packed = bytearray() vertex_size = 12 * 4 for vertex in vertices: vertices_packed += struct.pack("= IGNORE_THRESHOLD or math.fabs(offset.y) >= IGNORE_THRESHOLD or math.fabs(offset.z) >= IGNORE_THRESHOLD or \ math.fabs(scale.x - 1.0) >= IGNORE_THRESHOLD or math.fabs(scale.y - 1.0) >= IGNORE_THRESHOLD or math.fabs(scale.z - 1.0) >= IGNORE_THRESHOLD or \ math.fabs(rotation.x) >= IGNORE_THRESHOLD or math.fabs(rotation.y) >= IGNORE_THRESHOLD or math.fabs(rotation.z) >= IGNORE_THRESHOLD or math.fabs(rotation.w - 1.0) >= IGNORE_THRESHOLD: if pose_bone.name not in affected_bones: affected_bones.append(pose_bone.name) first_keyframe = True for keyframe_index in range(len(keyframe_list)): if first_keyframe: first_keyframe = False else: file.write(b",") #glTF uses bpy.ops.nla.bake(); is it necessary? bpy.context.scene.frame_set(keyframe_list[keyframe_index]) if keyframe_index > 0: last_frame_count = (keyframe_list[keyframe_index] - keyframe_list[keyframe_index - 1]) else: last_frame_count = keyframe_list[-1] if keyframe_index < len(keyframe_list) - 1: next_frame_count = (keyframe_list[keyframe_index + 1] - keyframe_list[keyframe_index]) else: next_frame_count = keyframe_list[0] keyframe_interval = next_frame_count / frame_rate file.write(b"\n\t\t{\n\t\t\t\"interval\": " + format_json_float(keyframe_interval).encode() + b",\n\t\t\t\"bones\": {") first_bone = True for pose_bone in blender_object.pose.bones: #TODO: This is imperfect; if a bone ever moves, it'll be encoded every frame, which is nonideal if pose_bone.name not in affected_bones: continue if first_bone: first_bone = False else: file.write(b",") file.write(b"\n\t\t\t\t\"" + pose_bone.name.encode() + b"\": {") offset, scale, rotation = read_pose_bone_state(pose_bone) offset_curve = (1, 1, 0, 0) scale_curve = (1, 1, 0, 0) rotation_curve = (1, 1, 0, 0) if pose_bone.name in keyframe_dict[keyframe_list[keyframe_index]]: if "location" in keyframe_dict[keyframe_list[keyframe_index]][pose_bone.name]: location_curve = transform_curve(keyframe_dict[keyframe_list[keyframe_index]][pose_bone.name]["location"], last_frame_count, next_frame_count) if "scale" in keyframe_dict[keyframe_list[keyframe_index]][pose_bone.name]: scale_curve = transform_curve(keyframe_dict[keyframe_list[keyframe_index]][pose_bone.name]["scale"], last_frame_count, next_frame_count) if "rotation_quaternion" in keyframe_dict[keyframe_list[keyframe_index]][pose_bone.name]: rotation_curve = transform_curve(keyframe_dict[keyframe_list[keyframe_index]][pose_bone.name]["rotation_quaternion"], last_frame_count, next_frame_count) file.write(b"\n\t\t\t\t\t\"offset\": {\"x\": " + format_json_float(offset.x).encode() + b", \"y\": " + format_json_float(offset.y).encode() + b", \"z\": " + format_json_float(offset.z).encode() + b"},") file.write(b"\n\t\t\t\t\t\"offset_curve\": {\"x_in\": " + format_json_float(offset_curve[0]).encode() + b", \"y_in\": " + format_json_float(offset_curve[1]).encode() + b", \"x_out\": " + format_json_float(offset_curve[2]).encode() + b", \"y_out\": " + format_json_float(offset_curve[3]).encode() + b"},") file.write(b"\n\t\t\t\t\t\"scale\": {\"x\": " + format_json_float(scale.x).encode() + b", \"y\": " + format_json_float(scale.y).encode() + b", \"z\": " + format_json_float(scale.z).encode() + b"},") file.write(b"\n\t\t\t\t\t\"scale_curve\": {\"x_in\": " + format_json_float(scale_curve[0]).encode() + b", \"y_in\": " + format_json_float(scale_curve[1]).encode() + b", \"x_out\": " + format_json_float(scale_curve[2]).encode() + b", \"y_out\": " + format_json_float(scale_curve[3]).encode() + b"},") file.write(b"\n\t\t\t\t\t\"rotation\": {\"x\": " + format_json_float(rotation.x).encode() + b", \"y\": " + format_json_float(rotation.y).encode() + b", \"z\": " + format_json_float(rotation.z).encode() + b", \"w\": " + format_json_float(rotation.w).encode() + b"},") file.write(b"\n\t\t\t\t\t\"rotation_curve\": {\"x_in\": " + format_json_float(rotation_curve[0]).encode() + b", \"y_in\": " + format_json_float(rotation_curve[1]).encode() + b", \"x_out\": " + format_json_float(rotation_curve[2]).encode() + b", \"y_out\": " + format_json_float(rotation_curve[3]).encode() + b"}") file.write(b"\n\t\t\t\t}") file.write(b"\n\t\t\t}\n\t\t}") file.write(b"\n\t],\n\t\"markers\": {") first_marker = True for pose_marker in blender_action.pose_markers: if first_marker: first_marker = False else: file.write(b",") file.write(b"\n\t\t\"" + escape_string(pose_marker.name).encode() + b"\": " + format_json_float(pose_marker.frame / frame_rate).encode()) file.write(b"\n\t}\n}") file.close() blender_object.animation_data.action = saved_action class ExportStem3D(bpy.types.Operator, ExportHelper): bl_idname = 'export_scene.stem3d' bl_label = 'Export Stem3D' filename_ext = '.stem3d' filter_glob = StringProperty(default='*.stem3d', options={'HIDDEN'}) export_format = 'ASCII' def execute(self, context): try: os.mkdir(self.filepath) except FileExistsError: pass for blender_mesh in bpy.data.meshes: write_mesh(context, os.path.join(self.filepath, bpy.path.ensure_ext(blender_mesh.name, ".mesh")), blender_mesh, False) for blender_material in bpy.data.materials: write_material(context, os.path.join(self.filepath, bpy.path.ensure_ext(blender_material.name, ".material")), blender_material, False) for blender_armature in bpy.data.armatures: write_armature(context, os.path.join(self.filepath, bpy.path.ensure_ext(blender_armature.name, ".armature")), blender_armature, False) for blender_action in bpy.data.actions: write_action(context, os.path.join(self.filepath, bpy.path.ensure_ext(blender_action.name, ".animation")), blender_action, False) return {'FINISHED'} def register(): bpy.utils.register_class(ExportStem3D) bpy.types.INFO_MT_file_export.append(menu_func_export_stem3d) def unregister(): bpy.utils.unregister_class(ExportStem3D) bpy.types.INFO_MT_file_export.remove(menu_func_export_stem3d) def menu_func_export_stem3d(self, context): self.layout.operator(ExportStem3D.bl_idname, text='stem3d (JSON)')