Skip to content

Instantly share code, notes, and snippets.

@miuru0
Created August 14, 2023 13:41
Show Gist options
  • Save miuru0/1a2dedd9f6d21d4635f0fd7f803bccd5 to your computer and use it in GitHub Desktop.
Save miuru0/1a2dedd9f6d21d4635f0fd7f803bccd5 to your computer and use it in GitHub Desktop.
Roblox mesh version 4.0.0 blender import script
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