import bpy,bmesh,bpy_extras,mathutils import pathlib,zipfile,time,os,tempfile,math import struct, shutil from bpy_extras import io_utils, node_shader_utils from . import UTILS_constants, UTILS_functions, UTILS_file_io, UTILS_zip_helper, UTILS_virtools_prop class BALLANCE_OT_export_bm(bpy.types.Operator, bpy_extras.io_utils.ExportHelper): """Save a Ballance Map File (BM file spec 1.4)""" bl_idname = "ballance.export_bm" bl_label = 'Export BM' bl_options = {'PRESET'} # ExportHelper mixin class uses this filename_ext = ".bmx" filter_glob: bpy.props.StringProperty( default="*.bmx", options={'HIDDEN'}, maxlen=255, # Max internal buffer length, longer would be clamped. ) export_mode: bpy.props.EnumProperty( name="Export mode", items=(('COLLECTION', "Collection", "Export a collection"), ('OBJECT', "Objects", "Export an objects"), ), ) def execute(self, context): if ((self.export_mode == 'COLLECTION' and context.scene.BallanceBlenderPluginProperty.collection_picker is None) or (self.export_mode == 'OBJECT' and context.scene.BallanceBlenderPluginProperty.object_picker is None)): UTILS_functions.show_message_box(("No specific target", ), "Lost parameter", 'ERROR') else: prefs = bpy.context.preferences.addons[__package__].preferences if self.export_mode == 'COLLECTION': export_bm(context, self.filepath, prefs.no_component_collection, self.export_mode, context.scene.BallanceBlenderPluginProperty.collection_picker) elif self.export_mode == 'OBJECT': export_bm(context, self.filepath, prefs.no_component_collection, self.export_mode, context.scene.BallanceBlenderPluginProperty.object_picker) return {'FINISHED'} def draw(self, context): layout = self.layout layout.prop(self, "export_mode") if self.export_mode == 'COLLECTION': layout.prop(context.scene.BallanceBlenderPluginProperty, "collection_picker") elif self.export_mode == 'OBJECT': layout.prop(context.scene.BallanceBlenderPluginProperty, "object_picker") def export_bm(context, bmx_filepath, prefs_fncg, opts_exportMode, opts_exportTarget): # ============================================ alloc a temp folder utils_tempFolderObj = tempfile.TemporaryDirectory() utils_tempFolder = utils_tempFolderObj.name utils_tempTextureFolder = os.path.join(utils_tempFolder, "Texture") os.makedirs(utils_tempTextureFolder) # ============================================ # find export target. # do not need check them validation in there. # just collect them. if opts_exportMode== "COLLECTION": objectList = opts_exportTarget.objects else: objectList = [opts_exportTarget, ] # try get fncg collection # fncg stands with forced non-component group try: object_fncgCollection = bpy.data.collections[prefs_fncg] except: object_fncgCollection = None # ============================================ export with open(os.path.join(utils_tempFolder, "index.bm"), "wb") as finfo: UTILS_file_io.write_uint32(finfo, UTILS_constants.bmfile_currentVersion) # ====================== export object meshSet = set() meshList = [] meshCount = 0 with open(os.path.join(utils_tempFolder, "object.bm"), "wb") as fobject: for obj in objectList: # only export mesh object if obj.type != 'MESH': continue # clean no mesh object object_blenderMesh = obj.data if object_blenderMesh is None: continue # check component if (object_fncgCollection is not None) and (obj.name in object_fncgCollection.objects): # it should be set as normal object forcely object_isComponent = False else: # check isComponent normally object_isComponent = UTILS_functions.is_component(obj.name) # triangle first and then group if not object_isComponent: if object_blenderMesh not in meshSet: _mesh_triangulate(object_blenderMesh) meshSet.add(object_blenderMesh) meshList.append(object_blenderMesh) object_meshIndex = meshCount meshCount += 1 else: object_meshIndex = meshList.index(object_blenderMesh) else: object_meshIndex = UTILS_functions.get_component_id(obj.name) # get visibility object_isHidden = not obj.visible_get() # try get grouping data object_groupList = UTILS_virtools_prop.get_virtools_group_data(obj) # ======================= # write to files # write finfo first UTILS_file_io.write_string(finfo, obj.name) UTILS_file_io.write_uint8(finfo, UTILS_constants.BmfileInfoType.OBJECT) UTILS_file_io.write_uint64(finfo, fobject.tell()) # write fobject UTILS_file_io.write_bool(fobject, object_isComponent) UTILS_file_io.write_bool(fobject, object_isHidden) UTILS_file_io.write_world_matrix(fobject, obj.matrix_world) UTILS_file_io.write_uint32(fobject, len(object_groupList)) for item in object_groupList: UTILS_file_io.write_string(fobject, item) UTILS_file_io.write_uint32(fobject, object_meshIndex) # ====================== export mesh materialSet = set() materialList = [] with open(os.path.join(utils_tempFolder, "mesh.bm"), "wb") as fmesh: for mesh in meshList: # split normals mesh.calc_normals_split() # write finfo first UTILS_file_io.write_string(finfo, mesh.name) UTILS_file_io.write_uint8(finfo, UTILS_constants.BmfileInfoType.MESH) UTILS_file_io.write_uint64(finfo, fmesh.tell()) # write fmesh # vertices mesh_vecList = mesh.vertices[:] UTILS_file_io.write_uint32(fmesh, len(mesh_vecList)) for vec in mesh_vecList: #swap yz UTILS_file_io.write_3vector(fmesh,vec.co[0],vec.co[2],vec.co[1]) # uv mesh_faceIndexPairs = [(face, index) for index, face in enumerate(mesh.polygons)] UTILS_file_io.write_uint32(fmesh, len(mesh_faceIndexPairs) * 3) if mesh.uv_layers.active is not None: uv_layer = mesh.uv_layers.active.data[:] for f, f_index in mesh_faceIndexPairs: # it should be triangle face, otherwise throw a error if (f.loop_total != 3): raise Exception("Not a triangle", f.poly.loop_total) for loop_index in range(f.loop_start, f.loop_start + f.loop_total): uv = uv_layer[loop_index].uv # reverse v UTILS_file_io.write_2vector(fmesh, uv[0], -uv[1]) else: # no uv data. write garbage for i in range(len(mesh_faceIndexPairs) * 3): UTILS_file_io.write_2vector(fmesh, 0.0, 0.0) # normals UTILS_file_io.write_uint32(fmesh, len(mesh_faceIndexPairs) * 3) for f, f_index in mesh_faceIndexPairs: # no need to check triangle again for loop_index in range(f.loop_start, f.loop_start + f.loop_total): nml = mesh.loops[loop_index].normal # swap yz UTILS_file_io.write_3vector(fmesh, nml[0], nml[2], nml[1]) # face # get material first mesh_usedBlenderMtl = mesh.materials[:] mesh_noMaterial = len(mesh_usedBlenderMtl) == 0 for mat in mesh_usedBlenderMtl: if mat not in materialSet: materialSet.add(mat) materialList.append(mat) UTILS_file_io.write_uint32(fmesh, len(mesh_faceIndexPairs)) mesh_vtIndex = [] mesh_vnIndex = [] mesh_vIndex = [] for f, f_index in mesh_faceIndexPairs: # confirm material use if mesh_noMaterial: mesh_materialIndex = 0 else: mesh_materialIndex = materialList.index(mesh_usedBlenderMtl[f.material_index]) # export face mesh_vtIndex.clear() mesh_vnIndex.clear() mesh_vIndex.clear() counter = 0 for loop_index in range(f.loop_start, f.loop_start + f.loop_total): mesh_vIndex.append(mesh.loops[loop_index].vertex_index) mesh_vnIndex.append(f_index * 3 + counter) mesh_vtIndex.append(f_index * 3 + counter) counter += 1 # reverse vertices sort UTILS_file_io.write_face(fmesh, mesh_vIndex[2], mesh_vtIndex[2], mesh_vnIndex[2], mesh_vIndex[1], mesh_vtIndex[1], mesh_vnIndex[1], mesh_vIndex[0], mesh_vtIndex[0], mesh_vnIndex[0]) # set used material UTILS_file_io.write_bool(fmesh, not mesh_noMaterial) UTILS_file_io.write_uint32(fmesh, mesh_materialIndex) # free splited normals mesh.free_normals_split() # ====================== export material textureSet = set() textureList = [] textureCount = 0 with open(os.path.join(utils_tempFolder, "material.bm"), "wb") as fmaterial: for material in materialList: # write finfo first UTILS_file_io.write_string(finfo, material.name) UTILS_file_io.write_uint8(finfo, UTILS_constants.BmfileInfoType.MATERIAL) UTILS_file_io.write_uint64(finfo, fmaterial.tell()) # try get original written data (material_enableVirtoolsMat, material_colAmbient, material_colDiffuse, material_colSpecular, material_colEmissive, material_specularPower, material_alphaTest, material_alphaBlend, material_zBuffer, material_twoSided, material_texture) = UTILS_virtools_prop.get_virtools_material_data(material) # only try get from Principled BSDF when we couldn't get from virtools_material props if not material_enableVirtoolsMat: v = UTILS_functions.parse_material_nodes(material) if v is not None: (material_enableVirtoolsMat, material_colAmbient, material_colDiffuse, material_colSpecular, material_colEmissive, material_specularPower, material_alphaTest, material_alphaBlend, material_zBuffer, material_twoSided, material_texture) = v # check texture index if material_texture is None: material_useTexture = False material_textureIndex = 0 else: # add into texture list if material_texture not in textureSet: textureSet.add(material_texture) textureList.append(material_texture) textureIndex = textureCount textureCount += 1 else: textureIndex = textureList.index(material_texture) material_useTexture = True material_textureIndex = textureIndex UTILS_file_io.write_color(fmaterial, material_colAmbient) UTILS_file_io.write_color(fmaterial, material_colDiffuse) UTILS_file_io.write_color(fmaterial, material_colSpecular) UTILS_file_io.write_color(fmaterial, material_colEmissive) UTILS_file_io.write_float(fmaterial, material_specularPower) UTILS_file_io.write_bool(fmaterial, material_alphaTest) UTILS_file_io.write_bool(fmaterial, material_alphaBlend) UTILS_file_io.write_bool(fmaterial, material_zBuffer) UTILS_file_io.write_bool(fmaterial, material_twoSided) UTILS_file_io.write_bool(fmaterial, material_useTexture) UTILS_file_io.write_uint32(fmaterial, material_textureIndex) # ====================== export texture texture_blenderFilePath = os.path.dirname(bpy.data.filepath) texture_existedTextureFilepath = set() with open(os.path.join(utils_tempFolder, "texture.bm"), "wb") as ftexture: for texture in textureList: # write finfo first UTILS_file_io.write_string(finfo, texture.name) UTILS_file_io.write_uint8(finfo, UTILS_constants.BmfileInfoType.TEXTURE) UTILS_file_io.write_uint64(finfo, ftexture.tell()) # confirm whether it is internal texture # get absolute texture path texture_filepath = io_utils.path_reference(texture.filepath, texture_blenderFilePath, utils_tempTextureFolder, 'ABSOLUTE', "", None, texture.library) # get file name and write it texture_filename = os.path.basename(texture_filepath) UTILS_file_io.write_string(ftexture, texture_filename) if (_is_external_texture(texture_filename)): # write directly, use Ballance texture UTILS_file_io.write_bool(ftexture, True) else: # copy internal texture, if this file is copied, do not copy it again UTILS_file_io.write_bool(ftexture, False) if texture_filename not in texture_existedTextureFilepath: shutil.copy(texture_filepath, os.path.join(utils_tempTextureFolder, texture_filename)) texture_existedTextureFilepath.add(texture_filename) # ============================================ # save zip and clean up folder UTILS_zip_helper.compress(utils_tempFolder, bmx_filepath) utils_tempFolderObj.cleanup() # ========================================== # blender related functions def _is_external_texture(name): if name in UTILS_constants.bmfile_externalTextureSet: return True else: return False def _mesh_triangulate(me): bm = bmesh.new() bm.from_mesh(me) bmesh.ops.triangulate(bm, faces=bm.faces) bm.to_mesh(me) bm.free() def _set_value_when_none(obj, newValue): if obj is None: return newValue else: return obj