Created
August 14, 2023 13:41
-
-
Save miuru0/1a2dedd9f6d21d4635f0fd7f803bccd5 to your computer and use it in GitHub Desktop.
Roblox mesh version 4.0.0 blender import script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import bpy | |
import struct | |
from mathutils import Vector | |
def readu32(inf): | |
return struct.unpack("<I", inf.read(4))[0] | |
def readu16(inf): | |
return struct.unpack("<H", inf.read(2))[0] | |
def read32(inf): | |
return struct.unpack("<i", inf.read(4))[0] | |
def read16(inf): | |
return struct.unpack("<h", inf.read(2))[0] | |
def readu8(inf): | |
return struct.unpack("<B", inf.read(1))[0] | |
def read8(inf): | |
return struct.unpack("<b", inf.read(1))[0] | |
def readf(inf): | |
return struct.unpack("<f", inf.read(4))[0] | |
class Header: | |
def __init__(self, f): | |
f.read(13) | |
self.header_size = readu16(f) | |
self.lod_type = readu16(f) | |
self.num_verts = readu32(f) | |
self.num_faces = readu32(f) | |
self.num_lods = readu16(f) | |
self.num_bones = readu16(f) | |
self.bone_name_buffer_size = readu32(f) | |
self.num_subsets = readu16(f) | |
self.num_hq_lods = readu8(f) | |
self.unused = readu8(f) | |
class Vertex: | |
def __init__(self, f): | |
self.px = readf(f) | |
self.py = readf(f) | |
self.pz = readf(f) | |
self.nx = readf(f) | |
self.ny = readf(f) | |
self.nz = readf(f) | |
self.tu = readf(f) | |
self.tv = readf(f) | |
self.tx = read8(f) | |
self.ty = read8(f) | |
self.tz = read8(f) | |
self.ts = read8(f) | |
self.r = readu8(f) | |
self.g = readu8(f) | |
self.b = readu8(f) | |
self.a = readu8(f) | |
class Envelope: | |
def __init__(self, f): | |
self.bones = [] | |
for i in range(4): | |
self.bones.append(readu8(f)) | |
self.weights = [] | |
for i in range(4): | |
self.weights.append(readu8(f)) | |
class Face: | |
def __init__(self, f): | |
self.a = readu32(f) | |
self.b = readu32(f) | |
self.c = readu32(f) | |
class Bone: | |
def __init__(self, f): | |
self.bone_name_index = readu32(f) | |
self.parent_index = readu16(f) | |
self.lod_parent_index = readu16(f) | |
self.culling = readf(f) | |
self.r00 = readf(f) | |
self.r01 = readf(f) | |
self.r02 = readf(f) | |
self.r10 = readf(f) | |
self.r11 = readf(f) | |
self.r12 = readf(f) | |
self.r20 = readf(f) | |
self.r21 = readf(f) | |
self.r22 = readf(f) | |
self.x = readf(f) | |
self.y = readf(f) | |
self.z = readf(f) | |
class MeshSubset: | |
def __init__(self, f): | |
self.faces_begin = readu32(f) | |
self.faces_length = readu32(f) | |
self.verts_begin = readu32(f) | |
self.verts_length = readu32(f) | |
self.num_bone_indices = readu32(f) | |
self.bone_indices = [] | |
for i in range(26): | |
self.bone_indices.append(readu16(f)) | |
class Mesh: | |
def __init__(self, header, verts, envelopes, faces, lods, bones, name_table, subsets): | |
self.header = header | |
self.verts = verts | |
self.envelopes = envelopes | |
self.faces = faces | |
self.lods = lods | |
self.bones = bones | |
self.name_table = name_table | |
self.subsets = subsets | |
def read_name_table(table, index): | |
tab = table[index:] | |
final_index = tab.index(0) | |
finall = bytes(bytearray(tab[:final_index])).decode('utf-8') | |
return finall | |
def load_mesh(f): | |
header = Header(f) | |
vertices = [] | |
envelopes = None | |
faces = [] | |
lods = [] | |
bones = [] | |
raw_name_table = [] | |
subsets = [] | |
for i in range(header.num_verts): | |
vertices.append(Vertex(f)) | |
if header.num_bones > 0: | |
envelopes = [] | |
for i in range(header.num_verts): | |
envelopes.append(Envelope(f)) | |
for i in range(header.num_faces): | |
faces.append(Face(f)) | |
for i in range(header.num_lods): | |
lods.append(readu32(f)) | |
for i in range(header.num_bones): | |
bones.append(Bone(f)) | |
for i in range(header.bone_name_buffer_size): | |
raw_name_table.append(readu8(f)) | |
for i in range(header.num_subsets): | |
subsets.append(MeshSubset(f)) | |
return Mesh(header, vertices, envelopes, faces, lods, bones, raw_name_table, subsets) | |
def get_root_bones(bone_list): | |
roots = [] | |
lff = 0 | |
for bone in bone_list: | |
if bone.parent_index == 0xFFFF: | |
roots.append(lff) | |
lff += 1 | |
return roots | |
def get_descendants(bone_list, parent_bone): | |
descendants = [] | |
i = 0 | |
for bone in bone_list: | |
if bone.parent_index == parent_bone: | |
descendants.append(i) | |
i += 1 | |
return descendants | |
def set_ebs(bones, bone_names, current_bone, armature): | |
for bone in get_descendants(bones, current_bone): | |
if current_bone == 0xFFFF: # If root | |
print(armature.edit_bones) | |
print(bones) | |
armature.edit_bones[-1].name = bone_names[bone] | |
else: | |
selected = get_selected(armature)[0] | |
bp = bpy.ops.armature.bone_primitive_add(name=bone_names[bone]) | |
armature.edit_bones[-1].head = Vector((bones[bone].x, bones[bone].z, bones[bone].y)) | |
armature.edit_bones[-1].tail = Vector((bones[bone].x, bones[bone].z, bones[bone].y + 1)) | |
armature.edit_bones[-1].parent = selected | |
set_ebs(bones, bone_names, bone, armature) | |
select_parent(armature) | |
if current_bone != 0xFFFF: | |
return Vector((bones[current_bone].x, bones[current_bone].z, bones[current_bone].y)) | |
def select_parent(armature): | |
current_selection = get_selected(armature) | |
if len(current_selection) > 0: | |
if current_selection[0].parent: | |
bpy.ops.armature.select_all(action='DESELECT') | |
print("got") | |
current_selection[0].parent.select = True | |
current_selection[0].parent.select_head = True | |
current_selection[0].parent.select_tail = True | |
bpy.context.view_layer.update() | |
def get_selected(armature): | |
selected_bones = [bone for bone in armature.edit_bones if bone.select or bone.select_head or bone.select_tail] | |
return selected_bones | |
def read_some_data(context, filepath, use_some_setting): | |
print("running read_some_data...") | |
f = open(filepath, 'rb') | |
mesh = load_mesh(f) | |
f.close() | |
numer = 0 | |
verts = [] | |
for i in mesh.verts: | |
verts.append((i.px, i.pz, i.py)) | |
# Add Every SubMesh As different model | |
for subset in mesh.subsets: | |
print(subset.bone_indices) | |
faces = [] | |
uv = [] | |
for i in range(subset.faces_begin, subset.faces_begin + subset.faces_length): | |
faces.append((mesh.faces[i].a, mesh.faces[i].b, mesh.faces[i].c)) | |
face = mesh.faces[i] | |
uv += [ | |
mesh.verts[face.a].tu, mesh.verts[face.a].tv * -1 + 1, | |
mesh.verts[face.b].tu, mesh.verts[face.b].tv * -1 + 1, | |
mesh.verts[face.c].tu, mesh.verts[face.c].tv * -1 + 1 | |
] | |
mesh_data = bpy.data.meshes.new("cube_mesh_data" + str(numer)) | |
mesh_data.from_pydata(verts, [], faces) | |
mesh_data.update() | |
uv_layer = mesh_data.uv_layers.new(name="UVMap") | |
uv_layer.data.foreach_set("uv", uv) | |
obj = bpy.data.objects.new("My_Object" + str(numer), mesh_data) | |
scene = bpy.context.scene | |
scene.collection.objects.link(obj) | |
obj.select_set(True) | |
bpy.context.view_layer.objects.active = obj # Set object as active | |
# Bones preperation | |
bones_vertex = [] | |
bone_names = [] | |
for bone in mesh.bones: | |
bone_name = read_name_table(mesh.name_table, bone.bone_name_index) | |
bone_names.append(bone_name) | |
group = obj.vertex_groups.new(name=bone_name) | |
bones_vertex.append(group) | |
print(bone.parent_index) | |
# Bone Weights | |
for vert_i in range(subset.verts_begin, subset.verts_length): | |
envelope = mesh.envelopes[vert_i] | |
for i in range(4): | |
bone_vertex = bones_vertex[subset.bone_indices[envelope.bones[i]]] | |
vertex_weight = envelope.weights[i] / 255.0 | |
bone_vertex.add([vert_i], vertex_weight, 'ADD') | |
numer += 1 | |
# Armature Bones | |
loc = mesh.bones[get_root_bones(mesh.bones)[0]] | |
bpy.ops.object.armature_add(enter_editmode=True, align='WORLD', | |
location=(0, 0, 0), scale=(1, 1, 1)) | |
armature = bpy.data.armatures[-1] | |
armature.edit_bones[-1].head = Vector((loc.x, loc.z, loc.y)) | |
armature.edit_bones[-1].tail = Vector((loc.x, loc.z, loc.y + 1)) | |
set_ebs(mesh.bones, bone_names, 0xFFFF, armature) | |
bpy.ops.object.mode_set(mode='OBJECT') | |
obj.select_set(True) | |
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] | |
bpy.ops.object.parent_set(type='ARMATURE') | |
print(bpy.context.selected_objects) | |
# would normally load the data here | |
return {'FINISHED'} | |
# ImportHelper is a helper class, defines filename and | |
# invoke() function which calls the file selector. | |
from bpy_extras.io_utils import ImportHelper | |
from bpy.props import StringProperty, BoolProperty, EnumProperty | |
from bpy.types import Operator | |
class ImportSomeData(Operator, ImportHelper): | |
"""This appears in the tooltip of the operator and in the generated docs""" | |
bl_idname = "import_test.some_data" # important since its how bpy.ops.import_test.some_data is constructed | |
bl_label = "Import Some Data" | |
# ImportHelper mixin class uses this | |
filename_ext = ".txt" | |
filter_glob: StringProperty( | |
default="*.txt", | |
options={'HIDDEN'}, | |
maxlen=255, # Max internal buffer length, longer would be clamped. | |
) | |
# List of operator properties, the attributes will be assigned | |
# to the class instance from the operator settings before calling. | |
use_setting: BoolProperty( | |
name="Example Boolean", | |
description="Example Tooltip", | |
default=True, | |
) | |
type: EnumProperty( | |
name="Example Enum", | |
description="Choose between two items", | |
items=( | |
('OPT_A', "First Option", "Description one"), | |
('OPT_B', "Second Option", "Description two"), | |
), | |
default='OPT_A', | |
) | |
def execute(self, context): | |
return read_some_data(context, self.filepath, self.use_setting) | |
# Only needed if you want to add into a dynamic menu. | |
def menu_func_import(self, context): | |
self.layout.operator(ImportSomeData.bl_idname, text="Text Import Operator") | |
# Register and add to the "file selector" menu (required to use F3 search "Text Import Operator" for quick access). | |
def register(): | |
bpy.utils.register_class(ImportSomeData) | |
bpy.types.TOPBAR_MT_file_import.append(menu_func_import) | |
def unregister(): | |
bpy.utils.unregister_class(ImportSomeData) | |
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) | |
if __name__ == "__main__": | |
register() | |
# test call | |
bpy.ops.import_test.some_data('INVOKE_DEFAULT') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment