From e264c85a04a8f868d11a220b7b41013d87a1614d Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Sun, 3 Apr 2022 22:48:12 +0800 Subject: [PATCH] refactor project. preparing v3.0 development. no debug current --- README.md | 2 +- README_ZH.md | 2 +- ballance_blender_plugin/BMFILE_export.py | 363 +++++++ ballance_blender_plugin/BMFILE_import.py | 357 +++++++ ...reedsmax_align.py => MODS_3dsmax_align.py} | 18 +- .../{flatten_uv.py => MODS_flatten_uv.py} | 13 +- .../{rail_uv.py => MODS_rail_uv.py} | 31 +- .../NAMES_rename_via_group.py | 35 + .../OBJS_add_components.py | 56 ++ .../{add_floor.py => OBJS_add_floors.py} | 170 ++-- .../{add_elements.py => OBJS_add_rails.py} | 64 +- .../{config.py => UTILS_constants.py} | 41 +- ballance_blender_plugin/UTILS_file_io.py | 96 ++ ballance_blender_plugin/UTILS_functions.py | 222 +++++ .../{preferences.py => UTILS_preferences.py} | 0 ballance_blender_plugin/UTILS_zip_helper.py | 48 + ballance_blender_plugin/__init__.py | 140 +-- ballance_blender_plugin/bm_import_export.py | 907 ------------------ ballance_blender_plugin/no_uv_checker.py | 48 - ballance_blender_plugin/utils.py | 21 - 20 files changed, 1421 insertions(+), 1213 deletions(-) create mode 100644 ballance_blender_plugin/BMFILE_export.py create mode 100644 ballance_blender_plugin/BMFILE_import.py rename ballance_blender_plugin/{threedsmax_align.py => MODS_3dsmax_align.py} (87%) rename ballance_blender_plugin/{flatten_uv.py => MODS_flatten_uv.py} (88%) rename ballance_blender_plugin/{rail_uv.py => MODS_rail_uv.py} (82%) create mode 100644 ballance_blender_plugin/NAMES_rename_via_group.py create mode 100644 ballance_blender_plugin/OBJS_add_components.py rename ballance_blender_plugin/{add_floor.py => OBJS_add_floors.py} (69%) rename ballance_blender_plugin/{add_elements.py => OBJS_add_rails.py} (54%) rename ballance_blender_plugin/{config.py => UTILS_constants.py} (87%) create mode 100644 ballance_blender_plugin/UTILS_file_io.py create mode 100644 ballance_blender_plugin/UTILS_functions.py rename ballance_blender_plugin/{preferences.py => UTILS_preferences.py} (100%) create mode 100644 ballance_blender_plugin/UTILS_zip_helper.py delete mode 100644 ballance_blender_plugin/bm_import_export.py delete mode 100644 ballance_blender_plugin/no_uv_checker.py delete mode 100644 ballance_blender_plugin/utils.py diff --git a/README.md b/README.md index 8b6ed70..cd62dbb 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ In the dialog, you can select the material to be used. You can also choose the u You can also select the projection axis for better UV distribution. -### Flatten UV +#### Flatten UV In the object editing mode, it is a operator which is used to attach the currently selected surface to the UV. And you can specific the edge which will be attached into the V axis. Note that only convex faces are supported. diff --git a/README_ZH.md b/README_ZH.md index f1233c3..1d60f27 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -50,7 +50,7 @@ Ballance 3D是一套简单的用于制图3D相关的轻型工具集合,可以 还可以选择投影轴以获取更好的UV分布。 -### Flatten UV +#### Flatten UV 在物体编辑模式下,用于将当前选中面按某一边贴附到V轴上的模式,展开到UV上。注意,只支持凸边面。 diff --git a/ballance_blender_plugin/BMFILE_export.py b/ballance_blender_plugin/BMFILE_export.py new file mode 100644 index 0000000..1b0fbd9 --- /dev/null +++ b/ballance_blender_plugin/BMFILE_export.py @@ -0,0 +1,363 @@ +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 + +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'} + filename_ext = ".bmx" + + 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 = 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, bm_current_version) + + # ====================== 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 = 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 = get_component_id(obj.name) + + # get visibility + object_isHidden = not obj.visible_get() + + # try get grouping data + object_groupList = _try_get_custom_property(obj, 'virtools-group') + object_groupList = _set_value_when_none(object_groupList, []) + + # ======================= + # 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_worldMatrix(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_colAmbient = _try_get_custom_property(material, 'virtools-ambient') + material_colDiffuse = _try_get_custom_property(material, 'virtools-diffuse') + material_colSpecular = _try_get_custom_property(material, 'virtools-specular') + material_colEmissive = _try_get_custom_property(material, 'virtools-emissive') + material_specularPower = _try_get_custom_property(material, 'virtools-power') + + # get basic color + mat_wrap = node_shader_utils.PrincipledBSDFWrapper(material) + if mat_wrap: + use_mirror = mat_wrap.metallic != 0.0 + if use_mirror: + material_colAmbient = _set_value_when_none(material_colAmbient, (mat_wrap.metallic, mat_wrap.metallic, mat_wrap.metallic)) + else: + material_colAmbient = _set_value_when_none(material_colAmbient, (1.0, 1.0, 1.0)) + material_colDiffuse = _set_value_when_none(material_colDiffuse, (mat_wrap.base_color[0], mat_wrap.base_color[1], mat_wrap.base_color[2])) + material_colSpecular = _set_value_when_none(material_colSpecular, (mat_wrap.specular, mat_wrap.specular, mat_wrap.specular)) + material_colEmissive = _set_value_when_none(material_colEmissive, mat_wrap.emission_color[:3]) + material_specularPower = _set_value_when_none(material_specularPower, 0.0) + + # confirm texture + tex_wrap = getattr(mat_wrap, "base_color_texture", None) + if tex_wrap: + image = tex_wrap.image + if image: + # add into texture list + if image not in textureSet: + textureSet.add(image) + textureList.append(image) + textureIndex = textureCount + textureCount += 1 + else: + textureIndex = textureList.index(image) + + material_useTexture = True + material_textureIndex = textureIndex + else: + # no texture + material_useTexture = False + material_textureIndex = 0 + else: + # no texture + material_useTexture = False + material_textureIndex = 0 + + else: + # no Principled BSDF. write garbage + material_colAmbient = _set_value_when_none(material_colAmbient, (0.8, 0.8, 0.8)) + material_colDiffuse = _set_value_when_none(material_colDiffuse, (0.8, 0.8, 0.8)) + material_colSpecular = _set_value_when_none(material_colSpecular, (0.8, 0.8, 0.8)) + material_colEmissive = _set_value_when_none(material_colEmissive, (0.8, 0.8, 0.8)) + material_specularPower = _set_value_when_none(material_specularPower, 0.0) + + material_useTexture = False + material_textureIndex = 0 + + 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_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 config.external_texture_list: + 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 _try_get_custom_property(obj, field): + try: + return obj[field] + except: + return None + +def _set_value_when_none(obj, newValue): + if obj is None: + return newValue + else: + return obj + diff --git a/ballance_blender_plugin/BMFILE_import.py b/ballance_blender_plugin/BMFILE_import.py new file mode 100644 index 0000000..8bd96b9 --- /dev/null +++ b/ballance_blender_plugin/BMFILE_import.py @@ -0,0 +1,357 @@ +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 bpy_extras.io_utils import unpack_list +from bpy_extras.image_utils import load_image +from . import UTILS_constants, UTILS_functions, UTILS_file_io, UTILS_zip_helper + +class BALLANCE_OT_import_bm(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): + """Load a Ballance Map File (BM file spec 1.4)""" + bl_idname = "ballance.import_bm" + bl_label = "Import BM " + bl_options = {'PRESET', 'UNDO'} + filename_ext = ".bmx" + + texture_conflict_strategy: bpy.props.EnumProperty( + name="Texture name conflict", + items=(('NEW', "New instance", "Create a new instance"), + ('CURRENT', "Use current", "Use current"),), + description="Define how to process texture name conflict", + default='CURRENT', + ) + + material_conflict_strategy: bpy.props.EnumProperty( + name="Material name conflict", + items=(('RENAME', "Rename", "Rename the new one"), + ('CURRENT', "Use current", "Use current"),), + description="Define how to process material name conflict", + default='RENAME', + ) + + mesh_conflict_strategy: bpy.props.EnumProperty( + name="Mesh name conflict", + items=(('RENAME', "Rename", "Rename the new one"), + ('CURRENT', "Use current", "Use current"),), + description="Define how to process mesh name conflict", + default='RENAME', + ) + + object_conflict_strategy: bpy.props.EnumProperty( + name="Object name conflict", + items=(('RENAME', "Rename", "Rename the new one"), + ('CURRENT', "Use current", "Use current"),), + description="Define how to process object name conflict", + default='RENAME', + ) + + @classmethod + def poll(self, context): + prefs = bpy.context.preferences.addons[__package__].preferences + return (os.path.isdir(prefs.temp_texture_folder) and os.path.isdir(prefs.external_folder)) + + def execute(self, context): + prefs = bpy.context.preferences.addons[__package__].preferences + import_bm(context, self.filepath, + prefs.no_component_collection, prefs.external_folder, prefs.temp_texture_folder, + self.texture_conflict_strategy, self.material_conflict_strategy, + self.mesh_conflict_strategy, self.object_conflict_strategy) + return {'FINISHED'} + + +def import_bm(context, bmx_filepath, prefs_fncg, prefs_externalTexture, prefs_tempTextureFolder, opts_texture, opts_material, opts_mesh, opts_object): + # ============================================ + # alloc a temp folder for decompress + utils_tempFolderObj = tempfile.TemporaryDirectory() + utils_tempFolder = utils_tempFolderObj.name + utils_tempTextureFolder = os.path.join(utils_tempFolder, "Texture") + # decompress + UTILS_zip_helper.decompress(utils_tempFolder, bmx_filepath) + + # ============================================ + # read bmx file officially + # index.bm + objectList = [] + meshList = [] + materialList = [] + textureList = [] + with open(os.path.join(utils_tempFolder, "index.bm"), "rb") as findex: + # check version first + index_gottenVersion = UTILS_file_io.read_uint32(findex) + if (index_gottenVersion != UTILS_constants.bmfile_currentVersion): + # clean temp folder, output error + UTILS_functions.show_message_box( + ("Unsupported BM spec. Expect: {} Gotten: {}".format(UTILS_constants.bmfile_currentVersion, index_gottenVersion), ), + "Unsupported BM spec", 'ERROR') + findex.close() + utils_tempFolderObj.cleanup() + return + + # collect block header data + while len(peek_stream(findex)) != 0: + # read + index_name = UTILS_file_io.read_string(findex) + index_type = UTILS_file_io.read_uint8(findex) + index_offset = UTILS_file_io.read_uint64(findex) + index_blockCache = _InfoBlockHelper(index_name, index_offset) + + # grouping into list + if index_type == UTILS_constants.BmfileInfoType.OBJECT: + objectList.append(index_blockCache) + elif index_type == UTILS_constants.BmfileInfoType.MESH: + meshList.append(index_blockCache) + elif index_type == UTILS_constants.BmfileInfoType.MATERIAL: + materialList.append(index_blockCache) + elif index_type == UTILS_constants.BmfileInfoType.TEXTURE: + textureList.append(index_blockCache) + else: + pass + + + # texture.bm + with open(os.path.join(utils_tempFolder, "texture.bm"), "rb") as ftexture: + for item in textureList: + # seek to block + ftexture.seek(item.offset, os.SEEK_SET) + + # read data + texture_filename = UTILS_file_io.read_string(ftexture) + texture_isExternal = UTILS_file_io.read_bool(ftexture) + if texture_isExternal: + (texture_target, skip_init) = UTILS_functions.create_instance_with_option( + UTILS_constants.BmfileInfoType.TEXTURE, item.name, opts_texture, + extra_texture_path= texture_filename, extra_texture_path= prefs_externalTexture) + else: + # not external. copy temp file into blender temp. then use it. + # try copy. if fail, don't need to do more + try: + shutil.copy(os.path.join(utils_tempTextureFolder, texture_filename), + os.path.join(prefs_tempTextureFolder, texture_filename)) + except: + pass + + (texture_target, skip_init) = UTILS_functions.create_instance_with_option( + UTILS_constants.BmfileInfoType.TEXTURE, item.name, opts_texture, + extra_texture_path= texture_filename, extra_texture_path= prefs_tempTextureFolder) + + # setup name and blender data for header + item.blender_data = texture_target + + # material.bm + # WARNING: this code is shared with add_floor - create_or_get_material() + with open(os.path.join(utils_tempFolder, "material.bm"), "rb") as fmaterial: + for item in materialList: + # seek to block + fmaterial.seek(item.offset, os.SEEK_SET) + + # read data + material_colAmbient = UTILS_file_io.read_3vector(fmaterial) + material_colDiffuse = UTILS_file_io.read_3vector(fmaterial) + material_colSpecular = UTILS_file_io.read_3vector(fmaterial) + material_colEmissive = UTILS_file_io.read_3vector(fmaterial) + material_specularPower = UTILS_file_io.read_float(fmaterial) + material_useTexture = UTILS_file_io.read_bool(fmaterial) + material_texture = UTILS_file_io.read_uint32(fmaterial) + + # alloc basic material + (material_target, skip_init) = create_instance_with_option( + UTILS_constants.BmfileInfoType.MATERIAL, item.name, opts_material) + item.blender_data = material_target + if skip_init: + continue + + # try create material nodes + UTILS_functions.create_material_nodes(material_target, + material_colAmbient, material_colDiffuse, material_colSpecular, material_colEmissive, + material_specularPower, + textureList[material_texture].blender_data if material_useTexture else None) + + # mesh.bm + # WARNING: this code is shared with add_floor + with open(os.path.join(utils_tempFolder, "mesh.bm"), "rb") as fmesh: + mesh_vList=[] + mesh_vtList=[] + mesh_vnList=[] + mesh_faceList=[] + mesh_materialSolt = [] + for item in meshList: + fmesh.seek(item.offset, os.SEEK_SET) + + # create real mesh + (mesh_target, skip_init) = create_instance_with_option( + UTILS_constants.BmfileInfoType.MESH, item.name, opts_mesh) + item.blender_data = mesh_target + if skip_init: + continue + + mesh_vList.clear() + mesh_vtList.clear() + mesh_vnList.clear() + mesh_faceList.clear() + mesh_materialSolt.clear() + # in first read, store all data into list + mesh_listCount = UTILS_file_io.read_uint32(fmesh) + for i in range(mesh_listCount): + cache = UTILS_file_io.read_3vector(fmesh) + # switch yz + mesh_vList.append((cache[0], cache[2], cache[1])) + mesh_listCount = UTILS_file_io.read_uint32(fmesh) + for i in range(mesh_listCount): + cache = UTILS_file_io.read_2vector(fmesh) + # reverse v + mesh_vtList.append((cache[0], -cache[1])) + mesh_listCount = UTILS_file_io.read_uint32(fmesh) + for i in range(mesh_listCount): + cache = UTILS_file_io.read_3vector(fmesh) + # switch yz + mesh_vnList.append((cache[0], cache[2], cache[1])) + + mesh_listCount = UTILS_file_io.read_uint32(fmesh) + for i in range(mesh_listCount): + mesh_faceData = UTILS_file_io.read_face(fmesh) + mesh_useMaterial = UTILS_file_io.read_bool(fmesh) + mesh_materialIndex = UTILS_file_io.read_uint32(fmesh) + + if mesh_useMaterial: + mesh_neededMaterial = materialList[mesh_materialIndex].blender_data + if mesh_neededMaterial in mesh_materialSolt: + mesh_blenderMtlIndex = materialSolt.index(mesh_neededMaterial) + else: + mesh_blenderMtlIndex = len(mesh_materialSolt) + mesh_materialSolt.append(mesh_neededMaterial) + else: + mesh_blenderMtlIndex = -1 + + # we need invert triangle sort + mesh_faceList.append(( + mesh_faceData[6], mesh_faceData[7], mesh_faceData[8], + mesh_faceData[3], mesh_faceData[4], mesh_faceData[5], + mesh_faceData[0], mesh_faceData[1], mesh_faceData[2], + mesh_blenderMtlIndex + )) + + # and then we need add material solt for this mesh + for mat in mesh_materialSolt: + mesh_target.materials.append(mat) + + # then, we need add correspond count for vertices + mesh_target.vertices.add(len(vList)) + mesh_target.loops.add(len(faceList)*3) # triangle face confirm + mesh_target.polygons.add(len(faceList)) + mesh_target.uv_layers.new(do_init=False) + mesh_target.create_normals_split() + + # add vertices data + mesh_target.vertices.foreach_set("co", unpack_list(vList)) + mesh_target.loops.foreach_set("vertex_index", unpack_list(_flat_vertices_index(mesh_faceList))) + mesh_target.loops.foreach_set("normal", unpack_list(_flat_vertices_normal(mesh_faceList, mesh_vnList))) + mesh_target.uv_layers[0].data.foreach_set("uv", unpack_list(_flat_vertices_uv(mesh_faceList, mesh_vtList))) + for i in range(len(faceList)): + mesh_target.polygons[i].loop_start = i * 3 + mesh_target.polygons[i].loop_total = 3 + if faceList[i][9] != -1: + mesh_target.polygons[i].material_index = faceList[i][9] + + mesh_target.polygons[i].use_smooth = True + + mesh_target.validate(clean_customdata=False) + mesh_target.update(calc_edges=False, calc_edges_loose=False) + + + # object + with open(os.path.join(utils_tempFolder, "object.bm"), "rb") as fobject: + + # we need get needed collection first + blender_viewLayer = context.view_layer + blender_collection = blender_viewLayer.active_layer_collection.collection + if prefs_fncg == "": + # fncg stands with Forced Non-Component Group + object_fncgCollection = None + else: + try: + # try get collection + object_fncgCollection = bpy.data.collections[prefs_fncg] + except: + # fail to get, create new one under active collection instead + object_fncgCollection = bpy.data.collections.new(prefs_fncg) + blender_collection.children.link(object_fncgCollection) + + # start process it + object_groupList = [] + for item in objectList: + fobject.seek(item.offset, os.SEEK_SET) + + # read data + object_isComponent = UTILS_file_io.read_bool(fobject) + #object_isForcedNoComponent = UTILS_file_io.read_bool(fobject) + object_isHidden = UTILS_file_io.read_bool(fobject) + object_worldMatrix = UTILS_file_io.read_worldMaterix(fobject) + object_groupListCount = UTILS_file_io.read_uint32(fobject) + object_groupList.clear() + for i in range(object_groupListCount): + object_groupList.append(UTILS_file_io.read_string(fobject)) + object_meshIndex = UTILS_file_io.read_uint32(fobject) + + # got mesh first + if object_isComponent: + object_neededMesh = UTILS_functions.load_component(object_meshIndex) + else: + object_neededMesh = meshList[object_meshIndex].blender_data + + # create real object + (object_target, skip_init) = create_instance_with_option( + UTILS_constants.BmfileInfoType.OBJECT, item.name, opts_object, + extraMesh=object_neededMesh) + if skip_init: + continue + + # link to correct collection + if (object_fncgCollection is not None) and (not object_isComponent) and UTILS_functions.is_component(item.name): + # a object should be grouped into fncg should fufill following requirements + # fncg is not null + # this object is a normal object + # but its name match component format + object_fncgCollection.objects.link(object_target) + else: + # otherwise, group it into normal collection + blender_collection.objects.link(object_target) + object_target.matrix_world = object_worldMatrix + object_target.hide_set(object_isHidden) + + # write custom property + if len(object_groupList) != 0: + object_target['virtools-group'] = tuple(object_groupList) + + # update view layer after all objects has been imported + blender_viewLayer.update() + + # release temp folder + utils_tempFolderObj.cleanup() + + +# ========================================== +# blender related functions + +class _InfoBlockHelper(): + def __init__(self, name, offset): + self.name = name + self.offset = offset + self.blender_data = None + +def _flat_vertices_index(faceList): + for item in faceList: + yield (item[0], ) + yield (item[3], ) + yield (item[6], ) + +def _flat_vertices_normal(faceList, vnList): + for item in faceList: + yield vnList[item[2]] + yield vnList[item[5]] + yield vnList[item[8]] + +def _flat_vertices_uv(faceList, vtList): + for item in faceList: + yield vtList[item[1]] + yield vtList[item[4]] + yield vtList[item[7]] diff --git a/ballance_blender_plugin/threedsmax_align.py b/ballance_blender_plugin/MODS_3dsmax_align.py similarity index 87% rename from ballance_blender_plugin/threedsmax_align.py rename to ballance_blender_plugin/MODS_3dsmax_align.py index cea3e82..adf351d 100644 --- a/ballance_blender_plugin/threedsmax_align.py +++ b/ballance_blender_plugin/MODS_3dsmax_align.py @@ -1,5 +1,5 @@ -import bpy,mathutils -from . import utils +import bpy, mathutils +from . import UTILS_functions class BALLANCE_OT_super_align(bpy.types.Operator): """Align object with 3ds Max way""" @@ -31,10 +31,10 @@ class BALLANCE_OT_super_align(bpy.types.Operator): @classmethod def poll(self, context): - return check_align_target() + return _check_align_target() def execute(self, context): - align_object(self.align_x, self.align_y, self.align_z, self.current_references, self.target_references) + _align_object(self.align_x, self.align_y, self.align_z, self.current_references, self.target_references) return {'FINISHED'} def invoke(self, context, event): @@ -56,7 +56,7 @@ class BALLANCE_OT_super_align(bpy.types.Operator): # ============================== method -def check_align_target(): +def _check_align_target(): if bpy.context.active_object is None: return False @@ -69,14 +69,14 @@ def check_align_target(): return True -def align_object(use_x, use_y, use_z, currentMode, targetMode): +def _align_object(use_x, use_y, use_z, currentMode, targetMode): if not (use_x or use_y or use_z): return # calc active object data currentObj = bpy.context.active_object currentObjBbox = [currentObj.matrix_world @ mathutils.Vector(corner) for corner in currentObj.bound_box] - currentObjRef = provideObjRefPoint(currentObj, currentObjBbox, currentMode) + currentObjRef = _provide_obj_reference_point(currentObj, currentObjBbox, currentMode) # calc target targetObjList = bpy.context.selected_objects[:] @@ -86,7 +86,7 @@ def align_object(use_x, use_y, use_z, currentMode, targetMode): # process each obj for targetObj in targetObjList: targetObjBbox = [targetObj.matrix_world @ mathutils.Vector(corner) for corner in targetObj.bound_box] - targetObjRef = provideObjRefPoint(targetObj, targetObjBbox, targetMode) + targetObjRef = _provide_obj_reference_point(targetObj, targetObjBbox, targetMode) if use_x: targetObj.location.x += currentObjRef.x - targetObjRef.x @@ -95,7 +95,7 @@ def align_object(use_x, use_y, use_z, currentMode, targetMode): if use_z: targetObj.location.z += currentObjRef.z - targetObjRef.z -def provideObjRefPoint(obj, vecList, mode): +def _provide_obj_reference_point(obj, vecList, mode): refPoint = mathutils.Vector((0, 0, 0)) if (mode == 'MIN'): diff --git a/ballance_blender_plugin/flatten_uv.py b/ballance_blender_plugin/MODS_flatten_uv.py similarity index 88% rename from ballance_blender_plugin/flatten_uv.py rename to ballance_blender_plugin/MODS_flatten_uv.py index aea742c..f004cc5 100644 --- a/ballance_blender_plugin/flatten_uv.py +++ b/ballance_blender_plugin/MODS_flatten_uv.py @@ -1,6 +1,6 @@ import bpy,mathutils import bmesh -from . import utils +from . import UTILS_functions class BALLANCE_OT_flatten_uv(bpy.types.Operator): """Flatten selected face UV. Only works for convex face""" @@ -9,7 +9,7 @@ class BALLANCE_OT_flatten_uv(bpy.types.Operator): bl_options = {'UNDO'} reference_edge : bpy.props.IntProperty( - name="Reference_edge", + name="Reference edge", description="The references edge of UV. It will be placed in V axis.", min=0, soft_min=0, @@ -33,16 +33,19 @@ class BALLANCE_OT_flatten_uv(bpy.types.Operator): return wm.invoke_props_dialog(self) def execute(self, context): - no_processed_count = real_flatten_uv(bpy.context.active_object.data, self.reference_edge) + no_processed_count = _real_flatten_uv(bpy.context.active_object.data, self.reference_edge) if no_processed_count != 0: - utils.ShowMessageBox(("{} faces may not be processed correctly because they have problem.".format(no_processed_count), ), "Warning", 'ERROR') + UTILS_functions.show_message_box( + ("{} faces may not be processed correctly because they have problem.".format(no_processed_count), ), + "Warning", 'ERROR' + ) return {'FINISHED'} def draw(self, context): layout = self.layout layout.prop(self, "reference_edge") -def real_flatten_uv(mesh, reference_edge): +def _real_flatten_uv(mesh, reference_edge): no_processed_count = 0 if mesh.uv_layers.active is None: diff --git a/ballance_blender_plugin/rail_uv.py b/ballance_blender_plugin/MODS_rail_uv.py similarity index 82% rename from ballance_blender_plugin/rail_uv.py rename to ballance_blender_plugin/MODS_rail_uv.py index fc78e9d..9ae9f98 100644 --- a/ballance_blender_plugin/rail_uv.py +++ b/ballance_blender_plugin/MODS_rail_uv.py @@ -1,7 +1,7 @@ import bpy,bmesh import mathutils import bpy.types -from . import utils, preferences +from . import UTILS_functions class BALLANCE_OT_rail_uv(bpy.types.Operator): """Create a UV for rail""" @@ -38,7 +38,7 @@ class BALLANCE_OT_rail_uv(bpy.types.Operator): @classmethod def poll(self, context): - return check_rail_target() + return _check_rail_target() def invoke(self, context, event): wm = context.window_manager @@ -46,9 +46,9 @@ class BALLANCE_OT_rail_uv(bpy.types.Operator): def execute(self, context): if context.scene.BallanceBlenderPluginProperty.material_picker == None: - utils.ShowMessageBox(("No specific material", ), "Lost parameter", 'ERROR') + UTILS_functions.show_message_box(("No specific material", ), "Lost parameter", 'ERROR') else: - create_rail_uv(self.uv_type, context.scene.BallanceBlenderPluginProperty.material_picker, self.uv_scale, self.projection_axis) + _create_rail_uv(self.uv_type, context.scene.BallanceBlenderPluginProperty.material_picker, self.uv_scale, self.projection_axis) return {'FINISHED'} def draw(self, context): @@ -62,7 +62,7 @@ class BALLANCE_OT_rail_uv(bpy.types.Operator): # ====================== method -def check_rail_target(): +def _check_rail_target(): for obj in bpy.context.selected_objects: if obj.type != 'MESH': continue @@ -71,7 +71,7 @@ def check_rail_target(): return True return False -def get_distance(iterator): +def _get_distance(iterator): is_first_min = True is_first_max = True max_value = 0.0 @@ -93,7 +93,7 @@ def get_distance(iterator): return max_value - min_value -def create_rail_uv(rail_type, material_pointer, scale_size, projection_axis): +def _create_rail_uv(rail_type, material_pointer, scale_size, projection_axis): objList = [] ignoredObj = [] for obj in bpy.context.selected_objects: @@ -125,18 +125,18 @@ def create_rail_uv(rail_type, material_pointer, scale_size, projection_axis): # calc proper scale if projection_axis == 'X': maxLength = max( - get_distance(vec.co[1] for vec in vecList), - get_distance(vec.co[2] for vec in vecList) + _get_distance(vec.co[1] for vec in vecList), + _get_distance(vec.co[2] for vec in vecList) ) elif projection_axis == 'Y': maxLength = max( - get_distance(vec.co[0] for vec in vecList), - get_distance(vec.co[2] for vec in vecList) + _get_distance(vec.co[0] for vec in vecList), + _get_distance(vec.co[2] for vec in vecList) ) elif projection_axis == 'Z': maxLength = max( - get_distance(vec.co[0] for vec in vecList), - get_distance(vec.co[1] for vec in vecList) + _get_distance(vec.co[0] for vec in vecList), + _get_distance(vec.co[1] for vec in vecList) ) real_scale = 1.0 / maxLength @@ -166,4 +166,7 @@ def create_rail_uv(rail_type, material_pointer, scale_size, projection_axis): uv_layer[loop_index].uv[1] = vecList[index].co[1] * real_scale if len(ignoredObj) != 0: - utils.ShowMessageBox(("Following objects are not processed due to they are not suit for this function now: ", ) + tuple(ignoredObj), "Execution result", 'INFO') + UTILS_functions.show_message_box( + ("Following objects are not processed due to they are not suit for this function now: ", ) + tuple(ignoredObj), + "Execution result", 'INFO' + ) diff --git a/ballance_blender_plugin/NAMES_rename_via_group.py b/ballance_blender_plugin/NAMES_rename_via_group.py new file mode 100644 index 0000000..06e2bc1 --- /dev/null +++ b/ballance_blender_plugin/NAMES_rename_via_group.py @@ -0,0 +1,35 @@ +import bpy,bmesh +import mathutils +import bpy.types +from . import UTILS_functions + +class BALLANCE_OT_rename_via_group(bpy.types.Operator): + """Rename object via Virtools groups""" + bl_idname = "ballance.rename_via_group" + bl_label = "Rename via Group" + bl_options = {'UNDO'} + + name_standard: bpy.props.EnumProperty( + name="Name Standard", + description="Choose your prefered name standard", + items=( + ("YYC", "YYC Tools Chains", "YYC Tools Chains name standard."), + ("IMENGYU", "Imengyu Ballance", "Auto grouping name standard for Imengyu/Ballance") + ), + ) + + @classmethod + def poll(self, context): + return True + #return _check_rail_target() + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def execute(self, context): + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + layout.prop(self, "name_standard") \ No newline at end of file diff --git a/ballance_blender_plugin/OBJS_add_components.py b/ballance_blender_plugin/OBJS_add_components.py new file mode 100644 index 0000000..56dee4e --- /dev/null +++ b/ballance_blender_plugin/OBJS_add_components.py @@ -0,0 +1,56 @@ +import bpy, mathutils +from . import UTILS_constants, UTILS_functions + +# ================================================= actual add + +class BALLANCE_OT_add_components(bpy.types.Operator): + """Add sector related elements""" + bl_idname = "ballance.add_components" + bl_label = "Add elements" + bl_options = {'UNDO'} + + elements_type: bpy.props.EnumProperty( + name="Type", + description="This element type", + items=tuple(map(lambda x: (x, x, ""), UTILS_constants.componentList)), + ) + + attentionElements = ["PC_TwoFlames", "PR_Resetpoint"] + uniqueElements = ["PS_FourFlames", "PE_Balloon"] + + elements_sector: bpy.props.IntProperty( + name="Sector", + description="Define which sector the object will be grouped in", + min=1, + max=8, + default=1, + ) + + def execute(self, context): + # get name + if self.elements_type in self.uniqueElements: + finalObjectName = self.elements_type + "_01" + elif self.elements_type in self.attentionElements: + finalObjectName = self.elements_type + "_0" + str(self.elements_sector) + else: + finalObjectName = self.elements_type + "_0" + str(self.elements_sector) + "_" + + # create object + loadedMesh = UTILS_functions.load_component( + UTILS_constants.componentList.index(self.elements_type)) + obj = bpy.data.objects.new(finalObjectName, loadedMesh) + UTILS_functions.add_into_scene_and_move_to_cursor(obj) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.prop(self, "elements_type") + if self.elements_type not in self.uniqueElements: + layout.prop(self, "elements_sector") + if self.elements_type in self.attentionElements: + layout.label(text="Please note that sector is suffix.") diff --git a/ballance_blender_plugin/add_floor.py b/ballance_blender_plugin/OBJS_add_floors.py similarity index 69% rename from ballance_blender_plugin/add_floor.py rename to ballance_blender_plugin/OBJS_add_floors.py index 2d8cb32..d2acbfd 100644 --- a/ballance_blender_plugin/add_floor.py +++ b/ballance_blender_plugin/OBJS_add_floors.py @@ -3,18 +3,18 @@ import os, math from bpy_extras import io_utils,node_shader_utils # from bpy_extras.io_utils import unpack_list from bpy_extras.image_utils import load_image -from . import utils, config +from . import UTILS_constants, UTILS_functions -class BALLANCE_OT_add_floor(bpy.types.Operator): +class BALLANCE_OT_add_floors(bpy.types.Operator): """Add Ballance floor""" - bl_idname = "ballance.add_floor" + bl_idname = "ballance.add_floors" bl_label = "Add floor" bl_options = {'UNDO'} floor_type: bpy.props.EnumProperty( name="Type", description="Floor type", - items=tuple((x, x, "") for x in config.floor_block_dict.keys()), + items=tuple((x, x, "") for x in UTILS_constants.floor_blockDict.keys()), ) expand_length_1 : bpy.props.IntProperty( @@ -39,16 +39,16 @@ class BALLANCE_OT_add_floor(bpy.types.Operator): ) use_2d_top : bpy.props.BoolProperty( - name="Top side" + name="Top edge" ) use_2d_right : bpy.props.BoolProperty( - name="Right side" + name="Right edge" ) use_2d_bottom : bpy.props.BoolProperty( - name="Bottom side" + name="Bottom edge" ) use_2d_left : bpy.props.BoolProperty( - name="Left side" + name="Left edge" ) use_3d_top : bpy.props.BoolProperty( name="Top face" @@ -65,10 +65,14 @@ class BALLANCE_OT_add_floor(bpy.types.Operator): return os.path.isdir(prefs.external_folder) def execute(self, context): + # get prefs + prefs = bpy.context.preferences.addons[__package__].preferences + prefs_externalTexture = prefs.external_folder + # load mesh objmesh = bpy.data.meshes.new('done_') - if self.floor_type in config.floor_basic_block_list: - load_basic_floor( + if self.floor_type in UTILS_constants.floor_basicBlockList: + _load_basic_floor( objmesh, self.floor_type, 'R0', @@ -81,9 +85,10 @@ class BALLANCE_OT_add_floor(bpy.types.Operator): self.use_2d_left, self.use_3d_top, self.use_3d_bottom), - (0.0, 0.0)) - elif self.floor_type in config.floor_derived_block_list: - load_derived_floor( + (0.0, 0.0), + prefs_externalTexture) + elif self.floor_type in UTILS_constants.floor_derivedBlockList: + _load_derived_floor( objmesh, self.floor_type, self.height_multiplier, @@ -94,7 +99,10 @@ class BALLANCE_OT_add_floor(bpy.types.Operator): self.use_2d_bottom, self.use_2d_left, self.use_3d_top, - self.use_3d_bottom)) + self.use_3d_bottom), + prefs_externalTexture) + else: + raise Exception("Fatal error: unknow floor type.") # normalization mesh objmesh.validate(clean_customdata=False) @@ -102,7 +110,7 @@ class BALLANCE_OT_add_floor(bpy.types.Operator): # create object and link it obj=bpy.data.objects.new('A_Floor_BMERevenge_', objmesh) - utils.AddSceneAndMove2Cursor(obj) + UTILS_functions.add_into_scene_and_move_to_cursor(obj) return {'FINISHED'} def invoke(self, context, event): @@ -111,7 +119,7 @@ class BALLANCE_OT_add_floor(bpy.types.Operator): def draw(self, context): # get floor prototype - floor_prototype = config.floor_block_dict[self.floor_type] + floor_prototype = UTILS_constants.floor_blockDict[self.floor_type] # try sync default value if self.previous_floor_type != self.floor_type: @@ -142,13 +150,13 @@ class BALLANCE_OT_add_floor(bpy.types.Operator): col.label(text="Expand mode: " + floor_prototype['ExpandType']) grids = col.grid_flow(row_major=True, columns=3) grids.separator() - grids.label(text=config.floor_expand_direction_map[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][0]) + grids.label(text=UTILS_constants.floor_expandDirectionMap[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][0]) grids.separator() - grids.label(text=config.floor_expand_direction_map[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][3]) - grids.template_icon(icon_value = config.blenderIcon_floor_dict[self.floor_type]) - grids.label(text=config.floor_expand_direction_map[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][1]) + grids.label(text=UTILS_constants.floor_expandDirectionMap[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][3]) + grids.template_icon(icon_value = UTILS_constants.icons_floorDict[self.floor_type]) + grids.label(text=UTILS_constants.floor_expandDirectionMap[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][1]) grids.separator() - grids.label(text=config.floor_expand_direction_map[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][2]) + grids.label(text=UTILS_constants.floor_expandDirectionMap[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][2]) grids.separator() col.separator() @@ -164,13 +172,13 @@ class BALLANCE_OT_add_floor(bpy.types.Operator): grids.prop(self, "use_2d_top") grids.separator() grids.prop(self, "use_2d_left") - grids.template_icon(icon_value = config.blenderIcon_floor_dict[self.floor_type]) + grids.template_icon(icon_value = UTILS_constants.icons_floorDict[self.floor_type]) grids.prop(self, "use_2d_right") grids.separator() grids.prop(self, "use_2d_bottom") grids.separator() -def face_fallback(normal_face, expand_face, height): +def _face_fallback(normal_face, expand_face, height): if expand_face == None: return normal_face @@ -179,43 +187,44 @@ def face_fallback(normal_face, expand_face, height): else: return expand_face -def create_or_get_material(material_name): +def _create_or_get_material(material_name, prefs_externalTexture): # WARNING: this code is shared with bm_import_export - deconflict_name = "BMERevenge_" + material_name - try: - m = bpy.data.materials[deconflict_name] - except: - # it is not existed, we need create a new one - m = bpy.data.materials.new(deconflict_name) - # we need init it. - # load texture first - externalTextureFolder = bpy.context.preferences.addons[__package__].preferences.external_folder - txur = load_image(config.floor_texture_corresponding_map[material_name], externalTextureFolder, check_existing=False) # force reload, we don't want it is shared with normal material - # create material and link texture - m.use_nodes=True - for node in m.node_tree.nodes: - m.node_tree.nodes.remove(node) - bnode=m.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") - mnode=m.node_tree.nodes.new(type="ShaderNodeOutputMaterial") - m.node_tree.links.new(bnode.outputs[0],mnode.inputs[0]) + deconflict_mtl_name = "BMERevenge_" + material_name - inode=m.node_tree.nodes.new(type="ShaderNodeTexImage") - inode.image=txur - m.node_tree.links.new(inode.outputs[0],bnode.inputs[0]) + # create or get material + (mtl, skip_init) = UTILS_functions.create_instance_with_option( + UTILS_constants.BmfileInfoType.MATERIAL, + deconflict_mtl_name, 'CURRENT' + ) + if skip_init: + return mtl - # write custom property - for try_item in config.floor_material_statistic: - if material_name in try_item['member']: - m['virtools-ambient'] = try_item['data']['ambient'] - m['virtools-diffuse'] = try_item['data']['diffuse'] - m['virtools-specular'] = try_item['data']['specular'] - m['virtools-emissive'] = try_item['data']['emissive'] - m['virtools-power'] = try_item['data']['power'] - break + # initialize material parameter + # load texture first + texture_filename = UTILS_constants.floor_textureReflactMap[material_name] + deconflict_texture_name = "BMERevenge_" + texture_filename + (texture, skip_init) = UTILS_functions.create_instance_with_option( + UTILS_constants.BmfileInfoType.TEXTURE, + deconflict_texture_name, 'CURRENT', + extra_texture_path = prefs_externalTexture, extra_texture_filename = texture_filename + ) + # iterate material statistic to get corresponding mtl data + for try_item in UTILS_constants.floor_materialStatistic: + if material_name in try_item['member']: + # got it + # set material data + UTILS_functions.create_material_nodes(mtl, + try_item['data']['ambient'], try_item['data']['diffuse'], + try_item['data']['specular'], try_item['data']['emissive'], + try_item['data']['power'], + texture) + break + + # return mtl return m -def solve_vec_data(str_data, d1, d2, d3, unit, unit_height): +def _solve_vec_data(str_data, d1, d2, d3, unit, unit_height): sp = str_data.split(';') sp_point = sp[0].split(',') vec = [float(sp_point[0]), float(sp_point[1]), float(sp_point[2])] @@ -236,7 +245,7 @@ def solve_vec_data(str_data, d1, d2, d3, unit, unit_height): return vec -def rotate_translate_vec(vec, rotation, unit, extra_translate): +def _rotate_translate_vec(vec, rotation, unit, extra_translate): vec[0] -= unit / 2 vec[1] -= unit / 2 @@ -260,7 +269,7 @@ def rotate_translate_vec(vec, rotation, unit, extra_translate): ) -def solve_uv_data(str_data, d1, d2, d3, unit): +def _solve_uv_data(str_data, d1, d2, d3, unit): sp = str_data.split(';') sp_point = sp[0].split(',') vec = [float(sp_point[0]), float(sp_point[1])] @@ -281,7 +290,7 @@ def solve_uv_data(str_data, d1, d2, d3, unit): return tuple(vec) -def solve_normal_data(point1, point2, point3): +def _solve_normal_data(point1, point2, point3): vector1 = ( point2[0] - point1[0], point2[1] - point1[1], @@ -309,7 +318,7 @@ def solve_normal_data(point1, point2, point3): return tuple(nor) -def solve_smashed_position(str_data, d1, d2): +def _solve_smashed_position(str_data, d1, d2): sp=str_data.split(';') sp_pos = sp[0].split(',') sp_sync = sp[1].split(',') @@ -325,7 +334,7 @@ def solve_smashed_position(str_data, d1, d2): return tuple(vec) -def virtual_foreach_set(collection, field, base_num, data): +def _virtual_foreach_set(collection, field, base_num, data): counter = 0 for i in data: exec("a[j]." + field + "=q", {}, { @@ -345,8 +354,8 @@ sides_struct should be a tuple and it always have 6 bool items WARNING: this code is shared with bm import export ''' -def load_basic_floor(mesh, floor_type, rotation, height_multiplier, d1, d2, sides_struct, extra_translate): - floor_prototype = config.floor_block_dict[floor_type] +def _load_basic_floor(mesh, floor_type, rotation, height_multiplier, d1, d2, sides_struct, extra_translate, prefs_externalTexture): + floor_prototype = UTILS_constants.floor_blockDict[floor_type] # set some unit height_unit = 5.0 @@ -360,13 +369,13 @@ def load_basic_floor(mesh, floor_type, rotation, height_multiplier, d1, d2, side # got all needed faces needCreatedFaces = [] if sides_struct[0]: - needCreatedFaces.append(face_fallback(floor_prototype['TwoDTopSide'], floor_prototype['TwoDTopSideExpand'], height_multiplier)) + needCreatedFaces.append(_face_fallback(floor_prototype['TwoDTopSide'], floor_prototype['TwoDTopSideExpand'], height_multiplier)) if sides_struct[1]: - needCreatedFaces.append(face_fallback(floor_prototype['TwoDRightSide'], floor_prototype['TwoDRightSideExpand'], height_multiplier)) + needCreatedFaces.append(_face_fallback(floor_prototype['TwoDRightSide'], floor_prototype['TwoDRightSideExpand'], height_multiplier)) if sides_struct[2]: - needCreatedFaces.append(face_fallback(floor_prototype['TwoDBottomSide'], floor_prototype['TwoDBottomSideExpand'], height_multiplier)) + needCreatedFaces.append(_face_fallback(floor_prototype['TwoDBottomSide'], floor_prototype['TwoDBottomSideExpand'], height_multiplier)) if sides_struct[3]: - needCreatedFaces.append(face_fallback(floor_prototype['TwoDLeftSide'], floor_prototype['TwoDLeftSideExpand'], height_multiplier)) + needCreatedFaces.append(_face_fallback(floor_prototype['TwoDLeftSide'], floor_prototype['TwoDLeftSideExpand'], height_multiplier)) if sides_struct[4]: needCreatedFaces.append(floor_prototype['ThreeDTopFace']) if sides_struct[5]: @@ -382,7 +391,7 @@ def load_basic_floor(mesh, floor_type, rotation, height_multiplier, d1, d2, side new_texture = face['Textures'] if new_texture not in materialDict.keys(): # try get from existed solt - pending_material = create_or_get_material(new_texture) + pending_material = _create_or_get_material(new_texture, prefs_externalTexture) if pending_material not in allmat: # no matched. add it mesh.materials.append(pending_material) @@ -406,12 +415,12 @@ def load_basic_floor(mesh, floor_type, rotation, height_multiplier, d1, d2, side for face_define in needCreatedFaces: base_indices = len(vecList) for vec in face_define['Vertices']: - vecList.append(rotate_translate_vec( - solve_vec_data(vec, d1, d2, height_multiplier, block_3dworld_unit, height_unit), + vecList.append(_rotate_translate_vec( + _solve_vec_data(vec, d1, d2, height_multiplier, block_3dworld_unit, height_unit), rotation, block_3dworld_unit, extra_translate)) for uv in face_define['UVs']: - uvList.append(solve_uv_data(uv, d1, d2, height_multiplier, block_uvworld_unit)) + uvList.append(_solve_uv_data(uv, d1, d2, height_multiplier, block_uvworld_unit)) for face in face_define['Faces']: if face['Type'] == 'RECTANGLE': @@ -431,7 +440,7 @@ def load_basic_floor(mesh, floor_type, rotation, height_multiplier, d1, d2, side indCount = 3 # we need calc normal and push it into list - point_normal = solve_normal_data(vecList[vec_indices[0]], vecList[vec_indices[1]], vecList[vec_indices[2]]) + point_normal = _solve_normal_data(vecList[vec_indices[0]], vecList[vec_indices[1]], vecList[vec_indices[2]]) for i in range(indCount): normalList.append(point_normal) @@ -454,10 +463,10 @@ def load_basic_floor(mesh, floor_type, rotation, height_multiplier, d1, d2, side # if no uv, create it mesh.uv_layers.new(do_init=False) - virtual_foreach_set(mesh.vertices, "co", global_offset_vec, vecList) - virtual_foreach_set(mesh.loops, "vertex_index", global_offset_loops, faceList) - virtual_foreach_set(mesh.loops, "normal", global_offset_loops, normalList) - virtual_foreach_set(mesh.uv_layers[0].data, "uv", global_offset_loops, uvList) + _virtual_foreach_set(mesh.vertices, "co", global_offset_vec, vecList) + _virtual_foreach_set(mesh.loops, "vertex_index", global_offset_loops, faceList) + _virtual_foreach_set(mesh.loops, "normal", global_offset_loops, normalList) + _virtual_foreach_set(mesh.uv_layers[0].data, "uv", global_offset_loops, uvList) cache_counter = 0 for i in range(len(faceMatList)): @@ -469,8 +478,8 @@ def load_basic_floor(mesh, floor_type, rotation, height_multiplier, d1, d2, side cache_counter += indCount -def load_derived_floor(mesh, floor_type, height_multiplier, d1, d2, sides_struct): - floor_prototype = config.floor_block_dict[floor_type] +def _load_derived_floor(mesh, floor_type, height_multiplier, d1, d2, sides_struct, prefs_externalTexture): + floor_prototype = UTILS_constants.floor_blockDict[floor_type] # set some unit if floor_prototype['UnitSize'] == 'Small': @@ -492,13 +501,13 @@ def load_derived_floor(mesh, floor_type, height_multiplier, d1, d2, sides_struct # iterate smahsed blocks for blk in floor_prototype['SmashedBlocks']: - start_pos = solve_smashed_position(blk['StartPosition'], d1, d2) - expand_pos = solve_smashed_position(blk['ExpandPosition'], d1, d2) + start_pos = _solve_smashed_position(blk['StartPosition'], d1, d2) + expand_pos = _solve_smashed_position(blk['ExpandPosition'], d1, d2) sides_data = tuple(sides_dict[x] for x in blk['SideSync'].split(';')) # call basic floor creator - load_basic_floor( + _load_basic_floor( mesh, blk['Type'], blk['Rotation'], @@ -506,7 +515,8 @@ def load_derived_floor(mesh, floor_type, height_multiplier, d1, d2, sides_struct expand_pos[0], expand_pos[1], sides_data, - (start_pos[0] * block_3dworld_unit, start_pos[1] * block_3dworld_unit) + (start_pos[0] * block_3dworld_unit, start_pos[1] * block_3dworld_unit), + prefs_externalTexture ) diff --git a/ballance_blender_plugin/add_elements.py b/ballance_blender_plugin/OBJS_add_rails.py similarity index 54% rename from ballance_blender_plugin/add_elements.py rename to ballance_blender_plugin/OBJS_add_rails.py index 01edb8c..aa65684 100644 --- a/ballance_blender_plugin/add_elements.py +++ b/ballance_blender_plugin/OBJS_add_rails.py @@ -1,62 +1,9 @@ -import bpy,mathutils -from . import utils, config, bm_import_export +import bpy, mathutils +from . import UTILS_functions -# ================================================= actual add - -class BALLANCE_OT_add_elements(bpy.types.Operator): - """Add sector related elements""" - bl_idname = "ballance.add_elements" - bl_label = "Add elements" - bl_options = {'UNDO'} - - elements_type: bpy.props.EnumProperty( - name="Type", - description="This element type", - items=tuple(map(lambda x: (x, x, ""), config.component_list)), - ) - - attentionElements = ["PC_TwoFlames", "PR_Resetpoint"] - uniqueElements = ["PS_FourFlames", "PE_Balloon"] - - elements_sector: bpy.props.IntProperty( - name="Sector", - description="Define which sector the object will be grouped in", - min=1, - max=8, - default=1, - ) - - def execute(self, context): - # get name - if self.elements_type in self.uniqueElements: - finalObjectName = self.elements_type + "_01" - elif self.elements_type in self.attentionElements: - finalObjectName = self.elements_type + "_0" + str(self.elements_sector) - else: - finalObjectName = self.elements_type + "_0" + str(self.elements_sector) + "_" - - # create object - loadedMesh = bm_import_export.load_component(config.component_list.index(self.elements_type)) - obj = bpy.data.objects.new(finalObjectName, loadedMesh) - utils.AddSceneAndMove2Cursor(obj) - - return {'FINISHED'} - - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_props_dialog(self) - - def draw(self, context): - layout = self.layout - layout.prop(self, "elements_type") - if self.elements_type not in self.uniqueElements: - layout.prop(self, "elements_sector") - if self.elements_type in self.attentionElements: - layout.label(text="Please note that sector is suffix.") - -class BALLANCE_OT_add_rail(bpy.types.Operator): +class BALLANCE_OT_add_rails(bpy.types.Operator): """Add rail""" - bl_idname = "ballance.add_rail" + bl_idname = "ballance.add_rails" bl_label = "Add rail section" bl_options = {'UNDO'} @@ -113,7 +60,7 @@ class BALLANCE_OT_add_rail(bpy.types.Operator): bpy.ops.object.join() # apply 3d cursor - utils.Move2Cursor(firstObj) + UTILS_functions.add_into_scene_and_move_to_cursor(obj) return {'FINISHED'} @@ -127,3 +74,4 @@ class BALLANCE_OT_add_rail(bpy.types.Operator): layout.prop(self, "rail_radius") if self.rail_type == 'DOUBLE': layout.prop(self, "rail_span") + diff --git a/ballance_blender_plugin/config.py b/ballance_blender_plugin/UTILS_constants.py similarity index 87% rename from ballance_blender_plugin/config.py rename to ballance_blender_plugin/UTILS_constants.py index 0c849b1..605e8fc 100644 --- a/ballance_blender_plugin/config.py +++ b/ballance_blender_plugin/UTILS_constants.py @@ -1,7 +1,18 @@ import json import os -external_texture_list = set([ +bmfile_currentVersion = 14 +bmfile_flagUnicode = 0x800 +bmfile_flagDeflatedMaximum = 0x2 +bmfile_globalComment = 'Use BM Spec 1.4'.encode('utf-8') + +class BmfileInfoType(): + OBJECT = 0 + MESH = 1 + MATERIAL = 2 + TEXTURE = 3 + +bmfile_externalTextureSet = set([ "atari.avi", "atari.bmp", "Ball_LightningSphere1.bmp", @@ -85,7 +96,7 @@ external_texture_list = set([ "Wood_Raft.bmp" ]) -component_list = [ +bmfile_componentList = [ "P_Extra_Life", "P_Extra_Point", "P_Trafo_Paper", @@ -120,7 +131,7 @@ format: key is diection, value is a dict dict's key is expand mode, value is a tuple tuple always have 4 items, it means (TOP_STR, RIGHT_STR, BOTTOM_STR, LEFT_STR) ''' -floor_expand_direction_map = { +floor_expandDirectionMap = { "PositiveX": { "Static": ("X", "X", "X", "X"), "Column": ("X", "X", "D1", "X"), @@ -143,7 +154,7 @@ floor_expand_direction_map = { } } -floor_texture_corresponding_map = { +floor_textureReflactionMap = { "FloorSide": "Floor_Side.bmp", "FloorTopBorder": "Floor_Top_Border.bmp", "FloorTopBorder_ForSide": "Floor_Top_Border.bmp", @@ -157,8 +168,8 @@ floor_texture_corresponding_map = { "BallStone": "Ball_Stone.bmp" } -# WARNING: this data is shared with BallanceVirtoolsPlugin - mapping_BM.cpp - fix_blender_texture -floor_material_statistic = [ +# WARNING: this data is shared with `BallanceVirtoolsPlugin/bvh/features/mapping/fix_texture.cpp` +floor_materialStatistic = [ { "member": [ "FloorSide", @@ -215,19 +226,19 @@ floor_material_statistic = [ } ] -floor_block_dict = {} -floor_basic_block_list = [] -floor_derived_block_list = [] +floor_blockDict = {} +floor_basicBlockList = [] +floor_derivedBlockList = [] with open(os.path.join(os.path.dirname(__file__), "json", "BasicBlock.json")) as fp: for item in json.load(fp): - floor_basic_block_list.append(item["Type"]) - floor_block_dict[item["Type"]] = item + floor_basicBlockList.append(item["Type"]) + floor_blockDict[item["Type"]] = item with open(os.path.join(os.path.dirname(__file__), "json", "DerivedBlock.json")) as fp: for item in json.load(fp): - floor_derived_block_list.append(item["Type"]) - floor_block_dict[item["Type"]] = item + floor_derivedBlockList.append(item["Type"]) + floor_blockDict[item["Type"]] = item -blenderIcon_floor = None -blenderIcon_floor_dict = {} +icons_floor = None +icons_floorDict = {} # blenderIcon_elements = None # blenderIcon_elements_dict = {} \ No newline at end of file diff --git a/ballance_blender_plugin/UTILS_file_io.py b/ballance_blender_plugin/UTILS_file_io.py new file mode 100644 index 0000000..0baf0e0 --- /dev/null +++ b/ballance_blender_plugin/UTILS_file_io.py @@ -0,0 +1,96 @@ +import bpy,bmesh,bpy_extras,mathutils +import struct,shutil + +# writer + +def write_string(fs,str): + count=len(str) + write_uint32(fs,count) + fs.write(str.encode("utf_32_le")) + +def write_uint8(fs,num): + fs.write(struct.pack("B", num)) + +def write_uint32(fs,num): + fs.write(struct.pack("I", num)) + +def write_uint64(fs,num): + fs.write(struct.pack("Q", num)) + +def write_bool(fs,boolean): + if boolean: + write_uint8(fs, 1) + else: + write_uint8(fs, 0) + +def write_float(fs,fl): + fs.write(struct.pack("f", fl)) + +def write_worldMatrix(fs, matt): + mat = matt.transposed() + fs.write(struct.pack("ffffffffffffffff", + mat[0][0],mat[0][2], mat[0][1], mat[0][3], + mat[2][0],mat[2][2], mat[2][1], mat[2][3], + mat[1][0],mat[1][2], mat[1][1], mat[1][3], + mat[3][0],mat[3][2], mat[3][1], mat[3][3])) + +def write_3vector(fs, x, y ,z): + fs.write(struct.pack("fff", x, y ,z)) + +def write_color(fs, colors): + write_3vector(fs, colors[0], colors[1], colors[2]) + +def write_2vector(fs, u, v): + fs.write(struct.pack("ff", u, v)) + +def write_face(fs, v1, vt1, vn1, v2, vt2, vn2, v3, vt3, vn3): + fs.write(struct.pack("IIIIIIIII", v1, vt1, vn1, v2, vt2, vn2, v3, vt3, vn3)) + +# reader + +def peek_stream(fs): + res = fs.read(1) + fs.seek(-1, os.SEEK_CUR) + return res + +def read_float(fs): + return struct.unpack("f", fs.read(4))[0] + +def read_uint8(fs): + return struct.unpack("B", fs.read(1))[0] + +def read_uint32(fs): + return struct.unpack("I", fs.read(4))[0] + +def read_uint64(fs): + return struct.unpack("Q", fs.read(8))[0] + +def read_string(fs): + count = read_uint32(fs) + return fs.read(count*4).decode("utf_32_le") + +def read_bool(fs): + return read_uint8(fs) != 0 + +def read_world_materix(fs): + p = struct.unpack("ffffffffffffffff", fs.read(4*4*4)) + res = mathutils.Matrix(( + (p[0], p[2], p[1], p[3]), + (p[8], p[10], p[9], p[11]), + (p[4], p[6], p[5], p[7]), + (p[12], p[14], p[13], p[15]))) + return res.transposed() + +def read_3vector(fs): + return struct.unpack("fff", fs.read(3*4)) + +def read_2vector(fs): + return struct.unpack("ff", fs.read(2*4)) + +def read_face(fs): + return struct.unpack("IIIIIIIII", fs.read(4*9)) + +def read_component_face(fs): + return struct.unpack("IIIIII", fs.read(4*6)) + + diff --git a/ballance_blender_plugin/UTILS_functions.py b/ballance_blender_plugin/UTILS_functions.py new file mode 100644 index 0000000..3fddc5f --- /dev/null +++ b/ballance_blender_plugin/UTILS_functions.py @@ -0,0 +1,222 @@ +import bpy, bmesh, bpy_extras, mathutils +import struct, shutil +from bpy_extras.io_utils import unpack_list +from bpy_extras import io_utils, node_shader_utils +from . import UTILS_file_io, UTILS_constants + +# ================================= +# scene operation + +def show_message_box(message, title, icon): + + def draw(self, context): + layout = self.layout + for item in message: + layout.label(text=item, translate=False) + + bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) + +def add_into_scene_and_move_to_cursor(obj): + Move2Cursor(obj) + + view_layer = bpy.context.view_layer + collection = view_layer.active_layer_collection.collection + collection.objects.link(obj) + +def move_to_cursor(obj): + obj.location = bpy.context.scene.cursor.location + +# ================================= +# is compoent + +def is_component(name): + return get_component_id(name) != -1 + +def get_component_id(name): + for ind, comp in enumerate(UTILS_constants.bmfile_componentList): + if name.startswith(comp): + return ind + return -1 + +# ================================= +# create material + +def create_material_nodes(input_mtl, ambient, diffuse, specular, emissive, + specular_power, texture): + + # adding material nodes + input_mtl.use_nodes=True + for node in input_mtl.node_tree.nodes: + input_mtl.node_tree.nodes.remove(node) + bnode = input_mtl.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") + mnode = input_mtl.node_tree.nodes.new(type="ShaderNodeOutputMaterial") + input_mtl.node_tree.links.new(bnode.outputs[0],mnode.inputs[0]) + + input_mtl.metallic = sum(ambient) / 3 + input_mtl.diffuse_color = [i for i in diffuse] + [1] + input_mtl.specular_color = specular + input_mtl.specular_intensity = specular_power + + # adding a texture + if texture is not None: + inode = input_mtl.node_tree.nodes.new(type="ShaderNodeTexImage") + inode.image = texture + input_mtl.node_tree.links.new(inode.outputs[0], bnode.inputs[0]) + + # write custom property + input_mtl['virtools-ambient'] = ambient + input_mtl['virtools-diffuse'] = diffuse + input_mtl['virtools-specular'] = specular + input_mtl['virtools-emissive'] = emissive + input_mtl['virtools-power'] = specular_power + +# ================================= +# load component + +def load_component(component_id): + # get file first + component_name = UTILS_constants.bmfile_componentList[component_id] + selected_file = os.path.join( + os.path.dirname(__file__), + 'meshes', + component_name + '.bin' + ) + + # read file. please note this sector is sync with import_bm's mesh's code. when something change, please change each other. + fmesh = open(selected_file, 'rb') + + # create real mesh, we don't need to consider name. blender will solve duplicated name + mesh = bpy.data.meshes.new('mesh_' + component_name) + + vList = [] + vnList = [] + faceList = [] + # in first read, store all data into list + listCount = UTILS_file_io.read_uint32(fmesh) + for i in range(listCount): + cache = UTILS_file_io.read_3vector(fmesh) + # switch yz + vList.append((cache[0], cache[2], cache[1])) + listCount = UTILS_file_io.read_uint32(fmesh) + for i in range(listCount): + cache = UTILS_file_io.read_3vector(fmesh) + # switch yz + vnList.append((cache[0], cache[2], cache[1])) + + listCount = UTILS_file_io.read_uint32(fmesh) + for i in range(listCount): + faceData = UTILS_file_io.read_component_face(fmesh) + + # we need invert triangle sort + faceList.append(( + faceData[4], faceData[5], + faceData[2], faceData[3], + faceData[0], faceData[1] + )) + + # then, we need add correspond count for vertices + mesh.vertices.add(len(vList)) + mesh.loops.add(len(faceList)*3) # triangle face confirmed + mesh.polygons.add(len(faceList)) + mesh.create_normals_split() + + # add vertices data + mesh.vertices.foreach_set("co", unpack_list(vList)) + mesh.loops.foreach_set("vertex_index", unpack_list(_flat_component_vertices_index(faceList))) + mesh.loops.foreach_set("normal", unpack_list(_flat_component_vertices_normal(faceList, vnList))) + for i in range(len(faceList)): + mesh.polygons[i].loop_start = i * 3 + mesh.polygons[i].loop_total = 3 + + mesh.polygons[i].use_smooth = True + + mesh.validate(clean_customdata=False) + mesh.update(calc_edges=False, calc_edges_loose=False) + + fmesh.close() + + return mesh + + +def _flat_component_vertices_index(faceList): + for item in faceList: + yield (item[0], ) + yield (item[2], ) + yield (item[4], ) + +def _flat_component_vertices_normal(faceList, vnList): + for item in faceList: + yield vnList[item[1]] + yield vnList[item[3]] + yield vnList[item[5]] + +# ================================= +# create instance with option + +def create_instance_with_option(instance_type, instance_name, instance_opt, + extra_mesh = None, extra_texture_path = None, extra_texture_filename = None): + """ + Create instance with opetions + + `instance_type`, `instance_name`, `instance_opt` are essential for each type instances. + For object, you should provide `extra_mesh`. + For texture, you should provide `extra_texture_path` and `extra_texture_filename`. + + """ + + def get_instance(): + try: + if instance_type == UTILS_constants.BmfileInfoType.OBJECT: + temp_instance = bpy.data.objects[instance_name] + elif instance_type == UTILS_constants.BmfileInfoType.MESH: + temp_instance = bpy.data.meshes[instance_name] + elif instance_type == UTILS_constants.BmfileInfoType.MATERIAL: + temp_instance = bpy.data.materials[instance_name] + elif instance_type == UTILS_constants.BmfileInfoType.TEXTURE: + temp_instance = bpy.data.textures[instance_name] + + temp_is_existed = True + except: + temp_is_existed = False + + return (temp_instance, temp_is_existed) + + def create_instance(): + if instType == UTILS_constants.BmfileInfoType.OBJECT: + instance_obj = bpy.data.objects.new(instance_name, extra_mesh) + instance_obj.name = instance_name + elif instType == UTILS_constants.BmfileInfoType.MESH: + instance_obj = bpy.data.meshes.new(instance_name) + instance_obj.name = instance_name + elif instType == UTILS_constants.BmfileInfoType.MATERIAL: + instance_obj = bpy.data.materials.new(instance_name) + instance_obj.name = instance_name + elif instance_type == UTILS_constants.BmfileInfoType.TEXTURE: + # this command will also check current available texture + # because `get_instance()` only check texture name + # but this strategy is not based on texture filepath, so this create method will + # correct this problem + instance_obj = load_image(extra_texture_filename, extra_texture_path, check_existing=(instance_opt == 'CURRENT')) + instance_obj.name = instance_name + + return instance_obj + + # analyze options + if (not isinstance(instance_opt, str)) or instance_opt == 'RENAME': + # create new instance + # or always create new instance if opts is not string + temp_instance = create_instance() + temp_skip_init = True + elif instance_opt == 'CURRENT': + # try get instance + (temp_instance, temp_is_existed) = get_instance() + # if got instance successfully, we do not create new one, otherwise, we should + # create new instance + if not temp_is_existed: + temp_instance = create_instance() + temp_skip_init = False + else: + temp_skip_init = True + + return (temp_instance, temp_skip_init) + diff --git a/ballance_blender_plugin/preferences.py b/ballance_blender_plugin/UTILS_preferences.py similarity index 100% rename from ballance_blender_plugin/preferences.py rename to ballance_blender_plugin/UTILS_preferences.py diff --git a/ballance_blender_plugin/UTILS_zip_helper.py b/ballance_blender_plugin/UTILS_zip_helper.py new file mode 100644 index 0000000..3fdef26 --- /dev/null +++ b/ballance_blender_plugin/UTILS_zip_helper.py @@ -0,0 +1,48 @@ +import pathlib, zipfile, os, shutil +from . import UTILS_constants + +def compress(folder, zip_file): + # remove target file first + if os.path.isfile(zip_file): + os.remove(zip_file) + + # compress data + with zipfile.ZipFile(zip_file, mode= 'w', compression= zipfile.ZIP_DEFLATED, compresslevel= 9, allowZip64= False) as zip_obj: + # set global comment + zip_obj.comment = UTILS_constants.bmfile_globalComment + + # iterate folder and add files + for folder_name, subfolders, filenames in os.walk(folder): + for filename in filenames: + # construct zip_entry + abstract_filepath = os.path.join(folder_name, filename) + relative_filepath = os.path.relpath(abstract_filepath, folder) + zip_entry = zipfile.ZipInfo.from_file(abstract_filepath, arcname= relative_filepath) + zip_entry.compress_type = zipfile.ZIP_DEFLATED + + # compress file + with open(abstract_filepath, 'rb') as fs_in: + with zip_obj.open(zip_entry, mode= 'w') as zip_in: + # References + # https://stackoverflow.com/questions/53254622/zipfile-header-language-encoding-bit-set-differently-between-python2-and-python3 + # set unicode flag after opening internal file. + # for the shit implement of python module zipfile, we need set Deflated:Maximum manually + zip_entry.flag_bits |= UTILS_constants.bmfile_flagUnicode + zip_entry.flag_bits |= UTILS_constants.bmfile_flagDeflatedMaximum + # copy file + shutil.copyfileobj(fs_in, zip_in) + + +def decompress(folder, zip_file): + with zipfile.ZipFile(zip_file, mode= 'r', compression= zipfile.ZIP_DEFLATED, compresslevel= 9, allowZip64= False) as zip_obj: + for zip_entry in zip_obj.infolist(): + if (zip_entry.flag_bits & UTILS_constants.bmfile_flagUnicode) == 0: + # lost unicode flag, throw error + raise Exception("Zip Entry lost UNICODE flag.") + + # decompress file + zip_obj.extract(zip_entry, path= folder) + + + + \ No newline at end of file diff --git a/ballance_blender_plugin/__init__.py b/ballance_blender_plugin/__init__.py index aaef418..3584e48 100644 --- a/ballance_blender_plugin/__init__.py +++ b/ballance_blender_plugin/__init__.py @@ -2,7 +2,7 @@ bl_info={ "name":"Ballance Blender Plugin", "description":"Ballance mapping tools for Blender", "author":"yyc12345", - "version":(2,0), + "version":(3,0), "blender":(2,83,0), "category":"Object", "support":"TESTING", @@ -11,36 +11,55 @@ bl_info={ "tracker_url": "https://github.com/yyc12345/BallanceBlenderHelper/issues" } -# ============================================= import system +# ============================================= +# import system import bpy,bpy_extras import bpy.utils.previews import os # import my code (with reload) if "bpy" in locals(): import importlib - if "bm_import_export" in locals(): - importlib.reload(bm_import_export) - if "rail_uv" in locals(): - importlib.reload(rail_uv) - if "utils" in locals(): - importlib.reload(utils) - if "config" in locals(): - importlib.reload(config) - if "preferences" in locals(): - importlib.reload(preferences) - if "threedsmax_align" in locals(): - importlib.reload(threedsmax_align) - if "no_uv_checker" in locals(): - importlib.reload(no_uv_checker) - if "add_elements" in locals(): - importlib.reload(add_elements) - if "add_floor" in locals(): - importlib.reload(add_floor) - if "flatten_uv" in locals(): - importlib.reload(flatten_uv) -from . import config, utils, bm_import_export, rail_uv, preferences, threedsmax_align, no_uv_checker, add_elements, add_floor, flatten_uv + if "UTILS_constants" in locals(): + importlib.reload(UTILS_constants) + if "UTILS_functions" in locals(): + importlib.reload(UTILS_functions) + if "UTILS_preferences" in locals(): + importlib.reload(UTILS_preferences) + if "UTILS_file_io" in locals(): + importlib.reload(UTILS_file_io) + if "UTILS_zip_helper" in locals(): + importlib.reload(UTILS_zip_helper) -# ============================================= menu system + if "BMFILE_export" in locals(): + importlib.reload(BMFILE_export) + if "BMFILE_import" in locals(): + importlib.reload(BMFILE_import) + + if "MODS_rail_uv" in locals(): + importlib.reload(MODS_rail_uv) + if "MODS_3dsmax_align" in locals(): + importlib.reload(MODS_3dsmax_align) + if "MODS_flatten_uv" in locals(): + importlib.reload(MODS_flatten_uv) + + if "OBJS_add_components" in locals(): + importlib.reload(OBJS_add_components) + if "OBJS_add_floors" in locals(): + importlib.reload(OBJS_add_floors) + if "OBJS_add_rails" in locals(): + importlib.reload(OBJS_add_rails) + + if "NAMES_rename_via_group" in locals(): + importlib.reload(NAMES_rename_via_group) + +from . import UTILS_constants, UTILS_functions, UTILS_preferences +from . import BMFILE_export, BMFILE_import +from . import MODS_3dsmax_align, MODS_flatten_uv, MODS_rail_uv +from . import OBJS_add_components, OBJS_add_floors, OBJS_add_rails +from . import NAMES_rename_via_group + +# ============================================= +# menu system class BALLANCE_MT_ThreeDViewerMenu(bpy.types.Menu): """Ballance related 3D operator""" @@ -50,10 +69,9 @@ class BALLANCE_MT_ThreeDViewerMenu(bpy.types.Menu): def draw(self, context): layout = self.layout - layout.operator("ballance.super_align") - layout.operator("ballance.rail_uv") - layout.operator("ballance.no_uv_checker") - layout.operator("ballance.flatten_uv") + layout.operator(MODS_3dsmax_align.BALLANCE_OT_super_align.bl_idname) + layout.operator(MODS_rail_uv.BALLANCE_OT_rail_uv.bl_idname) + layout.operator(MODS_flatten_uv.BALLANCE_OT_flatten_uv.bl_idname) class BALLANCE_MT_AddFloorMenu(bpy.types.Menu): """Add Ballance floor""" @@ -64,41 +82,46 @@ class BALLANCE_MT_AddFloorMenu(bpy.types.Menu): layout = self.layout layout.label(text="Basic floor") - for item in config.floor_basic_block_list: - cop = layout.operator("ballance.add_floor", text=item, icon_value = config.blenderIcon_floor_dict[item]) + for item in UTILS_constants.floor_basicBlock_list: + cop = layout.operator( + OBJS_add_floors.BALLANCE_OT_add_floors.bl_idname, + text=item, icon_value = UTILS_constants.icons_floorDict[item]) cop.floor_type = item layout.separator() layout.label(text="Derived floor") - for item in config.floor_derived_block_list: - cop = layout.operator("ballance.add_floor", text=item, icon_value = config.blenderIcon_floor_dict[item]) + for item in UTILS_constants.floor_derivedBlockList: + cop = layout.operator( + OBJS_add_floors.BALLANCE_OT_add_floors.bl_idname, + text=item, icon_value = UTILS_constants.icons_floorDict[item]) cop.floor_type = item -# ============================================= blender call system +# ============================================= +# blender call system classes = ( - preferences.BallanceBlenderPluginPreferences, - preferences.MyPropertyGroup, + UTILS_preferences.BallanceBlenderPluginPreferences, + UTILS_preferences.MyPropertyGroup, - bm_import_export.BALLANCE_OT_import_bm, - bm_import_export.BALLANCE_OT_export_bm, - rail_uv.BALLANCE_OT_rail_uv, - threedsmax_align.BALLANCE_OT_super_align, - no_uv_checker.BALLANCE_OT_no_uv_checker, - flatten_uv.BALLANCE_OT_flatten_uv, + BMFILE_import.BALLANCE_OT_import_bm, + BMFILE_export.BALLANCE_OT_export_bm, + + MODS_rail_uv.BALLANCE_OT_rail_uv, + MODS_3dsmax_align.BALLANCE_OT_super_align, + MODS_flatten_uv.BALLANCE_OT_flatten_uv, BALLANCE_MT_ThreeDViewerMenu, - add_elements.BALLANCE_OT_add_elements, - add_elements.BALLANCE_OT_add_rail, - add_floor.BALLANCE_OT_add_floor, + OBJS_add_components.BALLANCE_OT_add_components, + OBJS_add_rails.BALLANCE_OT_add_rails, + OBJS_add_floors.BALLANCE_OT_add_floors, BALLANCE_MT_AddFloorMenu ) def menu_func_bm_import(self, context): - self.layout.operator(bm_import_export.BALLANCE_OT_import_bm.bl_idname, text="Ballance Map (.bmx)") + self.layout.operator(BMFILE_import.BALLANCE_OT_import_bm.bl_idname, text="Ballance Map (.bmx)") def menu_func_bm_export(self, context): - self.layout.operator(bm_import_export.BALLANCE_OT_export_bm.bl_idname, text="Ballance Map (.bmx)") + self.layout.operator(BMFILE_export.BALLANCE_OT_export_bm.bl_idname, text="Ballance Map (.bmx)") def menu_func_ballance_3d(self, context): layout = self.layout layout.menu(BALLANCE_MT_ThreeDViewerMenu.bl_idname) @@ -106,29 +129,37 @@ def menu_func_ballance_add(self, context): layout = self.layout layout.separator() layout.label(text="Ballance") - layout.operator_menu_enum("ballance.add_elements", "elements_type", icon='MESH_ICOSPHERE', text="Elements") - layout.operator("ballance.add_rail", icon='MESH_CIRCLE', text="Rail section") + layout.operator_menu_enum( + OBJS_add_components.BALLANCE_OT_add_components.bl_idname, + "elements_type", icon='MESH_ICOSPHERE', text="Elements") + layout.operator(OBJS_add_rails.BALLANCE_OT_add_rails.bl_idname, icon='MESH_CIRCLE', text="Rail section") layout.menu(BALLANCE_MT_AddFloorMenu.bl_idname, icon='MESH_CUBE') +def menu_func_ballance_rename(self, context): + layout = self.layout + layout.separator() + layout.label(text="Ballance") + layout.operator(NAMES_rename_via_group.BALLANCE_OT_rename_via_group.bl_idname, text="Rename via Group") def register(): # we need init all icon first icon_path = os.path.join(os.path.dirname(__file__), "icons") - config.blenderIcon_floor = bpy.utils.previews.new() - for key, value in config.floor_block_dict.items(): + UTILS_constants.icons_floor = bpy.utils.previews.new() + for key, value in UTILS_constants.floor_blockDict.items(): blockIconName = "Ballance_FloorIcon_" + key - config.blenderIcon_floor.load(blockIconName, os.path.join(icon_path, "floor", value["BindingDisplayTexture"]), 'IMAGE') - config.blenderIcon_floor_dict[key] = config.blenderIcon_floor[blockIconName].icon_id + UTILS_constants.icons_floor.load(blockIconName, os.path.join(icon_path, "floor", value["BindingDisplayTexture"]), 'IMAGE') + UTILS_constants.icons_floorDict[key] = UTILS_constants.icons_floor[blockIconName].icon_id for cls in classes: bpy.utils.register_class(cls) - bpy.types.Scene.BallanceBlenderPluginProperty = bpy.props.PointerProperty(type=preferences.MyPropertyGroup) + bpy.types.Scene.BallanceBlenderPluginProperty = bpy.props.PointerProperty(type=UTILS_preferences.MyPropertyGroup) bpy.types.TOPBAR_MT_file_import.append(menu_func_bm_import) bpy.types.TOPBAR_MT_file_export.append(menu_func_bm_export) bpy.types.VIEW3D_HT_header.append(menu_func_ballance_3d) bpy.types.VIEW3D_MT_add.append(menu_func_ballance_add) + bpy.types.COLLECTION_MT_context_menu.append(menu_func_ballance_rename) def unregister(): bpy.types.TOPBAR_MT_file_import.remove(menu_func_bm_import) @@ -136,12 +167,13 @@ def unregister(): bpy.types.VIEW3D_HT_header.remove(menu_func_ballance_3d) bpy.types.VIEW3D_MT_add.remove(menu_func_ballance_add) + bpy.types.COLLECTION_MT_context_menu.remove(menu_func_ballance_rename) for cls in classes: bpy.utils.unregister_class(cls) # we need uninstall all icon after all classes unregister - bpy.utils.previews.remove(config.blenderIcon_floor) + bpy.utils.previews.remove(UTILS_constants.icons_floor) if __name__=="__main__": register() \ No newline at end of file diff --git a/ballance_blender_plugin/bm_import_export.py b/ballance_blender_plugin/bm_import_export.py deleted file mode 100644 index b92207e..0000000 --- a/ballance_blender_plugin/bm_import_export.py +++ /dev/null @@ -1,907 +0,0 @@ -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 bpy_extras.io_utils import unpack_list -from bpy_extras.image_utils import load_image -from . import utils, config - -class BALLANCE_OT_import_bm(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): - """Load a Ballance Map File (BM file spec 1.3)""" - bl_idname = "ballance.import_bm" - bl_label = "Import BM " - bl_options = {'PRESET', 'UNDO'} - filename_ext = ".bmx" - - texture_conflict_strategy: bpy.props.EnumProperty( - name="Texture name conflict", - items=(('NEW', "New instance", "Create a new instance"), - ('CURRENT', "Use current", "Use current"),), - description="Define how to process texture name conflict", - default='CURRENT', - ) - - material_conflict_strategy: bpy.props.EnumProperty( - name="Material name conflict", - items=(('RENAME', "Rename", "Rename the new one"), - ('CURRENT', "Use current", "Use current"),), - description="Define how to process material name conflict", - default='RENAME', - ) - - mesh_conflict_strategy: bpy.props.EnumProperty( - name="Mesh name conflict", - items=(('RENAME', "Rename", "Rename the new one"), - ('CURRENT', "Use current", "Use current"),), - description="Define how to process mesh name conflict", - default='RENAME', - ) - - object_conflict_strategy: bpy.props.EnumProperty( - name="Object name conflict", - items=(('RENAME', "Rename", "Rename the new one"), - ('CURRENT', "Use current", "Use current"),), - description="Define how to process object name conflict", - default='RENAME', - ) - - @classmethod - def poll(self, context): - prefs = bpy.context.preferences.addons[__package__].preferences - return (os.path.isdir(prefs.temp_texture_folder) and os.path.isdir(prefs.external_folder)) - - def execute(self, context): - prefs = bpy.context.preferences.addons[__package__].preferences - import_bm(context, self.filepath, prefs.external_folder, prefs.temp_texture_folder, - self.texture_conflict_strategy, self.material_conflict_strategy, self.mesh_conflict_strategy, self.object_conflict_strategy) - return {'FINISHED'} - -class BALLANCE_OT_export_bm(bpy.types.Operator, bpy_extras.io_utils.ExportHelper): - """Save a Ballance Map File (BM file spec 1.3)""" - bl_idname = "ballance.export_bm" - bl_label = 'Export BM' - bl_options = {'PRESET'} - filename_ext = ".bmx" - - 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.ShowMessageBox(("No specific target", ), "Lost parameter", 'ERROR') - else: - if self.export_mode == 'COLLECTION': - export_bm(context, self.filepath, self.export_mode, context.scene.BallanceBlenderPluginProperty.collection_picker) - elif self.export_mode == 'OBJECT': - export_bm(context, self.filepath, 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") - -# ========================================== method - -bm_current_version = 13 - -def import_bm(context,filepath,externalTexture,blenderTempFolder, textureOpt, materialOpt, meshOpt, objectOpt): - # ============================================ alloc a temp folder - tempFolderObj = tempfile.TemporaryDirectory() - tempFolder = tempFolderObj.name - # debug - # print(tempFolder) - tempTextureFolder = os.path.join(tempFolder, "Texture") - prefs = bpy.context.preferences.addons[__package__].preferences - blenderTempTextureFolder = prefs.temp_texture_folder - externalTextureFolder = prefs.external_folder - - with zipfile.ZipFile(filepath, 'r', zipfile.ZIP_DEFLATED, 9) as zipObj: - zipObj.extractall(tempFolder) - - # index.bm - objectList = [] - meshList = [] - materialList = [] - textureList = [] - with open(os.path.join(tempFolder, "index.bm"), "rb") as findex: - # judge version first - gotten_version = read_uint32(findex) - if (gotten_version != bm_current_version): - utils.ShowMessageBox(("Unsupported BM spec. Expect: {} Gotten: {}".format(bm_current_version, gotten_version), ), "Unsupported BM spec", 'ERROR') - findex.close() - tempFolderObj.cleanup() - return - - while len(peek_stream(findex)) != 0: - index_name = read_string(findex) - index_type = read_uint8(findex) - index_offset = read_uint64(findex) - blockCache = info_block_helper(index_name, index_offset) - if index_type == info_bm_type.OBJECT: - objectList.append(blockCache) - elif index_type == info_bm_type.MESH: - meshList.append(blockCache) - elif index_type == info_bm_type.MATERIAL: - materialList.append(blockCache) - elif index_type == info_bm_type.TEXTURE: - textureList.append(blockCache) - else: - pass - - - # texture.bm - with open(os.path.join(tempFolder, "texture.bm"), "rb") as ftexture: - for item in textureList: - ftexture.seek(item.offset, os.SEEK_SET) - texture_filename = read_string(ftexture) - texture_isExternal = read_bool(ftexture) - if texture_isExternal: - txur = load_image(texture_filename, externalTextureFolder, check_existing=(textureOpt == 'CURRENT')) - item.blenderData = txur - else: - # not external. copy temp file into blender temp. then use it. - # try copy. if fail, don't need to do more - try: - shutil.copy(os.path.join(tempTextureFolder, texture_filename), os.path.join(blenderTempTextureFolder, texture_filename)) - except: - pass - txur = load_image(texture_filename, blenderTempTextureFolder, check_existing=(textureOpt == 'CURRENT')) - item.blenderData = txur - txur.name = item.name - - # material.bm - # WARNING: this code is shared with add_floor - create_or_get_material() - with open(os.path.join(tempFolder, "material.bm"), "rb") as fmaterial: - for item in materialList: - fmaterial.seek(item.offset, os.SEEK_SET) - - # read data - material_colAmbient = read_3vector(fmaterial) - material_colDiffuse = read_3vector(fmaterial) - material_colSpecular = read_3vector(fmaterial) - material_colEmissive = read_3vector(fmaterial) - material_specularPower = read_float(fmaterial) - material_useTexture = read_bool(fmaterial) - material_texture = read_uint32(fmaterial) - - # create basic material - (m, needSkip) = createInstanceWithOption(info_bm_type.MATERIAL, item.name, materialOpt) - item.blenderData = m - if needSkip: - continue - - m.use_nodes=True - for node in m.node_tree.nodes: - m.node_tree.nodes.remove(node) - bnode=m.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") - mnode=m.node_tree.nodes.new(type="ShaderNodeOutputMaterial") - m.node_tree.links.new(bnode.outputs[0],mnode.inputs[0]) - - m.metallic = sum(material_colAmbient) / 3 - m.diffuse_color = [i for i in material_colDiffuse] + [1] - m.specular_color = material_colSpecular - m.specular_intensity = material_specularPower - - # create a texture - if material_useTexture: - inode=m.node_tree.nodes.new(type="ShaderNodeTexImage") - inode.image=textureList[material_texture].blenderData - m.node_tree.links.new(inode.outputs[0],bnode.inputs[0]) - - # write custom property - m['virtools-ambient'] = material_colAmbient - m['virtools-diffuse'] = material_colDiffuse - m['virtools-specular'] = material_colSpecular - m['virtools-emissive'] = material_colEmissive - m['virtools-power'] = material_specularPower - - - # mesh.bm - # WARNING: this code is shared with add_floor - with open(os.path.join(tempFolder, "mesh.bm"), "rb") as fmesh: - vList=[] - vtList=[] - vnList=[] - faceList=[] - materialSolt = [] - for item in meshList: - fmesh.seek(item.offset, os.SEEK_SET) - - # create real mesh - (mesh, needSkip) = createInstanceWithOption(info_bm_type.MESH, item.name, meshOpt) - item.blenderData = mesh - if needSkip: - continue - - vList.clear() - vtList.clear() - vnList.clear() - faceList.clear() - materialSolt.clear() - # in first read, store all data into list - listCount = read_uint32(fmesh) - for i in range(listCount): - cache = read_3vector(fmesh) - # switch yz - vList.append((cache[0], cache[2], cache[1])) - listCount = read_uint32(fmesh) - for i in range(listCount): - cache = read_2vector(fmesh) - # reverse v - vtList.append((cache[0], -cache[1])) - listCount = read_uint32(fmesh) - for i in range(listCount): - cache = read_3vector(fmesh) - # switch yz - vnList.append((cache[0], cache[2], cache[1])) - - listCount = read_uint32(fmesh) - for i in range(listCount): - faceData = read_face(fmesh) - mesh_useMaterial = read_bool(fmesh) - mesh_materialIndex = read_uint32(fmesh) - - if mesh_useMaterial: - neededMaterial = materialList[mesh_materialIndex].blenderData - if neededMaterial in materialSolt: - neededIndex = materialSolt.index(neededMaterial) - else: - neededIndex = len(materialSolt) - materialSolt.append(neededMaterial) - else: - neededIndex = -1 - - # we need invert triangle sort - faceList.append(( - faceData[6], faceData[7], faceData[8], - faceData[3], faceData[4], faceData[5], - faceData[0], faceData[1], faceData[2], - neededIndex - )) - - # and then we need add material solt for this mesh - for mat in materialSolt: - mesh.materials.append(mat) - - # then, we need add correspond count for vertices - mesh.vertices.add(len(vList)) - mesh.loops.add(len(faceList)*3) # triangle face confirm - mesh.polygons.add(len(faceList)) - mesh.uv_layers.new(do_init=False) - mesh.create_normals_split() - - # add vertices data - mesh.vertices.foreach_set("co", unpack_list(vList)) - mesh.loops.foreach_set("vertex_index", unpack_list(flat_vertices_index(faceList))) - mesh.loops.foreach_set("normal", unpack_list(flat_vertices_normal(faceList, vnList))) - mesh.uv_layers[0].data.foreach_set("uv", unpack_list(flat_vertices_uv(faceList, vtList))) - for i in range(len(faceList)): - mesh.polygons[i].loop_start = i * 3 - mesh.polygons[i].loop_total = 3 - if faceList[i][9] != -1: - mesh.polygons[i].material_index = faceList[i][9] - - mesh.polygons[i].use_smooth = True - - mesh.validate(clean_customdata=False) - mesh.update(calc_edges=False, calc_edges_loose=False) - - - # object - with open(os.path.join(tempFolder, "object.bm"), "rb") as fobject: - - # we need get needed collection first - view_layer = context.view_layer - collection = view_layer.active_layer_collection.collection - if prefs.no_component_collection == "": - forcedCollection = None - else: - try: - forcedCollection = bpy.data.collections[prefs.no_component_collection] - except: - forcedCollection = bpy.data.collections.new(prefs.no_component_collection) - view_layer.active_layer_collection.collection.children.link(forcedCollection) - - # start process it - object_groupList = [] - for item in objectList: - fobject.seek(item.offset, os.SEEK_SET) - - # read data - object_isComponent = read_bool(fobject) - object_isForcedNoComponent = read_bool(fobject) - object_isHidden = read_bool(fobject) - object_worldMatrix = read_worldMaterix(fobject) - object_groupListCount = read_uint32(fobject) - object_groupList.clear() - for i in range(object_groupListCount): - object_groupList.append(read_string(fobject)) - object_meshIndex = read_uint32(fobject) - - # got mesh first - if object_isComponent: - neededMesh = load_component(object_meshIndex) - else: - neededMesh = meshList[object_meshIndex].blenderData - - # create real object - (obj, needSkip) = createInstanceWithOption(info_bm_type.OBJECT, item.name, objectOpt, extraMesh=neededMesh) - if needSkip: - continue - if (not object_isComponent) and object_isForcedNoComponent and (forcedCollection is not None): - forcedCollection.objects.link(obj) - else: - collection.objects.link(obj) - obj.matrix_world = object_worldMatrix - obj.hide_set(object_isHidden) - - # write custom property - if len(object_groupList) != 0: - obj['virtools-group'] = tuple(object_groupList) - - view_layer.update() - - tempFolderObj.cleanup() - -def export_bm(context, filepath, export_mode, export_target): - # ============================================ alloc a temp folder - tempFolderObj = tempfile.TemporaryDirectory() - tempFolder = tempFolderObj.name - # debug - # tempFolder = "G:\\ziptest" - tempTextureFolder = os.path.join(tempFolder, "Texture") - os.makedirs(tempTextureFolder) - prefs = bpy.context.preferences.addons[__package__].preferences - - # ============================================ find export target. don't need judge them in there. just collect them - if export_mode== "COLLECTION": - objectList = export_target.objects - else: - objectList = [export_target] - - # try get forcedCollection - try: - forcedCollection = bpy.data.collections[prefs.no_component_collection] - except: - forcedCollection = None - - # ============================================ export - with open(os.path.join(tempFolder, "index.bm"), "wb") as finfo: - write_uint32(finfo, bm_current_version) - - # ====================== export object - meshSet = set() - meshList = [] - meshCount = 0 - with open(os.path.join(tempFolder, "object.bm"), "wb") as fobject: - for obj in objectList: - # only export mesh object - if obj.type != 'MESH': - continue - - # clean no mesh object - currentMesh = obj.data - if currentMesh is None: - continue - - # judge component - object_isComponent = is_component(obj.name) - object_isForcedNoComponent = False - if (forcedCollection is not None) and (obj.name in forcedCollection.objects): - # change it to forced no component - object_isComponent = False - object_isForcedNoComponent = True - - # triangle first and then group - if not object_isComponent: - if currentMesh not in meshSet: - mesh_triangulate(currentMesh) - meshSet.add(currentMesh) - meshList.append(currentMesh) - meshId = meshCount - meshCount += 1 - else: - meshId = meshList.index(currentMesh) - else: - meshId = get_component_id(obj.name) - - # get visibility - object_isHidden = not obj.visible_get() - - # try get grouping data - object_groupList = try_get_custom_property(obj, 'virtools-group') - object_groupList = set_value_when_none(object_groupList, []) - - # write finfo first - write_string(finfo, obj.name) - write_uint8(finfo, info_bm_type.OBJECT) - write_uint64(finfo, fobject.tell()) - - # write fobject - write_bool(fobject, object_isComponent) - write_bool(fobject, object_isForcedNoComponent) - write_bool(fobject, object_isHidden) - write_worldMatrix(fobject, obj.matrix_world) - write_uint32(fobject, len(object_groupList)) - for item in object_groupList: - write_string(fobject, item) - write_uint32(fobject, meshId) - - # ====================== export mesh - materialSet = set() - materialList = [] - with open(os.path.join(tempFolder, "mesh.bm"), "wb") as fmesh: - for mesh in meshList: - mesh.calc_normals_split() - - # write finfo first - write_string(finfo, mesh.name) - write_uint8(finfo, info_bm_type.MESH) - write_uint64(finfo, fmesh.tell()) - - # write fmesh - # vertices - vecList = mesh.vertices[:] - write_uint32(fmesh, len(vecList)) - for vec in vecList: - #swap yz - write_3vector(fmesh,vec.co[0],vec.co[2],vec.co[1]) - - # uv - face_index_pairs = [(face, index) for index, face in enumerate(mesh.polygons)] - write_uint32(fmesh, len(face_index_pairs) * 3) - if mesh.uv_layers.active is not None: - uv_layer = mesh.uv_layers.active.data[:] - for f, f_index in face_index_pairs: - # 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 - write_2vector(fmesh, uv[0], -uv[1]) - else: - # no uv data. write garbage - for i in range(len(face_index_pairs) * 3): - write_2vector(fmesh, 0.0, 0.0) - - # normals - write_uint32(fmesh, len(face_index_pairs) * 3) - for f, f_index in face_index_pairs: - # 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 - write_3vector(fmesh, nml[0], nml[2], nml[1]) - - # face - # get material first - currentMat = mesh.materials[:] - noMaterial = len(currentMat) == 0 - for mat in currentMat: - if mat not in materialSet: - materialSet.add(mat) - materialList.append(mat) - - write_uint32(fmesh, len(face_index_pairs)) - vtIndex = [] - vnIndex = [] - vIndex = [] - for f, f_index in face_index_pairs: - # confirm material use - if noMaterial: - usedMat = 0 - else: - usedMat = materialList.index(currentMat[f.material_index]) - - # export face - vtIndex.clear() - vnIndex.clear() - vIndex.clear() - - counter = 0 - for loop_index in range(f.loop_start, f.loop_start + f.loop_total): - vIndex.append(mesh.loops[loop_index].vertex_index) - vnIndex.append(f_index * 3 + counter) - vtIndex.append(f_index * 3 + counter) - counter += 1 - # reverse vertices sort - write_face(fmesh, - vIndex[2], vtIndex[2], vnIndex[2], - vIndex[1], vtIndex[1], vnIndex[1], - vIndex[0], vtIndex[0], vnIndex[0]) - - # set used material - write_bool(fmesh, not noMaterial) - write_uint32(fmesh, usedMat) - - mesh.free_normals_split() - - # ====================== export material - textureSet = set() - textureList = [] - textureCount = 0 - with open(os.path.join(tempFolder, "material.bm"), "wb") as fmaterial: - for material in materialList: - # write finfo first - write_string(finfo, material.name) - write_uint8(finfo, info_bm_type.MATERIAL) - write_uint64(finfo, fmaterial.tell()) - - # try get original written data - material_colAmbient = try_get_custom_property(material, 'virtools-ambient') - material_colDiffuse = try_get_custom_property(material, 'virtools-diffuse') - material_colSpecular = try_get_custom_property(material, 'virtools-specular') - material_colEmissive = try_get_custom_property(material, 'virtools-emissive') - material_specularPower = try_get_custom_property(material, 'virtools-power') - - # get basic color - mat_wrap = node_shader_utils.PrincipledBSDFWrapper(material) - if mat_wrap: - use_mirror = mat_wrap.metallic != 0.0 - if use_mirror: - material_colAmbient = set_value_when_none(material_colAmbient, (mat_wrap.metallic, mat_wrap.metallic, mat_wrap.metallic)) - else: - material_colAmbient = set_value_when_none(material_colAmbient, (1.0, 1.0, 1.0)) - material_colDiffuse = set_value_when_none(material_colDiffuse, (mat_wrap.base_color[0], mat_wrap.base_color[1], mat_wrap.base_color[2])) - material_colSpecular = set_value_when_none(material_colSpecular, (mat_wrap.specular, mat_wrap.specular, mat_wrap.specular)) - material_colEmissive = set_value_when_none(material_colEmissive, mat_wrap.emission_color[:3]) - material_specularPower = set_value_when_none(material_specularPower, 0.0) - - # confirm texture - tex_wrap = getattr(mat_wrap, "base_color_texture", None) - if tex_wrap: - image = tex_wrap.image - if image: - # add into texture list - if image not in textureSet: - textureSet.add(image) - textureList.append(image) - currentTexture = textureCount - textureCount += 1 - else: - currentTexture = textureList.index(image) - - material_useTexture = True - material_texture = currentTexture - else: - # no texture - material_useTexture = False - material_texture = 0 - else: - # no texture - material_useTexture = False - material_texture = 0 - - else: - # no Principled BSDF. write garbage - material_colAmbient = set_value_when_none(material_colAmbient, (0.8, 0.8, 0.8)) - material_colDiffuse = set_value_when_none(material_colDiffuse, (0.8, 0.8, 0.8)) - material_colSpecular = set_value_when_none(material_colSpecular, (0.8, 0.8, 0.8)) - material_colEmissive = set_value_when_none(material_colEmissive, (0.8, 0.8, 0.8)) - material_specularPower = set_value_when_none(material_specularPower, 0.0) - - material_useTexture = False - material_texture = 0 - - write_color(fmaterial, material_colAmbient) - write_color(fmaterial, material_colDiffuse) - write_color(fmaterial, material_colSpecular) - write_color(fmaterial, material_colEmissive) - write_float(fmaterial, material_specularPower) - write_bool(fmaterial, material_useTexture) - write_uint32(fmaterial, material_texture) - - - # ====================== export texture - source_dir = os.path.dirname(bpy.data.filepath) - existed_texture = set() - with open(os.path.join(tempFolder, "texture.bm"), "wb") as ftexture: - for texture in textureList: - # write finfo first - write_string(finfo, texture.name) - write_uint8(finfo, info_bm_type.TEXTURE) - write_uint64(finfo, ftexture.tell()) - - # confirm internal - texture_filepath = io_utils.path_reference(texture.filepath, source_dir, tempTextureFolder, - 'ABSOLUTE', "", None, texture.library) - filename = os.path.basename(texture_filepath) - write_string(ftexture, filename) - if (is_external_texture(filename)): - write_bool(ftexture, True) - else: - # copy internal texture, if this file is copied, do not copy it again - write_bool(ftexture, False) - if filename not in existed_texture: - shutil.copy(texture_filepath, os.path.join(tempTextureFolder, filename)) - existed_texture.add(filename) - - - # ============================================ save zip and clean up folder - if os.path.isfile(filepath): - os.remove(filepath) - with zipfile.ZipFile(filepath, 'w', zipfile.ZIP_DEFLATED, 9) as zipObj: - for folderName, subfolders, filenames in os.walk(tempFolder): - for filename in filenames: - filePath = os.path.join(folderName, filename) - arcname=os.path.relpath(filePath, tempFolder) - zipObj.write(filePath, arcname) - tempFolderObj.cleanup() - -# ======================================================================================= export / import assistant - -# shared - -class info_bm_type(): - OBJECT = 0 - MESH = 1 - MATERIAL = 2 - TEXTURE = 3 - -# import - -class info_block_helper(): - def __init__(self, name, offset): - self.name = name - self.offset = offset - self.blenderData = None - -def createInstanceWithOption(instType, instName, instOpt, extraMesh = None): - if instType == info_bm_type.OBJECT: - target = bpy.data.objects - args = (instName, extraMesh) - elif instType == info_bm_type.MESH: - target = bpy.data.meshes - args = (instName, ) - elif instType == info_bm_type.MATERIAL: - target = bpy.data.materials - args = (instName, ) - - if instOpt == 'RENAME': - tempInst = target.new(*args) - tempSkip = False - elif instOpt == 'CURRENT': - try: - tempInst = target[instName] - tempSkip = True - except: - tempInst = target.new(*args) - tempSkip = False - - return (tempInst, tempSkip) - -# NOTE: this function also used by add_elements.py -def load_component(component_id): - # get file first - compName = config.component_list[component_id] - selectedFile = os.path.join( - os.path.dirname(__file__), - 'meshes', - compName + '.bin' - ) - - # read file. please note this sector is sync with import_bm's mesh's code. when something change, please change each other. - fmesh = open(selectedFile, 'rb') - - # create real mesh, we don't need to consider name. blender will solve duplicated name - mesh = bpy.data.meshes.new('mesh_' + compName) - - vList = [] - vnList = [] - faceList = [] - # in first read, store all data into list - listCount = read_uint32(fmesh) - for i in range(listCount): - cache = read_3vector(fmesh) - # switch yz - vList.append((cache[0], cache[2], cache[1])) - listCount = read_uint32(fmesh) - for i in range(listCount): - cache = read_3vector(fmesh) - # switch yz - vnList.append((cache[0], cache[2], cache[1])) - - listCount = read_uint32(fmesh) - for i in range(listCount): - faceData = read_component_face(fmesh) - - # we need invert triangle sort - faceList.append(( - faceData[4], faceData[5], - faceData[2], faceData[3], - faceData[0], faceData[1] - )) - - # then, we need add correspond count for vertices - mesh.vertices.add(len(vList)) - mesh.loops.add(len(faceList)*3) # triangle face confirm - mesh.polygons.add(len(faceList)) - mesh.create_normals_split() - - # add vertices data - mesh.vertices.foreach_set("co", unpack_list(vList)) - mesh.loops.foreach_set("vertex_index", unpack_list(flat_component_vertices_index(faceList))) - mesh.loops.foreach_set("normal", unpack_list(flat_component_vertices_normal(faceList, vnList))) - for i in range(len(faceList)): - mesh.polygons[i].loop_start = i * 3 - mesh.polygons[i].loop_total = 3 - - mesh.polygons[i].use_smooth = True - - mesh.validate(clean_customdata=False) - mesh.update(calc_edges=False, calc_edges_loose=False) - - fmesh.close() - - return mesh - -def flat_vertices_index(faceList): - for item in faceList: - yield (item[0], ) - yield (item[3], ) - yield (item[6], ) - -def flat_vertices_normal(faceList, vnList): - for item in faceList: - yield vnList[item[2]] - yield vnList[item[5]] - yield vnList[item[8]] - -def flat_vertices_uv(faceList, vtList): - for item in faceList: - yield vtList[item[1]] - yield vtList[item[4]] - yield vtList[item[7]] - -def flat_component_vertices_index(faceList): - for item in faceList: - yield (item[0], ) - yield (item[2], ) - yield (item[4], ) - -def flat_component_vertices_normal(faceList, vnList): - for item in faceList: - yield vnList[item[1]] - yield vnList[item[3]] - yield vnList[item[5]] - -# export - -def is_component(name): - return get_component_id(name) != -1 - -def get_component_id(name): - for ind, comp in enumerate(config.component_list): - if name.startswith(comp): - return ind - return -1 - -def is_external_texture(name): - if name in config.external_texture_list: - 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 try_get_custom_property(obj, field): - try: - return obj[field] - except: - return None - -def set_value_when_none(obj, newValue): - if obj is None: - return newValue - else: - return obj - -# ======================================================================================= file io assistant - -# import - -def peek_stream(fs): - res = fs.read(1) - fs.seek(-1, os.SEEK_CUR) - return res - -def read_float(fs): - return struct.unpack("f", fs.read(4))[0] - -def read_uint8(fs): - return struct.unpack("B", fs.read(1))[0] - -def read_uint32(fs): - return struct.unpack("I", fs.read(4))[0] - -def read_uint64(fs): - return struct.unpack("Q", fs.read(8))[0] - -def read_string(fs): - count = read_uint32(fs) - return fs.read(count*4).decode("utf_32_le") - -def read_bool(fs): - return read_uint8(fs) != 0 - -def read_worldMaterix(fs): - p = struct.unpack("ffffffffffffffff", fs.read(4*4*4)) - res = mathutils.Matrix(( - (p[0], p[2], p[1], p[3]), - (p[8], p[10], p[9], p[11]), - (p[4], p[6], p[5], p[7]), - (p[12], p[14], p[13], p[15]))) - return res.transposed() - -def read_3vector(fs): - return struct.unpack("fff", fs.read(3*4)) - -def read_2vector(fs): - return struct.unpack("ff", fs.read(2*4)) - -def read_face(fs): - return struct.unpack("IIIIIIIII", fs.read(4*9)) - -def read_component_face(fs): - return struct.unpack("IIIIII", fs.read(4*6)) - -# export - -def write_string(fs,str): - count=len(str) - write_uint32(fs,count) - fs.write(str.encode("utf_32_le")) - -def write_uint8(fs,num): - fs.write(struct.pack("B", num)) - -def write_uint32(fs,num): - fs.write(struct.pack("I", num)) - -def write_uint64(fs,num): - fs.write(struct.pack("Q", num)) - -def write_bool(fs,boolean): - if boolean: - write_uint8(fs, 1) - else: - write_uint8(fs, 0) - -def write_float(fs,fl): - fs.write(struct.pack("f", fl)) - -def write_worldMatrix(fs, matt): - mat = matt.transposed() - fs.write(struct.pack("ffffffffffffffff", - mat[0][0],mat[0][2], mat[0][1], mat[0][3], - mat[2][0],mat[2][2], mat[2][1], mat[2][3], - mat[1][0],mat[1][2], mat[1][1], mat[1][3], - mat[3][0],mat[3][2], mat[3][1], mat[3][3])) - -def write_3vector(fs, x, y ,z): - fs.write(struct.pack("fff", x, y ,z)) - -def write_color(fs, colors): - write_3vector(fs, colors[0], colors[1], colors[2]) - -def write_2vector(fs, u, v): - fs.write(struct.pack("ff", u, v)) - -def write_face(fs, v1, vt1, vn1, v2, vt2, vn2, v3, vt3, vn3): - fs.write(struct.pack("IIIIIIIII", v1, vt1, vn1, v2, vt2, vn2, v3, vt3, vn3)) - diff --git a/ballance_blender_plugin/no_uv_checker.py b/ballance_blender_plugin/no_uv_checker.py deleted file mode 100644 index 9660e0e..0000000 --- a/ballance_blender_plugin/no_uv_checker.py +++ /dev/null @@ -1,48 +0,0 @@ -import bpy,bmesh -from . import utils - -class BALLANCE_OT_no_uv_checker(bpy.types.Operator): - """Check whether the currently selected object has UV""" - bl_idname = "ballance.no_uv_checker" - bl_label = "Check UV" - bl_options = {'UNDO'} - - @classmethod - def poll(self, context): - return check_valid_target() - - def execute(self, context): - check_target() - return {'FINISHED'} - -# ====================== method - -def check_valid_target(): - return (len(bpy.context.selected_objects) > 0) - -def check_target(): - noUVObject = [] - invalidObjectCount = 0 - for obj in bpy.context.selected_objects: - if obj.type != 'MESH': - invalidObjectCount+=1 - continue - if obj.mode != 'OBJECT': - invalidObjectCount+=1 - continue - if obj.data.uv_layers.active is None: - noUVObject.append(obj.name) - - if len(noUVObject) > 4: - print("Following object don't have UV:") - for item in noUVObject: - print(item) - - utils.ShowMessageBox(( - "All objects: {}".format(len(bpy.context.selected_objects)), - "Skipped: {}".format(invalidObjectCount), - "No UV Count: {}".format(len(noUVObject)), - "", - "Following object don't have UV: " - ) + tuple(noUVObject[:4]) + - (("Too much objects don't have UV. Please open terminal to browse them." if len(noUVObject) > 4 else "") ,), "Check result", 'INFO') diff --git a/ballance_blender_plugin/utils.py b/ballance_blender_plugin/utils.py deleted file mode 100644 index a4aa152..0000000 --- a/ballance_blender_plugin/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -import bpy -from bpy_extras.io_utils import unpack_list - -def ShowMessageBox(message, title, icon): - - def draw(self, context): - layout = self.layout - for item in message: - layout.label(text=item, translate=False) - - bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) - -def AddSceneAndMove2Cursor(obj): - Move2Cursor(obj) - - view_layer = bpy.context.view_layer - collection = view_layer.active_layer_collection.collection - collection.objects.link(obj) - -def Move2Cursor(obj): - obj.location = bpy.context.scene.cursor.location