ported to blender 2.8, pushed to version 1.0

master
joric 2019-08-13 22:55:19 +05:00
parent e69bef90df
commit c648ca286a
5 changed files with 308 additions and 235 deletions

View File

@ -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

View File

@ -19,9 +19,9 @@
# <pep8-80 compliant>
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()

View File

@ -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("<uv face=", face.index, ">")
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:

View File

@ -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)