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 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 ## Installation
* Userspace method: click "File" - "User Preferences" - "Add-ons" - "Install Add-on from File". * 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. 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 * 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. %APPDATA%\Blender Foundation\Blender\2.80\scripts\addons\io_scene_b3d.
* Search and enable add-on in "User Preferences" - "Add-ons". Click "Save User Settings" afterwards.
Then enable add-on in "User Preferences" - "Add-ons". Click "Save User Settings" afterwards.
## Debugging ## Debugging
* Userspace method: every time you make a change the script has to be reloaded using Reload Scripts command (F8). * 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+D in Object Mode. It resets scene, reloads the script and imports test file. * Alternative method: my shortcut, Shift+Ctrl+F 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).
## TODO ## TODO
### Import ### 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. * Nodes use original quaternion rotation that affects user interface.
Maybe convert them into euler angles. 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 ## 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. * The import script based on a heavily rewriten (new reader) script from Glogow Poland Mariusz Szkaradek.
I've had to rewrite all the chunk reader stuff and all the import stuff, because Blender API * The export script uses portions of script by Diego 'GaNDaLDF' Parisi (ported to Blender 2.8) under GPL license.
has heavily changed since then. * The b3d format documentation (b3dfile_specs.txt) doesn't have a clear license (I assume Public Domain).
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.
## Alternatives ## Alternatives

View File

@ -19,9 +19,9 @@
# <pep8-80 compliant> # <pep8-80 compliant>
bl_info = { bl_info = {
"name": "Blitz 3D format", "name": "Blitz 3D format (.b3d)",
"author": "Joric", "author": "Joric",
"blender": (2, 74, 0), "blender": (2, 80, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "Import-Export B3D, meshes, uvs, materials, textures, " "description": "Import-Export B3D, meshes, uvs, materials, textures, "
"cameras & lamps", "cameras & lamps",
@ -49,24 +49,22 @@ from bpy.props import (
from bpy_extras.io_utils import ( from bpy_extras.io_utils import (
ImportHelper, ImportHelper,
ExportHelper, ExportHelper,
orientation_helper_factory, orientation_helper,
axis_conversion, axis_conversion,
) )
IOB3DOrientationHelper = orientation_helper_factory("IOB3DOrientationHelper", axis_forward='Y', axis_up='Z') @orientation_helper(axis_forward='Y', axis_up='Z')
class ImportB3D(bpy.types.Operator, ImportHelper):
class ImportB3D(bpy.types.Operator, ImportHelper, IOB3DOrientationHelper):
"""Import from B3D file format (.b3d)""" """Import from B3D file format (.b3d)"""
bl_idname = "import_scene.blitz3d_b3d" bl_idname = "import_scene.blitz3d_b3d"
bl_label = 'Import B3D' bl_label = 'Import B3D'
bl_options = {'UNDO'} bl_options = {'UNDO'}
filename_ext = ".b3d" 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", name="Size Constraint",
description="Scale the model by 10 until it reaches the " description="Scale the model by 10 until it reaches the "
"size constraint (0 to disable)", "size constraint (0 to disable)",
@ -74,13 +72,13 @@ class ImportB3D(bpy.types.Operator, ImportHelper, IOB3DOrientationHelper):
soft_min=0.0, soft_max=1000.0, soft_min=0.0, soft_max=1000.0,
default=10.0, default=10.0,
) )
use_image_search = BoolProperty( use_image_search : BoolProperty(
name="Image Search", name="Image Search",
description="Search subdirectories for any associated images " description="Search subdirectories for any associated images "
"(Warning, may be slow)", "(Warning, may be slow)",
default=True, default=True,
) )
use_apply_transform = BoolProperty( use_apply_transform : BoolProperty(
name="Apply Transform", name="Apply Transform",
description="Workaround for object transformations " description="Workaround for object transformations "
"importing incorrectly", "importing incorrectly",
@ -103,18 +101,20 @@ class ImportB3D(bpy.types.Operator, ImportHelper, IOB3DOrientationHelper):
return import_b3d.load(self, context, **keywords) 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)""" """Export to B3D file format (.b3d)"""
bl_idname = "export_scene.blitz3d_b3d" bl_idname = "export_scene.blitz3d_b3d"
bl_label = 'Export B3D' bl_label = 'Export B3D'
filename_ext = ".b3d" filename_ext = ".b3d"
filter_glob = StringProperty(
filter_glob: StringProperty(
default="*.b3d", default="*.b3d",
options={'HIDDEN'}, options={'HIDDEN'},
) )
use_selection = BoolProperty( use_selection: BoolProperty(
name="Selection Only", name="Selection Only",
description="Export selected objects only", description="Export selected objects only",
default=False, default=False,
@ -147,60 +147,77 @@ def menu_func_import(self, context):
class DebugMacro(bpy.types.Operator): class DebugMacro(bpy.types.Operator):
bl_idname = "object.debug_macro" bl_idname = "object.debug_macro"
bl_label = "b3d debug" bl_label = "Debug Macro"
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
from . import import_b3d from . import import_b3d
from . import export_b3d
filepath = bpy.props.StringProperty(name="filepath", default=import_b3d.filepath) filepath = bpy.props.StringProperty(name="filepath", default=import_b3d.filepath)
def execute(self, context): def execute(self, context: bpy.context):
import sys,imp import sys,imp
print("b3d, loading", self.filepath)
for material in bpy.data.materials: for material in bpy.data.materials:
bpy.data.materials.remove(material) bpy.data.materials.remove(material)
for obj in bpy.context.screen.scene.objects: for obj in bpy.context.scene.objects:
bpy.data.objects.remove(obj, True) bpy.data.objects.remove(obj, do_unlink=True)
module = sys.modules['io_scene_b3d'] module = sys.modules['io_scene_b3d']
imp.reload(module) imp.reload(module)
import_b3d.load(self, context, filepath=self.filepath) 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.viewnumpad(type='FRONT', align_active=False)
bpy.ops.view3d.view_all(use_all_regions=True, center=True) bpy.ops.view3d.view_all(use_all_regions=True, center=True)
if bpy.context.region_data.is_perspective: if bpy.context.region_data.is_perspective:
bpy.ops.view3d.view_persportho() bpy.ops.view3d.view_persportho()
"""
return {'FINISHED'} return {'FINISHED'}
addon_keymaps = [] addon_keymaps = []
def register(): classes = (
bpy.utils.register_module(__name__) ImportB3D,
ExportB3D,
DebugMacro
)
bpy.types.INFO_MT_file_import.append(menu_func_import) def register():
bpy.types.INFO_MT_file_export.append(menu_func_export) 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 # handle the keymap
wm = bpy.context.window_manager wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY') if wm.keyconfigs.addon:
kmi = km.keymap_items.new(DebugMacro.bl_idname, 'D', 'PRESS', ctrl=True, shift=True) km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type='EMPTY')
addon_keymaps.append((km, kmi)) kmi = km.keymap_items.new(DebugMacro.bl_idname, 'F', 'PRESS', ctrl=True, shift=True)
addon_keymaps.append((km, kmi))
def unregister(): 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.TOPBAR_MT_file_import.remove(menu_func_import)
bpy.types.INFO_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
# handle the keymap # handle the keymap
for km, kmi in addon_keymaps: for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi) km.keymap_items.remove(kmi)
addon_keymaps.clear() del addon_keymaps[:]
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View File

@ -2,20 +2,21 @@
""" """
Name: 'B3D Exporter (.b3d)...' Name: 'B3D Exporter (.b3d)...'
Blender: 259 Blender: 280
Group: 'Export' Group: 'Export'
Tooltip: 'Export to Blitz3D file format (.b3d)' Tooltip: 'Export to Blitz3D file format (.b3d)'
""" """
__author__ = ["iego 'GaNDaLDF' Parisi, MTLZ (is06), Joerg Henrichs, Marianne Gagnon"] __author__ = ["Diego 'GaNDaLDF' Parisi, MTLZ (is06), Joerg Henrichs, Marianne Gagnon, Joric"]
__url__ = ["www.gandaldf.com"] __url__ = ["https://github.com/joric/io_scene_b3d"]
__version__ = "3.0" __version__ = "3.2"
__bpydoc__ = """\ __bpydoc__ = """\
""" """
# BLITZ3D EXPORTER 3.0 # BLITZ3D EXPORTER 3.2
# Copyright (C) 2009 by Diego "GaNDaLDF" Parisi - www.gandaldf.com # Copyright (C) 2009 by Diego "GaNDaLDF" Parisi - www.gandaldf.com
# Lightmap issue fixed by Capricorn 76 Pty. Ltd. - www.capricorn76.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.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) # With changes by Marianne Gagnon and Joerg Henrichs, supertuxkart.sf.net (Copyright (C) 2011-2012)
# #
# LICENSE: # LICENSE:
@ -36,9 +37,9 @@ __bpydoc__ = """\
bl_info = { bl_info = {
"name": "B3D (BLITZ3D) Model Exporter", "name": "B3D (BLITZ3D) Model Exporter",
"description": "Exports a blender scene or object to the B3D (BLITZ3D) format", "description": "Exports a blender scene or object to the B3D (BLITZ3D) format",
"author": "Diego 'GaNDaLDF' Parisi, MTLZ (is06), Joerg Henrichs, Marianne Gagnon", "author": "Diego 'GaNDaLDF' Parisi, MTLZ (is06), Joerg Henrichs, Marianne Gagnon, Joric",
"version": (3,1), "version": (3,2),
"blender": (2, 5, 9), "blender": (2, 8, 0),
"api": 31236, "api": 31236,
"location": "File > Export", "location": "File > Export",
"warning": '', # used for warning icon and text in addons panel "warning": '', # used for warning icon and text in addons panel
@ -179,17 +180,19 @@ def tesselate_if_needed(objdata):
def getUVTextures(obj_data): def getUVTextures(obj_data):
# BMesh in blender 2.63 broke this # BMesh in blender 2.63 broke this
if bpy.app.version[1] >= 63: #if bpy.app.version[1] >= 63:
return tesselate_if_needed(obj_data).tessface_uv_textures # return tesselate_if_needed(obj_data).tessface_uv_textures #2.8 breaks this
else: #else:
return obj_data.uv_textures # return obj_data.uv_textures
return obj_data.uv_layers
def getFaces(obj_data): def getFaces(obj_data):
# BMesh in blender 2.63 broke this # BMesh in blender 2.63 broke this
if bpy.app.version[1] >= 63: #if bpy.app.version[1] >= 63:
return tesselate_if_needed(obj_data).tessfaces # return tesselate_if_needed(obj_data).tessfaces
else: #else:
return obj_data.faces # return obj_data.faces
return obj_data.polygons
def getVertexColors(obj_data): def getVertexColors(obj_data):
# BMesh in blender 2.63 broke this # BMesh in blender 2.63 broke this
@ -198,6 +201,16 @@ def getVertexColors(obj_data):
else: else:
return obj_data.vertex_colors 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 ==== # ==== Write TEXS Chunk ====
def write_texs(objects=[]): def write_texs(objects=[]):
global b3d_parameters global b3d_parameters
@ -297,11 +310,9 @@ def write_texs(objects=[]):
#data.activeUVLayer = uvlayer #data.activeUVLayer = uvlayer
#if DEBUG: print("<uv face=", face.index, ">") #if DEBUG: print("<uv face=", face.index, ">")
img = getUVTextures(data)[iuvlayer].data[face.index].image img = getFaceImage(face)
if img: if img:
if img.filepath in trimmed_paths: if img.filepath in trimmed_paths:
img_name = trimmed_paths[img.filepath] img_name = trimmed_paths[img.filepath]
else: else:
@ -389,12 +400,12 @@ def write_brus(objects=[]):
if face.index >= len(uv_textures[iuvlayer].data): if face.index >= len(uv_textures[iuvlayer].data):
continue continue
img = uv_textures[iuvlayer].data[face.index].image img = getFaceImage(face)
if not img: if not img:
continue continue
img_found = 1 img_found = 1
if img.filepath in trimmed_paths: if img.filepath in trimmed_paths:
@ -424,7 +435,7 @@ def write_brus(objects=[]):
mat_colr = mat_data.diffuse_color[0] mat_colr = mat_data.diffuse_color[0]
mat_colg = mat_data.diffuse_color[1] mat_colg = mat_data.diffuse_color[1]
mat_colb = mat_data.diffuse_color[2] 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 mat_name = mat_data.name
if not mat_name in brus_stack: if not mat_name in brus_stack:
@ -618,7 +629,7 @@ def write_node(objects=[]):
matrix = TRANS_MATRIX.copy() matrix = TRANS_MATRIX.copy()
scale_matrix = mathutils.Matrix() scale_matrix = mathutils.Matrix()
else: else:
matrix = obj.matrix_world*TRANS_MATRIX matrix = obj.matrix_world @ TRANS_MATRIX
scale_matrix = obj.matrix_world.copy() 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[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])) #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]]) 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..... # FIXME: that's ugly, find a clean way to change the matrix.....
if bpy.app.version[1] >= 62: if bpy.app.version[1] >= 62:
@ -717,7 +728,7 @@ def write_node(objects=[]):
#print("==== "+bone.name+" ====") #print("==== "+bone.name+" ====")
#print("Without parent") #print("Without parent")
m = arm_matrix*bone.matrix_local m = arm_matrix @ bone.matrix_local
#c = arm.matrix_world #c = arm.matrix_world
#print("A : [%.3f %.3f %.3f %.3f]" % (c[0][0], c[0][1], c[0][2], c[0][3])) #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[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])) #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 #c = par_matrix
#print("C : [%.3f %.3f %.3f %.3f]" % (c[0][0], c[0][1], c[0][2], c[0][3])) #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 arm_matrix = arm.matrix_world
transform = mathutils.Matrix([[-1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]]) 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(): for bone_name in arm.data.bones.keys():
#bone_matrix = mathutils.Matrix(arm_pose.bones[bone_name].poseMatrix) #bone_matrix = mathutils.Matrix(arm_pose.bones[bone_name].poseMatrix)
@ -801,7 +812,7 @@ def write_node(objects=[]):
# if has parent # if has parent
if bone[BONE_PARENT]: if bone[BONE_PARENT]:
par_matrix = mathutils.Matrix(arm_pose.bones[bone[BONE_PARENT].name].matrix) 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: else:
if b3d_parameters.get("local-space"): 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]]) 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("arm_matrix = ", arm_matrix)
# print("bone_matrix = ", bone_matrix) # print("bone_matrix = ", bone_matrix)
bone_matrix = arm_matrix*bone_matrix bone_matrix = arm_matrix @ bone_matrix
#if frame_count == 1: #if frame_count == 1:
# print("arm_matrix*bone_matrix", bone_matrix) # 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: if arm_action:
data = obj.data data = obj.data
else: else:
data = obj.to_mesh(the_scene, True, 'PREVIEW') data = obj.to_mesh()
temp_buf += write_int(-1) #Brush ID temp_buf += write_int(-1) #Brush ID
temp_buf += write_node_mesh_vrts(obj, data, obj_count, arm_action, exp_root) #NODE MESH VRTS 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() mesh_matrix = obj.matrix_world.copy()
#import time #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)) uv_layers_count = len(getUVTextures(data))
for face in getFaces(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() #a = time.time()
if arm_action: if arm_action:
v = mesh_matrix * data.vertices[vert].co v = mesh_matrix @ data.vertices[vert].co
vert_matrix = mathutils.Matrix.Translation(v) vert_matrix = mathutils.Matrix.Translation(v)
else: else:
vert_matrix = mathutils.Matrix.Translation(data.vertices[vert].co) vert_matrix = mathutils.Matrix.Translation(data.vertices[vert].co)
vert_matrix *= TRANS_MATRIX vert_matrix @= TRANS_MATRIX
vcoord = vert_matrix.to_translation() vcoord = vert_matrix.to_translation()
temp_buf.append(write_float_triplet(vcoord.x, vcoord.z, vcoord.y)) 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) norm_matrix = mathutils.Matrix.Translation(data.vertices[vert].normal)
if arm_action: 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() normal_vector = norm_matrix.to_translation()
temp_buf.append(write_float_triplet(normal_vector.x, #NX 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: except:
pass pass
vertex_groups[ivert][vg.name] = w 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() #e = time.time()
#time_in_b2 += e - d #time_in_b2 += e - d
"""
# ==== !!bottleneck here!! (40% of the function) # ==== !!bottleneck here!! (40% of the function)
if vertex_id == 0: if vertex_id == 0:
for iuvlayer in range(uv_layers_count): 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): for iuvlayer in range(uv_layers_count):
uv = getUVTextures(data)[iuvlayer].data[face.index].uv4 uv = getUVTextures(data)[iuvlayer].data[face.index].uv4
temp_buf.append(write_float_couple(uv[0], 1-uv[1]) ) # U, V temp_buf.append(write_float_couple(uv[0], 1-uv[1]) ) # U, V
"""
#f = time.time() #f = time.time()
#time_in_b3 += f - e #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: if iuvlayer >= uv_layer_count:
continue continue
img_id = -1 img_id = -1
img = uv_textures[iuvlayer].data[face.index].image img = getFaceImage(face)
if img: if img:
if img.filepath in trimmed_paths: if img.filepath in trimmed_paths:
img_name = trimmed_paths[img.filepath] img_name = trimmed_paths[img.filepath]
else: else:

View File

@ -22,26 +22,25 @@ def flip(v):
def flip_all(v): def flip_all(v):
return [y for y in [flip(x) for x in v]] return [y for y in [flip(x) for x in v]]
armatures = [] material_mapping = {}
bonesdata = []
weighting = {} weighting = {}
bones_ids = {}
bones_node = None
"""
def make_skeleton(node): def make_skeleton(node):
objName = 'armature' objName = 'armature'
a = bpy.data.objects.new(objName, bpy.data.armatures.new(objName)) a = bpy.data.objects.new(objName, bpy.data.armatures.new(objName))
armatures.append(a); 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.select_set(state=True)
a.show_x_ray = True a.show_in_front = True
a.data.draw_type = 'STICK' a.data.display_type = 'STICK'
bpy.context.scene.objects.active = a
bpy.context.view_layer.objects.active = a
bpy.ops.object.mode_set(mode='EDIT',toggle=False) 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 # delete all objects with the same names as the bones
for name, pos, rot, parent_id in bonesdata: 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') 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) # get parent mesh (hardcoded so far)
objName = 'anim' objName = 'anim'
@ -87,7 +90,7 @@ def make_skeleton(node):
# create vertex groups # create vertex groups
for bone in a.data.bones.values(): 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(): if bone.name in weighting.keys():
for vertex_id, weight in weighting[bone.name]: for vertex_id, weight in weighting[bone.name]:
#vertex_id = remaps[objName][vertex_id] #vertex_id = remaps[objName][vertex_id]
@ -112,15 +115,21 @@ def make_skeleton(node):
bpy.context.scene.frame_end = node.frames - 1 bpy.context.scene.frame_end = node.frames - 1
""" ## ANIMATION!
bone_string = 'Bip01' bone_string = 'Bip01'
bone = {'name' : bone_string}
curvesLoc = None curvesLoc = None
curvesRot = None curvesRot = None
bone_string = "pose.bones[\"{}\"].".format(bone.name) 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): for keyframe in range(node.frames):
if curvesLoc and curvesRot: break if curvesLoc and curvesRot: break
if keyframe.pos and not curvesLoc: 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]] curvesRot[i].keyframe_points[-1].co = [keyframe.frame, bone.rotation_quaternion[i]]
#curve = action.fcurves.new(data_path=bone_string + "rotation_quaternion",index=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) mesh = bpy.data.meshes.new(node.name)
# join face arrays # join face arrays
@ -213,76 +189,121 @@ def import_mesh(node):
ob = bpy.data.objects.new(node.name, mesh) ob = bpy.data.objects.new(node.name, mesh)
# assign uv coordinates # assign uv coordinates
vert_uvs = [(0,0) if len(uv)==0 else (uv[0], 1-uv[1]) for uv in node.uvs] bpymesh = ob.data
me = ob.data uvs = [(0,0) if len(uv)==0 else (uv[0], 1-uv[1]) for uv in node.uvs]
me.uv_textures.new() uvlist = [i for poly in bpymesh.polygons for vidx in poly.vertices for i in uvs[vidx]]
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.uv_layers.new().data.foreach_set('uv', uvlist)
# assign materials and textures # adding object materials (insert-ordered)
mat_slots = {} 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: for face in node.faces:
if face.brush_id in materials: for _ in face.indices:
mat = materials[face.brush_id] ob.data.polygons[poly].material_index = face.brush_id
ob.data.materials.append(mat) poly += 1
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
return ob return ob
def import_node(node, parent): def select_recursive(root):
global armatures, bonesdata, weighting, bones_ids, bones_node 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: if 'vertices' in node and 'faces' in node:
ob = import_mesh(node) ob = import_mesh(node, parent)
else: elif 'bones' in node:
ob = import_bone(node, parent)
elif node.name:
ob = bpy.data.objects.new(node.name, None) ob = bpy.data.objects.new(node.name, None)
ctx.scene.objects.link(ob)
ob.rotation_mode='QUATERNION' if ob:
ob.rotation_quaternion = flip(node.rotation) ctx.scene.collection.objects.link(ob)
ob.scale = flip(node.scale)
ob.location = flip(node.position)
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:
if parent.name in bones_ids.keys(): ob.parent = parent
parent_id = bones_ids[parent.name]
bonesdata.append([bone_name,None,None,parent_id])
bones_ids[bone_name] = len(bonesdata)-1
# fill weighting map for later use ob.rotation_mode='QUATERNION'
w = [] ob.rotation_quaternion = flip(node.rotation)
for vert_id, weight in node['bones']: ob.scale = flip(node.scale)
w.append((vert_id, weight)) ob.location = flip(node.position)
weighting[bone_name] = w
if 'bones' in node and not bones_node: for x in node.nodes:
print(bones_node) import_node_recursive(x, ob)
bones_node = node
return ob
def walk(root, parent=None):
for node in root.nodes:
ob = import_node(node, parent)
walk(node, ob)
def load_b3d(filepath, def load_b3d(filepath,
context, context,
@ -290,44 +311,48 @@ def load_b3d(filepath,
IMAGE_SEARCH=True, IMAGE_SEARCH=True,
APPLY_MATRIX=True, APPLY_MATRIX=True,
global_matrix=None): global_matrix=None):
global ctx global ctx
global material_mapping
ctx = context ctx = context
data = B3DTree().parse(filepath) data = B3DTree().parse(filepath)
global images, materials
images = {}
materials = {}
# load images # load images
images = {}
dirname = os.path.dirname(filepath) dirname = os.path.dirname(filepath)
for i, texture in enumerate(data['textures'] if 'textures' in data else []): for i, texture in enumerate(data['textures'] if 'textures' in data else []):
texture_name = os.path.basename(texture['name']) texture_name = os.path.basename(texture['name'])
for mat in data.materials: for mat in data.materials:
if mat.tids[0]==i: if mat.tids[0]==i:
images[i] = load_image(texture_name, dirname, check_existing=True, images[i] = (texture_name, load_image(texture_name, dirname, check_existing=True,
place_holder=False, recursive=IMAGE_SEARCH) place_holder=False, recursive=IMAGE_SEARCH))
# create materials # create materials
material_mapping = {}
for i, mat in enumerate(data.materials if 'materials' in data else []): for i, mat in enumerate(data.materials if 'materials' in data else []):
name = mat.name material = bpy.data.materials.new(mat.name)
material = bpy.data.materials.new(name) material_mapping[i] = material.name
material.diffuse_color = mat.rgba[:-1] material.diffuse_color = mat.rgba
material.alpha = mat.rgba[3] material.blend_method = 'MULTIPLY' if mat.rgba[3] < 1.0 else 'OPAQUE'
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
global armatures, bonesdata, weighting, bones_ids, bones_node tid = mat.tids[0] if len(mat.tids) else -1
walk(data)
if data.frames: if tid in images:
make_skeleton(data) 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, def load(operator,
context, context,
@ -348,7 +373,9 @@ def load(operator,
return {'FINISHED'} 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/levels/level1.b3d'
#filepath = 'C:/Games/GnomE/media/models/gnome/go.b3d' #filepath = 'C:/Games/GnomE/media/models/gnome/go.b3d'
#filepath = 'C:/Games/GnomE/media/models/flag/flag.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__": if __name__ == "__main__":
p = B3DDebugParser() p = B3DDebugParser()
p.parse(filepath) p.parse(filepath)