-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from AlphaSatanOmega/main
0.3.0 - Importer/Exporter merge
- Loading branch information
Showing
4 changed files
with
735 additions
and
622 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,268 +1,67 @@ | ||
bl_info = { | ||
"name": "Granblue Fantasy Relink Mesh Importer", | ||
"author": "WistfulHopes", | ||
"version": (1, 0, 0), | ||
"blender": (3, 0, 0), | ||
"location": "File > Import-Export", | ||
"description": "A script to import meshes from Granblue Fantasy Relink", | ||
"name": "Granblue Fantasy Relink Blender Tools", | ||
"author": "WistfulHopes & AlphaSatanOmega", | ||
"version": (0, 3, 0), | ||
"blender": (3, 5, 0), | ||
"location": "File > Import/Export", | ||
"description": "Tool to import & export models from Granblue Fantasy Relink", | ||
"warning": "", | ||
"category": "Import-Export", | ||
} | ||
|
||
from .Entities.ModelInfo import ModelInfo | ||
from .Entities.ModelSkeleton import ModelSkeleton | ||
# Reloads the addons on script reload | ||
# Good for editing script | ||
if "bpy" in locals(): | ||
import importlib | ||
if "gbfr_import" in locals(): | ||
importlib.reload(gbfr_import) | ||
if "gbfr_export" in locals(): | ||
importlib.reload(gbfr_export) | ||
|
||
import bpy | ||
import bmesh | ||
import mathutils | ||
import struct | ||
import os | ||
|
||
def utils_set_mode(mode): | ||
if bpy.ops.object.mode_set.poll(): | ||
bpy.ops.object.mode_set(mode=mode, toggle=False) | ||
|
||
|
||
def parse_skeleton(filepath, CurCollection): | ||
if os.path.isfile(os.path.splitext(filepath)[0] + ".skeleton"): | ||
buf = open(os.path.splitext(filepath)[0] + ".skeleton", 'rb').read() | ||
buf = bytearray(buf) | ||
skeleton = ModelSkeleton.GetRootAs(buf, 0) | ||
|
||
armature_data = bpy.data.armatures.new("Armature") | ||
armature_obj = bpy.data.objects.new("Armature", armature_data) | ||
CurCollection.objects.link(armature_obj) | ||
bpy.context.view_layer.objects.active = armature_obj | ||
bpy.ops.object.mode_set(mode='EDIT', toggle=False) | ||
|
||
SkelTable = [] | ||
for n in range(skeleton.BodyLength()): | ||
bone = skeleton.Body(n) | ||
pos = (bone.Position().X(), bone.Position().Y(), bone.Position().Z()) | ||
quat = (bone.Quat().W(), bone.Quat().X(), bone.Quat().Y(), bone.Quat().Z()) | ||
parent_index = bone.ParentId() | ||
|
||
SkelTable.append({"Pos":pos,"Rot":quat}) | ||
|
||
name = bone.Name().decode('ascii') | ||
|
||
edit_bone = armature_obj.data.edit_bones.new(name) | ||
edit_bone.use_connect = False | ||
edit_bone.use_inherit_rotation = True | ||
edit_bone.inherit_scale = 'FULL' | ||
edit_bone.use_local_location = True | ||
edit_bone.head = (0,0,0) | ||
edit_bone.tail = (0,0.05,0) | ||
|
||
if parent_index != 65535: | ||
edit_bone.parent = armature_obj.data.edit_bones[parent_index] | ||
|
||
utils_set_mode('POSE') | ||
for x in range(skeleton.BodyLength()): | ||
pbone = armature_obj.pose.bones[x] | ||
pbone.rotation_mode = 'QUATERNION' | ||
pbone.rotation_quaternion = SkelTable[x]["Rot"] | ||
pbone.location = SkelTable[x]["Pos"] | ||
bpy.ops.pose.armature_apply() | ||
utils_set_mode('OBJECT') | ||
|
||
bpy.ops.object.mode_set(mode='OBJECT') | ||
|
||
return armature_obj | ||
|
||
|
||
|
||
def parse_mesh_info(filepath): | ||
buf = open(filepath, 'rb').read() | ||
buf = bytearray(buf) | ||
model_info = ModelInfo.GetRootAs(buf, 0) | ||
|
||
return model_info | ||
|
||
|
||
def read_some_data(context, filepath): | ||
CurCollection = bpy.data.collections.new("Mesh Collection") | ||
bpy.context.scene.collection.children.link(CurCollection) | ||
|
||
mesh_info = parse_mesh_info(filepath) | ||
armature = parse_skeleton(filepath, CurCollection) | ||
|
||
DeformJointsTable = [] | ||
for n in range(mesh_info.BonesToWeightIndicesLength()): | ||
DeformJointsTable.append(mesh_info.BonesToWeightIndices(n)) | ||
|
||
LOD = mesh_info.Lodinfos(0) | ||
|
||
f = open(os.path.splitext(filepath)[0] + ".mmesh", 'rb') | ||
|
||
vert_count = LOD.VertCount() | ||
face_count = LOD.PolyCountX3() // 3 | ||
|
||
VertTable = [] | ||
NormalTable = [] | ||
TangentTable = [] | ||
UVTable = [] | ||
WeightIndicesTable = [] | ||
WeightTable = [] | ||
FaceTable = [] | ||
|
||
for n in range(vert_count): | ||
VertTable.append(struct.unpack('<fff', f.read(4*3))) | ||
normal = struct.unpack('<eee', f.read(2*3)) | ||
normal = (-normal[0], -normal[1], -normal[2]) | ||
NormalTable.append(normal) | ||
f.seek(2,1) | ||
TangentTable.append(struct.unpack('<eee', f.read(2*3))) | ||
f.seek(2,1) | ||
UVTable.append(struct.unpack('<ee', f.read(2*2))) | ||
|
||
if armature is not None: | ||
if LOD.BufferTypes() & 2: | ||
f.seek(LOD.MeshBuffers(1).Offset()) | ||
for n in range(vert_count): | ||
i0 = int.from_bytes(f.read(2),byteorder='little') | ||
i1 = int.from_bytes(f.read(2),byteorder='little') | ||
i2 = int.from_bytes(f.read(2),byteorder='little') | ||
i3 = int.from_bytes(f.read(2),byteorder='little') | ||
|
||
weight_indices = [DeformJointsTable[i0],DeformJointsTable[i1],DeformJointsTable[i2],DeformJointsTable[i3]] | ||
WeightIndicesTable.append(weight_indices) | ||
|
||
if LOD.BufferTypes() & 8: | ||
if LOD.BufferTypes() & 4: | ||
f.seek(LOD.MeshBuffers(3).Offset()) | ||
else: | ||
f.seek(LOD.MeshBuffers(2).Offset()) | ||
for n in range(vert_count): | ||
WeightTable.append(struct.unpack('<HHHH', f.read(2*4))) | ||
|
||
f.seek(LOD.MeshBuffers(LOD.MeshBuffersLength() - 1).Offset()) | ||
for n in range(face_count): | ||
FaceTable.append(struct.unpack('<III', f.read(4*3))) | ||
|
||
f.close() | ||
del f | ||
|
||
mesh1 = bpy.data.meshes.new("Mesh") | ||
mesh1.use_auto_smooth = True | ||
obj = bpy.data.objects.new("Obj",mesh1) | ||
CurCollection.objects.link(obj) | ||
bpy.context.view_layer.objects.active = obj | ||
obj.select_set(True) | ||
mesh = bpy.context.object.data | ||
bm = bmesh.new() | ||
for v in VertTable: | ||
bm.verts.new((v[0],v[1],v[2])) | ||
list = [v for v in bm.verts] | ||
for f in FaceTable: | ||
try: | ||
bm.faces.new((list[f[0]],list[f[1]],list[f[2]])) | ||
except: | ||
pass | ||
bm.to_mesh(mesh) | ||
|
||
uv_layer = bm.loops.layers.uv.verify() | ||
Normals = [] | ||
for f in bm.faces: | ||
f.smooth=True | ||
for l in f.loops: | ||
if NormalTable != []: | ||
Normals.append(NormalTable[l.vert.index]) | ||
luv = l[uv_layer] | ||
try: | ||
luv.uv = UVTable[l.vert.index] | ||
except: | ||
continue | ||
bm.to_mesh(mesh) | ||
|
||
if NormalTable != []: | ||
mesh1.normals_split_custom_set(Normals) | ||
|
||
if armature is not None: | ||
for v in range(vert_count): | ||
for n in range(4): | ||
group_name = armature.data.bones[WeightIndicesTable[v][n]].name | ||
if obj.vertex_groups.find(group_name) == -1: | ||
TempVG = obj.vertex_groups.new(name = group_name) | ||
else: | ||
TempVG = obj.vertex_groups[obj.vertex_groups.find(group_name)] | ||
|
||
TempVG.add([v], float(WeightTable[v][n]) / 65535, 'ADD') | ||
|
||
mat_counter = 0 | ||
|
||
for i in range(mesh_info.SubMeshesLength()): | ||
sub_mesh = mesh_info.SubMeshes(i) | ||
for j in range(LOD.ChunksLength()): | ||
chunk = LOD.Chunks(j) | ||
if chunk.SubMesh() != i: | ||
continue | ||
mat = bpy.data.materials.new(name=sub_mesh.Name().decode() + "." + str(chunk.Material())) | ||
obj.data.materials.append(mat) | ||
|
||
for p in range(chunk.Offset() // 3, chunk.Offset() // 3 + chunk.Count() // 3): | ||
obj.data.polygons[p].material_index = mat_counter | ||
|
||
mat_counter += 1 | ||
|
||
if armature is not None: | ||
ArmMod = obj.modifiers.new("Armature","ARMATURE") | ||
ArmMod.object = armature | ||
obj.parent = armature | ||
armature.rotation_euler = (1.5707963705062866,0,0) | ||
|
||
obj.select_set(True) | ||
bpy.ops.object.mode_set(mode='EDIT', toggle=False) | ||
bpy.ops.mesh.select_all(action='SELECT') | ||
bpy.ops.mesh.flip_normals() | ||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False) | ||
|
||
return {'FINISHED'} | ||
|
||
from . import gbfr_import, gbfr_export | ||
# from .Entities.ModelInfo import ModelInfo | ||
# from .Entities.ModelSkeleton import ModelSkeleton | ||
|
||
# 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): | ||
"""Importer for Granblue Fantasy Relink meshes""" | ||
bl_idname = "gbfr.mesh" # important since its how bpy.ops.import_test.some_data is constructed | ||
bl_label = "Import" | ||
|
||
# ImportHelper mix-in class uses this. | ||
filename_ext = ".minfo" | ||
|
||
filter_glob: StringProperty( | ||
default="*.minfo", | ||
options={'HIDDEN'}, | ||
maxlen=255, # Max internal buffer length, longer would be clamped. | ||
# Addon preferences, where users will specify flatc.exe path | ||
class AddonPreferences(bpy.types.AddonPreferences): | ||
bl_idname = __name__ | ||
|
||
# Define a custom property for storing the flatc file path | ||
flatc_file_path: StringProperty( | ||
name="flatc.exe filepath", | ||
description="File path to flatc.exe be used for export.", | ||
subtype='FILE_PATH', | ||
) | ||
|
||
def execute(self, context): | ||
return read_some_data(context, self.filepath) | ||
|
||
|
||
# Only needed if you want to add into a dynamic menu. | ||
def menu_func_import(self, context): | ||
self.layout.operator(ImportSomeData.bl_idname, text="Granblue Fantasy Relink .minfo") | ||
|
||
def draw(self, context): | ||
layout = self.layout | ||
layout.prop(self, "flatc_file_path") | ||
|
||
|
||
# Register and add to the "file selector" menu (required to use F3 search "Text Import Operator" for quick access). | ||
# Register importer & exporter | ||
def register(): | ||
bpy.utils.register_class(ImportSomeData) | ||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import) | ||
|
||
gbfr_import.register() | ||
gbfr_export.register() | ||
bpy.utils.register_class(AddonPreferences) | ||
|
||
def unregister(): | ||
bpy.utils.unregister_class(ImportSomeData) | ||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) | ||
|
||
gbfr_import.unregister() | ||
gbfr_export.unregister() | ||
bpy.utils.unregister_class(AddonPreferences) | ||
|
||
#Run the addon | ||
if __name__ == "__main__": | ||
register() | ||
|
||
# test call | ||
bpy.ops.gbfr.mesh('INVOKE_DEFAULT') | ||
|
||
# bpy.ops.gbfr.mesh('INVOKE_DEFAULT') |
Oops, something went wrong.