yyc12345
6fe856fa8e
- change bl_info["support"] to COMMUNITY. because Blender do not support TESTINg anymore. - now plugin should be installed in addons folder, not addons_contrib, due to blender changes. - remove the reference about mesh.polypons.loop_total. because it is readonly now. (blender 3.6 changed) - change uv assign method. use new properties instead. (MeshUVLoop is deprecated in blender 3.5 and removed in blender 4.0)
375 lines
16 KiB
Python
375 lines
16 KiB
Python
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, UTILS_virtools_prop, UTILS_icons_manager
|
|
|
|
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'}
|
|
|
|
# ImportHelper mixin class uses this
|
|
filename_ext = ".bmx"
|
|
filter_glob: bpy.props.StringProperty(
|
|
default="*.bmx",
|
|
options={'HIDDEN'},
|
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
|
)
|
|
|
|
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)
|
|
|
|
self.report({'INFO'}, "BM File Import Finished.")
|
|
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", UTILS_icons_manager.blender_error_icon)
|
|
findex.close()
|
|
utils_tempFolderObj.cleanup()
|
|
return
|
|
|
|
# collect block header data
|
|
while len(UTILS_file_io.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_filename= 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_filename= 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_alphaTest = UTILS_file_io.read_bool(fmaterial)
|
|
material_alphaBlend = UTILS_file_io.read_bool(fmaterial)
|
|
material_zBuffer = UTILS_file_io.read_bool(fmaterial)
|
|
material_twoSided = UTILS_file_io.read_bool(fmaterial)
|
|
material_useTexture = UTILS_file_io.read_bool(fmaterial)
|
|
material_texture = UTILS_file_io.read_uint32(fmaterial)
|
|
|
|
# alloc basic material
|
|
(material_target, skip_init) = UTILS_functions.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_blender_material(material_target,
|
|
(True,
|
|
material_colAmbient, material_colDiffuse, material_colSpecular, material_colEmissive, material_specularPower,
|
|
material_alphaTest, material_alphaBlend, material_zBuffer, material_twoSided,
|
|
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) = UTILS_functions.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 = mesh_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(mesh_vList))
|
|
mesh_target.loops.add(len(mesh_faceList)*3) # triangle face confirm
|
|
mesh_target.polygons.add(len(mesh_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(mesh_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].uv.foreach_set("vector", unpack_list(_flat_vertices_uv(mesh_faceList, mesh_vtList))) # Blender 3.5 CHANGED
|
|
for i in range(len(mesh_faceList)):
|
|
mesh_target.polygons[i].loop_start = i * 3
|
|
# mesh_target.polygons[i].loop_total = 3 # Blender 3.6 CHANGED
|
|
if mesh_faceList[i][9] != -1:
|
|
mesh_target.polygons[i].material_index = mesh_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_world_materix(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) = UTILS_functions.create_instance_with_option(
|
|
UTILS_constants.BmfileInfoType.OBJECT, item.name, opts_object,
|
|
extra_mesh=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 check 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:
|
|
UTILS_virtools_prop.fill_virtools_group_data(object_target, tuple(object_groupList))
|
|
else:
|
|
UTILS_virtools_prop.fill_virtools_group_data(object_target, None)
|
|
|
|
# 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]]
|