2022-04-03 22:48:12 +08:00
|
|
|
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
|
2023-01-29 10:32:43 +08:00
|
|
|
from . import UTILS_constants, UTILS_functions, UTILS_file_io, UTILS_zip_helper, UTILS_virtools_prop, UTILS_icons_manager
|
2022-04-03 22:48:12 +08:00
|
|
|
|
|
|
|
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'}
|
2022-04-04 22:04:11 +08:00
|
|
|
|
|
|
|
# ExportHelper mixin class uses this
|
2022-04-03 22:48:12 +08:00
|
|
|
filename_ext = ".bmx"
|
2022-04-04 22:04:11 +08:00
|
|
|
filter_glob: bpy.props.StringProperty(
|
|
|
|
default="*.bmx",
|
|
|
|
options={'HIDDEN'},
|
|
|
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
|
|
|
)
|
2022-04-03 22:48:12 +08:00
|
|
|
|
|
|
|
export_mode: bpy.props.EnumProperty(
|
|
|
|
name="Export mode",
|
|
|
|
items=(('COLLECTION', "Collection", "Export a collection"),
|
|
|
|
('OBJECT', "Objects", "Export an objects"),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
def execute(self, context):
|
2023-01-10 19:35:15 +08:00
|
|
|
# detect edit mode
|
|
|
|
in_edit_mode = False
|
|
|
|
if bpy.context.object and bpy.context.object.mode == "EDIT":
|
|
|
|
in_edit_mode = True
|
|
|
|
bpy.ops.object.editmode_toggle()
|
|
|
|
|
2022-04-04 11:30:04 +08:00
|
|
|
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)):
|
2023-01-29 10:32:43 +08:00
|
|
|
UTILS_functions.show_message_box(("No specific target", ), "Lost parameter", UTILS_icons_manager.blender_error_icon)
|
2022-04-03 22:48:12 +08:00
|
|
|
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)
|
2023-01-10 19:35:15 +08:00
|
|
|
|
|
|
|
# restore edit mode
|
|
|
|
if in_edit_mode:
|
|
|
|
bpy.ops.object.editmode_toggle()
|
|
|
|
|
2023-01-29 10:32:43 +08:00
|
|
|
self.report({'INFO'}, "BM File Export Finished.")
|
2022-04-03 22:48:12 +08:00
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
def draw(self, context):
|
|
|
|
layout = self.layout
|
2022-12-30 10:06:02 +08:00
|
|
|
layout.prop(self, "export_mode", expand=True)
|
2022-04-03 22:48:12 +08:00
|
|
|
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()
|
2022-04-04 11:30:04 +08:00
|
|
|
utils_tempFolder = utils_tempFolderObj.name
|
2022-04-03 22:48:12 +08:00
|
|
|
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:
|
2022-04-04 11:30:04 +08:00
|
|
|
UTILS_file_io.write_uint32(finfo, UTILS_constants.bmfile_currentVersion)
|
2022-04-03 22:48:12 +08:00
|
|
|
|
|
|
|
# ====================== 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
|
2022-04-04 11:30:04 +08:00
|
|
|
object_isComponent = UTILS_functions.is_component(obj.name)
|
2022-04-03 22:48:12 +08:00
|
|
|
|
|
|
|
# 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:
|
2022-04-04 11:30:04 +08:00
|
|
|
object_meshIndex = UTILS_functions.get_component_id(obj.name)
|
2022-04-03 22:48:12 +08:00
|
|
|
|
|
|
|
# get visibility
|
|
|
|
object_isHidden = not obj.visible_get()
|
|
|
|
|
|
|
|
# try get grouping data
|
2022-04-09 14:41:32 +08:00
|
|
|
object_groupList = UTILS_virtools_prop.get_virtools_group_data(obj)
|
2022-04-03 22:48:12 +08:00
|
|
|
|
|
|
|
# =======================
|
|
|
|
# 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)
|
2022-04-04 11:30:04 +08:00
|
|
|
UTILS_file_io.write_world_matrix(fobject, obj.matrix_world)
|
2022-04-03 22:48:12 +08:00
|
|
|
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
|
2022-11-20 12:02:15 +08:00
|
|
|
(material_enableVirtoolsMat,
|
|
|
|
material_colAmbient, material_colDiffuse, material_colSpecular, material_colEmissive, material_specularPower,
|
2022-07-23 17:22:44 +08:00
|
|
|
material_alphaTest, material_alphaBlend, material_zBuffer, material_twoSided,
|
|
|
|
material_texture) = UTILS_virtools_prop.get_virtools_material_data(material)
|
|
|
|
|
|
|
|
# only try get from Principled BSDF when we couldn't get from virtools_material props
|
2022-11-20 12:02:15 +08:00
|
|
|
if not material_enableVirtoolsMat:
|
|
|
|
v = UTILS_functions.parse_material_nodes(material)
|
|
|
|
if v is not None:
|
|
|
|
(material_enableVirtoolsMat,
|
|
|
|
material_colAmbient, material_colDiffuse, material_colSpecular, material_colEmissive, material_specularPower,
|
|
|
|
material_alphaTest, material_alphaBlend, material_zBuffer, material_twoSided,
|
|
|
|
material_texture) = v
|
|
|
|
|
2022-07-23 17:22:44 +08:00
|
|
|
# check texture index
|
|
|
|
if material_texture is None:
|
2022-04-03 22:48:12 +08:00
|
|
|
material_useTexture = False
|
|
|
|
material_textureIndex = 0
|
2022-07-23 17:22:44 +08:00
|
|
|
else:
|
|
|
|
# add into texture list
|
|
|
|
if material_texture not in textureSet:
|
|
|
|
textureSet.add(material_texture)
|
|
|
|
textureList.append(material_texture)
|
|
|
|
textureIndex = textureCount
|
|
|
|
textureCount += 1
|
|
|
|
else:
|
|
|
|
textureIndex = textureList.index(material_texture)
|
|
|
|
|
|
|
|
material_useTexture = True
|
|
|
|
material_textureIndex = textureIndex
|
2022-04-03 22:48:12 +08:00
|
|
|
|
|
|
|
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)
|
2022-07-23 17:22:44 +08:00
|
|
|
UTILS_file_io.write_bool(fmaterial, material_alphaTest)
|
|
|
|
UTILS_file_io.write_bool(fmaterial, material_alphaBlend)
|
|
|
|
UTILS_file_io.write_bool(fmaterial, material_zBuffer)
|
|
|
|
UTILS_file_io.write_bool(fmaterial, material_twoSided)
|
2022-04-03 22:48:12 +08:00
|
|
|
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):
|
2022-04-04 11:30:04 +08:00
|
|
|
if name in UTILS_constants.bmfile_externalTextureSet:
|
2022-04-03 22:48:12 +08:00
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _mesh_triangulate(me):
|
|
|
|
bm = bmesh.new()
|
|
|
|
bm.from_mesh(me)
|
|
|
|
bmesh.ops.triangulate(bm, faces=bm.faces)
|
|
|
|
bm.to_mesh(me)
|
|
|
|
bm.free()
|
|
|
|
|
|
|
|
def _set_value_when_none(obj, newValue):
|
|
|
|
if obj is None:
|
|
|
|
return newValue
|
|
|
|
else:
|
|
|
|
return obj
|
|
|
|
|