diff --git a/io_scene_b3d/B3DParser.py b/B3DParser.py similarity index 100% rename from io_scene_b3d/B3DParser.py rename to B3DParser.py diff --git a/README.md b/README.md index 3a38226..23ca4f0 100644 --- a/README.md +++ b/README.md @@ -2,52 +2,39 @@ Blender Import-Export script for Blitz 3D .b3d files +## Download + +You may download plugin zip in the [releases](https://github.com/joric/io_scene_b3d/releases) section + ## Installation * Userspace method: click "File" - "User Preferences" - "Add-ons" - "Install Add-on from File". The add-on zip file should contain io_scene_b3d directory, including the directory itself. * Alternative method: copy or symlink the io_scene_b3d directory to blender user directory, e.g. to -%APPDATA%\Blender Foundation\Blender\2.79\scripts\addons\io_scene_b3d. -* Search and enable add-on in "User Preferences" - "Add-ons". Click "Save User Settings" afterwards. +%APPDATA%\Blender Foundation\Blender\2.80\scripts\addons\io_scene_b3d. + +Then enable add-on in "User Preferences" - "Add-ons". Click "Save User Settings" afterwards. ## Debugging -* Userspace method: every time you make a change the script has to be reloaded using Reload Scripts command (F8). -* Alternative method: my shortcut, Shift+Ctrl+D in Object Mode. It resets scene, reloads the script and imports test file. -* Somewhat simpler method (Windows only), an autohotkey script I wrote (see the [autohotkey](https://github.com/joric/io_scene_b3d/tree/autohotkey) branch). +* Userspace method: every time you make a change the script has to be reloaded (press F3, search for Reload Scripts). +* Alternative method: my shortcut, Shift+Ctrl+F in Object Mode. It resets scene, reloads the script and imports test file. ## TODO ### Import -* Mind that animation is not yet implemented. Working on it! +* Animation is not yet implemented in version 1.0. Check master branch for updates. * Nodes use original quaternion rotation that affects user interface. Maybe convert them into euler angles. -* Sometimes objects appear joined together in a single mesh (an attempt on hardware instancing, I guess). -I'm splitting objects with multiple meshes into separate objects but I can't effectively -split large pre-baked mashes. Probably solvable with point cloud matching -(considering that objects also can overlap). - -### Export - -* Exported files (script by Diego 'GaNDaLDF' Parisi) sometimes contain animation keys -that go outside the animation. Assimp doesn't import them so I've added an extra frame, just to be safe. -It's better to recalculate the animation using existing keys. -UPDATE: could not reproduce, reverted. Will double check later. ## License -This is all GPL 2.0. Pull requests welcome. +This software is covered by GPL 2.0. Pull requests are welcome. -The import script is a heavily rewriten script from Glogow Poland Mariusz Szkaradek. -I've had to rewrite all the chunk reader stuff and all the import stuff, because Blender API -has heavily changed since then. - -The export script uses portions (copied almost verbatim, just ported to Blender Import-Export format) -from supertuxcart project by Diego 'GaNDaLDF' Parisi. Since it's all GPL-licensed, he shouldn't mind. - -The b3d format documentation (b3dfile_specs.txt) doesn't have a clear license (I assume Public Domain) -but it was hard to find, so I just put it here in the repository as well. +* The import script based on a heavily rewriten (new reader) script from Glogow Poland Mariusz Szkaradek. +* The export script uses portions of script by Diego 'GaNDaLDF' Parisi (ported to Blender 2.8) under GPL license. +* The b3d format documentation (b3dfile_specs.txt) doesn't have a clear license (I assume Public Domain). ## Alternatives diff --git a/io_scene_b3d/__init__.py b/__init__.py similarity index 76% rename from io_scene_b3d/__init__.py rename to __init__.py index 7435db3..51b8b54 100644 --- a/io_scene_b3d/__init__.py +++ b/__init__.py @@ -19,9 +19,9 @@ # bl_info = { - "name": "Blitz 3D format", + "name": "Blitz 3D format (.b3d)", "author": "Joric", - "blender": (2, 74, 0), + "blender": (2, 80, 0), "location": "File > Import-Export", "description": "Import-Export B3D, meshes, uvs, materials, textures, " "cameras & lamps", @@ -49,24 +49,22 @@ from bpy.props import ( from bpy_extras.io_utils import ( ImportHelper, ExportHelper, - orientation_helper_factory, + orientation_helper, axis_conversion, ) -IOB3DOrientationHelper = orientation_helper_factory("IOB3DOrientationHelper", axis_forward='Y', axis_up='Z') - - -class ImportB3D(bpy.types.Operator, ImportHelper, IOB3DOrientationHelper): +@orientation_helper(axis_forward='Y', axis_up='Z') +class ImportB3D(bpy.types.Operator, ImportHelper): """Import from B3D file format (.b3d)""" bl_idname = "import_scene.blitz3d_b3d" bl_label = 'Import B3D' bl_options = {'UNDO'} filename_ext = ".b3d" - filter_glob = StringProperty(default="*.b3d", options={'HIDDEN'}) + filter_glob : StringProperty(default="*.b3d", options={'HIDDEN'}) - constrain_size = FloatProperty( + constrain_size : FloatProperty( name="Size Constraint", description="Scale the model by 10 until it reaches the " "size constraint (0 to disable)", @@ -74,13 +72,13 @@ class ImportB3D(bpy.types.Operator, ImportHelper, IOB3DOrientationHelper): soft_min=0.0, soft_max=1000.0, default=10.0, ) - use_image_search = BoolProperty( + use_image_search : BoolProperty( name="Image Search", description="Search subdirectories for any associated images " "(Warning, may be slow)", default=True, ) - use_apply_transform = BoolProperty( + use_apply_transform : BoolProperty( name="Apply Transform", description="Workaround for object transformations " "importing incorrectly", @@ -103,18 +101,20 @@ class ImportB3D(bpy.types.Operator, ImportHelper, IOB3DOrientationHelper): return import_b3d.load(self, context, **keywords) -class ExportB3D(bpy.types.Operator, ExportHelper, IOB3DOrientationHelper): +@orientation_helper(axis_forward='Y', axis_up='Z') +class ExportB3D(bpy.types.Operator, ExportHelper): """Export to B3D file format (.b3d)""" bl_idname = "export_scene.blitz3d_b3d" bl_label = 'Export B3D' filename_ext = ".b3d" - filter_glob = StringProperty( + + filter_glob: StringProperty( default="*.b3d", options={'HIDDEN'}, ) - use_selection = BoolProperty( + use_selection: BoolProperty( name="Selection Only", description="Export selected objects only", default=False, @@ -147,60 +147,77 @@ def menu_func_import(self, context): class DebugMacro(bpy.types.Operator): bl_idname = "object.debug_macro" - bl_label = "b3d debug" + bl_label = "Debug Macro" bl_options = {'REGISTER', 'UNDO'} from . import import_b3d + from . import export_b3d filepath = bpy.props.StringProperty(name="filepath", default=import_b3d.filepath) - def execute(self, context): + def execute(self, context: bpy.context): import sys,imp + print("b3d, loading", self.filepath) + for material in bpy.data.materials: bpy.data.materials.remove(material) - for obj in bpy.context.screen.scene.objects: - bpy.data.objects.remove(obj, True) + for obj in bpy.context.scene.objects: + bpy.data.objects.remove(obj, do_unlink=True) module = sys.modules['io_scene_b3d'] imp.reload(module) import_b3d.load(self, context, filepath=self.filepath) + export_b3d.save(self, context, filepath=self.filepath.replace('.b3d','.exported.b3d')) + """ bpy.ops.view3d.viewnumpad(type='FRONT', align_active=False) + bpy.ops.view3d.view_all(use_all_regions=True, center=True) if bpy.context.region_data.is_perspective: bpy.ops.view3d.view_persportho() + """ return {'FINISHED'} addon_keymaps = [] -def register(): - bpy.utils.register_module(__name__) +classes = ( + ImportB3D, + ExportB3D, + DebugMacro +) - bpy.types.INFO_MT_file_import.append(menu_func_import) - bpy.types.INFO_MT_file_export.append(menu_func_export) +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) # handle the keymap wm = bpy.context.window_manager - km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY') - kmi = km.keymap_items.new(DebugMacro.bl_idname, 'D', 'PRESS', ctrl=True, shift=True) - addon_keymaps.append((km, kmi)) + if wm.keyconfigs.addon: + km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type='EMPTY') + kmi = km.keymap_items.new(DebugMacro.bl_idname, 'F', 'PRESS', ctrl=True, shift=True) + addon_keymaps.append((km, kmi)) + def unregister(): - bpy.utils.unregister_module(__name__) + for cls in classes: + bpy.utils.unregister_class(cls) - bpy.types.INFO_MT_file_import.remove(menu_func_import) - bpy.types.INFO_MT_file_export.remove(menu_func_export) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) # handle the keymap for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) - addon_keymaps.clear() + del addon_keymaps[:] if __name__ == "__main__": register() diff --git a/io_scene_b3d/export_b3d.py b/export_b3d.py similarity index 95% rename from io_scene_b3d/export_b3d.py rename to export_b3d.py index 788aef0..0408b05 100644 --- a/io_scene_b3d/export_b3d.py +++ b/export_b3d.py @@ -2,20 +2,21 @@ """ Name: 'B3D Exporter (.b3d)...' -Blender: 259 +Blender: 280 Group: 'Export' Tooltip: 'Export to Blitz3D file format (.b3d)' """ -__author__ = ["iego 'GaNDaLDF' Parisi, MTLZ (is06), Joerg Henrichs, Marianne Gagnon"] -__url__ = ["www.gandaldf.com"] -__version__ = "3.0" +__author__ = ["Diego 'GaNDaLDF' Parisi, MTLZ (is06), Joerg Henrichs, Marianne Gagnon, Joric"] +__url__ = ["https://github.com/joric/io_scene_b3d"] +__version__ = "3.2" __bpydoc__ = """\ """ -# BLITZ3D EXPORTER 3.0 +# BLITZ3D EXPORTER 3.2 # Copyright (C) 2009 by Diego "GaNDaLDF" Parisi - www.gandaldf.com # Lightmap issue fixed by Capricorn 76 Pty. Ltd. - www.capricorn76.com # Blender 2.63 compatiblity based on work by MTLZ, www.is06.com +# Blender 2.80 compatibility by Joric # With changes by Marianne Gagnon and Joerg Henrichs, supertuxkart.sf.net (Copyright (C) 2011-2012) # # LICENSE: @@ -36,9 +37,9 @@ __bpydoc__ = """\ bl_info = { "name": "B3D (BLITZ3D) Model Exporter", "description": "Exports a blender scene or object to the B3D (BLITZ3D) format", - "author": "Diego 'GaNDaLDF' Parisi, MTLZ (is06), Joerg Henrichs, Marianne Gagnon", - "version": (3,1), - "blender": (2, 5, 9), + "author": "Diego 'GaNDaLDF' Parisi, MTLZ (is06), Joerg Henrichs, Marianne Gagnon, Joric", + "version": (3,2), + "blender": (2, 8, 0), "api": 31236, "location": "File > Export", "warning": '', # used for warning icon and text in addons panel @@ -179,17 +180,19 @@ def tesselate_if_needed(objdata): def getUVTextures(obj_data): # BMesh in blender 2.63 broke this - if bpy.app.version[1] >= 63: - return tesselate_if_needed(obj_data).tessface_uv_textures - else: - return obj_data.uv_textures + #if bpy.app.version[1] >= 63: + # return tesselate_if_needed(obj_data).tessface_uv_textures #2.8 breaks this + #else: + # return obj_data.uv_textures + return obj_data.uv_layers def getFaces(obj_data): # BMesh in blender 2.63 broke this - if bpy.app.version[1] >= 63: - return tesselate_if_needed(obj_data).tessfaces - else: - return obj_data.faces + #if bpy.app.version[1] >= 63: + # return tesselate_if_needed(obj_data).tessfaces + #else: + # return obj_data.faces + return obj_data.polygons def getVertexColors(obj_data): # BMesh in blender 2.63 broke this @@ -198,6 +201,16 @@ def getVertexColors(obj_data): else: return obj_data.vertex_colors +def getFaceImage(face): + try: + material = obj.data.materials[face.material_index] + texImage = material.node_tree.nodes["Image Texture"] + return texImage.image + except: + pass + return None + + # ==== Write TEXS Chunk ==== def write_texs(objects=[]): global b3d_parameters @@ -297,11 +310,9 @@ def write_texs(objects=[]): #data.activeUVLayer = uvlayer #if DEBUG: print("") - - img = getUVTextures(data)[iuvlayer].data[face.index].image - + + img = getFaceImage(face) if img: - if img.filepath in trimmed_paths: img_name = trimmed_paths[img.filepath] else: @@ -389,12 +400,12 @@ def write_brus(objects=[]): if face.index >= len(uv_textures[iuvlayer].data): continue - - img = uv_textures[iuvlayer].data[face.index].image - + + img = getFaceImage(face) + if not img: continue - + img_found = 1 if img.filepath in trimmed_paths: @@ -424,7 +435,7 @@ def write_brus(objects=[]): mat_colr = mat_data.diffuse_color[0] mat_colg = mat_data.diffuse_color[1] mat_colb = mat_data.diffuse_color[2] - mat_alpha = mat_data.alpha + mat_alpha = 1.0 # mat_data.alpha # 2.8 fail! mat_name = mat_data.name if not mat_name in brus_stack: @@ -618,7 +629,7 @@ def write_node(objects=[]): matrix = TRANS_MATRIX.copy() scale_matrix = mathutils.Matrix() else: - matrix = obj.matrix_world*TRANS_MATRIX + matrix = obj.matrix_world @ TRANS_MATRIX scale_matrix = obj.matrix_world.copy() @@ -690,10 +701,10 @@ def write_node(objects=[]): #print(" [%.2f %.2f %.2f %.2f]" % (b[2][0], b[2][1], b[2][2], b[2][3])) #print(" [%.2f %.2f %.2f %.2f]" % (b[3][0], b[3][1], b[3][2], b[3][3])) - par_matrix = b * a + par_matrix = b @ a transform = mathutils.Matrix([[1,0,0,0],[0,0,-1,0],[0,-1,0,0],[0,0,0,1]]) - par_matrix = transform*par_matrix*transform + par_matrix = transform @ par_matrix @ transform # FIXME: that's ugly, find a clean way to change the matrix..... if bpy.app.version[1] >= 62: @@ -717,7 +728,7 @@ def write_node(objects=[]): #print("==== "+bone.name+" ====") #print("Without parent") - m = arm_matrix*bone.matrix_local + m = arm_matrix @ bone.matrix_local #c = arm.matrix_world #print("A : [%.3f %.3f %.3f %.3f]" % (c[0][0], c[0][1], c[0][2], c[0][3])) @@ -731,7 +742,7 @@ def write_node(objects=[]): #print(" [%.3f %.3f %.3f %.3f]" % (c[2][0], c[2][1], c[2][2], c[2][3])) #print(" [%.3f %.3f %.3f %.3f]" % (c[3][0], c[3][1], c[3][2], c[3][3])) - par_matrix = m*mathutils.Matrix([[-1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]]) + par_matrix = m @ mathutils.Matrix([[-1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]]) #c = par_matrix #print("C : [%.3f %.3f %.3f %.3f]" % (c[0][0], c[0][1], c[0][2], c[0][3])) @@ -763,7 +774,7 @@ def write_node(objects=[]): arm_matrix = arm.matrix_world transform = mathutils.Matrix([[-1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]]) - arm_matrix = transform*arm_matrix + arm_matrix = transform @ arm_matrix for bone_name in arm.data.bones.keys(): #bone_matrix = mathutils.Matrix(arm_pose.bones[bone_name].poseMatrix) @@ -801,7 +812,7 @@ def write_node(objects=[]): # if has parent if bone[BONE_PARENT]: par_matrix = mathutils.Matrix(arm_pose.bones[bone[BONE_PARENT].name].matrix) - bone_matrix = par_matrix.inverted()*bone_matrix + bone_matrix = par_matrix.inverted() @ bone_matrix else: if b3d_parameters.get("local-space"): bone_matrix = bone_matrix*mathutils.Matrix([[-1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]]) @@ -812,7 +823,7 @@ def write_node(objects=[]): # print("arm_matrix = ", arm_matrix) # print("bone_matrix = ", bone_matrix) - bone_matrix = arm_matrix*bone_matrix + bone_matrix = arm_matrix @ bone_matrix #if frame_count == 1: # print("arm_matrix*bone_matrix", bone_matrix) @@ -992,7 +1003,7 @@ def write_node_mesh(obj,obj_count,arm_action,exp_root): if arm_action: data = obj.data else: - data = obj.to_mesh(the_scene, True, 'PREVIEW') + data = obj.to_mesh() temp_buf += write_int(-1) #Brush ID temp_buf += write_node_mesh_vrts(obj, data, obj_count, arm_action, exp_root) #NODE MESH VRTS @@ -1063,7 +1074,23 @@ def write_node_mesh_vrts(obj, data, obj_count, arm_action, exp_root): mesh_matrix = obj.matrix_world.copy() #import time - + + # new! 2.8 let's precalculate loop indices for every face and vertex id + me = data + + my_uvs = {} + + for f in me.polygons: + + my_uvs[f.index] = [] + + for i in f.loop_indices: + l = me.loops[i] + v = me.vertices[l.vertex_index] + for j,ul in enumerate(me.uv_layers): + uv = ul.data[l.index].uv + my_uvs[f.index].append(uv) + uv_layers_count = len(getUVTextures(data)) for face in getFaces(data): @@ -1084,12 +1111,12 @@ def write_node_mesh_vrts(obj, data, obj_count, arm_action, exp_root): #a = time.time() if arm_action: - v = mesh_matrix * data.vertices[vert].co + v = mesh_matrix @ data.vertices[vert].co vert_matrix = mathutils.Matrix.Translation(v) else: vert_matrix = mathutils.Matrix.Translation(data.vertices[vert].co) - vert_matrix *= TRANS_MATRIX + vert_matrix @= TRANS_MATRIX vcoord = vert_matrix.to_translation() temp_buf.append(write_float_triplet(vcoord.x, vcoord.z, vcoord.y)) @@ -1101,9 +1128,9 @@ def write_node_mesh_vrts(obj, data, obj_count, arm_action, exp_root): norm_matrix = mathutils.Matrix.Translation(data.vertices[vert].normal) if arm_action: - norm_matrix *= mesh_matrix + norm_matrix @= mesh_matrix - norm_matrix *= TRANS_MATRIX + norm_matrix @= TRANS_MATRIX normal_vector = norm_matrix.to_translation() temp_buf.append(write_float_triplet(normal_vector.x, #NX @@ -1139,10 +1166,23 @@ def write_node_mesh_vrts(obj, data, obj_count, arm_action, exp_root): except: pass vertex_groups[ivert][vg.name] = w - + + + # NEW! 2.8 code to write uv from face and vertex_id + # vertex_id, vert is in enumerate (face.vertices) + # face is from data.polygons + # uv_layers_count is from data.uv_layers + + + for iuvlayer in range(uv_layers_count): + uv = my_uvs[face.index][vertex_id] + temp_buf.append(write_float_couple(uv[0], 1-uv[1]) ) + + #e = time.time() #time_in_b2 += e - d - + + """ # ==== !!bottleneck here!! (40% of the function) if vertex_id == 0: for iuvlayer in range(uv_layers_count): @@ -1160,6 +1200,7 @@ def write_node_mesh_vrts(obj, data, obj_count, arm_action, exp_root): for iuvlayer in range(uv_layers_count): uv = getUVTextures(data)[iuvlayer].data[face.index].uv4 temp_buf.append(write_float_couple(uv[0], 1-uv[1]) ) # U, V + """ #f = time.time() #time_in_b3 += f - e @@ -1209,12 +1250,12 @@ def write_node_mesh_tris(obj, data, obj_count,arm_action,exp_root): if iuvlayer >= uv_layer_count: continue - + img_id = -1 - - img = uv_textures[iuvlayer].data[face.index].image + + img = getFaceImage(face) + if img: - if img.filepath in trimmed_paths: img_name = trimmed_paths[img.filepath] else: diff --git a/io_scene_b3d/import_b3d.py b/import_b3d.py similarity index 52% rename from io_scene_b3d/import_b3d.py rename to import_b3d.py index dbb53f6..2a4cdb0 100644 --- a/io_scene_b3d/import_b3d.py +++ b/import_b3d.py @@ -22,26 +22,25 @@ def flip(v): def flip_all(v): return [y for y in [flip(x) for x in v]] -armatures = [] -bonesdata = [] +material_mapping = {} weighting = {} -bones_ids = {} -bones_node = None +""" def make_skeleton(node): objName = 'armature' a = bpy.data.objects.new(objName, bpy.data.armatures.new(objName)) armatures.append(a); - ctx.scene.objects.link(a) + ctx.scene.collection.objects.link(a) - for i in bpy.context.selected_objects: i.select = False #deselect all objects + for i in bpy.context.selected_objects: i.select_set(state=False) - a.select = True - a.show_x_ray = True - a.data.draw_type = 'STICK' - bpy.context.scene.objects.active = a + a.select_set(state=True) + a.show_in_front = True + a.data.display_type = 'STICK' + + bpy.context.view_layer.objects.active = a bpy.ops.object.mode_set(mode='EDIT',toggle=False) @@ -68,11 +67,15 @@ def make_skeleton(node): # delete all objects with the same names as the bones for name, pos, rot, parent_id in bonesdata: - bpy.data.objects.remove(bpy.data.objects[name]) + try: + bpy.data.objects.remove(bpy.data.objects[name]) + except: + pass bpy.ops.object.mode_set(mode='OBJECT') - for i in bpy.context.selected_objects: i.select = False #deselect all objects + #for i in bpy.context.selected_objects: i.select = False #deselect all objects + for i in bpy.context.selected_objects: i.select_set(state=False) #deselect all objects #2.8 fails # get parent mesh (hardcoded so far) objName = 'anim' @@ -87,7 +90,7 @@ def make_skeleton(node): # create vertex groups for bone in a.data.bones.values(): - group = ob.vertex_groups.new(bone.name) + group = ob.vertex_groups.new(name=bone.name) if bone.name in weighting.keys(): for vertex_id, weight in weighting[bone.name]: #vertex_id = remaps[objName][vertex_id] @@ -112,15 +115,21 @@ def make_skeleton(node): bpy.context.scene.frame_end = node.frames - 1 - """ + ## ANIMATION! bone_string = 'Bip01' - bone = {'name' : bone_string} curvesLoc = None curvesRot = None bone_string = "pose.bones[\"{}\"].".format(bone.name) - group = action.groups.new(name=bone.name) + group = action.groups.new(name=bone_string) + for bone_id, (name, keys, rot, parent_id) in enumerate(bonesdata): + for frame in range(node.frames): + # (unoptimized) walk through all keys and select the frame + for key in keys: + if key.frame==frame: + pass + #print(name, key) for keyframe in range(node.frames): if curvesLoc and curvesRot: break if keyframe.pos and not curvesLoc: @@ -158,44 +167,11 @@ def make_skeleton(node): curvesRot[i].keyframe_points[-1].co = [keyframe.frame, bone.rotation_quaternion[i]] #curve = action.fcurves.new(data_path=bone_string + "rotation_quaternion",index=i) - """ +""" +def import_mesh(node, parent): + global material_mapping -def assign_material_slots(ob, node, mat_slots): - bpy.context.scene.objects.active = ob - bpy.ops.object.mode_set(mode='EDIT') - me = ob.data - bm = bmesh.from_edit_mesh(me) - bm.faces.ensure_lookup_table() - start = 0 - for face in node.faces: - numfaces = len(face.indices) - for i in range(numfaces): - bm.faces[start+i].material_index = mat_slots[face.brush_id] - start += numfaces - bmesh.update_edit_mesh(me, True) - bpy.ops.object.mode_set(mode='OBJECT') - -def postprocess(ob, mesh, node): - ops = bpy.ops - bpy.context.scene.objects.active = ob - ops.object.mode_set(mode='EDIT') - ops.mesh.select_all(action='SELECT') - - ops.mesh.remove_doubles(threshold=0) - bpy.ops.mesh.tris_convert_to_quads() - - ops.mesh.select_all(action='DESELECT') - ops.object.mode_set(mode='OBJECT') - - # smooth normals - mesh.use_auto_smooth = True - mesh.auto_smooth_angle = 3.145926*0.2 - ops.object.select_all(action="SELECT") - ops.object.shade_smooth() - bpy.ops.object.mode_set(mode='OBJECT') - -def import_mesh(node): mesh = bpy.data.meshes.new(node.name) # join face arrays @@ -213,76 +189,121 @@ def import_mesh(node): ob = bpy.data.objects.new(node.name, mesh) # assign uv coordinates - vert_uvs = [(0,0) if len(uv)==0 else (uv[0], 1-uv[1]) for uv in node.uvs] - me = ob.data - me.uv_textures.new() - me.uv_layers[-1].data.foreach_set("uv", [uv for pair in [vert_uvs[l.vertex_index] for l in me.loops] for uv in pair]) + bpymesh = ob.data + uvs = [(0,0) if len(uv)==0 else (uv[0], 1-uv[1]) for uv in node.uvs] + uvlist = [i for poly in bpymesh.polygons for vidx in poly.vertices for i in uvs[vidx]] + bpymesh.uv_layers.new().data.foreach_set('uv', uvlist) - # assign materials and textures - mat_slots = {} + # adding object materials (insert-ordered) + for key, value in material_mapping.items(): + ob.data.materials.append(bpy.data.materials[value]) + + # assign material_indexes + poly = 0 for face in node.faces: - if face.brush_id in materials: - mat = materials[face.brush_id] - ob.data.materials.append(mat) - mat_slots[face.brush_id] = len(ob.data.materials)-1 - for uv_face in ob.data.uv_textures.active.data: - if mat.active_texture: - uv_face.image = mat.active_texture.image - - # link object to scene - ctx.scene.objects.link(ob) - - if len(node.faces)>1: - assign_material_slots(ob, node, mat_slots) - - #postprocess(ob, mesh, node) # breaks weighting + for _ in face.indices: + ob.data.polygons[poly].material_index = face.brush_id + poly += 1 return ob -def import_node(node, parent): - global armatures, bonesdata, weighting, bones_ids, bones_node +def select_recursive(root): + for c in root.children: + select_recursive(c) + root.select_set(state=True) + +def make_armature_recursive(root, a, parent_bone): + bone = a.data.edit_bones.new(root.name) + v = root.matrix_world.to_translation() + bone.tail = v + # bone.head = (v[0]-0.01,v[1],v[2]) # large handles! + bone.parent = parent_bone + if bone.parent: + bone.head = bone.parent.tail + parent_bone = bone + for c in root.children: + make_armature_recursive(c, a, parent_bone) + +def make_armatures(): + global ctx + global imported_armatures, weighting + + for dummy_root in imported_armatures: + objName = 'armature' + a = bpy.data.objects.new(objName, bpy.data.armatures.new(objName)) + ctx.scene.collection.objects.link(a) + for i in bpy.context.selected_objects: i.select_set(state=False) + a.select_set(state=True) + a.show_in_front = True + a.data.display_type = 'OCTAHEDRAL' + bpy.context.view_layer.objects.active = a + + bpy.ops.object.mode_set(mode='EDIT',toggle=False) + make_armature_recursive(dummy_root, a, None) + bpy.ops.object.mode_set(mode='OBJECT',toggle=False) + + # set ob to mesh object + ob = dummy_root.parent + a.parent = ob + + # delete dummy objects hierarchy + for i in bpy.context.selected_objects: i.select_set(state=False) + select_recursive(dummy_root) + bpy.ops.object.delete(use_global=True) + + # apply armature modifier + modifier = ob.modifiers.new(type="ARMATURE", name="armature") + modifier.object = a + + # create vertex groups + for bone in a.data.bones.values(): + group = ob.vertex_groups.new(name=bone.name) + if bone.name in weighting.keys(): + for vertex_id, weight in weighting[bone.name]: + group_indices = [vertex_id] + group.add(group_indices, weight, 'REPLACE') + a.parent.data.update() + +def import_bone(node, parent=None): + global imported_armatures, weighting + # add dummy objects to calculate bone positions later + ob = bpy.data.objects.new(node.name, None) + + # fill weighting map for later use + w = [] + for vert_id, weight in node['bones']: + w.append((vert_id, weight)) + weighting[node.name] = w + + # check parent, add root armature + if parent and parent.type=='MESH': + imported_armatures.append(ob) + + return ob + +def import_node_recursive(node, parent=None): + ob = None if 'vertices' in node and 'faces' in node: - ob = import_mesh(node) - else: + ob = import_mesh(node, parent) + elif 'bones' in node: + ob = import_bone(node, parent) + elif node.name: ob = bpy.data.objects.new(node.name, None) - ctx.scene.objects.link(ob) - ob.rotation_mode='QUATERNION' - ob.rotation_quaternion = flip(node.rotation) - ob.scale = flip(node.scale) - ob.location = flip(node.position) + if ob: + ctx.scene.collection.objects.link(ob) - if parent: - ob.parent = parent - - if 'bones' in node: - bone_name = node.name - - # we need numeric parent_id for bonesdata - parent_id = -1 if parent: - if parent.name in bones_ids.keys(): - parent_id = bones_ids[parent.name] - bonesdata.append([bone_name,None,None,parent_id]) - bones_ids[bone_name] = len(bonesdata)-1 + ob.parent = parent - # fill weighting map for later use - w = [] - for vert_id, weight in node['bones']: - w.append((vert_id, weight)) - weighting[bone_name] = w + ob.rotation_mode='QUATERNION' + ob.rotation_quaternion = flip(node.rotation) + ob.scale = flip(node.scale) + ob.location = flip(node.position) - if 'bones' in node and not bones_node: - print(bones_node) - bones_node = node - - return ob - -def walk(root, parent=None): - for node in root.nodes: - ob = import_node(node, parent) - walk(node, ob) + for x in node.nodes: + import_node_recursive(x, ob) def load_b3d(filepath, context, @@ -290,44 +311,48 @@ def load_b3d(filepath, IMAGE_SEARCH=True, APPLY_MATRIX=True, global_matrix=None): + global ctx + global material_mapping + ctx = context data = B3DTree().parse(filepath) - global images, materials - images = {} - materials = {} - # load images + images = {} dirname = os.path.dirname(filepath) for i, texture in enumerate(data['textures'] if 'textures' in data else []): texture_name = os.path.basename(texture['name']) for mat in data.materials: if mat.tids[0]==i: - images[i] = load_image(texture_name, dirname, check_existing=True, - place_holder=False, recursive=IMAGE_SEARCH) + images[i] = (texture_name, load_image(texture_name, dirname, check_existing=True, + place_holder=False, recursive=IMAGE_SEARCH)) # create materials + material_mapping = {} for i, mat in enumerate(data.materials if 'materials' in data else []): - name = mat.name - material = bpy.data.materials.new(name) - material.diffuse_color = mat.rgba[:-1] - material.alpha = mat.rgba[3] - material.use_transparency = material.alpha < 1 - texture = bpy.data.textures.new(name=name, type='IMAGE') - tid = mat.tids[0] - if tid in images: - texture.image = images[tid] - mtex = material.texture_slots.add() - mtex.texture = texture - mtex.texture_coords = 'UV' - mtex.use_map_color_diffuse = True - materials[i] = material + material = bpy.data.materials.new(mat.name) + material_mapping[i] = material.name + material.diffuse_color = mat.rgba + material.blend_method = 'MULTIPLY' if mat.rgba[3] < 1.0 else 'OPAQUE' - global armatures, bonesdata, weighting, bones_ids, bones_node - walk(data) - if data.frames: - make_skeleton(data) + tid = mat.tids[0] if len(mat.tids) else -1 + + if tid in images: + name, image = images[tid] + texture = bpy.data.textures.new(name=name, type='IMAGE') + material.use_nodes = True + bsdf = material.node_tree.nodes["Principled BSDF"] + texImage = material.node_tree.nodes.new('ShaderNodeTexImage') + texImage.image = image + material.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) + + global imported_armatures, weighting + imported_armatures = [] + weighting = {} + + import_node_recursive(data) + make_armatures() def load(operator, context, @@ -348,7 +373,9 @@ def load(operator, return {'FINISHED'} -filepath = 'C:/Games/GnomE/media/models/gnome/model.b3d' +#filepath = 'D:/Projects/github/io_scene_b3d/testing/gooey.b3d' +filepath = 'C:/Games/GnomE/media/models/ded/ded.b3d' +#filepath = 'C:/Games/GnomE/media/models/gnome/model.b3d' #filepath = 'C:/Games/GnomE/media/levels/level1.b3d' #filepath = 'C:/Games/GnomE/media/models/gnome/go.b3d' #filepath = 'C:/Games/GnomE/media/models/flag/flag.b3d' @@ -356,3 +383,4 @@ filepath = 'C:/Games/GnomE/media/models/gnome/model.b3d' if __name__ == "__main__": p = B3DDebugParser() p.parse(filepath) + \ No newline at end of file