# Armory Scene Exporter # http://armory3d.org/ # # Based on Open Game Engine Exchange # http://opengex.org/ # Export plugin for Blender by Eric Lengyel # Copyright 2015, Terathon Software LLC # # This software is licensed under the Creative Commons # Attribution-ShareAlike 3.0 Unported License: # http://creativecommons.org/licenses/by-sa/3.0/deed.en_US import os import bpy import math from mathutils import * import time import subprocess import shutil import arm.utils import arm.write_probes as write_probes import arm.assets as assets import arm.log as log import arm.material.make as make_material import arm.material.mat_batch as mat_batch import arm.make_renderpath as make_renderpath NodeTypeNode = 0 NodeTypeBone = 1 NodeTypeMesh = 2 NodeTypeLamp = 3 NodeTypeCamera = 4 NodeTypeSpeaker = 5 NodeTypeDecal = 6 AnimationTypeSampled = 0 AnimationTypeLinear = 1 AnimationTypeBezier = 2 ExportEpsilon = 1.0e-6 structIdentifier = ["object", "bone_object", "mesh_object", "lamp_object", "camera_object", "speaker_object", "decal_object"] subtranslationName = ["xloc", "yloc", "zloc"] subrotationName = ["xrot", "yrot", "zrot"] subscaleName = ["xscl", "yscl", "zscl"] deltaSubtranslationName = ["dxloc", "dyloc", "dzloc"] deltaSubrotationName = ["dxrot", "dyrot", "dzrot"] deltaSubscaleName = ["dxscl", "dyscl", "dzscl"] axisName = ["x", "y", "z"] class Vertex: # Based on https://github.com/Kupoman/blendergltf/blob/master/blendergltf.py __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index") def __init__(self, mesh, loop): self.vertex_index = loop.vertex_index loop_idx = loop.index self.co = mesh.vertices[self.vertex_index].co[:] self.normal = loop.normal[:] self.uvs = tuple(layer.data[loop_idx].uv[:] for layer in mesh.uv_layers) self.col = [0.0, 0.0, 0.0] if len(mesh.vertex_colors) > 0: self.col = mesh.vertex_colors[0].data[loop_idx].color[:] # self.colors = tuple(layer.data[loop_idx].color[:] for layer in mesh.vertex_colors) self.loop_indices = [loop_idx] # Take the four most influential groups # groups = sorted(mesh.vertices[self.vertex_index].groups, key=lambda group: group.weight, reverse=True) # if len(groups) > 4: # groups = groups[:4] # self.bone_weights = [group.weight for group in groups] # self.bone_indices = [group.group for group in groups] # self.bone_count = len(self.bone_weights) self.index = 0 def __hash__(self): return hash((self.co, self.normal, self.uvs)) def __eq__(self, other): eq = ( (self.co == other.co) and (self.normal == other.normal) and (self.uvs == other.uvs) and (self.col == other.col) ) if eq: indices = self.loop_indices + other.loop_indices self.loop_indices = indices other.loop_indices = indices return eq class ArmoryExporter: '''Export to Armory format''' def write_matrix(self, matrix): return [matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3], matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3], matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3]] # def write_vector3d(self, vector): # return [vector[0], vector[1], vector[2]] def get_meshes_file_path(self, object_id, compressed=False): index = self.filepath.rfind('/') mesh_fp = self.filepath[:(index + 1)] + 'meshes/' if not os.path.exists(mesh_fp): os.makedirs(mesh_fp) ext = '.zip' if compressed else '.arm' return mesh_fp + object_id + ext @staticmethod def get_bobject_type(bobject): if bobject.type == "MESH": if len(bobject.data.polygons) != 0: return NodeTypeMesh elif bobject.type == "FONT": return NodeTypeMesh elif bobject.type == "META": # Metaball return NodeTypeMesh elif bobject.type == "LAMP": return NodeTypeLamp elif bobject.type == "CAMERA": return NodeTypeCamera elif bobject.type == "SPEAKER": return NodeTypeSpeaker return NodeTypeNode @staticmethod def get_shape_keys(mesh): if not hasattr(mesh, 'shape_keys'): # Metaball return None shape_keys = mesh.shape_keys if shape_keys and len(shape_keys.key_blocks) > 1: return shape_keys return None def find_bone(self, name): for bobject_ref in self.bobjectBoneArray.items(): if bobject_ref[0].name == name: return bobject_ref return None @staticmethod def classify_animation_curve(fcurve): linear_count = 0 bezier_count = 0 for key in fcurve.keyframe_points: interp = key.interpolation if interp == "LINEAR": linear_count += 1 elif interp == "BEZIER": bezier_count += 1 else: return AnimationTypeSampled if bezier_count == 0: return AnimationTypeLinear elif linear_count == 0: return AnimationTypeBezier return AnimationTypeSampled # @staticmethod # def animation_keys_different(fcurve): # key_count = len(fcurve.keyframe_points) # if key_count > 0: # key1 = fcurve.keyframe_points[0].co[1] # for i in range(1, key_count): # key2 = fcurve.keyframe_points[i].co[1] # if math.fabs(key2 - key1) > ExportEpsilon: # return True # return False # @staticmethod # def animation_tangents_nonzero(fcurve): # key_count = len(fcurve.keyframe_points) # if key_count > 0: # key = fcurve.keyframe_points[0].co[1] # left = fcurve.keyframe_points[0].handle_left[1] # right = fcurve.keyframe_points[0].handle_right[1] # if (math.fabs(key - left) > ExportEpsilon) or (math.fabs(right - key) > ExportEpsilon): # return True # for i in range(1, key_count): # key = fcurve.keyframe_points[i].co[1] # left = fcurve.keyframe_points[i].handle_left[1] # right = fcurve.keyframe_points[i].handle_right[1] # if (math.fabs(key - left) > ExportEpsilon) or (math.fabs(right - key) > ExportEpsilon): # return True # return False # @staticmethod # def matrices_different(m1, m2): # for i in range(4): # for j in range(4): # if math.fabs(m1[i][j] - m2[i][j]) > ExportEpsilon: # return True # return False @staticmethod def collect_bone_animation(armature, name): path = "pose.bones[\"" + name + "\"]." curve_array = [] if armature.animation_data: action = armature.animation_data.action if action: for fcurve in action.fcurves: if fcurve.data_path.startswith(path): curve_array.append(fcurve) return curve_array # @staticmethod # def animation_present(fcurve, kind): # if kind != AnimationTypeBezier: # return ArmoryExporter.animation_keys_different(fcurve) # return ((ArmoryExporter.animation_keys_different(fcurve)) or (ArmoryExporter.animation_tangents_nonzero(fcurve))) @staticmethod def calc_tangent(v0, v1, v2, uv0, uv1, uv2): deltaPos1 = v1 - v0 deltaPos2 = v2 - v0 deltaUV1 = uv1 - uv0 deltaUV2 = uv2 - uv0 d = (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x) if d != 0: r = 1.0 / d else: r = 1.0 tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y) * r # bitangent = (deltaPos2 * deltaUV1.x - deltaPos1 * deltaUV2.x) * r return tangent def export_bone(self, armature, bone, scene, o, action): bobjectRef = self.bobjectBoneArray.get(bone) if bobjectRef: o['type'] = structIdentifier[bobjectRef["objectType"]] o['name'] = bobjectRef["structName"] self.export_bone_transform(armature, bone, scene, o, action) o['children'] = [] for subbobject in bone.children: so = {} self.export_bone(armature, subbobject, scene, so, action) o['children'].append(so) def export_pose_markers(self, oanim, action): if action.pose_markers == None or len(action.pose_markers) == 0: return oanim['marker_frames'] = [] oanim['marker_names'] = [] for m in action.pose_markers: oanim['marker_frames'].append(int(m.frame)) oanim['marker_names'].append(m.name) def export_object_sampled_animation(self, bobject, scene, o): # This function exports animation as full 4x4 matrices for each frame animation_flag = False # m1 = bobject.matrix_local.copy() # Font in # for i in range(self.beginFrame, self.endFrame): # scene.frame_set(i) # m2 = bobject.matrix_local # if ArmoryExporter.matrices_different(m1, m2): # animation_flag = True # break animation_flag = bobject.animation_data != None and bobject.animation_data.action != None and bobject.type != 'ARMATURE' # Font out if animation_flag: if not 'object_actions' in o: o['object_actions'] = [] action = bobject.animation_data.action aname = arm.utils.safestr(arm.utils.asset_name(action)) fp = self.get_meshes_file_path('action_' + aname, compressed=self.is_compress(bobject.data)) assets.add(fp) ext = '.zip' if self.is_compress(bobject.data) else '' o['object_actions'].append('action_' + aname + ext) oaction = {} oaction['sampled'] = True oaction['name'] = action.name oanim = {} oaction['anim'] = oanim tracko = {} tracko['target'] = "transform" tracko['frames'] = [] begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) end_frame += 1 for i in range(begin_frame, end_frame): tracko['frames'].append(int(i - begin_frame)) tracko['frames'].append(int(end_frame)) tracko['values'] = [] for i in range(begin_frame, end_frame): scene.frame_set(i) tracko['values'] += self.write_matrix(bobject.matrix_local) # Continuos array of matrix transforms oanim['tracks'] = [tracko] self.export_pose_markers(oanim, action) if True: #action.arm_cached == False or not os.path.exists(fp): print('Exporting object action ' + aname) actionf = {} actionf['objects'] = [] actionf['objects'].append(oaction) oaction['type'] = 'object' oaction['name'] = aname oaction['data_ref'] = '' oaction['transform'] = [] arm.utils.write_arm(fp, actionf) def export_key_frames(self, fcurve): keyo = [] key_count = len(fcurve.keyframe_points) for i in range(key_count): frame = fcurve.keyframe_points[i].co[0] - self.beginFrame keyo.append(int(frame)) return keyo def export_key_frame_control_points(self, fcurve): keyminuso = [] key_count = len(fcurve.keyframe_points) for i in range(key_count): ctrl = fcurve.keyframe_points[i].handle_left[0] - self.beginFrame keyminuso.append(ctrl) keypluso = [] for i in range(key_count): ctrl = fcurve.keyframe_points[i].handle_right[0] - self.beginFrame keypluso.append(ctrl) return keyminuso, keypluso def export_key_values(self, fcurve): keyo = [] key_count = len(fcurve.keyframe_points) for i in range(key_count): value = fcurve.keyframe_points[i].co[1] keyo.append(value) return keyo def export_key_value_control_points(self, fcurve): keyminuso = [] key_count = len(fcurve.keyframe_points) for i in range(key_count): ctrl = fcurve.keyframe_points[i].handle_left[1] keyminuso.append(ctrl) keypluso = [] for i in range(key_count): ctrl = fcurve.keyframe_points[i].handle_right[1] keypluso.append(ctrl) return keypluso, keypluso def export_animation_track(self, fcurve, kind, target, newline): # This function exports a single animation track. The curve types for the # Frame and Value structures are given by the kind parameter. tracko = {} tracko['target'] = target if kind != AnimationTypeBezier: tracko['frames'] = self.export_key_frames(fcurve) tracko['values'] = self.export_key_values(fcurve) else: tracko['curve'] = 'bezier' tracko['frames'] = self.export_key_frames(fcurve) tracko['frames_control_plus'], tracko['frames_control_minus'] = self.export_key_frame_control_points(fcurve) tracko['values'] = self.export_key_values(fcurve) tracko['values_control_plus'], tracko['values_control_minus'] = self.export_key_value_control_points(fcurve) return tracko def export_object_transform(self, bobject, scene, o): locAnimCurve = [None, None, None] rotAnimCurve = [None, None, None] sclAnimCurve = [None, None, None] locAnimKind = [0, 0, 0] rotAnimKind = [0, 0, 0] sclAnimKind = [0, 0, 0] deltaPosAnimCurve = [None, None, None] deltaRotAnimCurve = [None, None, None] deltaSclAnimCurve = [None, None, None] deltaPosAnimKind = [0, 0, 0] deltaRotAnimKind = [0, 0, 0] deltaSclAnimKind = [0, 0, 0] locationAnimated = False rotationAnimated = False scaleAnimated = False locAnimated = [False, False, False] rotAnimated = [False, False, False] sclAnimated = [False, False, False] deltaPositionAnimated = False deltaRotationAnimated = False deltaScaleAnimated = False deltaPosAnimated = [False, False, False] deltaRotAnimated = [False, False, False] deltaSclAnimated = [False, False, False] mode = bobject.rotation_mode sampledAnimation = ArmoryExporter.sample_animation_flag or mode == "QUATERNION" or mode == "AXIS_ANGLE" if not sampledAnimation and bobject.animation_data and bobject.type != 'ARMATURE': action = bobject.animation_data.action if action: for fcurve in action.fcurves: kind = ArmoryExporter.classify_animation_curve(fcurve) if kind != AnimationTypeSampled: if fcurve.data_path == "location": for i in range(3): if (fcurve.array_index == i) and (not locAnimCurve[i]): locAnimCurve[i] = fcurve locAnimKind[i] = kind locAnimated[i] = True elif fcurve.data_path == "delta_location": for i in range(3): if (fcurve.array_index == i) and (not deltaPosAnimCurve[i]): deltaPosAnimCurve[i] = fcurve deltaPosAnimKind[i] = kind deltaPosAnimated[i] = True elif fcurve.data_path == "rotation_euler": for i in range(3): if (fcurve.array_index == i) and (not rotAnimCurve[i]): rotAnimCurve[i] = fcurve rotAnimKind[i] = kind rotAnimated[i] = True elif fcurve.data_path == "delta_rotation_euler": for i in range(3): if (fcurve.array_index == i) and (not deltaRotAnimCurve[i]): deltaRotAnimCurve[i] = fcurve deltaRotAnimKind[i] = kind deltaRotAnimated[i] = True elif fcurve.data_path == "scale": for i in range(3): if (fcurve.array_index == i) and (not sclAnimCurve[i]): sclAnimCurve[i] = fcurve sclAnimKind[i] = kind sclAnimated[i] = True elif fcurve.data_path == "delta_scale": for i in range(3): if (fcurve.array_index == i) and (not deltaSclAnimCurve[i]): deltaSclAnimCurve[i] = fcurve deltaSclAnimKind[i] = kind deltaSclAnimated[i] = True elif (fcurve.data_path == "rotation_axis_angle") or (fcurve.data_path == "rotation_quaternion") or (fcurve.data_path == "delta_rotation_quaternion"): sampledAnimation = True break else: sampledAnimation = True break locationAnimated = locAnimated[0] | locAnimated[1] | locAnimated[2] rotationAnimated = rotAnimated[0] | rotAnimated[1] | rotAnimated[2] scaleAnimated = sclAnimated[0] | sclAnimated[1] | sclAnimated[2] deltaPositionAnimated = deltaPosAnimated[0] | deltaPosAnimated[1] | deltaPosAnimated[2] deltaRotationAnimated = deltaRotAnimated[0] | deltaRotAnimated[1] | deltaRotAnimated[2] deltaScaleAnimated = deltaSclAnimated[0] | deltaSclAnimated[1] | deltaSclAnimated[2] if (sampledAnimation) or ((not locationAnimated) and (not rotationAnimated) and (not scaleAnimated) and (not deltaPositionAnimated) and (not deltaRotationAnimated) and (not deltaScaleAnimated)): # If there's no keyframe animation at all, then write the object transform as a single 4x4 matrix. # We might still be exporting sampled animation below. o['transform'] = {} if sampledAnimation: o['transform']['target'] = "transform" o['transform']['values'] = self.write_matrix(bobject.matrix_local) if sampledAnimation: self.export_object_sampled_animation(bobject, scene, o) else: # Animated structFlag = False o['transform'] = {} o['transform']['values'] = self.write_matrix(bobject.matrix_local) if not 'object_actions' in o: o['object_actions'] = [] action = bobject.animation_data.action aname = arm.utils.safestr(arm.utils.asset_name(action)) fp = self.get_meshes_file_path('action_' + aname, compressed=self.is_compress(bobject.data)) assets.add(fp) ext = '.zip' if self.is_compress(bobject.data) else '' o['object_actions'].append('action_' + aname + ext) oaction = {} oaction['name'] = action.name # Export the animation tracks oanim = {} oaction['anim'] = oanim oanim['begin'] = int(action.frame_range[0] - self.beginFrame) oanim['end'] = int(action.frame_range[1] - self.beginFrame) oanim['tracks'] = [] self.export_pose_markers(oanim, action) if locationAnimated: for i in range(3): if locAnimated[i]: tracko = self.export_animation_track(locAnimCurve[i], locAnimKind[i], subtranslationName[i], structFlag) oanim['tracks'].append(tracko) structFlag = True if rotationAnimated: for i in range(3): if rotAnimated[i]: tracko = self.export_animation_track(rotAnimCurve[i], rotAnimKind[i], subrotationName[i], structFlag) oanim['tracks'].append(tracko) structFlag = True if scaleAnimated: for i in range(3): if sclAnimated[i]: tracko = self.export_animation_track(sclAnimCurve[i], sclAnimKind[i], subscaleName[i], structFlag) oanim['tracks'].append(tracko) structFlag = True if deltaPositionAnimated: for i in range(3): if deltaPosAnimated[i]: tracko = self.export_animation_track(deltaPosAnimCurve[i], deltaPosAnimKind[i], deltaSubtranslationName[i], structFlag) oanim['tracks'].append(tracko) oanim['has_delta'] = True structFlag = True if deltaRotationAnimated: for i in range(3): if deltaRotAnimated[i]: tracko = self.export_animation_track(deltaRotAnimCurve[i], deltaRotAnimKind[i], deltaSubrotationName[i], structFlag) oanim['tracks'].append(tracko) oanim['has_delta'] = True structFlag = True if deltaScaleAnimated: for i in range(3): if deltaSclAnimated[i]: tracko = self.export_animation_track(deltaSclAnimCurve[i], deltaSclAnimKind[i], deltaSubscaleName[i], structFlag) oanim['tracks'].append(tracko) oanim['has_delta'] = True structFlag = True if True: #action.arm_cached == False or not os.path.exists(fp): print('Exporting object action ' + aname) actionf = {} actionf['objects'] = [] actionf['objects'].append(oaction) oaction['type'] = 'object' oaction['name'] = aname oaction['data_ref'] = '' oaction['transform'] = [] arm.utils.write_arm(fp, actionf) def process_bone(self, bone): if ArmoryExporter.export_all_flag or bone.select: self.bobjectBoneArray[bone] = {"objectType" : NodeTypeBone, "structName" : bone.name} for subbobject in bone.children: self.process_bone(subbobject) def process_bobject(self, bobject): if ArmoryExporter.export_all_flag or bobject.select: btype = ArmoryExporter.get_bobject_type(bobject) if ArmoryExporter.option_mesh_only and btype != NodeTypeMesh: return self.bobjectArray[bobject] = {"objectType" : btype, "structName" : arm.utils.asset_name(bobject)} if bobject.type == "ARMATURE": skeleton = bobject.data if skeleton: for bone in skeleton.bones: if not bone.parent: self.process_bone(bone) if bobject.type != 'MESH' or bobject.arm_instanced == False: for subbobject in bobject.children: self.process_bobject(subbobject) def process_skinned_meshes(self): for bobjectRef in self.bobjectArray.items(): if bobjectRef[1]["objectType"] == NodeTypeMesh: armature = bobjectRef[0].find_armature() if armature: for bone in armature.data.bones: boneRef = self.find_bone(bone.name) if boneRef: # If an object is used as a bone, then we force its type to be a bone boneRef[1]["objectType"] = NodeTypeBone def export_bone_transform(self, armature, bone, scene, o, action): curve_array = self.collect_bone_animation(armature, bone.name) animation = len(curve_array) != 0 or ArmoryExporter.sample_animation_flag transform = bone.matrix_local.copy() parent_bone = bone.parent if parent_bone: transform = parent_bone.matrix_local.inverted() * transform pose_bone = armature.pose.bones.get(bone.name) if pose_bone: transform = pose_bone.matrix.copy() parent_pose_bone = pose_bone.parent if parent_pose_bone: transform = parent_pose_bone.matrix.inverted_safe() * transform o['transform'] = {} o['transform']['values'] = self.write_matrix(transform) if animation and pose_bone: begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) # animation_flag = False # m1 = pose_bone.matrix.copy() # for i in range(begin_frame, end_frame): # scene.frame_set(i) # m2 = pose_bone.matrix # if ArmoryExporter.matrices_different(m1, m2): # animation_flag = True # break # if animation_flag: o['anim'] = {} tracko = {} o['anim']['tracks'] = [tracko] tracko['target'] = "transform" tracko['frames'] = [] for i in range(begin_frame, end_frame + 1): tracko['frames'].append(i - begin_frame) tracko['values'] = [] self.bone_tracks.append((tracko['values'], pose_bone)) def use_default_material(self, bobject, o): if arm.utils.export_bone_data(bobject): o['material_refs'].append('armdefaultskin') self.defaultSkinMaterialObjects.append(bobject) else: o['material_refs'].append('armdefault') self.defaultMaterialObjects.append(bobject) def export_material_ref(self, bobject, material, index, o): if material == None: # Use default for empty mat slots self.use_default_material(bobject, o) return if not material in self.materialArray: self.materialArray.append(material) o['material_refs'].append(arm.utils.asset_name(material)) def export_particle_system_ref(self, psys, index, o): if psys.settings in self.particleSystemArray: # or not modifier.show_render: return if psys.settings.dupli_object == None or psys.settings.render_type != 'OBJECT': return self.particleSystemArray[psys.settings] = {"structName" : psys.settings.name} pref = {} pref['name'] = psys.name pref['seed'] = psys.seed pref['particle'] = psys.settings.name o['particle_refs'].append(pref) def get_view3d_area(self): screen = bpy.context.window.screen for area in screen.areas: if area.type == 'VIEW_3D': return area return None def get_viewport_view_matrix(self): if self.play_area == None: self.play_area = self.get_view3d_area() if self.play_area == None: return None for space in self.play_area.spaces: if space.type == 'VIEW_3D': return space.region_3d.view_matrix return None def get_viewport_projection_matrix(self): if self.play_area == None: self.play_area = self.get_view3d_area() if self.play_area == None: return None, False for space in self.play_area.spaces: if space.type == 'VIEW_3D': # return space.region_3d.perspective_matrix # pesp = window * view return space.region_3d.window_matrix, space.region_3d.is_perspective return None, False def get_viewport_panels_w(self): w = 0 screen = bpy.context.window.screen for area in screen.areas: if area.type == 'VIEW_3D': for region in area.regions: if region.type == 'TOOLS' or region.type == 'UI': if region.width > 1: w += region.width return w def get_viewport_w(self): screen = bpy.context.window.screen for area in screen.areas: if area.type == 'VIEW_3D': for region in area.regions: if region.type == 'HEADER': # Use header to report full width, panels included return region.width return 0 def write_bone_matrices(self, scene, action): begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) if len(self.bone_tracks) > 0: for i in range(begin_frame, end_frame + 1): scene.frame_set(i) for track in self.bone_tracks: values, pose_bone = track[0], track[1] parent = pose_bone.parent if parent: values += self.write_matrix(parent.matrix.inverted_safe() * pose_bone.matrix) else: values += self.write_matrix(pose_bone.matrix) def has_baked_material(self, bobject, materials): for mat in materials: if mat == None: continue baked_mat = mat.name + '_' + bobject.name + '_baked' if baked_mat in bpy.data.materials: return True return False def slot_to_material(self, bobject, slot): mat = slot.material # Pick up backed material if present if mat != None: baked_mat = mat.name + '_' + bobject.name + '_baked' if baked_mat in bpy.data.materials: mat = bpy.data.materials[baked_mat] return mat def export_object(self, bobject, scene, parento=None): # This function exports a single object in the scene and includes its name, # object reference, material references (for meshes), and transform. # Subobjects are then exported recursively. if self.preprocess_object(bobject) == False: return bobjectRef = self.bobjectArray.get(bobject) if bobjectRef: type = bobjectRef["objectType"] # Linked object, not present in scene if bobject not in self.objectToArmObjectDict: o = {} o['traits'] = [] o['spawn'] = False self.objectToArmObjectDict[bobject] = o o = self.objectToArmObjectDict[bobject] o['type'] = structIdentifier[type] o['name'] = bobjectRef["structName"] if bobject.parent_type == "BONE": o['parent_bone'] = bobject.parent_bone if bobject.hide_render: o['visible'] = False if not bobject.cycles_visibility.camera: o['visible_mesh'] = False if not bobject.cycles_visibility.shadow: o['visible_shadow'] = False if bobject.arm_spawn == False: o['spawn'] = False if bobject.arm_mobile == False: o['mobile'] = False if bobject.dupli_type == 'GROUP' and bobject.dupli_group != None: o['group_ref'] = bobject.dupli_group.name if bobject.users_group != None and len(bobject.users_group) > 0: o['groups'] = [] for g in bobject.users_group: o['groups'].append(g.name) if bobject.arm_tilesheet != '': o['tilesheet_ref'] = bobject.arm_tilesheet o['tilesheet_action_ref'] = bobject.arm_tilesheet_action layer_found = False for l in self.active_layers: if bobject.layers[l] == True: layer_found = True break if bpy.app.version >= (2, 80, 1): # 2.8 Spawn all layers for now layer_found = True if layer_found == False: o['spawn'] = False # Export the object reference and material references objref = bobject.data if objref != None: objname = arm.utils.asset_name(objref) # Lods if bobject.type == 'MESH' and hasattr(objref, 'arm_lodlist') and len(objref.arm_lodlist) > 0: o['lods'] = [] for l in objref.arm_lodlist: if l.enabled_prop == False: continue lod = {} lod['object_ref'] = l.name lod['screen_size'] = l.screen_size_prop o['lods'].append(lod) if objref.arm_lod_material: o['lod_material'] = True if type == NodeTypeMesh: if not objref in self.meshArray: self.meshArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.meshArray[objref]["objectTable"].append(bobject) oid = arm.utils.safestr(self.meshArray[objref]["structName"]) if ArmoryExporter.option_mesh_per_file: ext = '' if not self.is_compress(objref) else '.zip' if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: ext = '.json' o['data_ref'] = 'mesh_' + oid + ext + '/' + oid else: o['data_ref'] = oid o['material_refs'] = [] for i in range(len(bobject.material_slots)): mat = self.slot_to_material(bobject, bobject.material_slots[i]) # Export ref self.export_material_ref(bobject, mat, i, o) # Decal flag if mat != None and mat.arm_decal: o['type'] = 'decal_object' # No material, mimic cycles and assign default if len(o['material_refs']) == 0: self.use_default_material(bobject, o) num_psys = len(bobject.particle_systems) if num_psys > 0: o['particle_refs'] = [] for i in range(0, num_psys): self.export_particle_system_ref(bobject.particle_systems[i], i, o) o['dimensions'] = [0.0, 0.0, 0.0] for i in range(0, 3): if bobject.scale[i] != 0: o['dimensions'][i] = bobject.dimensions[i] / bobject.scale[i] # Origin not in geometry center if hasattr(bobject.data, 'arm_aabb'): dx = bobject.data.arm_aabb[0] dy = bobject.data.arm_aabb[1] dz = bobject.data.arm_aabb[2] if dx > o['dimensions'][0]: o['dimensions'][0] = dx if dy > o['dimensions'][1]: o['dimensions'][1] = dy if dz > o['dimensions'][2]: o['dimensions'][2] = dz #shapeKeys = ArmoryExporter.get_shape_keys(objref) #if shapeKeys: # self.ExportMorphWeights(bobject, shapeKeys, scene, o) elif type == NodeTypeLamp: if not objref in self.lampArray: self.lampArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.lampArray[objref]["objectTable"].append(bobject) o['data_ref'] = self.lampArray[objref]["structName"] elif type == NodeTypeCamera: if 'spawn' in o and o['spawn'] == False: self.camera_spawned = False else: self.camera_spawned = True if not objref in self.cameraArray: self.cameraArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.cameraArray[objref]["objectTable"].append(bobject) o['data_ref'] = self.cameraArray[objref]["structName"] elif type == NodeTypeSpeaker: if not objref in self.speakerArray: self.speakerArray[objref] = {"structName" : objname, "objectTable" : [bobject]} else: self.speakerArray[objref]["objectTable"].append(bobject) o['data_ref'] = self.speakerArray[objref]["structName"] # Export the transform. If object is animated, then animation tracks are exported here if bobject.type != 'ARMATURE' and bobject.animation_data != None: action = bobject.animation_data.action export_actions = [action] for track in bobject.animation_data.nla_tracks: if track.strips == None: continue for strip in track.strips: if strip.action == None or strip.action in export_actions: continue export_actions.append(strip.action) orig_action = action for a in export_actions: bobject.animation_data.action = a self.export_object_transform(bobject, scene, o) if len(export_actions) >= 2 and export_actions[0] == None: # No action assigned o['object_actions'].insert(0, 'null') bobject.animation_data.action = orig_action else: self.export_object_transform(bobject, scene, o) # If the object is parented to a bone and is not relative, then undo the bone's transform if bobject.parent_type == "BONE": armature = bobject.parent.data bone = armature.bones[bobject.parent_bone] if not bone.use_relative_parent: bone_translation = Vector((0, bone.length, 0)) + bone.head o['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] # Viewport Camera - overwrite active camera matrix with viewport matrix if type == NodeTypeCamera and bpy.data.worlds['Arm'].arm_play_camera != 'Scene' and self.scene.camera != None and bobject.name == self.scene.camera.name: viewport_matrix = self.get_viewport_view_matrix() if viewport_matrix != None: o['transform']['values'] = self.write_matrix(viewport_matrix.inverted()) # Do not apply parent matrix o['local_transform_only'] = True if bobject.type == 'ARMATURE' and bobject.data != None: bdata = bobject.data # Armature data action = None # Reference start action adata = bobject.animation_data # Active action if adata != None: action = adata.action if action == None: log.warn('Object ' + bobject.name + ' - No action assigned, setting to pose') bobject.animation_data_create() actions = bpy.data.actions action = actions.get('armorypose') if action == None: action = actions.new(name='armorypose') # Export actions export_actions = [action] # hasattr - armature modifier may reference non-parent armature object to deform with if hasattr(adata, 'nla_tracks') and adata.nla_tracks != None: for track in adata.nla_tracks: if track.strips == None: continue for strip in track.strips: if strip.action == None: continue if strip.action.name == action.name: continue export_actions.append(strip.action) armatureid = arm.utils.safestr(arm.utils.asset_name(bdata)) ext = '.zip' if self.is_compress(bdata) else '' o['bone_actions'] = [] for action in export_actions: aname = arm.utils.safestr(arm.utils.asset_name(action)) o['bone_actions'].append('action_' + armatureid + '_' + aname + ext) orig_action = bobject.animation_data.action for action in export_actions: aname = arm.utils.safestr(arm.utils.asset_name(action)) bobject.animation_data.action = action fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=self.is_compress(bdata)) assets.add(fp) if bdata.arm_cached == False or not os.path.exists(fp): print('Exporting armature action ' + aname) bones = [] self.bone_tracks = [] for bone in bdata.bones: if not bone.parent: boneo = {} self.export_bone(bobject, bone, scene, boneo, action) bones.append(boneo) self.write_bone_matrices(scene, action) if len(bones) > 0 and 'anim' in bones[0]: self.export_pose_markers(bones[0]['anim'], action) # Save action separately action_obj = {} action_obj['name'] = aname action_obj['objects'] = bones arm.utils.write_arm(fp, action_obj) bobject.animation_data.action = orig_action # TODO: cache per action bdata.arm_cached = True if parento == None: self.output['objects'].append(o) else: parento['children'].append(o) self.post_export_object(bobject, o, type) if not hasattr(o, 'children') and len(bobject.children) > 0: o['children'] = [] if bobject.type != 'MESH' or bobject.arm_instanced == False: for subbobject in bobject.children: self.export_object(subbobject, scene, o) def export_skin(self, bobject, armature, vert_list, o): # This function exports all skinning data, which includes the skeleton # and per-vertex bone influence data oskin = {} o['skin'] = oskin # Write the skin bind pose transform otrans = {} oskin['transform'] = otrans otrans['values'] = self.write_matrix(bobject.matrix_world) # Write the bone object reference array oskin['bone_ref_array'] = [] oskin['bone_len_array'] = [] bone_array = armature.data.bones bone_count = len(bone_array) rpdat = arm.utils.get_rp() max_bones = rpdat.arm_skin_max_bones if bone_count > max_bones: log.warn(bobject.name + ' - ' + str(bone_count) + ' bones found, exceeds maximum of ' + str(max_bones) + ' bones defined - raise the value in Camera Data - Armory Render Props - Max Bones') for i in range(bone_count): boneRef = self.find_bone(bone_array[i].name) if boneRef: oskin['bone_ref_array'].append(boneRef[1]["structName"]) oskin['bone_len_array'].append(bone_array[i].length) else: oskin['bone_ref_array'].append("null") oskin['bone_len_array'].append(0.0) # Write the bind pose transform array oskin['transformsI'] = [] if rpdat.arm_skin == 'CPU': for i in range(bone_count): skeletonI = (armature.matrix_world * bone_array[i].matrix_local).inverted() oskin['transformsI'].append(self.write_matrix(skeletonI)) else: for i in range(bone_count): skeletonI = (armature.matrix_world * bone_array[i].matrix_local).inverted() skeletonI = skeletonI * bobject.matrix_world oskin['transformsI'].append(self.write_matrix(skeletonI)) # Export the per-vertex bone influence data group_remap = [] for group in bobject.vertex_groups: group_name = group.name for i in range(bone_count): if bone_array[i].name == group_name: group_remap.append(i) break else: group_remap.append(-1) bone_count_array = [] bone_index_array = [] bone_weight_array = [] warn_bones = False vertices = bobject.data.vertices for v in vert_list: bone_count = 0 total_weight = 0.0 bone_values = [] for g in vertices[v.vertex_index].groups: bone_index = group_remap[g.group] bone_weight = g.weight if bone_index >= 0 and bone_weight != 0.0: bone_values.append((bone_weight, bone_index)) total_weight += bone_weight bone_count += 1 # Take highest weights bone_values.sort() bone_values.reverse(); if bone_count > 4: # Four bones max bone_count = 4 bone_values = bone_values[:4] warn_bones = True bone_count_array.append(bone_count) for bv in bone_values: bone_weight_array.append(bv[0]) bone_index_array.append(bv[1]) if total_weight != 0.0: normalizer = 1.0 / total_weight for i in range(-bone_count, 0): bone_weight_array[i] *= normalizer if warn_bones: log.warn(bobject.name + ' - more than 4 bones influence single vertex - taking highest weights') # Write the bone count array. There is one entry per vertex. oskin['bone_count_array'] = bone_count_array # Write the bone index array. The number of entries is the sum of the bone counts for all vertices. oskin['bone_index_array'] = bone_index_array # Write the bone weight array. The number of entries is the sum of the bone counts for all vertices. oskin['bone_weight_array'] = bone_weight_array # Bone constraints for bone in armature.pose.bones: if len(bone.constraints) > 0: if 'constraints' not in oskin: oskin['constraints'] = [] self.add_constraints(bone, oskin, bone=True) # def export_skin_fast(self, bobject, armature, vert_list, o): # oskin = {} # o['skin'] = oskin # otrans = {} # oskin['transform'] = otrans # otrans['values'] = self.write_matrix(bobject.matrix_world) # oskin['bone_ref_array'] = [] # bone_array = armature.data.bones # bone_count = len(bone_array) # for i in range(bone_count): # boneRef = self.find_bone(bone_array[i].name) # if boneRef: # oskin['bone_ref_array'].append(boneRef[1]["structName"]) # else: # oskin['bone_ref_array'].append("null") # oskin['transforms'] = [] # for i in range(bone_count): # oskin['transforms'].append(self.write_matrix(armature.matrix_world * bone_array[i].matrix_local)) # bone_count_array = [] # bone_index_array = [] # bone_weight_array = [] # for vtx in vert_list: # bone_count_array.append(vtx.bone_count) # bone_index_array += vtx.bone_indices # bone_weight_array += vtx.bone_weights # oskin['bone_count_array'] = bone_count_array # oskin['bone_index_array'] = bone_index_array # oskin['bone_weight_array'] = bone_weight_array def calc_tangents(self, posa, nora, uva, ia): triangle_count = int(len(ia) / 3) vertex_count = int(len(posa) / 3) tangents = [0] * vertex_count * 3 # bitangents = [0] * vertex_count * 3 for i in range(0, triangle_count): i0 = ia[i * 3 + 0] i1 = ia[i * 3 + 1] i2 = ia[i * 3 + 2] # TODO: Slow v0 = Vector((posa[i0 * 3 + 0], posa[i0 * 3 + 1], posa[i0 * 3 + 2])) v1 = Vector((posa[i1 * 3 + 0], posa[i1 * 3 + 1], posa[i1 * 3 + 2])) v2 = Vector((posa[i2 * 3 + 0], posa[i2 * 3 + 1], posa[i2 * 3 + 2])) uv0 = Vector((uva[i0 * 2 + 0], uva[i0 * 2 + 1])) uv1 = Vector((uva[i1 * 2 + 0], uva[i1 * 2 + 1])) uv2 = Vector((uva[i2 * 2 + 0], uva[i2 * 2 + 1])) tangent = ArmoryExporter.calc_tangent(v0, v1, v2, uv0, uv1, uv2) tangents[i0 * 3 + 0] += tangent.x tangents[i0 * 3 + 1] += tangent.y tangents[i0 * 3 + 2] += tangent.z tangents[i1 * 3 + 0] += tangent.x tangents[i1 * 3 + 1] += tangent.y tangents[i1 * 3 + 2] += tangent.z tangents[i2 * 3 + 0] += tangent.x tangents[i2 * 3 + 1] += tangent.y tangents[i2 * 3 + 2] += tangent.z # bitangents[i0 * 3 + 0] += bitangent.x # bitangents[i0 * 3 + 1] += bitangent.y # bitangents[i0 * 3 + 2] += bitangent.z # bitangents[i1 * 3 + 0] += bitangent.x # bitangents[i1 * 3 + 1] += bitangent.y # bitangents[i1 * 3 + 2] += bitangent.z # bitangents[i2 * 3 + 0] += bitangent.x # bitangents[i2 * 3 + 1] += bitangent.y # bitangents[i2 * 3 + 2] += bitangent.z # Orthogonalize for i in range(0, vertex_count): # Slow t = Vector((tangents[i * 3], tangents[i * 3 + 1], tangents[i * 3 + 2])) # b = Vector((bitangents[i * 3], bitangents[i * 3 + 1], bitangents[i * 3 + 2])) n = Vector((nora[i * 3], nora[i * 3 + 1], nora[i * 3 + 2])) v = t - n * n.dot(t) v.normalize() # Calculate handedness # cnv = n.cross(v) # if cnv.dot(b) < 0.0: # v = v * -1.0 tangents[i * 3] = v.x tangents[i * 3 + 1] = v.y tangents[i * 3 + 2] = v.z return tangents def write_mesh(self, bobject, fp, o): # One mesh data per file if ArmoryExporter.option_mesh_per_file: mesh_obj = {} mesh_obj['mesh_datas'] = [o] arm.utils.write_arm(fp, mesh_obj) bobject.data.arm_cached = True bobject.arm_cached = True # if bobject.type != 'FONT' and bobject.type != 'META': # bobject.data.arm_cached_verts = len(bobject.data.vertices) # bobject.data.arm_cached_edges = len(bobject.data.edges) else: self.output['mesh_datas'].append(o) def make_va(self, attrib, size, values): va = {} va['attrib'] = attrib va['size'] = size va['values'] = values return va def export_mesh_data(self, exportMesh, bobject, fp, o): exportMesh.calc_normals_split() exportMesh.calc_tessface() # free_mpoly=True vert_list = { Vertex(exportMesh, loop) : 0 for loop in exportMesh.loops}.keys() num_verts = len(vert_list) num_uv_layers = len(exportMesh.uv_layers) has_tex = self.get_export_uvs(exportMesh) == True and num_uv_layers > 0 if self.has_baked_material(bobject, exportMesh.materials): has_tex = True has_tex1 = has_tex == True and num_uv_layers > 1 num_colors = len(exportMesh.vertex_colors) has_col = self.get_export_vcols(exportMesh) == True and num_colors > 0 has_tang = self.has_tangents(exportMesh) vdata = [0] * num_verts * 3 ndata = [0] * num_verts * 3 if has_tex: # Get active uvmap t0map = 0 uv_textures = exportMesh.uv_textures if uv_textures != None: if 'UVMap_baked' in uv_textures: for i in range(0, len(uv_textures)): if uv_textures[i].name == 'UVMap_baked': t0map = i break else: for i in range(0, len(uv_textures)): if uv_textures[i].active_render: t0map = i break t1map = 1 if t0map == 0 else 0 # Alloc data t0data = [0] * num_verts * 2 if has_tex1: t1data = [0] * num_verts * 2 if has_col: cdata = [0] * num_verts * 3 # va_stride = 3 + 3 # pos + nor # va_name = 'pos_nor' # if has_tex: # va_stride += 2 # va_name += '_tex' # if has_tex1: # va_stride += 2 # va_name += '_tex1' # if has_col > 0: # va_stride += 3 # va_name += '_col' # if has_tang: # va_stride += 3 # va_name += '_tang' # vdata = [0] * num_verts * va_stride # Make arrays for i, vtx in enumerate(vert_list): vtx.index = i co = vtx.co normal = vtx.normal for j in range(3): vdata[(i * 3) + j] = co[j] ndata[(i * 3) + j] = normal[j] if has_tex: t0data[i * 2] = vtx.uvs[t0map][0] t0data[i * 2 + 1] = 1.0 - vtx.uvs[t0map][1] # Reverse TCY if has_tex1: t1data[i * 2] = vtx.uvs[t1map][0] t1data[i * 2 + 1] = 1.0 - vtx.uvs[t1map][1] if has_col > 0: cdata[i * 3] = pow(vtx.col[0], 2.2) cdata[i * 3 + 1] = pow(vtx.col[1], 2.2) cdata[i * 3 + 2] = pow(vtx.col[2], 2.2) # Output o['vertex_arrays'] = [] pa = self.make_va('pos', 3, vdata) o['vertex_arrays'].append(pa) na = self.make_va('nor', 3, ndata) o['vertex_arrays'].append(na) if has_tex: ta = self.make_va('tex', 2, t0data) o['vertex_arrays'].append(ta) if has_tex1: ta1 = self.make_va('tex1', 2, t1data) o['vertex_arrays'].append(ta1) if has_col: ca = self.make_va('col', 3, cdata) o['vertex_arrays'].append(ca) # Indices prims = {ma.name if ma else '': [] for ma in exportMesh.materials} if not prims: prims = {'': []} vert_dict = {i : v for v in vert_list for i in v.loop_indices} for poly in exportMesh.polygons: first = poly.loop_start if len(exportMesh.materials) == 0: prim = prims[''] else: mat = exportMesh.materials[min(poly.material_index, len(exportMesh.materials) - 1)] prim = prims[mat.name if mat else ''] indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] if poly.loop_total == 3: prim += indices elif poly.loop_total > 3: for i in range(poly.loop_total-2): prim += (indices[-1], indices[i], indices[i + 1]) # Write indices o['index_arrays'] = [] for mat, prim in prims.items(): idata = [0] * len(prim) for i, v in enumerate(prim): idata[i] = v if len(idata) == 0: # No face assigned continue ia = {} ia['values'] = idata ia['material'] = 0 # Find material index for multi-mat mesh if len(exportMesh.materials) > 1: for i in range(0, len(exportMesh.materials)): if (exportMesh.materials[i] != None and mat == exportMesh.materials[i].name) or \ (exportMesh.materials[i] == None and mat == ''): # Default material for empty slots ia['material'] = i break o['index_arrays'].append(ia) # Sort by material index # o['index_arrays'] = sorted(o['index_arrays'], key=lambda k: k['material']) # Make tangents if has_tang: tanga_vals = self.calc_tangents(pa['values'], na['values'], ta['values'], o['index_arrays'][0]['values']) tanga = self.make_va('tang', 3, tanga_vals) o['vertex_arrays'].append(tanga) return vert_list def has_tangents(self, exportMesh): return self.get_export_uvs(exportMesh) == True and self.get_export_tangents(exportMesh) == True and len(exportMesh.uv_layers) > 0 def export_mesh(self, objectRef, scene): # This function exports a single mesh object table = objectRef[1]["objectTable"] bobject = table[0] oid = arm.utils.safestr(objectRef[1]["structName"]) # No export necessary if ArmoryExporter.option_mesh_per_file: fp = self.get_meshes_file_path('mesh_' + oid, compressed=self.is_compress(bobject.data)) assets.add(fp) # if hasattr(bobject.data, 'arm_sdfgen') and bobject.data.arm_sdfgen: # sdf_path = fp.replace('/mesh_', '/sdf_') # assets.add(sdf_path) if self.is_mesh_cached(bobject) == True and os.path.exists(fp): return # Check if mesh is using instanced rendering is_instanced, instance_offsets = self.object_process_instancing(bobject, objectRef[1]["objectTable"]) # Mesh users have different modifier stack for i in range(1, len(table)): if not self.mod_equal_stack(bobject, table[i]): log.warn('{0} users {1} and {2} differ in modifier stack - use Make Single User(U) - Object & Data for now'.format(oid, bobject.name, table[i].name)) break print('Exporting mesh ' + arm.utils.asset_name(bobject.data)) o = {} o['name'] = oid mesh = objectRef[0] structFlag = False; # Save the morph state if necessary activeShapeKeyIndex = bobject.active_shape_key_index showOnlyShapeKey = bobject.show_only_shape_key currentMorphValue = [] shapeKeys = ArmoryExporter.get_shape_keys(mesh) if shapeKeys: bobject.active_shape_key_index = 0 bobject.show_only_shape_key = True baseIndex = 0 relative = shapeKeys.use_relative if relative: morphCount = 0 baseName = shapeKeys.reference_key.name for block in shapeKeys.key_blocks: if block.name == baseName: baseIndex = morphCount break morphCount += 1 morphCount = 0 for block in shapeKeys.key_blocks: currentMorphValue.append(block.value) block.value = 0.0 if block.name != "": # self.IndentWrite(B"Morph (index = ", 0, structFlag) # self.WriteInt(morphCount) # if (relative) and (morphCount != baseIndex): # self.Write(B", base = ") # self.WriteInt(baseIndex) # self.Write(B")\n") # self.IndentWrite(B"{\n") # self.IndentWrite(B"Name {string {\"", 1) # self.Write(bytes(block.name, "UTF-8")) # self.Write(B"\"}}\n") # self.IndentWrite(B"}\n") # TODO structFlag = True morphCount += 1 shapeKeys.key_blocks[0].value = 1.0 mesh.update() armature = bobject.find_armature() apply_modifiers = not armature # Apply all modifiers to create a new mesh with tessfaces if bpy.app.version >= (2, 80, 1): # 2.8 exportMesh = bobject.to_mesh(scene, bpy.context.workspace.render_layer, apply_modifiers, "RENDER", True, False) else: exportMesh = bobject.to_mesh(scene, apply_modifiers, "RENDER", True, False) if exportMesh == None: log.warn(oid + ' was not exported') return if len(exportMesh.uv_layers) > 2: log.warn(oid + ' exceeds maximum of 2 UV Maps supported') # Process meshes vert_list = self.export_mesh_data(exportMesh, bobject, fp, o) if armature: self.export_skin(bobject, armature, vert_list, o) # Save aabb for va in o['vertex_arrays']: if va['attrib'].startswith('pos'): positions = va['values'] stride = 0 ar = va['attrib'].split('_') for a in ar: if a == 'pos' or a == 'nor' or a == 'col' or a == 'tang': stride += 3 elif a == 'tex' or a == 'tex1': stride += 2 elif a == 'bone' or a == 'weight': stride += 4 aabb_min = [-0.01, -0.01, -0.01] aabb_max = [0.01, 0.01, 0.01] i = 0 while i < len(positions): if positions[i] > aabb_max[0]: aabb_max[0] = positions[i]; if positions[i + 1] > aabb_max[1]: aabb_max[1] = positions[i + 1]; if positions[i + 2] > aabb_max[2]: aabb_max[2] = positions[i + 2]; if positions[i] < aabb_min[0]: aabb_min[0] = positions[i]; if positions[i + 1] < aabb_min[1]: aabb_min[1] = positions[i + 1]; if positions[i + 2] < aabb_min[2]: aabb_min[2] = positions[i + 2]; i += stride; if hasattr(bobject.data, 'arm_aabb'): bobject.data.arm_aabb = [abs(aabb_min[0]) + abs(aabb_max[0]), abs(aabb_min[1]) + abs(aabb_max[1]), abs(aabb_min[2]) + abs(aabb_max[2])] break # Not axis-aligned # arm_aabb = [bobject.matrix_world * Vector(v) for v in bobject.bound_box] # Restore the morph state if shapeKeys: bobject.active_shape_key_index = activeShapeKeyIndex bobject.show_only_shape_key = showOnlyShapeKey for m in range(len(currentMorphValue)): shapeKeys.key_blocks[m].value = currentMorphValue[m] mesh.update() # Save offset data for instanced rendering if is_instanced == True: o['instance_offsets'] = instance_offsets # Export usage if bobject.data.arm_dynamic_usage: o['dynamic_usage'] = bobject.data.arm_dynamic_usage self.write_mesh(bobject, fp, o) def export_lamp(self, objectRef): # This function exports a single lamp object o = {} o['name'] = objectRef[1]["structName"] objref = objectRef[0] objtype = objref.type if objtype == 'SUN': o['type'] = 'sun' elif objtype == 'POINT': o['type'] = 'point' elif objtype == 'SPOT': o['type'] = 'spot' o['spot_size'] = math.cos(objref.spot_size / 2) o['spot_blend'] = objref.spot_blend / 10 # Cycles defaults to 0.15 elif objtype == 'AREA': o['type'] = 'area' o['size'] = objref.size o['size_y'] = objref.size_y else: # Hemi o['type'] = 'sun' o['cast_shadow'] = objref.cycles.cast_shadow o['near_plane'] = objref.arm_clip_start o['far_plane'] = objref.arm_clip_end o['fov'] = objref.arm_fov o['shadows_bias'] = objref.arm_shadows_bias * 0.0001 rpdat = arm.utils.get_rp() if rpdat.rp_shadowmap == 'Off': o['shadowmap_size'] = 0 else: o['shadowmap_size'] = int(rpdat.rp_shadowmap) if o['type'] == 'sun': # Scale bias for ortho light matrix o['shadows_bias'] *= 25.0 if o['shadowmap_size'] > 1024: o['shadows_bias'] *= 1 / (o['shadowmap_size'] / 1024) # Less bias for bigger maps if (objtype == 'POINT' or objtype == 'SPOT') and objref.shadow_soft_size > 0.1: o['lamp_size'] = objref.shadow_soft_size * 10 # Match to Cycles gapi = arm.utils.get_gapi() mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' if objtype == 'POINT' and not mobile_mat: o['fov'] = 1.5708 # 90 deg o['shadowmap_cube'] = True o['shadows_bias'] *= 2.0 if bpy.app.version >= (2, 80, 1) and self.scene.view_render.engine == 'BLENDER_EEVEE': o['color'] = [objref.color[0], objref.color[1], objref.color[2]] o['strength'] = objref.energy if o['type'] == 'point' or o['type'] == 'spot': o['strength'] *= 1.5 elif o['type'] == 'sun': o['strength'] *= 0.325 elif objref.node_tree != None: tree = objref.node_tree for n in tree.nodes: # Emission only for now if n.type == 'EMISSION': col = n.inputs[0].default_value o['color'] = [col[0], col[1], col[2]] o['strength'] = n.inputs[1].default_value # Normalize lamp strength if o['type'] == 'point' or o['type'] == 'spot': o['strength'] *= 0.026 elif o['type'] == 'area': o['strength'] *= 0.26 elif o['type'] == 'sun': o['strength'] *= 0.325 # TODO: Lamp texture test.. if n.inputs[0].is_linked: color_node = n.inputs[0].links[0].from_node if color_node.type == 'TEX_IMAGE': o['color_texture'] = color_node.image.name break else: # o['color'] = [1.0, 1.0, 1.0] o['color'] = [objref.color[0], objref.color[1], objref.color[2]] o['strength'] = 100.0 * 0.026 o['type'] = 'point' self.output['lamp_datas'].append(o) def get_camera_clear_color(self): if self.scene.world == None: return [0.051, 0.051, 0.051, 1.0] if self.scene.world.node_tree == None: c = self.scene.world.horizon_color return [c[0], c[1], c[2], 1.0] if 'Background' in self.scene.world.node_tree.nodes: background_node = self.scene.world.node_tree.nodes['Background'] col = background_node.inputs[0].default_value strength = background_node.inputs[1].default_value ar = [col[0] * strength, col[1] * strength, col[2] * strength, col[3]] ar[0] = max(min(ar[0], 1.0), 0.0) ar[1] = max(min(ar[1], 1.0), 0.0) ar[2] = max(min(ar[2], 1.0), 0.0) ar[3] = max(min(ar[3], 1.0), 0.0) return ar else: return [0.051, 0.051, 0.051, 1.0] def extract_projection(self, o, proj, with_planes=True): a = proj[0][0] b = proj[1][1] c = proj[2][2] d = proj[2][3] k = (c - 1.0) / (c + 1.0) o['fov'] = 2.0 * math.atan(1.0 / b) if with_planes: o['near_plane'] = (d * (1.0 - k)) / (2.0 * k) o['far_plane'] = k * o['near_plane']; def export_camera(self, objectRef): o = {} o['name'] = objectRef[1]["structName"] objref = objectRef[0] camera = objectRef[1]["objectTable"][0] render = self.scene.render proj = camera.calc_matrix_camera( render.resolution_x, render.resolution_y, render.pixel_aspect_x, render.pixel_aspect_y) self.extract_projection(o, proj) wrd = bpy.data.worlds['Arm'] if wrd.arm_play_camera != 'Scene': pw = self.get_viewport_panels_w() # Tool shelf and properties hidden proj, is_persp = self.get_viewport_projection_matrix() windowed = not ArmoryExporter.in_viewport if proj != None and is_persp and (pw == 0 or windowed): self.extract_projection(o, proj, with_planes=False) if objref.type != 'PERSP': o['ortho_scale'] = objref.ortho_scale / (7.31429 / 2) o['near_plane'] = objref.clip_start o['far_plane'] = objref.clip_end if objref.arm_render_to_texture: o['render_to_texture'] = True o['texture_resolution_x'] = int(objref.arm_texture_resolution_x) o['texture_resolution_y'] = int(objref.arm_texture_resolution_y) o['frustum_culling'] = objref.arm_frustum_culling o['clear_color'] = self.get_camera_clear_color() self.output['camera_datas'].append(o) def export_speaker(self, objectRef): # This function exports a single speaker object o = {} o['name'] = objectRef[1]["structName"] objref = objectRef[0] if objref.sound: # Packed if objref.sound.packed_file != None: unpack_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked' if not os.path.exists(unpack_path): os.makedirs(unpack_path) unpack_filepath = unpack_path + '/' + objref.sound.name if os.path.isfile(unpack_filepath) == False or os.path.getsize(unpack_filepath) != objref.sound.packed_file.size: with open(unpack_filepath, 'wb') as f: f.write(objref.sound.packed_file.data) assets.add(unpack_filepath) # External else: assets.add(arm.utils.asset_path(objref.sound.filepath)) # Link sound to assets o['sound'] = arm.utils.extract_filename(objref.sound.filepath) else: o['sound'] = '' o['muted'] = objref.muted o['loop'] = objref.arm_loop o['stream'] = objref.arm_stream o['volume'] = objref.volume o['pitch'] = objref.pitch o['attenuation'] = objref.attenuation o['play_on_start'] = objref.arm_play_on_start self.output['speaker_datas'].append(o) def make_default_mat(self, mat_name, mat_objs): if mat_name in bpy.data.materials: return mat = bpy.data.materials.new(name=mat_name) # if default_exists: # mat.is_cached = True mat.use_nodes = True o = {} o['name'] = mat.name o['contexts'] = [] mat_users = dict() mat_users[mat] = mat_objs mat_armusers = dict() mat_armusers[mat] = [o] make_material.parse(mat, o, mat_users, mat_armusers) self.output['material_datas'].append(o) bpy.data.materials.remove(mat) rpdat = arm.utils.get_rp() if rpdat.arm_culling == False: o['override_context'] = {} o['override_context']['cull_mode'] = 'none' def export_materials(self): wrd = bpy.data.worlds['Arm'] # Keep materials with fake user for m in bpy.data.materials: if m.use_fake_user and m not in self.materialArray: self.materialArray.append(m) # Ensure the same order for merging materials self.materialArray.sort(key=lambda x: x.name) if wrd.arm_batch_materials: mat_users = self.materialToObjectDict mat_armusers = self.materialToArmObjectDict mat_batch.build(self.materialArray, mat_users, mat_armusers) transluc_used = False overlays_used = False decals_used = False # sss_used = False for material in self.materialArray: # If the material is unlinked, material becomes None if material == None: continue o = {} o['name'] = arm.utils.asset_name(material) if material.arm_skip_context != '': o['skip_context'] = material.arm_skip_context rpdat = arm.utils.get_rp() if material.arm_two_sided or rpdat.arm_culling == False: o['override_context'] = {} o['override_context']['cull_mode'] = 'none' elif material.arm_cull_mode != 'clockwise': o['override_context'] = {} o['override_context']['cull_mode'] = material.arm_cull_mode o['contexts'] = [] if not material.use_nodes: material.use_nodes = True mat_users = self.materialToObjectDict mat_armusers = self.materialToArmObjectDict sd, rpasses = make_material.parse(material, o, mat_users, mat_armusers) if 'translucent' in rpasses: transluc_used = True if 'overlay' in rpasses: overlays_used = True if 'decal' in rpasses: decals_used = True uv_export = False tang_export = False vcol_export = False vs_str = '' for con in sd['contexts']: for elem in con['vertex_structure']: if len(vs_str) > 0: vs_str += ',' vs_str += elem['name'] if elem['name'] == 'tang': tang_export = True elif elem['name'] == 'tex': uv_export = True elif elem['name'] == 'col': vcol_export = True for con in o['contexts']: # TODO: blend context if con['name'] == 'mesh' and material.arm_blending: con['name'] = 'blend' # TODO: use array and remove duplis to ensure correctness material.vertex_structure = vs_str if (material.export_tangents != tang_export) or \ (material.export_uvs != uv_export) or \ (material.export_vcols != vcol_export): material.export_uvs = uv_export material.export_vcols = vcol_export material.export_tangents = tang_export if material in self.materialToObjectDict: mat_users = self.materialToObjectDict[material] for ob in mat_users: ob.data.arm_cached = False self.output['material_datas'].append(o) material.is_cached = True # Object with no material assigned in the scene if len(self.defaultMaterialObjects) > 0: self.make_default_mat('armdefault', self.defaultMaterialObjects) if len(self.defaultSkinMaterialObjects) > 0: self.make_default_mat('armdefaultskin', self.defaultSkinMaterialObjects) # Auto-enable render-path featues rebuild_rp = False rpdat = arm.utils.get_rp() if rpdat.rp_translucency_state == 'Auto' and rpdat.rp_translucency != transluc_used: rpdat.rp_translucency = transluc_used rebuild_rp = True if rpdat.rp_overlays_state == 'Auto' and rpdat.rp_overlays != overlays_used: rpdat.rp_overlays = overlays_used rebuild_rp = True if rpdat.rp_decals_state == 'Auto' and rpdat.rp_decals != decals_used: rpdat.rp_decals = decals_used rebuild_rp = True # if rpdat.rp_sss_state == 'Auto' and rpdat.rp_sss != sss_used: # rpdat.rp_sss = sss_used # rebuild_rp = True if rebuild_rp: make_renderpath.build() def export_particle_systems(self): if len(self.particleSystemArray) > 0: self.output['particle_datas'] = [] for particleRef in self.particleSystemArray.items(): o = {} psettings = particleRef[0] if psettings == None: continue if psettings.dupli_object == None or psettings.render_type != 'OBJECT': continue o['name'] = particleRef[1]["structName"] if psettings.arm_gpu_sim: o['gpu_sim'] = True o['type'] = 0 if psettings.type == 'EMITTER' else 1 # HAIR o['loop'] = psettings.arm_loop o['render_emitter'] = psettings.use_render_emitter # Emission o['count'] = psettings.count * psettings.arm_count_mult o['frame_start'] = int(psettings.frame_start) o['frame_end'] = int(psettings.frame_end) o['lifetime'] = psettings.lifetime o['lifetime_random'] = psettings.lifetime_random o['emit_from'] = 1 if psettings.emit_from == 'VOLUME' else 0 # VERT, FACE # Velocity # o['normal_factor'] = psettings.normal_factor; # o['tangent_factor'] = psettings.tangent_factor; # o['tangent_phase'] = psettings.tangent_phase; o['object_align_factor'] = [psettings.object_align_factor[0], psettings.object_align_factor[1], psettings.object_align_factor[2]] # o['object_factor'] = psettings.object_factor; o['factor_random'] = psettings.factor_random # Physics o['physics_type'] = 1 if psettings.physics_type == 'NEWTON' else 0 o['particle_size'] = psettings.particle_size o['size_random'] = psettings.size_random o['mass'] = psettings.mass # Render o['dupli_object'] = psettings.dupli_object.name self.objectToArmObjectDict[psettings.dupli_object]['is_particle'] = True # Field weights o['weight_gravity'] = psettings.effector_weights.gravity self.output['particle_datas'].append(o) def export_tilesheets(self): wrd = bpy.data.worlds['Arm'] if len(wrd.arm_tilesheetlist) > 0: self.output['tilesheet_datas'] = [] for ts in wrd.arm_tilesheetlist: o = {} o['name'] = ts.name o['tilesx'] = ts.tilesx_prop o['tilesy'] = ts.tilesy_prop o['framerate'] = ts.framerate_prop o['actions'] = [] for tsa in ts.arm_tilesheetactionlist: ao = {} ao['name'] = tsa.name ao['start'] = tsa.start_prop ao['end'] = tsa.end_prop ao['loop'] = tsa.loop_prop o['actions'].append(ao) self.output['tilesheet_datas'].append(o) def export_worlds(self): worldRef = self.scene.world if worldRef != None: o = {} w = worldRef o['name'] = w.name self.post_export_world(w, o) self.output['world_datas'].append(o) def is_compress(self, obj): return ArmoryExporter.compress_enabled and obj.arm_compress def export_objects(self, scene): if not ArmoryExporter.option_mesh_only: self.output['lamp_datas'] = [] self.output['camera_datas'] = [] self.output['speaker_datas'] = [] for objectRef in self.lampArray.items(): self.export_lamp(objectRef) for objectRef in self.cameraArray.items(): self.export_camera(objectRef) # Keep sounds with fake user for sound in bpy.data.sounds: if sound.use_fake_user: assets.add(arm.utils.asset_path(sound.filepath)) for objectRef in self.speakerArray.items(): self.export_speaker(objectRef) for objectRef in self.meshArray.items(): self.output['mesh_datas'] = []; self.export_mesh(objectRef, scene) def execute(self, context, filepath, scene=None, write_capture_info=False, play_area=None): profile_time = time.time() self.output = {} self.filepath = filepath self.play_area = play_area self.scene = context.scene if scene == None else scene print('Exporting ' + arm.utils.asset_name(self.scene)) current_frame, current_subframe = scene.frame_current, scene.frame_subframe self.beginFrame = self.scene.frame_start self.output['frame_time'] = 1.0 / (self.scene.render.fps / self.scene.render.fps_base) if write_capture_info: self.output['capture_info'] = {} self.output['capture_info']['path'] = bpy.path.abspath(self.scene.render.filepath) self.output['capture_info']['frame_start'] = int(self.scene.frame_start) self.output['capture_info']['frame_end'] = int(self.scene.frame_end) self.bobjectArray = {} self.bobjectBoneArray = {} self.meshArray = {} self.lampArray = {} self.cameraArray = {} self.camera_spawned = False self.speakerArray = {} self.materialArray = [] self.particleSystemArray = {} self.worldArray = {} # Export all worlds self.boneParentArray = {} self.materialToObjectDict = dict() self.defaultMaterialObjects = [] # If no material is assigned, provide default to mimick cycles self.defaultSkinMaterialObjects = [] self.materialToArmObjectDict = dict() self.objectToArmObjectDict = dict() self.active_layers = [] self.bone_tracks = [] for i in range(0, len(self.scene.layers)): if self.scene.layers[i] == True: self.active_layers.append(i) self.preprocess() if bpy.app.version >= (2, 80, 1): # 2.8 scene_objects = self.scene.master_collection.collections[0].objects else: scene_objects = self.scene.objects for bobject in scene_objects: # Map objects to game objects o = {} o['traits'] = [] self.objectToArmObjectDict[bobject] = o # Process if not bobject.parent: self.process_bobject(bobject) self.process_skinned_meshes() self.output['name'] = arm.utils.safestr(self.scene.name) if self.filepath.endswith('.zip'): self.output['name'] += '.zip' elif not bpy.data.worlds['Arm'].arm_minimize: self.output['name'] += '.json' # Fix material variants # Skinned and non-skined objects can not share material matvars = [] matslots = [] for bo in scene_objects: if arm.utils.export_bone_data(bo): for slot in bo.material_slots: if slot.material == None: continue if slot.material.name.endswith('_armskin'): continue matslots.append(slot) mat_name = slot.material.name + '_armskin' mat = bpy.data.materials.get(mat_name) if mat == None: mat = slot.material.copy() mat.name = mat_name matvars.append(mat) slot.material = mat # Auto-bones wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() if rpdat.arm_skin_max_bones_auto: max_bones = 8 for armature in bpy.data.armatures: if max_bones < len(armature.bones): max_bones = len(armature.bones) rpdat.arm_skin_max_bones = max_bones self.output['objects'] = [] for bo in scene_objects: if not bo.parent: self.export_object(bo, self.scene) if len(bpy.data.groups) > 0: self.output['groups'] = [] for group in bpy.data.groups: # Blender automatically stores physics objects in this group, # can cause stuck unused objects, skip for now if group.name.startswith('RigidBodyWorld') or group.name.startswith('Trait|'): continue o = {} o['name'] = group.name o['object_refs'] = [] # Add unparented objects only, then instantiate full object child tree for bobject in group.objects: if bobject.parent == None and bobject.arm_export: # Add external linked objects if bobject.name not in scene_objects: # and bobject.ls_linked self.process_bobject(bobject) self.export_object(bobject, self.scene) o['object_refs'].append(arm.utils.asset_name(bobject)) else: o['object_refs'].append(bobject.name) self.output['groups'].append(o) if not ArmoryExporter.option_mesh_only: if self.scene.camera != None: self.output['camera_ref'] = self.scene.camera.name else: if self.scene.name == arm.utils.get_project_scene_name(): log.warn('No camera found in active scene') self.output['material_datas'] = [] self.export_materials() # Ensure same vertex structure for object materials if not wrd.arm_deinterleaved_buffers: for bobject in scene_objects: if len(bobject.material_slots) > 1: mat = self.slot_to_material(bobject, bobject.material_slots[0]) if mat == None: continue vs = mat.vertex_structure for i in range(1, len(bobject.material_slots)): nmat = self.slot_to_material(bobject, bobject.material_slots[i]) if nmat == None: continue if vs != nmat.vertex_structure: log.warn('Object ' + bobject.name + ' - unable to bind materials to vertex data, please separate object by material (select object - edit mode - P - By Material) or enable Deinterleaved Buffers in Armory Project - Flags') break self.export_particle_systems() self.output['world_datas'] = [] self.export_worlds() self.export_tilesheets() if self.scene.world != None: self.output['world_ref'] = self.scene.world.name if self.scene.use_gravity: self.output['gravity'] = [self.scene.gravity[0], self.scene.gravity[1], self.scene.gravity[2]] else: self.output['gravity'] = [0.0, 0.0, 0.0] self.export_objects(self.scene) if not self.camera_spawned: log.warn('No camera found in active scene layers') if (len(self.output['camera_datas']) == 0 and len(bpy.data.cameras) == 0) or not self.camera_spawned: log.warn('Creating default camera') o = {} o['name'] = 'DefaultCamera' o['near_plane'] = 0.1 o['far_plane'] = 100.0 o['fov'] = 0.85 # if ArmoryExporter.in_viewport: # Wrong P returned when no camera present? # pw = self.get_viewport_panels_w() # proj, is_persp = self.get_viewport_projection_matrix() # if pw == 0 and is_persp: # a = proj[0][0] # b = proj[1][1] # c = proj[2][2] # d = proj[2][3] # k = (c - 1.0) / (c + 1.0) # o['near_plane'] = (d * (1.0 - k)) / (2.0 * k) # o['far_plane'] = k * o['near_plane']; # o['fov'] = 2.0 * math.atan(1.0 / b) o['type'] = 'perspective' o['frustum_culling'] = True o['clear_color'] = self.get_camera_clear_color() self.output['camera_datas'].append(o) o = {} o['name'] = 'DefaultCamera' o['type'] = 'camera_object' o['data_ref'] = 'DefaultCamera' o['material_refs'] = [] o['transform'] = {} viewport_matrix = self.get_viewport_view_matrix() if viewport_matrix != None: o['transform']['values'] = self.write_matrix(viewport_matrix.inverted()) o['local_transform_only'] = True else: o['transform']['values'] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] o['traits'] = [] navigation_trait = {} navigation_trait['type'] = 'Script' navigation_trait['class_name'] = 'armory.trait.WalkNavigation' navigation_trait['parameters'] = [str(arm.utils.get_ease_viewport_camera()).lower()] o['traits'].append(navigation_trait) self.output['objects'].append(o) self.output['camera_ref'] = 'DefaultCamera' # Scene root traits if wrd.arm_physics != 'Disabled' and ArmoryExporter.export_physics: if not 'traits' in self.output: self.output['traits'] = [] x = {} x['type'] = 'Script' phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics == 'Bullet' else 'oimo' x['class_name'] = 'armory.trait.physics.' + phys_pkg + '.PhysicsWorld' rbw = self.scene.rigidbody_world if rbw != None and rbw.enabled: x['parameters'] = [str(rbw.time_scale), str(1 / rbw.steps_per_second)] self.output['traits'].append(x) if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation: if not 'traits' in self.output: self.output['traits'] = [] x = {} x['type'] = 'Script' x['class_name'] = 'armory.trait.navigation.Navigation' self.output['traits'].append(x) if wrd.arm_play_console: if not 'traits' in self.output: self.output['traits'] = [] ArmoryExporter.export_ui = True x = {} x['type'] = 'Script' x['class_name'] = 'armory.trait.internal.DebugConsole' x['parameters'] = [] self.output['traits'].append(x) if len(self.scene.arm_traitlist) > 0: if not 'traits' in self.output: self.output['traits'] = [] self.export_traits(self.scene, self.output) if 'traits' in self.output: for x in self.output['traits']: ArmoryExporter.import_traits.append(x['class_name']) # Write embedded data references if len(assets.embedded_data) > 0: self.output['embedded_datas'] = [] for file in assets.embedded_data: self.output['embedded_datas'].append(file) # Write scene file arm.utils.write_arm(self.filepath, self.output) # Remove created material variants for slot in matslots: # Set back to original material orig_mat = bpy.data.materials[slot.material.name[:-len('_armskin')]] orig_mat.export_uvs = slot.material.export_uvs orig_mat.export_vcols = slot.material.export_vcols orig_mat.export_tangents = slot.material.export_tangents orig_mat.is_cached = slot.material.is_cached slot.material = orig_mat for mat in matvars: bpy.data.materials.remove(mat, do_unlink=True) # Restore frame if scene.frame_current != current_frame: scene.frame_set(current_frame, current_subframe) print('Scene built in ' + str(time.time() - profile_time)) return {'FINISHED'} # Callbacks def is_mesh_cached(self, bobject): if bobject.type == 'FONT' or bobject.type == 'META': # No verts return bobject.data.arm_cached # if bobject.data.arm_cached_verts != len(bobject.data.vertices): # return False # if bobject.data.arm_cached_edges != len(bobject.data.edges): # return False if not bobject.arm_cached: return False return bobject.data.arm_cached def get_export_tangents(self, mesh): for m in mesh.materials: if m != None and m.export_tangents == True: return True return False def get_export_vcols(self, mesh): for m in mesh.materials: if m != None and m.export_vcols == True: return True return False def get_export_uvs(self, mesh): for m in mesh.materials: if m != None and m.export_uvs == True: return True return False def object_process_instancing(self, bobject, refs): is_instanced = False instance_offsets = None for n in refs: if n.arm_instanced == True: is_instanced = True # Save offset data instance_offsets = [0.0, 0.0, 0.0] # Include parent for sn in n.children: # Child hidden if sn.arm_export == False: continue # Do not take parent matrix into account loc = sn.matrix_local.to_translation() instance_offsets.append(loc.x) instance_offsets.append(loc.y) instance_offsets.append(loc.z) # m = sn.matrix_local # instance_offsets.append(m[0][3]) #* m[0][0]) # Scale # instance_offsets.append(m[1][3]) #* m[1][1]) # instance_offsets.append(m[2][3]) #* m[2][2]) break # Instance render groups with same children? # elif n.dupli_type == 'GROUP' and n.dupli_group != None: # is_instanced = True # instance_offsets = [] # for sn in bpy.data.groups[n.dupli_group].objects: # loc = sn.matrix_local.to_translation() # instance_offsets.append(loc.x) # instance_offsets.append(loc.y) # instance_offsets.append(loc.z) # break return is_instanced, instance_offsets def preprocess(self): wrd = bpy.data.worlds['Arm'] ArmoryExporter.export_all_flag = True ArmoryExporter.export_physics = False # Indicates whether rigid body is exported ArmoryExporter.export_navigation = False ArmoryExporter.export_ui = False if not hasattr(ArmoryExporter, 'compress_enabled'): ArmoryExporter.compress_enabled = False if not hasattr(ArmoryExporter, 'in_viewport'): ArmoryExporter.in_viewport = False if not hasattr(ArmoryExporter, 'import_traits'): ArmoryExporter.import_traits = [] # Referenced traits ArmoryExporter.option_mesh_only = False ArmoryExporter.option_mesh_per_file = True ArmoryExporter.sample_animation_flag = wrd.arm_sampled_animation # Used for material shader export and khafile ArmoryExporter.mesh_context = 'mesh' ArmoryExporter.mesh_context_empty = '' ArmoryExporter.shadows_context = 'shadowmap' ArmoryExporter.translucent_context = 'translucent' ArmoryExporter.overlay_context = 'overlay' def preprocess_object(self, bobject): # Returns false if object should not be exported export_object = True # Disabled object if bobject.arm_export == False: return False for m in bobject.modifiers: if m.type == 'OCEAN': # Do not export ocean mesh, just take specified constants export_object = False return export_object def post_export_object(self, bobject, o, type): # Export traits self.export_traits(bobject, o) wrd = bpy.data.worlds['Arm'] phys_enabled = wrd.arm_physics != 'Disabled' phys_pkg = 'bullet' if wrd.arm_physics == 'Bullet' else 'oimo' # Rigid body trait if bobject.rigid_body != None and phys_enabled: ArmoryExporter.export_physics = True rb = bobject.rigid_body shape = 0 # BOX if bobject.arm_rb_terrain: # Override selected shape as terrain.. shape = 7 elif rb.collision_shape == 'SPHERE': shape = 1 elif rb.collision_shape == 'CONVEX_HULL': shape = 2 elif rb.collision_shape == 'MESH': shape = 3 elif rb.collision_shape == 'CONE': shape = 4 elif rb.collision_shape == 'CYLINDER': shape = 5 elif rb.collision_shape == 'CAPSULE': shape = 6 body_mass = rb.mass is_static = not rb.enabled or (rb.type == 'PASSIVE' and not rb.kinematic) if is_static: body_mass = 0 x = {} x['type'] = 'Script' x['class_name'] = 'armory.trait.physics.' + phys_pkg + '.RigidBody' x['parameters'] = [str(body_mass), str(shape), str(rb.friction), str(rb.restitution)] if rb.use_margin: x['parameters'].append(str(rb.collision_margin)) else: x['parameters'].append('0.0') x['parameters'].append(str(rb.linear_damping)) x['parameters'].append(str(rb.angular_damping)) x['parameters'].append(str(rb.kinematic).lower()) lx = bobject.arm_rb_linear_factor[0] ly = bobject.arm_rb_linear_factor[1] lz = bobject.arm_rb_linear_factor[2] ax = bobject.arm_rb_angular_factor[0] ay = bobject.arm_rb_angular_factor[1] az = bobject.arm_rb_angular_factor[2] if bobject.lock_location[0]: lx = 0 if bobject.lock_location[1]: ly = 0 if bobject.lock_location[2]: lz = 0 if bobject.lock_rotation[0]: ax = 0 if bobject.lock_rotation[1]: ay = 0 if bobject.lock_rotation[2]: az = 0 lin_fac = '[{0}, {1}, {2}]'.format(str(lx), str(ly), str(lz)) ang_fac = '[{0}, {1}, {2}]'.format(str(ax), str(ay), str(az)) x['parameters'].append(lin_fac) x['parameters'].append(ang_fac) col_group = '' for b in rb.collision_groups: col_group = ('1' if b else '0') + col_group x['parameters'].append(str(int(col_group, 2))) x['parameters'].append(str(bobject.arm_rb_trigger).lower()) if rb.use_deactivation or bobject.arm_rb_force_deactivation: deact_params = lin_fac = '[{0}, {1}, {2}]'.format(str(rb.deactivate_linear_velocity), str(rb.deactivate_angular_velocity), str(bobject.arm_rb_deactivation_time)) x['parameters'].append(deact_params) else: x['parameters'].append('null') o['traits'].append(x) # Phys traits if phys_enabled: for m in bobject.modifiers: if m.type == 'CLOTH': self.add_softbody_mod(o, bobject, m, 0) # SoftShape.Cloth elif m.type == 'SOFT_BODY': self.add_softbody_mod(o, bobject, m, 1) # SoftShape.Volume elif m.type == 'HOOK': self.add_hook_mod(o, bobject, m.object.name, m.vertex_group) # Rigid body constraint rbc = bobject.rigid_body_constraint if rbc != None and rbc.enabled: self.add_rigidbody_constraint(o, rbc) # Camera traits if type == NodeTypeCamera: # Viewport camera enabled, attach navigation to active camera if self.scene.camera != None and bobject.name == self.scene.camera.name and bpy.data.worlds['Arm'].arm_play_camera != 'Scene': navigation_trait = {} navigation_trait['type'] = 'Script' navigation_trait['class_name'] = 'armory.trait.WalkNavigation' navigation_trait['parameters'] = [str(arm.utils.get_ease_viewport_camera()).lower()] o['traits'].append(navigation_trait) # Map objects to materials, can be used in later stages for i in range(len(bobject.material_slots)): mat = self.slot_to_material(bobject, bobject.material_slots[i]) if mat in self.materialToObjectDict: self.materialToObjectDict[mat].append(bobject) self.materialToArmObjectDict[mat].append(o) else: self.materialToObjectDict[mat] = [bobject] self.materialToArmObjectDict[mat] = [o] # Export constraints if len(bobject.constraints) > 0: o['constraints'] = [] self.add_constraints(bobject, o) for x in o['traits']: ArmoryExporter.import_traits.append(x['class_name']) def add_constraints(self, bobject, o, bone=False): for con in bobject.constraints: if con.mute: continue co = {} co['name'] = con.name co['type'] = con.type if bone: co['bone'] = bobject.name if con.type == 'COPY_LOCATION': co['target'] = con.target.name co['use_x'] = con.use_x co['use_y'] = con.use_y co['use_z'] = con.use_z co['invert_x'] = con.invert_x co['invert_y'] = con.invert_y co['invert_z'] = con.invert_z co['use_offset'] = con.use_offset co['influence'] = con.influence elif con.type == 'CHILD_OF': co['target'] = con.target.name co['influence'] = con.influence o['constraints'].append(co) def export_traits(self, bobject, o): if hasattr(bobject, 'arm_traitlist'): for t in bobject.arm_traitlist: if t.enabled_prop == False: continue x = {} if t.type_prop == 'Logic Nodes' and t.nodes_name_prop != '': x['type'] = 'Script' group_name = arm.utils.safesrc(t.nodes_name_prop[0].upper() + t.nodes_name_prop[1:]) x['class_name'] = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.node.' + group_name elif t.type_prop == 'WebAssembly': x['type'] = 'Script' x['class_name'] = 'armory.trait.internal.WasmScript' x['parameters'] = ["'" + t.webassembly_prop + "'"] elif t.type_prop == 'UI Canvas': ArmoryExporter.export_ui = True x['type'] = 'Script' x['class_name'] = 'armory.trait.internal.CanvasScript' x['parameters'] = ["'" + t.canvas_name_prop + "'"] # assets.add(assetpath) # Bundled is auto-added # Read file list and add canvas assets assetpath = arm.utils.get_fp() + '/Bundled/canvas/' + t.canvas_name_prop + '.files' if os.path.exists(assetpath): with open(assetpath) as f: fileList = f.read().splitlines() for asset in fileList: # Relative to the root/Bundled/canvas path asset = asset[6:] # Strip ../../ to start in project root assets.add(asset) else: # Haxe/Bundled Script if t.class_name_prop == '': # Empty class name, skip continue x['type'] = 'Script' if t.type_prop == 'Bundled Script': trait_prefix = 'armory.trait.' # TODO: temporary, export single mesh navmesh as obj if t.class_name_prop == 'NavMesh' and bobject.type == 'MESH' and bpy.data.worlds['Arm'].arm_navigation != 'Disabled': ArmoryExporter.export_navigation = True nav_path = arm.utils.get_fp_build() + '/compiled/Assets/navigation' if not os.path.exists(nav_path): os.makedirs(nav_path) nav_filepath = nav_path + '/nav_' + bobject.data.name + '.arm' assets.add(nav_filepath) # TODO: Implement cache #if os.path.isfile(nav_filepath) == False: override = {'selected_objects': [bobject]} # bobject.scale.y *= -1 # mesh = obj.data # for face in mesh.faces: # face.v.reverse() # bpy.ops.export_scene.obj(override, use_selection=True, filepath=nav_filepath, check_existing=False, use_normals=False, use_uvs=False, use_materials=False) # bobject.scale.y *= -1 with open(nav_filepath, 'w') as f: for v in bobject.data.vertices: f.write("v %.4f " % (v.co[0] * bobject.scale.x)) f.write("%.4f " % (v.co[2] * bobject.scale.z)) f.write("%.4f\n" % (v.co[1] * bobject.scale.y)) # Flipped for p in bobject.data.polygons: f.write("f") for i in reversed(p.vertices): # Flipped normals f.write(" %d" % (i + 1)) f.write("\n") else: trait_prefix = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.' x['class_name'] = trait_prefix + t.class_name_prop if len(t.arm_traitparamslist) > 0: x['parameters'] = [] for pt in t.arm_traitparamslist: # Append parameters x['parameters'].append(pt.name) if len(t.arm_traitpropslist) > 0: x['props'] = [] for pt in t.arm_traitpropslist: # Append props prop = pt.name.replace(')', '').split('(') x['props'].append(prop[0]) if(len(prop) > 1): if prop[1] == 'String': value = "'" + pt.value + "'" else: value = pt.value else: value = pt.value x['props'].append(value) o['traits'].append(x) def add_softbody_mod(self, o, bobject, soft_mod, soft_type): ArmoryExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics == 'Bullet' else 'oimo' assets.add_khafile_def('arm_physics_soft') trait = {} trait['type'] = 'Script' trait['class_name'] = 'armory.trait.physics.' + phys_pkg + '.SoftBody' if soft_type == 0: bend = soft_mod.settings.bending_stiffness elif soft_type == 1: bend = (soft_mod.settings.bend + 1.0) * 10 trait['parameters'] = [str(soft_type), str(bend), str(soft_mod.settings.mass), str(bobject.arm_soft_body_margin)] o['traits'].append(trait) if soft_type == 0 and soft_mod.settings.use_pin_cloth: self.add_hook_mod(o, bobject, '', soft_mod.settings.vertex_group_mass) def add_hook_mod(self, o, bobject, target_name, group_name): ArmoryExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics == 'Bullet' else 'oimo' trait = {} trait['type'] = 'Script' trait['class_name'] = 'armory.trait.physics.' + phys_pkg + '.PhysicsHook' verts = [] if group_name != '': group = bobject.vertex_groups[group_name].index for v in bobject.data.vertices: for g in v.groups: if g.group == group: verts.append(v.co.x) verts.append(v.co.y) verts.append(v.co.z) trait['parameters'] = ["'" + target_name + "'", str(verts)] o['traits'].append(trait) def add_rigidbody_constraint(self, o, rbc): rb1 = rbc.object1 rb2 = rbc.object2 ArmoryExporter.export_physics = True phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics == 'Bullet' else 'oimo' breaking_threshold = rbc.breaking_threshold if rbc.use_breaking else 0 trait = {} trait['type'] = 'Script' trait['class_name'] = 'armory.trait.physics.' + phys_pkg + '.PhysicsConstraint' trait['parameters'] = [\ "'" + rb1.name + "'", \ "'" + rb2.name + "'", \ "'" + rbc.type + "'", \ str(rbc.disable_collisions).lower(), \ str(breaking_threshold)] if rbc.type == "GENERIC": limits = [] limits.append(1 if rbc.use_limit_lin_x else 0) limits.append(rbc.limit_lin_x_lower) limits.append(rbc.limit_lin_x_upper) limits.append(1 if rbc.use_limit_lin_y else 0) limits.append(rbc.limit_lin_y_lower) limits.append(rbc.limit_lin_y_upper) limits.append(1 if rbc.use_limit_lin_z else 0) limits.append(rbc.limit_lin_z_lower) limits.append(rbc.limit_lin_z_upper) limits.append(1 if rbc.use_limit_ang_x else 0) limits.append(rbc.limit_ang_x_lower) limits.append(rbc.limit_ang_x_upper) limits.append(1 if rbc.use_limit_ang_y else 0) limits.append(rbc.limit_ang_y_lower) limits.append(rbc.limit_ang_y_upper) limits.append(1 if rbc.use_limit_ang_z else 0) limits.append(rbc.limit_ang_z_lower) limits.append(rbc.limit_ang_z_upper) trait['parameters'].append(str(limits)) o['traits'].append(trait) def post_export_world(self, world, o): wrd = bpy.data.worlds['Arm'] bgcol = world.arm_envtex_color if '_LDR' in wrd.world_defs: # No compositor used for i in range(0, 3): bgcol[i] = pow(bgcol[i], 1.0 / 2.2) o['background_color'] = arm.utils.color_to_int(bgcol) if '_EnvSky' in wrd.world_defs: # Sky data for probe o['sun_direction'] = list(world.arm_envtex_sun_direction) o['turbidity'] = world.arm_envtex_turbidity o['ground_albedo'] = world.arm_envtex_ground_albedo disable_hdr = world.arm_envtex_name.endswith('.jpg') if '_EnvTex' in wrd.world_defs or '_EnvImg' in wrd.world_defs: o['envmap'] = world.arm_envtex_name.rsplit('.', 1)[0] if disable_hdr: o['envmap'] += '.jpg' else: o['envmap'] += '.hdr' o['probes'] = [] # Main probe rpdat = arm.utils.get_rp() solid_mat = rpdat.arm_material_model == 'Solid' arm_irradiance = rpdat.arm_irradiance and not solid_mat arm_radiance = False radtex = world.arm_envtex_name.rsplit('.', 1)[0] irrsharmonics = world.arm_envtex_irr_name # Radiance if '_EnvTex' in wrd.world_defs: arm_radiance = rpdat.arm_radiance elif '_EnvSky' in wrd.world_defs and rpdat.arm_radiance_sky: arm_radiance = rpdat.arm_radiance radtex = 'hosek' num_mips = world.arm_envtex_num_mips strength = world.arm_envtex_strength mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' if mobile_mat: arm_radiance = False po = {} po['name'] = world.name if arm_irradiance: ext = '' if wrd.arm_minimize else '.json' po['irradiance'] = irrsharmonics + '_irradiance' + ext if arm_radiance: po['radiance'] = radtex + '_radiance' if disable_hdr: po['radiance'] += '.jpg' else: po['radiance'] += '.hdr' po['radiance_mipmaps'] = num_mips else: po['irradiance'] = '' # No irradiance data, fallback to default at runtime po['strength'] = strength po['blending'] = 1.0 po['volume'] = [0.0, 0.0, 0.0] po['volume_center'] = [0.0, 0.0, 0.0] o['probes'].append(po) # https://blender.stackexchange.com/questions/70629 def mod_equal(self, mod1, mod2): return all([getattr(mod1, prop, True) == getattr(mod2, prop, False) for prop in mod1.bl_rna.properties.keys()]) def mod_equal_stack(self, obj1, obj2): if len(obj1.modifiers) == 0 and len(obj2.modifiers) == 0: return True if len(obj1.modifiers) == 0 or len(obj2.modifiers) == 0: return False if len(obj1.modifiers) != len(obj2.modifiers): return False return all([self.mod_equal(m, obj2.modifiers[i]) for i,m in enumerate(obj1.modifiers)])