-
Notifications
You must be signed in to change notification settings - Fork 3
/
import_bfb.py
460 lines (437 loc) · 17.2 KB
/
import_bfb.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
import os
import time
import bpy
import mathutils
from struct import iter_unpack, unpack_from
from .common_bfb import *
from .bfmat import bfmat
def getstring128(x): return datastream[x:x+128].rstrip(b"\x00").decode("utf-8")
def getint(x): return unpack_from('i',datastream, x)[0]
def get_matrix(x): return mathutils.Matrix(list(iter_unpack('4f',datastream[x:x+64])))
def select_layer(layer_nr): return tuple(i == layer_nr for i in range(0, 20))
def log_error(error):
print(error)
global errors
errors.append(error)
def read_linked_list(pos,parent,level):
blockid, typeid, childstart, nextblockstart, u_cha, name = unpack_from("=4i b 64s", datastream, pos)
name = name.rstrip(b"\x00").decode("utf-8")
matrix = get_matrix(pos+81)
matrix.transpose()
#ordinary node, node with collision or lod level
if typeid == 1:
print("NODE:",name)
if parent:
ob = create_empty(parent,name,matrix)
#if this is a lod level, move it to its respective layer
if parent.name.startswith("lodgroup"):
ob.layers = select_layer(level)
#a dock node, a collision node, but nothing with a model, so move it to layer 6
else:
ob.layers = parent.layers
else:
if armature:
ob = armature
ob.name = name
ob.data.name = name
ob.matrix_local = matrix
else:
ob = create_empty(parent,name,matrix)
ob.layers = select_layer(5)
hascollision = getint(pos+153)
if hascollision == 1:
id2data[getint(pos+157)].parent = ob
if name.startswith("paint_"):
ob.layers = select_layer(5)
#lod group
elif typeid == 2:
print("LOD GROUP:",name)
ob = create_empty(parent,"lodgroup",matrix)
ob.layers = select_layer(5)
#mesh linker
elif typeid == 3:
print("MESH LINK:",name)
objID = getint(pos+161)
matname = getstring128(pos+169)
ob = id2data[objID]
ob.name = name
if parent:
ob.parent = parent
ob.matrix_local = matrix
create_material(ob,matname)
ob.layers = select_layer(level)
elif typeid == 4:
print("BILLBOARD:",name)
global camera
if not camera:
camera_data = bpy.data.cameras.new("TrackingCameraData")
camera = create_ob("TrackingCamera", camera_data)
camera.location = (2,-2,2)
camera.rotation_euler = (1.047,0.0,0.785)
hasobj = getint(pos+157)
objID = getint(pos+185)
matname = getstring128(pos+189)
ob = id2data[objID]
ob.name = name
ob.matrix_local = matrix
ob.parent = parent
create_material(ob,matname)
ob.layers = select_layer(level)
const = ob.constraints.new('COPY_ROTATION')
const.use_x = False
const.use_y = False
const.use_z = True
const.target = camera
#capsule collider link, only in actor meshes
elif typeid == 5:
collisionid = getint(pos+157)
#case-sensitive name
bone_name = bfbname_to_blendername(datastream[pos+161:pos+161+64])
ob = id2data[collisionid]
ob.parent = armature
ob.parent_bone = bone_name
ob.parent_type = 'BONE'
try:
ob.location.y = -armature.data.bones[bone_name].length
except:
ob.parent_bone = "Bip01"
log_error("Capsule collider "+name+" has no parent bone, set to Bip01!")
print("CAPSULE COLLIDER:",bone_name)
else:
print("Unknown type ID",typeid,"in block links!")
#if we have children, the newly created empty is their parent
if childstart!= 0:
pos = childstart
level = read_linked_list(pos,ob,level)
if parent:
#if this is a lod level, move it to its respective layer
if parent.name.startswith("lodgroup"):
level+= 1
#for the next block, the old empty is the parent
if nextblockstart!= 0:
pos = nextblockstart
level = read_linked_list(pos,parent,level)
return level
def get_tex_slot(mat, i):
while not mat.texture_slots[i]: mat.texture_slots.add()
return mat.texture_slots[i]
def create_material(ob,matname):
material = bfmat(dirname, matname+".bfmat")
for error in material.errors:
log_error(error)
if not material.root: return
fx = material.fx
cull_mode = material.CullMode
fps = bpy.context.scene.render.fps
#see which sub-shaders are used by this fx shader, and get the used ones in order
shaders = ("Base", "Decal", "Detail", "Gloss", "Glow", "Reflect")
tex_shaders = [name for i, name in sorted(zip([fx.find(s) for s in shaders], shaders)) if i > -1]
print("MATERIAL:",matname)
#only create the material if we haven't already created it, then just grab it
if matname not in bpy.data.materials:
mat = bpy.data.materials.new(matname)
mat.specular_intensity = 0.0
mat.ambient = 1
mat.alpha = 0
mat.use_transparent_shadows = True
mat.use_transparency = True
#we only disable it if both are explicitly set to "False"
#if we get "None" ie. not given in the bfmat, it is enabled
if material.AlphaTestEnable is False and material.AlphaBlendEnable is False:
mat.use_transparency = False
if material.MaterialAmbient:
mat.diffuse_color = material.MaterialAmbient[0:3]
if material.MaterialPower:
mat.diffuse_intensity= material.MaterialPower
for i, texture in enumerate(material.Texture):
if texture is not None:
if texture not in bpy.data.textures:
tex = bpy.data.textures.new(texture, type = 'IMAGE')
try:
img = bpy.data.images.load(material.find_recursive(texture+".dds"))
except:
print("Could not find image "+texture+".dds, generating blank image!")
img = bpy.data.images.new(texture+".dds",1,1)
tex.image = img
else: tex = bpy.data.textures[texture]
#now create the slot in the material for the texture
mtex = get_tex_slot(mat, i)
mtex.texture = tex
mtex.texture_coords = 'UV'
mtex.use_map_color_diffuse = True
mtex.use_map_density = True
#for the icon renderer
tex.filter_type = "BOX"
tex.filter_size = 0.1
#eg. African violets, but only in rendered view; but: glacier
tex.extension = "CLIP" if (cull_mode == "2" and mat.use_transparency) else "REPEAT"
#Shader effects
if i < len(tex_shaders):
if tex_shaders[i] == "Reflect":
mtex.blend_type = 'OVERLAY'
mtex.texture_coords = 'REFLECTION'
elif tex_shaders[i] == "Detail":
mtex.blend_type = 'OVERLAY'
mtex.use_map_alpha = False
mtex.invert = True
elif tex_shaders[i] == "Glow":
mtex.use_map_emit = True
mat.emit = 1
elif tex_shaders[i] == "Gloss":
mtex.use_map_specular = True
mat.specular_intensity = 1
elif tex_shaders[i] == "Decal":
mtex.use_map_alpha = False
# if "blink" in texture:
# mat.use_textures[i] = False
#see if there is an alternative UV index specified. If so, set it as the UV layer. If not, use i.
tex_index = material.TexCoordIndex[i]
mtex.uv_layer = tex_index if tex_index is not None else str(i)
#texture transform - static
tex_transform = material.TextureTransform[i]
if tex_transform:
matrix_4x4 = mathutils.Matrix(tex_transform)
mtex.offset = matrix_4x4.to_translation()
mtex.scale = matrix_4x4.to_scale()
#texture transform - animated
tex_anim = material.TextureAnimation[i]
if tex_anim:
for j, dtype in enumerate( ("offsetu", "offsetv") ):
for key in tex_anim[dtype]:
mtex.offset[j] = key[1]
mtex.keyframe_insert("offset", index = j, frame = int(key[0]*fps))
else: mat = bpy.data.materials[matname]
#now finally set all the textures we have in the mesh
me = ob.data
me.materials.append(mat)
#reversed so the last is shown
for mtex in reversed(mat.texture_slots):
if mtex:
try:
uv_i = int(mtex.uv_layer)
for texface in me.uv_textures[uv_i].data:
texface.image = mtex.texture.image
except:
print("No matching UV layer for Texture!")
#and for rendering, make sure each poly is assigned to the material
for f in me.polygons:
f.material_index = 0
def load(operator, context, filepath = "", use_custom_normals = False, mirror_mesh=False):
starttime = time.clock()
global errors
errors = []
global armature
global camera
global datastream
global dirname
global id2data
armature = None
camera = None
dirname, basename = os.path.split(filepath)
id2data = {}
skinned_meshes = []
#when no object exists, or when we are in edit mode when script is run
try: bpy.ops.object.mode_set(mode='OBJECT')
except: pass
print("\nImporting",basename)
with open(filepath, 'rb') as f:
datastream = f.read()
#used to access data from the BFB by ID
version, u_int, author, blockcount = unpack_from("i i 64s i", datastream, 8)
if version != 131073: log_error("Unsupported BFB version: "+str(version))
pos = 88
print("BFB Version:",version)
print("BFB Author:",author.rstrip(b"\x00").decode("utf-8"))
print("\nReading object blocks...")
for b in range(0, blockcount):
blockid, typeid, blockend, name = unpack_from("i h i 64s", datastream, pos)
try:
name = name.rstrip(b"\x00").decode("utf-8")
except: name = "NONE"
if typeid == 1:
type_name = "SphereCollider"
x, y, z, r = unpack_from("4f", datastream, pos+78)
id2data[blockid] = create_sphere(name, x, y, z, r)
elif typeid == 3:
type_name = "BoundingBox"
matrix = get_matrix(pos+78)
x, y, z = unpack_from("3f", datastream, pos+142)
id2data[blockid] = create_bounding_box(name, matrix.transposed(), x, y, z)
elif typeid == 4:
type_name = "Capsule"
s_x, s_y, s_z, e_x, e_y, e_z, r = unpack_from("3f 3f f", datastream, pos+78)
id2data[blockid] = create_capsule(name, mathutils.Vector((s_x, s_y, s_z)), mathutils.Vector((e_x, e_y, e_z)), r)
elif typeid == 6:
type_name = "MeshData"
BFRVertex, v_len, v_num = unpack_from("64s 2i", datastream, pos+77)
pos += 149
BFRVertex = BFRVertex.rstrip(b"\x00").decode("utf-8")[9:]
#This decodes the vertex format on the fly, should work on most if not all models. Some uncertainities about the last two, rare options.
formatstr = BFRVertex.replace("P","3f").replace("N","3f").replace("T0","2f").replace("T1","2f").replace("T2","2f").replace("T30","3f").replace("T31","3f").replace("T3D1","2f4B").replace("D","4B")
vertlist = list(iter_unpack(formatstr, datastream[pos : pos+v_len*v_num]))
u_cha, t_num = unpack_from("=b i", datastream, pos+v_len*v_num)
trilist = list(iter_unpack("3h", datastream[pos+v_len*v_num+5 : pos+v_len*v_num+5+t_num*2]))
#there are some dummies that do not work as they should but most seems to work.
#perhaps find a solution for D that works without texture
#4/17: changed vcol to ND to make it work without tex!
meshtypes = (("P",("ver",)), ("N",("nor",)), ("ND",("rgba",)), ("T0",("u0",)), ("T1",("u1",)), ("T2",("u2",)), ("T30",("u0","w",)), ("T31",("u1","c",)), ("T3D1",("u3","abcd",)), )
mesh_data = {"tri":trilist, "we":[]}
meshformat = []
#parse the meshtypes from the BFRVertex into dict and list for correct sorting
for letter, typetuple in meshtypes:
for meshtype in typetuple:
mesh_data[meshtype] = []
if letter in BFRVertex: meshformat.append(meshtype)
#sort the mesh_vert into the right mesh_data lists
for vert in vertlist:
x = 0
for meshtype in meshformat:
mesh_data[meshtype].append(vert[x : x+len(meshtype)])
x += len(meshtype)
id2data[blockid] = mesh_data
elif typeid in (5,8):
type_name = "Mesh"
dataid, u_int, t_sta, t_num, v_sta, v_num, f_num, x, y, z, s = unpack_from("7i 4f", datastream, pos+77)
mesh_data = id2data[dataid]
#this could be integrated in the mesh creator further down?
mesh = {}
for key in mesh_data:
if key == "tri":
mesh[key] = mesh_data[key][t_sta//3:(t_sta+t_num)//3]
else:
mesh[key] = mesh_data[key][v_sta:v_sta+v_num]
if typeid == 8:
type_name = "Mesh (skinned)"
numbones, numweights = unpack_from("2i", datastream, pos+121)
pos += 129
if not armature:
#create the armature
armData = bpy.data.armatures.new(basename[:-4])
armData.show_axes = True
armData.draw_type = 'STICK'
armature = create_ob(basename[:-4], armData)
armature.show_x_ray = True
bpy.ops.object.mode_set(mode = 'EDIT')
#read the armature block
mat_storage = {}
scales = {}
for x in range(0, numbones):
boneid, parentid, bonegroup, bonename = unpack_from("3b 64s", datastream, pos+x*131)
bonename = bfbname_to_blendername(bonename)
bind = mathutils.Matrix(list(iter_unpack('4f',datastream[pos+67+x*131:pos+67+x*131+64])))
#new support for bone scale
scale = bind.to_scale()[0]
if int(round(scale*1000)) != 1000:
# bind = mathutils.Matrix.Scale(1/scale, 4) * bind
scales[bonename] = scale
#create a bone
bone = armData.edit_bones.new(bonename)
#parent it and get the armature space matrix
if parentid > 0:
bind *= mat_storage[parentid]
bone.parent = armData.edit_bones[parentid-1]
#we store the bfb space armature matrix of each bone
mat_storage[boneid] = bind.copy()
#blender transposes matrices
bind.transpose()
#set transformation
bind = correction_global * correction_local * bind * correction_local.inverted()
tail, roll = mat3_to_vec_roll(bind.to_3x3())
bone.head = bind.to_translation()
bone.tail = tail + bone.head
bone.roll = roll
#fix the bone length
for edit_bone in armData.edit_bones:
fix_bone_length(edit_bone)
bpy.ops.object.mode_set(mode = 'OBJECT')
pos += numbones*131
bonenames = armature.data.bones.keys()
for vert in [((b0, w0), (b1, w1), (b2, w2), (b3, 1-w0-w1-w2)) for b0, b1, b2, b3, w0, w1, w2 in list(iter_unpack("4b 3f", datastream[pos:pos+numweights*16]))]:
mesh["we"].append([(bonenames[id], weight) for id, weight in vert if id > 0])
ob, me = mesh_from_data(name, mesh["ver"], mesh["tri"], False)
id2data[blockid] = ob
if mesh["we"]:
skinned_meshes.append(ob)
for i, vert in enumerate(mesh["we"]):
for bonename, weight in vert:
if bonename not in ob.vertex_groups: ob.vertex_groups.new(bonename)
ob.vertex_groups[bonename].add([i], weight, 'REPLACE')
#Do we have weights for the wind vertex shader? (UVW coordinates if you like) We store them as a vertex group so they can be easily modified.
if mesh["w"]:
print("Found fx_wind weights!")
ob.vertex_groups.new("fx_wind")
for i, vert in enumerate(mesh["w"]):
ob.vertex_groups["fx_wind"].add([i], vert[0], 'REPLACE')
for face in me.polygons: face.use_smooth = True
if use_custom_normals:
me.use_auto_smooth = True
me.normals_split_custom_set_from_vertices(mesh["nor"])
#UV: 1-V coordinate
for uv_layer in ("u0","u1","u2"):
if mesh[uv_layer]:
me.uv_textures.new(uv_layer[-1])
me.uv_layers[-1].data.foreach_set("uv", [uv for pair in [mesh[uv_layer][l.vertex_index] for l in me.loops] for uv in (pair[0], 1-pair[1])])
if mesh["rgba"]:
me.vertex_colors.new("RGB")
me.vertex_colors[-1].data.foreach_set("color", [c for col in [mesh["rgba"][l.vertex_index] for l in me.loops] for c in (col[2]/255, col[1]/255, col[0]/255)])
me.vertex_colors.new("AAA")
me.vertex_colors[-1].data.foreach_set("color", [c for col in [mesh["rgba"][l.vertex_index] for l in me.loops] for c in (col[3]/255, col[3]/255, col[3]/255)])
bpy.ops.object.mode_set(mode = 'EDIT')
if mirror_mesh and mesh["we"]:
bpy.ops.mesh.bisect(plane_co=(0,0,0), plane_no=(1,0,0), clear_inner=True)
bpy.ops.mesh.select_all(action='SELECT')
mod = ob.modifiers.new('Mirror', 'MIRROR')
mod.use_clip = True
mod.use_mirror_merge = True
mod.use_mirror_vertex_groups = True
mod.use_x = True
mod.merge_threshold = 0.001
#implement a custom remove doubles algorithm
#see which verts can be removed, find their indices and then make a new custom normals list
if not use_custom_normals:
bpy.ops.mesh.remove_doubles(threshold = 0.0001, use_unselected = False)
try:
bpy.ops.uv.seams_from_islands()
except:
log_error(ob.name+" has no UV coordinates!")
bpy.ops.object.mode_set(mode = 'OBJECT')
if mesh["we"]:
mod = ob.modifiers.new('SkinDeform', 'ARMATURE')
mod.object = armature
me.calc_normals()
else:
type_name = "unknown ID: "+str(typeid)
print('ID: %.i, Type: ' % int(blockid)+type_name+', End: %.i, Name: ' % int(blockend) + str(name))
pos = blockend
#Now comes the linked list part, it starts with the root block.
print("\nReading object hierarchy and creating blender objects...")
maxlod = read_linked_list(pos, None, 0)
#set the visible layers for this scene
bools = []
for i in range(20):
if i < maxlod or i == 5 or i == 0: bools.append(True)
else: bools.append(False)
bpy.context.scene.layers = bools
#handle scale on armature and meshes
if armature and scales:
#set inverse scale to all bones
for bonename, scale in scales.items():
pbone = armature.pose.bones[bonename]
pbone.matrix_basis = mathutils.Matrix.Scale(1/scale, 4)
bpy.context.scene.update()
#apply skin deformation
for ob in skinned_meshes:
ob.data = ob.to_mesh(bpy.context.scene, True, "PREVIEW", calc_tessface=False)
#remove scales from armature
bpy.context.scene.objects.active = armature
bpy.ops.object.mode_set(mode='POSE' )
bpy.ops.pose.armature_apply()
bpy.ops.object.mode_set(mode='OBJECT' )
#add scale back in as dummy action
scale_action = create_anim(armature, "!scale!")
for bonename, scale in scales.items():
fcurves = [scale_action.fcurves.new(data_path = 'pose.bones["'+bonename+'"].scale', index = i, action_group = bonename) for i in range(0, 3)]
for fcurve in fcurves: fcurve.keyframe_points.insert(0, scale)
success = '\nFinished BFB Import in %.2f seconds\n' %(time.clock()-starttime)
print(success)
return errors