379 lines
12 KiB
Python
379 lines
12 KiB
Python
import bpy
|
|
import os
|
|
import re
|
|
|
|
print("=== EXPORT STARTED ===")
|
|
|
|
blend_dir = os.path.dirname(bpy.data.filepath)
|
|
out_path = os.path.join(blend_dir, "pgout")
|
|
os.makedirs(out_path, exist_ok=True)
|
|
|
|
roomnames = []
|
|
armatures = {}
|
|
armature_anims = {}
|
|
|
|
def get_armature_keep_bones(armature: bpy.types.Armature):
|
|
keep_bones = set()
|
|
|
|
for bone in armature.data.bones:
|
|
bone_name = bone.name
|
|
is_tag = re.match(r"Tag.(\w+)", bone_name)
|
|
|
|
if bone.use_deform or is_tag:
|
|
keep_bones.add(bone)
|
|
|
|
while bone.parent:
|
|
bone = bone.parent
|
|
keep_bones.add(bone)
|
|
|
|
return keep_bones
|
|
|
|
def export_mesh(obj, output_path, export_luv = False, armature = None):
|
|
if obj.type != 'MESH':
|
|
raise TypeError("Selected object is not a mesh")
|
|
|
|
mesh = obj.data
|
|
mesh.calc_loop_triangles()
|
|
|
|
# Prepare UV layers
|
|
uv_layer = mesh.uv_layers.active.data
|
|
|
|
lightmap_uv_data = None
|
|
if export_luv:
|
|
lightmap_layer = mesh.uv_layers.get("LightmapUV")
|
|
lightmap_uv_data = lightmap_layer.data if lightmap_layer else None
|
|
|
|
export_bones = armature is not None
|
|
keep_bone_names = None
|
|
if export_bones:
|
|
keep_bone_names = set(bone.name for bone in get_armature_keep_bones(armature))
|
|
|
|
# Vertex deduplication map and list
|
|
vertex_map = {} # (position, normal, uv, lightmap_uv) -> vertex_index
|
|
unique_vertices = [] # list of strings (v ...)
|
|
|
|
# faces = [] # list of (v1, v2, v3) indices
|
|
material_faces = {} # material_index -> list of face indices
|
|
|
|
bone_map = {}
|
|
unique_bones = []
|
|
|
|
def get_bone_index(bone_name):
|
|
if bone_name not in bone_map:
|
|
bone_map[bone_name] = len(unique_bones)
|
|
unique_bones.append(bone_name)
|
|
return bone_map[bone_name]
|
|
|
|
for tri in mesh.loop_triangles:
|
|
face_indices = []
|
|
|
|
material_index = tri.material_index
|
|
|
|
# Get the material from the object's material slots
|
|
if material_index < len(obj.material_slots):
|
|
material = obj.material_slots[material_index].material
|
|
material_name = material.name if material else "Unknown"
|
|
else:
|
|
material_name = "Unknown"
|
|
|
|
if material_name not in material_faces:
|
|
material_faces[material_name] = []
|
|
|
|
faces = material_faces[material_name]
|
|
|
|
for loop_index in tri.loops:
|
|
loop = mesh.loops[loop_index]
|
|
vertex = mesh.vertices[loop.vertex_index]
|
|
|
|
pos = tuple(round(c, 6) for c in vertex.co)
|
|
normal = tuple(round(n, 6) for n in loop.normal)
|
|
uv = tuple(round(c, 6) for c in uv_layer[loop_index].uv)
|
|
|
|
if lightmap_uv_data:
|
|
luv = tuple(round(c, 6) for c in lightmap_uv_data[loop_index].uv)
|
|
else:
|
|
luv = (0.0, 0.0)
|
|
|
|
if export_bones:
|
|
deform_bones = []
|
|
|
|
for vert_group in vertex.groups:
|
|
weight = round(vert_group.weight, 3)
|
|
|
|
if weight < 0.001:
|
|
continue
|
|
|
|
group = obj.vertex_groups[vert_group.group]
|
|
|
|
if group.name not in keep_bone_names:
|
|
continue
|
|
|
|
deform_bones.append((group.name, weight))
|
|
|
|
deform_bones.sort(key=lambda x: x[1], reverse=True)
|
|
deform_bones = deform_bones[:4]
|
|
weight_sum = sum(w for _, w in deform_bones)
|
|
|
|
bones_data = tuple((get_bone_index(name), w / weight_sum) for name, w in deform_bones)
|
|
|
|
else:
|
|
bones_data = tuple()
|
|
|
|
key = (pos, normal, uv, luv, bones_data)
|
|
|
|
if key in vertex_map:
|
|
index = vertex_map[key]
|
|
else:
|
|
index = len(unique_vertices)
|
|
vertex_map[key] = index
|
|
|
|
# Create vertex line
|
|
vertex_line_comps = [
|
|
" ".join(f"{c:.3f}" for c in pos),
|
|
" ".join(f"{n:.3f}" for n in normal),
|
|
" ".join(f"{c:.3f}" for c in uv)
|
|
]
|
|
|
|
if export_luv:
|
|
vertex_line_comps.append(" ".join(f"{c:.3f}" for c in luv))
|
|
|
|
if export_bones:
|
|
vertex_line_comps.append(str(len(bones_data)))
|
|
vertex_line_comps.append(" ".join(f"{b[0]} {b[1]:.3f}" for b in bones_data))
|
|
|
|
vertex_line = "v " + " ".join(vertex_line_comps)
|
|
unique_vertices.append(vertex_line)
|
|
|
|
face_indices.append(index)
|
|
|
|
if len(face_indices) == 3:
|
|
faces.append(face_indices)
|
|
|
|
# Write to file
|
|
with open(output_path, 'w') as f:
|
|
if export_luv:
|
|
f.write("luv\n")
|
|
|
|
if export_bones:
|
|
f.write(f"skeleton {armature.name}\n")
|
|
|
|
for bone in unique_bones:
|
|
f.write(f"d {bone}\n")
|
|
|
|
f.write(f"# v <x y z> <nx ny nz> <u v>{' <lu lv>' if export_luv else ''}{' <num_bones> [<idx w>...]' if export_bones else ''}\n")
|
|
|
|
for v in unique_vertices:
|
|
f.write(v + "\n")
|
|
|
|
for material_name, faces in material_faces.items():
|
|
f.write(f"m {material_name}\n")
|
|
for face in faces:
|
|
f.write("f {} {} {}\n".format(*face))
|
|
|
|
print(f"Exported {obj.name} to: {output_path}")
|
|
|
|
# Output file path
|
|
# output_path = os.path.join(os.path.expanduser("~"), "Desktop", "exported_model.txt")
|
|
|
|
# obj = bpy.context.active_object
|
|
|
|
def rad_to_deg(radians):
|
|
return radians * (180.0 / 3.141592653589793)
|
|
|
|
def rotation_str(rotation):
|
|
return f"{rad_to_deg(rotation.x):.0f} {rad_to_deg(rotation.y):.0f} {rad_to_deg(rotation.z):.0f}"
|
|
|
|
def rotation_str2(rotation):
|
|
return f"{rad_to_deg(rotation.x):.3f} {rad_to_deg(rotation.y):.3f} {rad_to_deg(rotation.z):.3f}"
|
|
|
|
def position_str(position):
|
|
return f"{position.x:.4f} {position.y:.4f} {position.z:.4f}"
|
|
|
|
def scale_str(scale):
|
|
return f"{scale.x:.4f}"
|
|
|
|
def matrix_decompose_str(matrix):
|
|
translation, rotation, scale = matrix.decompose()
|
|
return f"{position_str(translation)} {rotation_str2(rotation.to_euler())} {scale_str(scale)}"
|
|
|
|
def get_path(name, ext):
|
|
return os.path.join(out_path, f"{name}.{ext}")
|
|
|
|
def export_armature(armature: bpy.types.Armature, output_path: str):
|
|
keep_bones = get_armature_keep_bones(armature)
|
|
|
|
# Export armature data (bones, etc.)
|
|
with open(output_path, 'w') as f:
|
|
f.write("# b <name> <parent> <x y z> <yaw pitch roll> <scale>\n")
|
|
|
|
for bone in armature.data.bones:
|
|
if not bone in keep_bones:
|
|
continue
|
|
|
|
parent = bone.parent
|
|
parent_name = parent.name if parent else "NONE"
|
|
bind_matrix = bone.matrix_local
|
|
f.write(f"b {bone.name} {parent_name} {matrix_decompose_str(bind_matrix)}\n")
|
|
|
|
anims = armature_anims.get(armature.name, [])
|
|
for (anim_name, anim_file_name) in anims:
|
|
f.write(f"anim {anim_name} {anim_file_name}\n")
|
|
|
|
print(f"Exported Armature: {armature.name} to {output_path}")
|
|
|
|
def vectors_similar(v1, v2, threshold):
|
|
return (v1 - v2).length < threshold
|
|
|
|
def frames_similar(f1, f2, threshold=0.00001):
|
|
_, t1, r1, s1, _ = f1
|
|
_, t2, r2, s2, _ = f2
|
|
|
|
if not vectors_similar(t1, t2, threshold):
|
|
return False
|
|
|
|
if r1.x - r2.x > threshold or r1.y - r2.y > threshold or r1.z - r2.z > threshold:
|
|
return False
|
|
|
|
if not vectors_similar(s1, s2, threshold):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def export_animation(action: bpy.types.Action, armature: bpy.types.Armature, output_path: str):
|
|
original_action = armature.animation_data.action
|
|
original_frame = bpy.context.scene.frame_current
|
|
|
|
armature.animation_data.action = action
|
|
|
|
keep_bones = get_armature_keep_bones(armature)
|
|
|
|
bone_frames = {bone.name: [] for bone in keep_bones}
|
|
|
|
_, end = map(int, action.frame_range)
|
|
fps = bpy.context.scene.render.fps
|
|
|
|
for frame in range(0, end):
|
|
bpy.context.scene.frame_set(frame)
|
|
bpy.context.view_layer.update()
|
|
|
|
for bone in keep_bones:
|
|
pose_bone = armature.pose.bones.get(bone.name)
|
|
|
|
if not pose_bone:
|
|
continue
|
|
|
|
matrix = (pose_bone.parent.matrix.inverted() @ pose_bone.matrix) if pose_bone.parent else pose_bone.matrix.copy()
|
|
|
|
translation, rotation, scale = matrix.decompose()
|
|
rotation_euler = rotation.to_euler()
|
|
current_frame = (frame, translation, rotation_euler, scale, matrix)
|
|
|
|
frame_list = bone_frames[bone.name]
|
|
|
|
if len(frame_list) > 0:
|
|
last_frame = frame_list[-1]
|
|
|
|
if frames_similar(last_frame, current_frame):
|
|
continue
|
|
|
|
frame_list.append(current_frame)
|
|
|
|
with open(output_path, 'w') as f:
|
|
f.write(f"frames {end}\n")
|
|
f.write(f"fps {fps}\n")
|
|
|
|
for bone_name, frames in bone_frames.items():
|
|
f.write(f"ch {bone_name}\n")
|
|
|
|
for frame in frames:
|
|
idx, _, _, _, matrix = frame
|
|
f.write(f"f {idx} {matrix_decompose_str(matrix)}\n")
|
|
|
|
print(f"Exported Animation: {action.name} to {output_path}")
|
|
|
|
bpy.context.scene.frame_set(original_frame)
|
|
armature.animation_data.action = original_action
|
|
|
|
print("Exporting rooms...")
|
|
|
|
for collection in bpy.context.scene.collection.children:
|
|
match = re.search(r"Room.(\w+)", collection.name)
|
|
if match:
|
|
room_id = match.group(1)
|
|
print(f"Found Room: {room_id}")
|
|
|
|
room_name = f"room_{room_id}"
|
|
roomnames.append(room_name)
|
|
info_path = os.path.join(blend_dir, "pgout", f"{room_name}.info")
|
|
os.makedirs(os.path.dirname(info_path), exist_ok=True)
|
|
|
|
with open(info_path, 'w') as info_file:
|
|
for obj in collection.objects:
|
|
if obj.type == 'MESH' and obj.name == collection.name:
|
|
export_mesh(obj, get_path(room_name, "mesh"), export_luv=True)
|
|
|
|
portal_match = re.search(r"Portal.(\w+).(\w+)", obj.name)
|
|
if portal_match:
|
|
portal_type = portal_match.group(1)
|
|
portal_id = portal_match.group(2)
|
|
|
|
#extract position, rotation and scale from the portal object
|
|
position = obj.location
|
|
rotation = obj.rotation_euler
|
|
scale = obj.scale
|
|
|
|
info_file.write(f"portal {portal_type} {portal_id} {position_str(position)} {rotation_str(rotation)} {scale_str(scale)}\n")
|
|
|
|
print("Exporting meshes...")
|
|
|
|
for object in bpy.data.objects:
|
|
match = re.search(r"Mesh.(\w+)", object.name)
|
|
if match:
|
|
mesh_name = match.group(1)
|
|
print(f"Found Non-Room Mesh: {mesh_name}")
|
|
|
|
armature = None
|
|
|
|
parent = object.parent
|
|
if parent and parent.type == 'ARMATURE':
|
|
armatures[parent.name] = parent
|
|
print(f" Is skeletal, Parent Armature: {parent.name}")
|
|
armature = parent
|
|
|
|
export_mesh(object, get_path(mesh_name, "mesh"), armature=armature)
|
|
|
|
|
|
armature_names = [name for name, _ in armatures.items()]
|
|
print("Armatures will be exported: ", armature_names)
|
|
|
|
actions = {}
|
|
armature_anims = {name: [] for name in armature_names}
|
|
|
|
for action in bpy.data.actions:
|
|
match = re.search(r"Anim.(\w+).(\w+)", action.name)
|
|
if match:
|
|
action_armature = match.group(1)
|
|
action_name = match.group(2)
|
|
|
|
if action_armature in armatures:
|
|
armature = armatures[action_armature]
|
|
action_file_name = f"{armature.name}_{action_name}"
|
|
actions[action.name] = (action, action_file_name, armature)
|
|
armature_anims[action_armature].append((action_name, action_file_name))
|
|
|
|
action_names = [f"{data[1]} ({data[2].name})" for name, data in actions.items()]
|
|
print("Actions will be exported: ", action_names)
|
|
|
|
for armature_name, armature in armatures.items():
|
|
export_armature(armature, get_path(armature_name, "skel"))
|
|
|
|
for action_name, (action, action_file_name, armature) in actions.items():
|
|
export_animation(action, armature, get_path(action_file_name, "anim"))
|
|
|
|
with open(get_path("rooms", "list"), 'w') as rooms_file:
|
|
for room_name in roomnames:
|
|
rooms_file.write(f"{room_name}\n")
|
|
|