511 lines
17 KiB
Python
511 lines
17 KiB
Python
import bpy,mathutils
|
|
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
|
|
|
|
class BALLANCE_OT_add_floor(bpy.types.Operator):
|
|
"""Add Ballance floor"""
|
|
bl_idname = "ballance.add_floor"
|
|
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()),
|
|
)
|
|
|
|
expand_length_1 : bpy.props.IntProperty(
|
|
name="D1 length",
|
|
description="The length of expand direction 1",
|
|
min=0,
|
|
default=0,
|
|
)
|
|
|
|
expand_length_2 : bpy.props.IntProperty(
|
|
name="D2 length",
|
|
description="The length of expand direction 2",
|
|
min=0,
|
|
default=0,
|
|
)
|
|
|
|
height_multiplier : bpy.props.FloatProperty(
|
|
name="Height",
|
|
description="The multiplier for height. Default height is 5",
|
|
min=0.0,
|
|
default=1.0,
|
|
)
|
|
|
|
use_2d_top : bpy.props.BoolProperty(
|
|
name="Top side"
|
|
)
|
|
use_2d_right : bpy.props.BoolProperty(
|
|
name="Right side"
|
|
)
|
|
use_2d_bottom : bpy.props.BoolProperty(
|
|
name="Bottom side"
|
|
)
|
|
use_2d_left : bpy.props.BoolProperty(
|
|
name="Left side"
|
|
)
|
|
use_3d_top : bpy.props.BoolProperty(
|
|
name="Top face"
|
|
)
|
|
use_3d_bottom : bpy.props.BoolProperty(
|
|
name="Bottom face"
|
|
)
|
|
|
|
previous_floor_type = ''
|
|
|
|
@classmethod
|
|
def poll(self, context):
|
|
prefs = bpy.context.preferences.addons[__package__].preferences
|
|
return os.path.isdir(prefs.external_folder)
|
|
|
|
def execute(self, context):
|
|
# load mesh
|
|
objmesh = bpy.data.meshes.new('done_')
|
|
if self.floor_type in config.floor_basic_block_list:
|
|
load_basic_floor(
|
|
objmesh,
|
|
self.floor_type,
|
|
'R0',
|
|
self.height_multiplier,
|
|
self.expand_length_1,
|
|
self.expand_length_2,
|
|
(self.use_2d_top,
|
|
self.use_2d_right,
|
|
self.use_2d_bottom,
|
|
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(
|
|
objmesh,
|
|
self.floor_type,
|
|
self.height_multiplier,
|
|
self.expand_length_1,
|
|
self.expand_length_2,
|
|
(self.use_2d_top,
|
|
self.use_2d_right,
|
|
self.use_2d_bottom,
|
|
self.use_2d_left,
|
|
self.use_3d_top,
|
|
self.use_3d_bottom))
|
|
|
|
# normalization mesh
|
|
objmesh.validate(clean_customdata=False)
|
|
objmesh.update(calc_edges=False, calc_edges_loose=False)
|
|
|
|
# create object and link it
|
|
obj=bpy.data.objects.new('A_Floor_BMERevenge_', objmesh)
|
|
utils.AddSceneAndMove2Cursor(obj)
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
wm = context.window_manager
|
|
return wm.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
# get floor prototype
|
|
floor_prototype = config.floor_block_dict[self.floor_type]
|
|
|
|
# try sync default value
|
|
if self.previous_floor_type != self.floor_type:
|
|
self.previous_floor_type = self.floor_type
|
|
|
|
default_sides = floor_prototype['DefaultSideConfig']
|
|
self.use_2d_top = default_sides['UseTwoDTop']
|
|
self.use_2d_right = default_sides['UseTwoDRight']
|
|
self.use_2d_bottom = default_sides['UseTwoDBottom']
|
|
self.use_2d_left = default_sides['UseTwoDLeft']
|
|
self.use_3d_top = default_sides['UseThreeDTop']
|
|
self.use_3d_bottom = default_sides['UseThreeDBottom']
|
|
|
|
# show property
|
|
layout = self.layout
|
|
col = layout.column()
|
|
col.label(text="Basic param")
|
|
col.prop(self, "floor_type")
|
|
col.prop(self, "height_multiplier")
|
|
|
|
col.separator()
|
|
col.label(text="Expand")
|
|
if floor_prototype['ExpandType'] == 'Column' or floor_prototype['ExpandType'] == 'Freedom':
|
|
col.prop(self, "expand_length_1")
|
|
if floor_prototype['ExpandType'] == 'Freedom':
|
|
col.prop(self, "expand_length_2")
|
|
col.label(text="Unit size: " + floor_prototype['UnitSize'])
|
|
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.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.separator()
|
|
grids.label(text=config.floor_expand_direction_map[floor_prototype['InitColumnDirection']][floor_prototype['ExpandType']][2])
|
|
grids.separator()
|
|
|
|
col.separator()
|
|
col.label(text="Faces")
|
|
row = col.row()
|
|
row.prop(self, "use_3d_top")
|
|
row.prop(self, "use_3d_bottom")
|
|
|
|
col.separator()
|
|
col.label(text="Sides")
|
|
grids = col.grid_flow(row_major=True, columns=3)
|
|
grids.separator()
|
|
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.prop(self, "use_2d_right")
|
|
grids.separator()
|
|
grids.prop(self, "use_2d_bottom")
|
|
grids.separator()
|
|
|
|
def face_fallback(normal_face, expand_face, height):
|
|
if expand_face == None:
|
|
return normal_face
|
|
|
|
if height <= 1.0:
|
|
return normal_face
|
|
else:
|
|
return expand_face
|
|
|
|
def create_or_get_material(material_name):
|
|
# 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])
|
|
|
|
inode=m.node_tree.nodes.new(type="ShaderNodeTexImage")
|
|
inode.image=txur
|
|
m.node_tree.links.new(inode.outputs[0],bnode.inputs[0])
|
|
|
|
# write custom property
|
|
# WARNING: this data is shared with BallanceVirtoolsPlugin - mapping_BM.cpp - fix_blender_texture
|
|
m['virtools-ambient'] = (0.0, 0.0, 0.0)
|
|
m['virtools-diffuse'] = (122 / 255.0, 122 / 255.0, 122 / 255.0) if material_name == 'FloorSide' else (1.0, 1.0, 1.0)
|
|
m['virtools-specular'] = (0.0, 0.0, 0.0) if material_name == 'FloorSide' else (80 / 255.0, 80 / 255.0, 80 / 255.0)
|
|
m['virtools-emissive'] = (104 / 255.0, 104 / 255.0, 104 / 255.0) if material_name == 'FloorSide' else (0.0, 0.0, 0.0)
|
|
m['virtools-power'] = 0.0
|
|
|
|
return m
|
|
|
|
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])]
|
|
|
|
for i in range(3):
|
|
symbol = sp[i+1]
|
|
if symbol == '':
|
|
continue
|
|
|
|
factor = 1.0 if symbol[0] == '+' else -1.0
|
|
p = symbol[1:]
|
|
if p == 'd1':
|
|
vec[i] += d1 * unit * factor
|
|
elif p == 'd2':
|
|
vec[i] += d2 * unit * factor
|
|
elif p == 'd3':
|
|
vec[i] += (d3 - 1) * unit_height * factor
|
|
|
|
return vec
|
|
|
|
def rotate_translate_vec(vec, rotation, unit, extra_translate):
|
|
vec[0] -= unit / 2
|
|
vec[1] -= unit / 2
|
|
|
|
if rotation == 'R0':
|
|
coso=1
|
|
sino=0
|
|
elif rotation == 'R90':
|
|
coso=0
|
|
sino=1
|
|
elif rotation == 'R180':
|
|
coso=-1
|
|
sino=0
|
|
elif rotation == 'R270':
|
|
coso=0
|
|
sino=-1
|
|
|
|
return (
|
|
coso * vec[0] - sino * vec[1] + unit / 2 + extra_translate[0],
|
|
sino * vec[0] + coso * vec[1] + unit / 2 + extra_translate[1],
|
|
vec[2]
|
|
)
|
|
|
|
|
|
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])]
|
|
|
|
for i in range(2):
|
|
symbol = sp[i+1]
|
|
if symbol == '':
|
|
continue
|
|
|
|
factor = 1.0 if symbol[0] == '+' else -1.0
|
|
p = symbol[1:]
|
|
if p == 'd1':
|
|
vec[i] += d1 * unit * factor
|
|
elif p == 'd2':
|
|
vec[i] += d2 * unit * factor
|
|
elif p == 'd3':
|
|
vec[i] += (d3 - 1) * unit * factor
|
|
|
|
return tuple(vec)
|
|
|
|
def solve_normal_data(point1, point2, point3):
|
|
vector1 = (
|
|
point2[0] - point1[0],
|
|
point2[1] - point1[1],
|
|
point2[2] - point1[2]
|
|
)
|
|
vector2 = (
|
|
point3[0] - point2[0],
|
|
point3[1] - point2[1],
|
|
point3[2] - point2[2]
|
|
)
|
|
|
|
# do vector x mutiply
|
|
# vector1 x vector2
|
|
nor = [
|
|
vector1[1] * vector2[2] - vector1[2] * vector2[1],
|
|
vector1[2] * vector2[0] - vector1[0] * vector2[2],
|
|
vector1[0] * vector2[1] - vector1[1] * vector2[0]
|
|
]
|
|
|
|
# do a normalization
|
|
length = math.sqrt(nor[0] ** 2 + nor[1] ** 2 + nor[2] ** 2)
|
|
nor[0] /= length
|
|
nor[1] /= length
|
|
nor[2] /= length
|
|
|
|
return tuple(nor)
|
|
|
|
def solve_smashed_position(str_data, d1, d2):
|
|
sp=str_data.split(';')
|
|
sp_pos = sp[0].split(',')
|
|
sp_sync = sp[1].split(',')
|
|
|
|
vec = [int(sp_pos[0]), int(sp_pos[1])]
|
|
|
|
for i in range(2):
|
|
offset = 0 if sp_sync[i * 2] == '' else int(sp_sync[i * 2])
|
|
if sp_sync[i*2+1] == 'd1':
|
|
vec[i] += d1 + offset
|
|
elif sp_sync[i*2+1] == 'd2':
|
|
vec[i] += d2 + offset
|
|
|
|
return tuple(vec)
|
|
|
|
def virtual_foreach_set(collection, field, base_num, data):
|
|
counter = 0
|
|
for i in data:
|
|
exec("a[j]." + field + "=q", {}, {
|
|
'a': collection,
|
|
'j': counter + base_num,
|
|
'q': i
|
|
})
|
|
counter+=1
|
|
|
|
|
|
|
|
'''
|
|
sides_struct should be a tuple and it always have 6 bool items
|
|
|
|
(use_2d_top, use_2d_right, use_2d_bottom, use_2d_left, use_3d_top, use_3d_bottom)
|
|
|
|
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]
|
|
|
|
# set some unit
|
|
height_unit = 5.0
|
|
if floor_prototype['UnitSize'] == 'Small':
|
|
block_3dworld_unit = 2.5
|
|
block_uvworld_unit = 0.5
|
|
elif floor_prototype['UnitSize'] == 'Large':
|
|
block_3dworld_unit = 5.0
|
|
block_uvworld_unit = 1.0
|
|
|
|
# got all needed faces
|
|
needCreatedFaces = []
|
|
if sides_struct[0]:
|
|
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))
|
|
if sides_struct[2]:
|
|
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))
|
|
if sides_struct[4]:
|
|
needCreatedFaces.append(floor_prototype['ThreeDTopFace'])
|
|
if sides_struct[5]:
|
|
needCreatedFaces.append(floor_prototype['ThreeDBottomFace'])
|
|
|
|
# resolve face
|
|
# solve material first
|
|
materialDict = {}
|
|
allmat = mesh.materials[:]
|
|
counter = len(allmat)
|
|
for face_define in needCreatedFaces:
|
|
for face in face_define['Faces']:
|
|
new_texture = face['Textures']
|
|
if new_texture not in materialDict.keys():
|
|
# try get from existed solt
|
|
pending_material = create_or_get_material(new_texture)
|
|
if pending_material not in allmat:
|
|
# no matched. add it
|
|
mesh.materials.append(pending_material)
|
|
materialDict[new_texture] = counter
|
|
counter += 1
|
|
else:
|
|
# use existed index
|
|
materialDict[new_texture] = allmat.index(pending_material)
|
|
|
|
# now, we can process real mesh
|
|
# load existed base count
|
|
global_offset_vec = len(mesh.vertices)
|
|
global_offset_polygons = len(mesh.polygons)
|
|
global_offset_loops = len(mesh.loops)
|
|
vecList = []
|
|
uvList = []
|
|
normalList = []
|
|
faceList = []
|
|
faceIndList = []
|
|
faceMatList = []
|
|
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),
|
|
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))
|
|
|
|
for face in face_define['Faces']:
|
|
if face['Type'] == 'RECTANGLE':
|
|
# rectangle
|
|
vec_indices = (
|
|
face['P1'] + base_indices,
|
|
face['P2'] + base_indices,
|
|
face['P3'] + base_indices,
|
|
face['P4'] + base_indices)
|
|
indCount = 4
|
|
elif face['Type'] == 'TRIANGLE':
|
|
# triangle
|
|
vec_indices = (
|
|
face['P1'] + base_indices,
|
|
face['P2'] + base_indices,
|
|
face['P3'] + base_indices)
|
|
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]])
|
|
for i in range(indCount):
|
|
normalList.append(point_normal)
|
|
|
|
# push indices into list
|
|
for i in range(indCount):
|
|
faceList.append(vec_indices[i] + global_offset_vec)
|
|
|
|
# push material into list
|
|
faceMatList.append(materialDict[face['Textures']])
|
|
|
|
# push face vec count into list
|
|
faceIndList.append(indCount)
|
|
|
|
# push data into blender struct
|
|
mesh.vertices.add(len(vecList))
|
|
mesh.loops.add(len(faceList))
|
|
mesh.polygons.add(len(faceMatList))
|
|
mesh.create_normals_split()
|
|
if mesh.uv_layers.active is None:
|
|
# 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)
|
|
|
|
cache_counter = 0
|
|
for i in range(len(faceMatList)):
|
|
indCount = faceIndList[i]
|
|
mesh.polygons[i + global_offset_polygons].loop_start = global_offset_loops + cache_counter
|
|
mesh.polygons[i + global_offset_polygons].loop_total = indCount
|
|
mesh.polygons[i + global_offset_polygons].material_index = faceMatList[i]
|
|
mesh.polygons[i + global_offset_polygons].use_smooth = True
|
|
cache_counter += indCount
|
|
|
|
|
|
def load_derived_floor(mesh, floor_type, height_multiplier, d1, d2, sides_struct):
|
|
floor_prototype = config.floor_block_dict[floor_type]
|
|
|
|
# set some unit
|
|
if floor_prototype['UnitSize'] == 'Small':
|
|
block_3dworld_unit = 2.5
|
|
elif floor_prototype['UnitSize'] == 'Large':
|
|
block_3dworld_unit = 5.0
|
|
|
|
# construct face dict
|
|
sides_dict = {
|
|
'True': True,
|
|
'False': False,
|
|
'2dTop': sides_struct[0],
|
|
'2dRight': sides_struct[1],
|
|
'2dBottom': sides_struct[2],
|
|
'2dLeft': sides_struct[3],
|
|
'3dTop': sides_struct[4],
|
|
'3dBottom': sides_struct[5]
|
|
}
|
|
|
|
# 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)
|
|
|
|
sides_data = tuple(sides_dict[x] for x in blk['SideSync'].split(';'))
|
|
|
|
# call basic floor creator
|
|
load_basic_floor(
|
|
mesh,
|
|
blk['Type'],
|
|
blk['Rotation'],
|
|
height_multiplier,
|
|
expand_pos[0],
|
|
expand_pos[1],
|
|
sides_data,
|
|
(start_pos[0] * block_3dworld_unit, start_pos[1] * block_3dworld_unit)
|
|
)
|
|
|
|
|