yyc12345
ddf6b7befe
- correct all icon usage. use 'CANCEL' instead of 'ERROR' to indicate error correctly. and move these icon const into UTILS_icons_manager.py - auto rename generated rail section to match the default name standard. - give more clear signal about import and export bm file by using Operator.report() function.
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].data.foreach_set("uv", unpack_list(_flat_vertices_uv(mesh_faceList, mesh_vtList)))
|
|
for i in range(len(mesh_faceList)):
|
|
mesh_target.polygons[i].loop_start = i * 3
|
|
mesh_target.polygons[i].loop_total = 3
|
|
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]]
|