refactor project. preparing v3.0 development. no debug current

This commit is contained in:
yyc12345 2022-04-03 22:48:12 +08:00
parent 9c24569a06
commit e264c85a04
20 changed files with 1421 additions and 1213 deletions

View File

@ -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.

View File

@ -50,7 +50,7 @@ Ballance 3D是一套简单的用于制图3D相关的轻型工具集合可以
还可以选择投影轴以获取更好的UV分布。
### Flatten UV
#### Flatten UV
在物体编辑模式下用于将当前选中面按某一边贴附到V轴上的模式展开到UV上。注意只支持凸边面。

View File

@ -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

View File

@ -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]]

View File

@ -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'):

View File

@ -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:

View File

@ -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'
)

View File

@ -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")

View File

@ -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.")

View File

@ -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
)

View File

@ -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")

View File

@ -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 = {}

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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))

View File

@ -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')

View File

@ -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