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)
365 lines
16 KiB
Python
365 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 . import UTILS_constants, UTILS_functions, UTILS_file_io, UTILS_zip_helper, UTILS_virtools_prop, UTILS_icons_manager
|
|
|
|
class BALLANCE_OT_export_bm(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
|
|
"""Save a Ballance Map File (BM file spec 1.4)"""
|
|
bl_idname = "ballance.export_bm"
|
|
bl_label = 'Export BM'
|
|
bl_options = {'PRESET'}
|
|
|
|
# ExportHelper mixin class uses this
|
|
filename_ext = ".bmx"
|
|
filter_glob: bpy.props.StringProperty(
|
|
default="*.bmx",
|
|
options={'HIDDEN'},
|
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
|
)
|
|
|
|
export_mode: bpy.props.EnumProperty(
|
|
name="Export mode",
|
|
items=(('COLLECTION', "Collection", "Export a collection"),
|
|
('OBJECT', "Objects", "Export an objects"),
|
|
),
|
|
)
|
|
|
|
def execute(self, context):
|
|
# 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()
|
|
|
|
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", UTILS_icons_manager.blender_error_icon)
|
|
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)
|
|
|
|
# restore edit mode
|
|
if in_edit_mode:
|
|
bpy.ops.object.editmode_toggle()
|
|
|
|
self.report({'INFO'}, "BM File Export Finished.")
|
|
return {'FINISHED'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.prop(self, "export_mode", expand=True)
|
|
if self.export_mode == 'COLLECTION':
|
|
layout.prop(context.scene.BallanceBlenderPluginProperty, "collection_picker")
|
|
elif self.export_mode == 'OBJECT':
|
|
layout.prop(context.scene.BallanceBlenderPluginProperty, "object_picker")
|
|
|
|
|
|
def export_bm(context, bmx_filepath, prefs_fncg, opts_exportMode, opts_exportTarget):
|
|
# ============================================ alloc a temp folder
|
|
utils_tempFolderObj = tempfile.TemporaryDirectory()
|
|
utils_tempFolder = utils_tempFolderObj.name
|
|
utils_tempTextureFolder = os.path.join(utils_tempFolder, "Texture")
|
|
os.makedirs(utils_tempTextureFolder)
|
|
|
|
# ============================================
|
|
# find export target.
|
|
# do not need check them validation in there.
|
|
# just collect them.
|
|
if opts_exportMode== "COLLECTION":
|
|
objectList = opts_exportTarget.objects
|
|
else:
|
|
objectList = [opts_exportTarget, ]
|
|
|
|
# try get fncg collection
|
|
# fncg stands with forced non-component group
|
|
try:
|
|
object_fncgCollection = bpy.data.collections[prefs_fncg]
|
|
except:
|
|
object_fncgCollection = None
|
|
|
|
# ============================================ export
|
|
with open(os.path.join(utils_tempFolder, "index.bm"), "wb") as finfo:
|
|
UTILS_file_io.write_uint32(finfo, UTILS_constants.bmfile_currentVersion)
|
|
|
|
# ====================== export object
|
|
meshSet = set()
|
|
meshList = []
|
|
meshCount = 0
|
|
with open(os.path.join(utils_tempFolder, "object.bm"), "wb") as fobject:
|
|
for obj in objectList:
|
|
# only export mesh object
|
|
if obj.type != 'MESH':
|
|
continue
|
|
|
|
# clean no mesh object
|
|
object_blenderMesh = obj.data
|
|
if object_blenderMesh is None:
|
|
continue
|
|
|
|
# check component
|
|
if (object_fncgCollection is not None) and (obj.name in object_fncgCollection.objects):
|
|
# it should be set as normal object forcely
|
|
object_isComponent = False
|
|
else:
|
|
# check isComponent normally
|
|
object_isComponent = UTILS_functions.is_component(obj.name)
|
|
|
|
# triangle first and then group
|
|
if not object_isComponent:
|
|
if object_blenderMesh not in meshSet:
|
|
_mesh_triangulate(object_blenderMesh)
|
|
meshSet.add(object_blenderMesh)
|
|
meshList.append(object_blenderMesh)
|
|
object_meshIndex = meshCount
|
|
meshCount += 1
|
|
else:
|
|
object_meshIndex = meshList.index(object_blenderMesh)
|
|
else:
|
|
object_meshIndex = UTILS_functions.get_component_id(obj.name)
|
|
|
|
# get visibility
|
|
object_isHidden = not obj.visible_get()
|
|
|
|
# try get grouping data
|
|
object_groupList = UTILS_virtools_prop.get_virtools_group_data(obj)
|
|
|
|
# =======================
|
|
# write to files
|
|
# write finfo first
|
|
UTILS_file_io.write_string(finfo, obj.name)
|
|
UTILS_file_io.write_uint8(finfo, UTILS_constants.BmfileInfoType.OBJECT)
|
|
UTILS_file_io.write_uint64(finfo, fobject.tell())
|
|
|
|
# write fobject
|
|
UTILS_file_io.write_bool(fobject, object_isComponent)
|
|
UTILS_file_io.write_bool(fobject, object_isHidden)
|
|
UTILS_file_io.write_world_matrix(fobject, obj.matrix_world)
|
|
UTILS_file_io.write_uint32(fobject, len(object_groupList))
|
|
for item in object_groupList:
|
|
UTILS_file_io.write_string(fobject, item)
|
|
UTILS_file_io.write_uint32(fobject, object_meshIndex)
|
|
|
|
# ====================== export mesh
|
|
materialSet = set()
|
|
materialList = []
|
|
with open(os.path.join(utils_tempFolder, "mesh.bm"), "wb") as fmesh:
|
|
for mesh in meshList:
|
|
# split normals
|
|
mesh.calc_normals_split()
|
|
|
|
# write finfo first
|
|
UTILS_file_io.write_string(finfo, mesh.name)
|
|
UTILS_file_io.write_uint8(finfo, UTILS_constants.BmfileInfoType.MESH)
|
|
UTILS_file_io.write_uint64(finfo, fmesh.tell())
|
|
|
|
# write fmesh
|
|
# vertices
|
|
mesh_vecList = mesh.vertices[:]
|
|
UTILS_file_io.write_uint32(fmesh, len(mesh_vecList))
|
|
for vec in mesh_vecList:
|
|
#swap yz
|
|
UTILS_file_io.write_3vector(fmesh,vec.co[0],vec.co[2],vec.co[1])
|
|
|
|
# uv
|
|
mesh_faceIndexPairs = [(face, index) for index, face in enumerate(mesh.polygons)]
|
|
UTILS_file_io.write_uint32(fmesh, len(mesh_faceIndexPairs) * 3)
|
|
if mesh.uv_layers.active is not None:
|
|
uv_layer = mesh.uv_layers.active.uv
|
|
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].vector
|
|
# 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:
|
|
# skip empty mtl slot
|
|
if mat is None:
|
|
continue
|
|
# add into mtl set
|
|
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
|
|
# a face without mtl have 2 situations. first is the whole object do not have mtl
|
|
# another is this face use an empty mtl slot.
|
|
mesh_faceNoMtl = mesh_noMaterial or (mesh_usedBlenderMtl[f.material_index] is None)
|
|
if mesh_faceNoMtl:
|
|
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_faceNoMtl)
|
|
UTILS_file_io.write_uint32(fmesh, mesh_materialIndex)
|
|
|
|
# free splited normals
|
|
mesh.free_normals_split()
|
|
|
|
# ====================== export material
|
|
textureSet = set()
|
|
textureList = []
|
|
textureCount = 0
|
|
with open(os.path.join(utils_tempFolder, "material.bm"), "wb") as fmaterial:
|
|
for material in materialList:
|
|
# write finfo first
|
|
UTILS_file_io.write_string(finfo, material.name)
|
|
UTILS_file_io.write_uint8(finfo, UTILS_constants.BmfileInfoType.MATERIAL)
|
|
UTILS_file_io.write_uint64(finfo, fmaterial.tell())
|
|
|
|
# try get original written data
|
|
(material_enableVirtoolsMat,
|
|
material_colAmbient, material_colDiffuse, material_colSpecular, material_colEmissive, material_specularPower,
|
|
material_alphaTest, material_alphaBlend, material_zBuffer, material_twoSided,
|
|
material_texture) = UTILS_virtools_prop.get_virtools_material_data(material)
|
|
|
|
# only try get from Principled BSDF when we couldn't get from virtools_material props
|
|
if not material_enableVirtoolsMat:
|
|
v = UTILS_functions.parse_material_nodes(material)
|
|
if v is not None:
|
|
(material_enableVirtoolsMat,
|
|
material_colAmbient, material_colDiffuse, material_colSpecular, material_colEmissive, material_specularPower,
|
|
material_alphaTest, material_alphaBlend, material_zBuffer, material_twoSided,
|
|
material_texture) = v
|
|
|
|
# check texture index
|
|
if material_texture is None:
|
|
material_useTexture = False
|
|
material_textureIndex = 0
|
|
else:
|
|
# add into texture list
|
|
if material_texture not in textureSet:
|
|
textureSet.add(material_texture)
|
|
textureList.append(material_texture)
|
|
textureIndex = textureCount
|
|
textureCount += 1
|
|
else:
|
|
textureIndex = textureList.index(material_texture)
|
|
|
|
material_useTexture = True
|
|
material_textureIndex = textureIndex
|
|
|
|
UTILS_file_io.write_color(fmaterial, material_colAmbient)
|
|
UTILS_file_io.write_color(fmaterial, material_colDiffuse)
|
|
UTILS_file_io.write_color(fmaterial, material_colSpecular)
|
|
UTILS_file_io.write_color(fmaterial, material_colEmissive)
|
|
UTILS_file_io.write_float(fmaterial, material_specularPower)
|
|
UTILS_file_io.write_bool(fmaterial, material_alphaTest)
|
|
UTILS_file_io.write_bool(fmaterial, material_alphaBlend)
|
|
UTILS_file_io.write_bool(fmaterial, material_zBuffer)
|
|
UTILS_file_io.write_bool(fmaterial, material_twoSided)
|
|
UTILS_file_io.write_bool(fmaterial, material_useTexture)
|
|
UTILS_file_io.write_uint32(fmaterial, material_textureIndex)
|
|
|
|
|
|
# ====================== export texture
|
|
texture_blenderFilePath = os.path.dirname(bpy.data.filepath)
|
|
texture_existedTextureFilepath = set()
|
|
with open(os.path.join(utils_tempFolder, "texture.bm"), "wb") as ftexture:
|
|
for texture in textureList:
|
|
# write finfo first
|
|
UTILS_file_io.write_string(finfo, texture.name)
|
|
UTILS_file_io.write_uint8(finfo, UTILS_constants.BmfileInfoType.TEXTURE)
|
|
UTILS_file_io.write_uint64(finfo, ftexture.tell())
|
|
|
|
# confirm whether it is internal texture
|
|
# get absolute texture path
|
|
texture_filepath = io_utils.path_reference(texture.filepath, texture_blenderFilePath, utils_tempTextureFolder,
|
|
'ABSOLUTE', "", None, texture.library)
|
|
# texture_filepath = bpy.path.abspath(texture.filepath, start=texture_blenderFilePath, library=texture.library)
|
|
|
|
# get file name and write it
|
|
texture_filename = os.path.basename(texture_filepath)
|
|
UTILS_file_io.write_string(ftexture, texture_filename)
|
|
|
|
if (_is_external_texture(texture_filename)):
|
|
# write directly, use Ballance texture
|
|
UTILS_file_io.write_bool(ftexture, True)
|
|
else:
|
|
# copy internal texture, if this file is copied, do not copy it again
|
|
UTILS_file_io.write_bool(ftexture, False)
|
|
if texture_filename not in texture_existedTextureFilepath:
|
|
shutil.copy(texture_filepath, os.path.join(utils_tempTextureFolder, texture_filename))
|
|
texture_existedTextureFilepath.add(texture_filename)
|
|
|
|
|
|
# ============================================
|
|
# save zip and clean up folder
|
|
UTILS_zip_helper.compress(utils_tempFolder, bmx_filepath)
|
|
utils_tempFolderObj.cleanup()
|
|
|
|
# ==========================================
|
|
# blender related functions
|
|
|
|
def _is_external_texture(name):
|
|
if name in UTILS_constants.bmfile_externalTextureSet:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def _mesh_triangulate(me):
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
bmesh.ops.triangulate(bm, faces=bm.faces)
|
|
bm.to_mesh(me)
|
|
bm.free()
|
|
|
|
def _set_value_when_none(obj, newValue):
|
|
if obj is None:
|
|
return newValue
|
|
else:
|
|
return obj
|
|
|