-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathdrs_exporter.py
860 lines (718 loc) · 32.5 KB
/
drs_exporter.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
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
import math
import os
import subprocess
from os.path import dirname, realpath
from typing import List, Tuple
import bpy
import bmesh
from mathutils import Vector, Matrix
from numpy import mat, rot90
from .drs_file import DRS, CDspMeshFile, BattleforgeMesh, Face, EmptyString, LevelOfDetail, MeshData, Refraction, Textures, Texture, Vertex, Materials, Flow, CGeoMesh, CGeoOBBTree, DrwResourceMeta, CGeoPrimitiveContainer, CDspJointMap, CollisionShape, CylinderShape, CGeoCylinder, BoxShape, CGeoAABox, SphereShape, CGeoSphere, CMatCoordinateSystem, OBBNode
resource_dir = dirname(realpath(__file__)) + "/resources"
def show_message_box(msg: str, Title: str = "Message Box", Icon: str = "INFO") -> None:
def DrawMessageBox(self, context):
self.layout.label(text=msg)
bpy.context.window_manager.popup_menu(DrawMessageBox, title=Title, icon=Icon)
def ResetViewport() -> None:
for Area in bpy.context.screen.areas:
if Area.type in ['IMAGE_EDITOR', 'VIEW_3D']:
Area.tag_redraw()
bpy.context.view_layer.update()
def search_for_object(object_name: str, collection: bpy.types.Collection) -> bpy.types.Object | None:
'''Search for an object in a collection and its children by name. Returns the object if found, otherwise None.'''
for obj in collection.objects:
if obj.name.find(object_name) != -1:
return obj
for coll in collection.children:
found_object = search_for_object(object_name, coll)
if found_object is not None:
return found_object
return None
def mirror_mesh_on_axis(obj, axis='y'):
"""
Mirror a mesh along a specified global axis using Blender's built-in operations and
correctly handle normals.
Parameters:
obj (bpy.types.Object): The object to mirror.
axis (str): Global axis to mirror along, should be 'x', 'y', or 'z'.
"""
print(f"Mirroring object {obj.name} along axis {axis}...")
# Validate axis
axis = axis.lower()
if axis not in ['x', 'y', 'z']:
raise ValueError("Invalid axis. Use 'x', 'y', or 'z'.")
# Deselect all objects
bpy.ops.object.select_all(action='DESELECT')
# Select the object
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
# Ensure the object is selected
if not obj.select_get():
raise ValueError(f"Object {obj.name} could not be selected.")
# Determine the constraint axis
constraint_axis = {
'x': (True, False, False),
'y': (False, True, False),
'z': (False, False, True)
}[axis]
# Apply the mirror transformation
bpy.ops.object.mode_set(mode='OBJECT') # Ensure in object mode
bpy.ops.transform.mirror(
orient_type='GLOBAL',
orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)),
orient_matrix_type='GLOBAL',
constraint_axis=constraint_axis
)
# Optional: update mesh data
obj.data.update()
def get_bb(obj) -> Tuple[Vector, Vector]:
'''Get the Bounding Box of an Object. Returns the minimum and maximum Vector of the Bounding Box.'''
bb_min = Vector((0, 0, 0))
bb_max = Vector((0, 0, 0))
if obj.type == "MESH":
for _vertex in obj.data.vertices:
_vertex = _vertex.co
if _vertex.x < bb_min.x:
bb_min.x = _vertex.x
if _vertex.y < bb_min.y:
bb_min.y = _vertex.y
if _vertex.z < bb_min.z:
bb_min.z = _vertex.z
if _vertex.x > bb_max.x:
bb_max.x = _vertex.x
if _vertex.y > bb_max.y:
bb_max.y = _vertex.y
if _vertex.z > bb_max.z:
bb_max.z = _vertex.z
return bb_min, bb_max
def get_scene_bb(collection: bpy.types.Collection) -> Tuple[Vector, Vector]:
'''Get the Bounding Box of the whole Scene. Returns the minimum and maximum Vector of the Bounding Box.'''
bb_min = Vector((0, 0, 0))
bb_max = Vector((0, 0, 0))
for obj in collection.objects:
if obj.type == "MESH":
BBMinObject, BBMaxObject = get_bb(obj)
if BBMinObject.x < bb_min.x:
bb_min.x = BBMinObject.x
if BBMinObject.y < bb_min.y:
bb_min.y = BBMinObject.y
if BBMinObject.z < bb_min.z:
bb_min.z = BBMinObject.z
if BBMaxObject.x > bb_max.x:
bb_max.x = BBMaxObject.x
if BBMaxObject.y > bb_max.y:
bb_max.y = BBMaxObject.y
if BBMaxObject.z > bb_max.z:
bb_max.z = BBMaxObject.z
return bb_min, bb_max
def create_cylinder(mesh: bpy.types.Mesh) -> CylinderShape:
'''Create a Cylinder Shape from a Mesh Object.'''
cylinder_shape = CylinderShape()
cylinder_shape.CoordSystem = CMatCoordinateSystem()
cylinder_shape.CoordSystem.Position = Vector((mesh.location.x, mesh.location.y - (mesh.dimensions.z / 2), mesh.location.z))
rotation = mesh.rotation_euler.copy()
rotation.x -= math.pi / 2
cylinder_shape.CoordSystem.Matrix = Matrix.LocRotScale(None, rotation, None).to_3x3()
cylinder_shape.CGeoCylinder = CGeoCylinder()
cylinder_shape.CGeoCylinder.Radius = mesh.dimensions.x / 2
cylinder_shape.CGeoCylinder.Height = mesh.dimensions.z
cylinder_shape.CGeoCylinder.Center = Vector((0, 0, 0))
return cylinder_shape
def create_box(mesh: bpy.types.Mesh) -> BoxShape:
'''Create a Box Shape from a Mesh Object.'''
box_shape = BoxShape()
box_shape.CoordSystem = CMatCoordinateSystem()
box_shape.CoordSystem.Position = Vector((mesh.location.x, mesh.location.y, mesh.location.z))
rotation = mesh.rotation_euler.copy()
rotation.x = -rotation.x
rotation.y = -rotation.y
rotation.z = -rotation.z
box_shape.CoordSystem.Matrix = Matrix.LocRotScale(None, rotation, None).to_3x3()
box_shape.CGeoAABox = CGeoAABox()
box_shape.CGeoAABox.UpperRightCorner = Vector((mesh.dimensions.x / 2, mesh.dimensions.y / 2, mesh.dimensions.z / 2))
box_shape.CGeoAABox.LowerLeftCorner = Vector((-mesh.dimensions.x / 2, -mesh.dimensions.y / 2, -mesh.dimensions.z / 2))
return box_shape
def create_sphere(mesh: bpy.types.Mesh) -> SphereShape:
'''Create a Sphere Shape from a Mesh Object.'''
sphere_shape = SphereShape()
sphere_shape.CoordSystem = CMatCoordinateSystem()
sphere_shape.CoordSystem.Position = Vector((mesh.location.x, mesh.location.y, mesh.location.z))
sphere_shape.CoordSystem.Matrix = Matrix.Identity(3)
sphere_shape.CGeoSphere = CGeoSphere()
sphere_shape.CGeoSphere.Center = Vector((0, 0, 0))
sphere_shape.CGeoSphere.Radius = mesh.dimensions.x / 2
return sphere_shape
def create_obb_node(unique_mesh: bpy.types.Mesh) -> OBBNode:
'''Create an OBB Node for the OBB Tree.'''
obb_node = OBBNode()
obb_node.NodeDepth = 0
obb_node.CurrentTriangleCount = 0
obb_node.MinimumTrianglesFound = len(unique_mesh.polygons)
obb_node.Unknown1 = 0
obb_node.Unknown2 = 0
obb_node.Unknown3 = 0
obb_node.OrientedBoundingBox = CMatCoordinateSystem()
obb_node.OrientedBoundingBox.Position = Vector((0, 0, 0)) # We need to update this later as we need to calculate the center of the mesh
obb_node.OrientedBoundingBox.Matrix = Matrix.Identity(3) # We need to update this later as we need to calculate the rotation of the mesh
return obb_node
def create_unique_mesh(source_collection: bpy.types.Collection) -> bpy.types.Mesh:
'''Create a Unique Mesh from a Collection of Meshes.'''
bm: bmesh.types.BMesh = bmesh.new()
# As we have a defined structure we need to find the CDspMeshFile object first
if source_collection.objects is None:
return None
cdspmeshfile_object = search_for_object("CDspMeshFile", source_collection)
if cdspmeshfile_object is None:
return None
# Fix if the object is the Mesh itself
if cdspmeshfile_object.type == "MESH":
bm.from_mesh(cdspmeshfile_object.data)
else:
# Now we can iterate over the Meshes
for child in cdspmeshfile_object.children:
if child.type == "MESH":
bm.from_mesh(child.data)
# Remove Duplicates
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
# Create the new Mesh
unique_mesh = bpy.data.meshes.new("unique_mesh")
bm.to_mesh(unique_mesh)
bm.free()
return unique_mesh
def create_mesh(mesh: bpy.types.Mesh, mesh_index: int, model_name: str, filepath: str) -> BattleforgeMesh:
'''Create a Battleforge Mesh from a Blender Mesh Object.'''
mesh.data.calc_tangents()
new_mesh = BattleforgeMesh()
new_mesh.VertexCount = len(mesh.data.vertices)
new_mesh.FaceCount = len(mesh.data.polygons)
new_mesh.Faces = []
new_mesh.MeshCount = 2
new_mesh.MeshData = []
_mesh_0_data = MeshData()
_mesh_0_data.Vertices = [Vertex() for _ in range(new_mesh.VertexCount)]
_mesh_0_data.Revision = 133121
_mesh_0_data.VertexSize = 32
_mesh_1_data = MeshData()
_mesh_1_data.Vertices = [Vertex() for _ in range(new_mesh.VertexCount)]
_mesh_1_data.Revision = 12288
_mesh_1_data.VertexSize = 24
for _face in mesh.data.polygons:
new_face = Face()
new_face.Indices = []
for index in _face.loop_indices:
vertex = mesh.data.loops[index]
position = mesh.data.vertices[vertex.vertex_index].co
normal = vertex.normal
#TODO: Maybe we need to flip the Y value of the Normal as we convert from OpenGL to DirectX
uv = mesh.data.uv_layers.active.data[index].uv.copy()
uv.y = -uv.y
_mesh_0_data.Vertices[vertex.vertex_index] = Vertex(Position=position, Normal=normal, Texture=uv)
if new_mesh.MeshCount > 1:
tangent = vertex.tangent
bitangent = vertex.bitangent_sign * normal.cross(tangent)
# Switch X and Y as the Tangent is flipped
tangent = Vector((tangent.y, tangent.x, tangent.z))
_mesh_1_data.Vertices[vertex.vertex_index] = Vertex(Tangent=tangent, Bitangent=bitangent)
new_face.Indices.append(vertex.vertex_index)
new_mesh.Faces.append(new_face)
new_mesh.MeshData.append(_mesh_0_data)
new_mesh.MeshData.append(_mesh_1_data)
# We need to investigate the Bounding Box further, as it seems to be wrong
new_mesh.BoundingBoxLowerLeftCorner, new_mesh.BoundingBoxUpperRightCorner = get_bb(mesh)
new_mesh.MaterialID = 25702
# Node Group for Access the Data
MeshMaterial: bpy.types.Material = mesh.material_slots[0].material
MaterialNodes: List[bpy.types.Node] = MeshMaterial.node_tree.nodes
# Find the DRS Node
for Node in MaterialNodes:
if Node.type == "GROUP":
if Node.node_tree.name.find("DRS") != -1:
ColorMap = Node.inputs[0]
# ColorAlpha = Node.inputs[1] # We don't need this
NormalMap = Node.inputs[2]
MetallicMap = Node.inputs[3]
RoughnessMap = Node.inputs[4]
EmissionMap = Node.inputs[5]
ScratchMap = Node.inputs[6]
DistortionMap = Node.inputs[7]
RefractionMap = Node.inputs[8]
# RefractionAlpha = Node.inputs[9] # We don't need this
RefractionColor = Node.inputs[10]
FluMap = Node.inputs[11]
# FluAlpha = Node.inputs[12] # We don't need this
break
if FluMap is None or FluMap.is_linked is False:
# -86061055: no MaterialStuff, no Fluid, no String, no LOD
new_mesh.MaterialParameters = -86061055
else:
# -86061050: All Materials
new_mesh.MaterialParameters = -86061050
new_mesh.MaterialStuff = 0
# Level of Detail
new_mesh.LevelOfDetail = LevelOfDetail() # We don't need to update the LOD
# Empty String
new_mesh.EmptyString = EmptyString() # We don't need to update the Empty String
# Flow
new_mesh.Flow = Flow() # Maybe later we can add some flow data in blender
# Individual Material Parameters depending on the MaterialID:
new_mesh.BoolParameter = 0
BoolParamBitFlag = 0
# Textures
new_mesh.Textures = Textures()
# Check if the ColorMap exists
if ColorMap is None:
ValueError("The ColorMap Node is unset!")
output_folder = os.path.dirname(filepath)
if ColorMap.is_linked:
new_mesh.Textures.Length+=1
ColMapTexture = Texture()
ColMapTexture.Name = model_name + "_" + str(mesh_index) + "_col"
ColMapTexture.Length = ColMapTexture.Name.__len__()
ColMapTexture.Identifier = 1684432499
new_mesh.Textures.Textures.append(ColMapTexture)
# Check ColorMap.links[0].from_node.image for the Image
if ColorMap.links[0].from_node.type == "TEX_IMAGE":
# Export the Image as a DDS File (DXT3)
_Img = ColorMap.links[0].from_node.image
if _Img is not None:
_TempPath = bpy.path.abspath("//") + ColMapTexture.Name + ".png"
_Img.file_format = "PNG"
_Img.save(filepath=_TempPath)
args = ["-ft", "dds", "-f", "DXT5", "-dx9", "-pow2", "-srgb", "-y", ColMapTexture.Name + ".dds", "-o", output_folder]
subprocess.run([resource_dir + "/texconv.exe", _TempPath] + args, check=False)
os.remove(_TempPath)
else:
ValueError("The ColorMap Texture is not an Image or the Image is None!")
if NormalMap is not None and NormalMap.is_linked:
new_mesh.Textures.Length+=1
NorMapTexture = Texture()
NorMapTexture.Name = model_name + "_" + str(mesh_index) + "_nor"
NorMapTexture.Length = NorMapTexture.Name.__len__()
NorMapTexture.Identifier = 1852992883
new_mesh.Textures.Textures.append(NorMapTexture)
BoolParamBitFlag += 100000000000000000
# Check NormalMap.links[0].from_node.image for the Image
if NormalMap.links[0].from_node.type == "TEX_IMAGE":
# Export the Image as a DDS File (DXT1)
_Img = NormalMap.links[0].from_node.image
if _Img is not None:
_TempPath = bpy.path.abspath("//") + NorMapTexture.Name + ".png"
_Img.file_format = "PNG"
_Img.save(filepath=_TempPath)
args = ["-ft", "dds", "-f", "DXT1", "-dx9", "-pow2", "-srgb", "-at", "0.0", "-y", NorMapTexture.Name + ".dds", "-o", output_folder]
subprocess.run([resource_dir + "/texconv.exe", _TempPath] + args, check=False)
os.remove(_TempPath)
else:
ValueError("The NormalMap Texture is not an Image or the Image is None!")
if (MetallicMap is not None and MetallicMap.is_linked) or (RoughnessMap is not None and RoughnessMap.is_linked) or (EmissionMap is not None and EmissionMap.is_linked):
new_mesh.Textures.Length+=1
MetMapTexture = Texture()
MetMapTexture.Name = model_name + "_" + str(mesh_index) + "_par"
MetMapTexture.Length = MetMapTexture.Name.__len__()
MetMapTexture.Identifier = 1936745324
new_mesh.Textures.Textures.append(MetMapTexture)
BoolParamBitFlag += 10000000000000000
# Par Map is a combination of Metallic, Roughness and Fluid Map and Emission Map. We need to combine them in an array and push them to the ImageToExport List
img_R, img_G, img_A = None, None, None
pixels_R, pixels_G, pixels_A = None, None, None
if MetallicMap is not None and MetallicMap.is_linked:
# This can either be a Map or a Separate RGB Node
if MetallicMap.links[0].from_node.type == "SEPRGB" or MetallicMap.links[0].from_node.type == "SEPARATE_COLOR":
# We ned to get the Input
img_R = MetallicMap.links[0].from_node.inputs[0].links[0].from_node.image
assert img_R is not None and img_R.type == "IMAGE"
pixels_R = img_R.pixels[:]
else:
img_R = MetallicMap.links[0].from_node.image
assert img_R is not None and img_R.pixels[:].__len__() > 0
pixels_R = img_R.pixels[:]
if RoughnessMap is not None and RoughnessMap.is_linked:
# This can either be a Map or a Separate RGB Node
if RoughnessMap.links[0].from_node.type == "SEPRGB" or RoughnessMap.links[0].from_node.type == "SEPARATE_COLOR":
# We ned to get the Input
img_G = RoughnessMap.links[0].from_node.inputs[0].links[0].from_node.image
assert img_G is not None and img_G.type == "IMAGE"
pixels_G = img_G.pixels[:]
else:
img_G = RoughnessMap.links[0].from_node.image
assert img_G is not None and img_G.pixels[:].__len__() > 0
pixels_G = img_G.pixels[:]
if EmissionMap is not None and EmissionMap.is_linked:
pass
if EmissionMap is not None and EmissionMap.is_linked:
# This can either be a Map or a Separate RGB Node
if EmissionMap.links[0].from_node.type == "SEPRGB" or EmissionMap.links[0].from_node.type == "SEPARATE_COLOR":
# We ned to get the Input
img_A = EmissionMap.links[0].from_node.inputs[0].links[0].from_node.image
assert img_A is not None and img_A.type == "IMAGE"
pixels_A = img_A.pixels[:]
else:
img_A = EmissionMap.links[0].from_node.image
assert img_A is not None and img_A.pixels[:].__len__() > 0
pixels_A = img_A.pixels[:]
# Get the Image Size by either the R, G or A Image
if img_R is not None:
Width = img_R.size[0]
Height = img_R.size[1]
elif img_G is not None:
Width = img_G.size[0]
Height = img_G.size[1]
elif img_A is not None:
Width = img_A.size[0]
Height = img_A.size[1]
else:
ValueError("No Image found for the Parameter Map!")
# Combine the Images
new_img = bpy.data.images.new(name=MetMapTexture.Name, width=Width, height=Height, alpha=True, float_buffer=False)
new_pixels = []
for i in range(0, Width * Height * 4, 4):
red_value = pixels_R[i] if pixels_R is not None else 0
green_value = pixels_G[i + 1] if pixels_G is not None else 0
# TODO: Fluid
blue_value = 0
alpha_value = pixels_A[i + 3] if pixels_A is not None else 0
new_pixels.extend([red_value, green_value, blue_value, alpha_value])
new_img.pixels = new_pixels
new_img.file_format = "PNG"
new_img.update()
# Export the Image as a DDS File (DXT5)
_TempPath = bpy.path.abspath("//") + MetMapTexture.Name + ".png"
new_img.save(filepath=_TempPath)
# convert the image to dds dxt5 by using texconv.exe in the resources folder
args = ["-ft", "dds", "-f", "DXT5", "-dx9", "-bc", "d", "-pow2", "-y", MetMapTexture.Name + ".dds", "-o", output_folder]
subprocess.run([resource_dir + "/texconv.exe", _TempPath] + args, check=False)
# Remove the Temp File
os.remove(_TempPath)
# Set the Bool Parameter by a bin -> dec conversion
new_mesh.BoolParameter = int(str(BoolParamBitFlag), 2)
# Refraction
Ref = Refraction()
Ref.Length = 1
Ref.RGB = list(RefractionColor.default_value)[:3]
new_mesh.Refraction = Ref
# Materials
new_mesh.Materials = Materials() # Almost no material data is used in the game, so we set it to defaults
return new_mesh
def create_cgeo_mesh(unique_mesh: bpy.types.Mesh) -> CGeoMesh:
'''Create a CGeoMesh from a Blender Mesh Object.'''
_cgeo_mesh = CGeoMesh()
_cgeo_mesh.IndexCount = len(unique_mesh.polygons) * 3
_cgeo_mesh.VertexCount = len(unique_mesh.vertices)
_cgeo_mesh.Faces = []
_cgeo_mesh.Vertices = []
for _face in unique_mesh.polygons:
new_face = Face()
new_face.Indices = [_face.vertices[0], _face.vertices[1], _face.vertices[2]]
_cgeo_mesh.Faces.append(new_face)
for _vertex in unique_mesh.vertices:
_cgeo_mesh.Vertices.append(Vector((_vertex.co.x, _vertex.co.y, _vertex.co.z, 1.0)))
return _cgeo_mesh
def create_cgeo_obb_tree(unique_mesh: bpy.types.Mesh) -> CGeoOBBTree:
'''Create a CGeoOBBTree from a Blender Mesh Object.'''
_cgeo_obb_tree = CGeoOBBTree()
_cgeo_obb_tree.TriangleCount = len(unique_mesh.polygons)
_cgeo_obb_tree.Faces = []
for _face in unique_mesh.polygons:
new_face = Face()
new_face.Indices = [_face.vertices[0], _face.vertices[1], _face.vertices[2]]
_cgeo_obb_tree.Faces.append(new_face)
_cgeo_obb_tree.MatrixCount = 0
_cgeo_obb_tree.OBBNodes = []
for _ in range(_cgeo_obb_tree.MatrixCount):
_cgeo_obb_tree.OBBNodes.append(create_obb_node(unique_mesh))
return _cgeo_obb_tree
def create_cdsp_meshfile(source_collection: bpy.types.Collection, model_name: str, filepath: str) -> CDspMeshFile:
'''Create a CDspMeshFile from a Collection of Meshes.'''
cdspmeshfile_object = search_for_object("CDspMeshFile", source_collection)
_cdsp_meshfile = CDspMeshFile()
_cdsp_meshfile.MeshCount = 0
# Check if the CDspMeshFile Object is a Mesh Object
if cdspmeshfile_object.type == "MESH":
_cdsp_meshfile.MeshCount += 1
_cdsp_meshfile.Meshes.append(create_mesh(cdspmeshfile_object, _cdsp_meshfile.MeshCount, model_name, filepath))
else:
for child in cdspmeshfile_object.children:
if child.type == "MESH":
_cdsp_meshfile.MeshCount += 1
_cdsp_meshfile.Meshes.append(create_mesh(child, _cdsp_meshfile.MeshCount, model_name, filepath))
_cdsp_meshfile.BoundingBoxLowerLeftCorner = Vector((0, 0, 0))
_cdsp_meshfile.BoundingBoxUpperRightCorner = Vector((0, 0, 0))
for _mesh in _cdsp_meshfile.Meshes:
_cdsp_meshfile.BoundingBoxLowerLeftCorner.x = min(_cdsp_meshfile.BoundingBoxLowerLeftCorner.x, _mesh.BoundingBoxLowerLeftCorner.x)
_cdsp_meshfile.BoundingBoxLowerLeftCorner.y = min(_cdsp_meshfile.BoundingBoxLowerLeftCorner.y, _mesh.BoundingBoxLowerLeftCorner.y)
_cdsp_meshfile.BoundingBoxLowerLeftCorner.z = min(_cdsp_meshfile.BoundingBoxLowerLeftCorner.z, _mesh.BoundingBoxLowerLeftCorner.z)
_cdsp_meshfile.BoundingBoxUpperRightCorner.x = max(_cdsp_meshfile.BoundingBoxUpperRightCorner.x, _mesh.BoundingBoxUpperRightCorner.x)
_cdsp_meshfile.BoundingBoxUpperRightCorner.y = max(_cdsp_meshfile.BoundingBoxUpperRightCorner.y, _mesh.BoundingBoxUpperRightCorner.y)
_cdsp_meshfile.BoundingBoxUpperRightCorner.z = max(_cdsp_meshfile.BoundingBoxUpperRightCorner.z, _mesh.BoundingBoxUpperRightCorner.z)
return _cdsp_meshfile
def create_cdsp_jointmap(empty = True) -> CDspJointMap:
'''Create a CDspJointMap. If empty is True, the CDspJointMap will be empty.'''
if empty:
return CDspJointMap()
else:
pass
def create_drw_resource_meta() -> DrwResourceMeta:
'''Create a DrwResourceMeta.'''
return DrwResourceMeta()
def create_cgeo_primitive_container() -> CGeoPrimitiveContainer:
'''Create a CGeoPrimitiveContainer.'''
return CGeoPrimitiveContainer()
def create_collision_shape(source_collection: bpy.types.Collection) -> CollisionShape:
'''Create a Collision Shape from a Collection of Meshes.'''
_collision_shape = CollisionShape()
collision_shape_object = search_for_object("CollisionShape", source_collection)
if collision_shape_object is None:
return None
for child in collision_shape_object.children:
if child.type == "MESH":
if child.name.find("Cylinder") != -1:
_collision_shape.CylinderCount += 1
_collision_shape.Cylinders.append(create_cylinder(child))
elif child.name.find("Sphere") != -1:
_collision_shape.SphereCount += 1
_collision_shape.Spheres.append(create_sphere(child))
elif child.name.find("Box") != -1:
_collision_shape.BoxCount += 1
_collision_shape.Boxes.append(create_box(child))
return _collision_shape
def set_origin_to_world_origin(source_collection: bpy.types.Collection) -> None:
for obj in source_collection.objects:
if obj.type == "MESH":
# Set the object's active scene to the current scene
bpy.context.view_layer.objects.active = obj
# Select the object
obj.select_set(True)
# Set the origin to the world origin (0, 0, 0)
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
# Deselect the object
obj.select_set(False)
# Move the cursor back to the world origin
bpy.context.scene.cursor.location = (0.0, 0.0, 0.0)
def export_static_object(operator, context, filepath: str, source_collection: bpy.types.Collection, use_apply_transform: bool) -> None:
'''Export a Static Object to a DRS File.'''
# TODO: We need to set the world matrix correctly for Battleforge Game Engine -> Matrix.Identity(4)
# Model Name COmes right after the DRSModel_ Prefix and before the _Static Suffix
model_name = source_collection.name[source_collection.name.find("DRSModel_") + 9:source_collection.name.find("_Static")]
# Create an empty DRS File
new_drs_file: DRS = DRS()
# First we need to set the origin of all meshes to the center of the scene
set_origin_to_world_origin(source_collection)
if use_apply_transform:
# Get CDspMeshFile Object
cdspmeshfile_object = search_for_object("CDspMeshFile", source_collection)
# Apply the Transformation to the CDspMeshFile Object
for child in cdspmeshfile_object.children:
if child.type == "MESH":
mirror_mesh_on_axis(child, axis='y')
# Get the CollisionShape Object
collision_shape_object = search_for_object("CollisionShape", source_collection)
if collision_shape_object is not None:
# Apply the Transformation to the CollisionShape Object
for child in collision_shape_object.children:
if child.type == "MESH":
mirror_mesh_on_axis(child, axis='y')
unique_mesh = create_unique_mesh(source_collection) # Works perfectly fine
if unique_mesh is None:
show_message_box("Could not create Unique Mesh from Collection, as no CDspMeshFile was found!", "Error", "ERROR")
return {"CANCELLED"}
# CGeoMesh
_cgeo_mesh: CGeoMesh = create_cgeo_mesh(unique_mesh) # Works perfectly fine
new_drs_file.PushNode("CGeoMesh", _cgeo_mesh)
# CGeoOBBTree
_cgeo_obb_tree: CGeoOBBTree = create_cgeo_obb_tree(unique_mesh) # Maybe not needed??? We use a simple apporach for now with just one OBBNode, which is the whole mesh
new_drs_file.PushNode("CGeoOBBTree", _cgeo_obb_tree)
# CDspJointMap
_cdsp_jointmap: CDspJointMap = create_cdsp_jointmap() # Not needed for static objects, means we can leave it empty
new_drs_file.PushNode("CDspJointMap", _cdsp_jointmap)
# CDspMeshFile
_cdsp_meshfile: CDspMeshFile = create_cdsp_meshfile(source_collection, model_name, filepath) # Works perfectly fine
new_drs_file.PushNode("CDspMeshFile", _cdsp_meshfile)
# drwResourceMeta
_drw_resource_meta: DrwResourceMeta = create_drw_resource_meta() # Dunno if needed or how to create it
new_drs_file.PushNode("DrwResourceMeta", _drw_resource_meta)
# CollisionShape
# TODO: check if it is exported correctly
_collision_shape: CollisionShape = create_collision_shape(source_collection) # Works perfectly fine
if _collision_shape is not None:
new_drs_file.PushNode("collisionShape", _collision_shape)
# CGeoPrimitiveContainer
_cgeo_primitive_container: CGeoPrimitiveContainer = create_cgeo_primitive_container() # Always empty
new_drs_file.PushNode("CGeoPrimitiveContainer", _cgeo_primitive_container)
# Save the DRS File
new_drs_file.Save(filepath)
def verify_models(source_collection: bpy.types.Collection):
'''Check if the Models are valid for the game. This includes the following checks:
- Check if the Meshes have more than 32767 Vertices'''
unified_mesh: bmesh.types.BMesh = bmesh.new()
for obj in source_collection.objects:
if obj.type == "MESH":
if len(obj.data.vertices) > 32767:
show_message_box("Mesh {} has more than 32767 Vertices. This is not supported by the game.".format(obj.name), "Error", "ERROR")
return False
unified_mesh.from_mesh(obj.data)
unified_mesh.verts.ensure_lookup_table()
unified_mesh.verts.index_update()
bmesh.ops.remove_doubles(unified_mesh, verts=unified_mesh.verts, dist=0.0001)
if len(unified_mesh.verts) > 32767:
show_message_box("The unified Mesh has more than 32767 Vertices. This is not supported by the game.", "Error", "ERROR")
return False
unified_mesh.free()
return True
def triangulate(source_collection: bpy.types.Collection) -> None:
# Get the CDspMeshFile Object
cdspmeshfile_object = search_for_object("CDspMeshFile", source_collection)
for child in cdspmeshfile_object.children:
if child.type == "MESH":
bpy.context.view_layer.objects.active = child
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(child.data)
non_tri_faces = [f for f in bm.faces if len(f.verts) > 3]
if non_tri_faces:
bmesh.ops.triangulate(bm, faces=non_tri_faces)
bmesh.update_edit_mesh(child.data)
bpy.ops.object.mode_set(mode='OBJECT')
def duplicate_collection_hierarchy(source_collection, parent_collection=None, link_to_scene=True):
# Create a new collection with a modified name
new_collection = bpy.data.collections.new(name=source_collection.name + "_Copy")
if link_to_scene:
bpy.context.scene.collection.children.link(new_collection)
if parent_collection:
parent_collection.children.link(new_collection)
# Dictionary to keep track of old to new object mappings
old_to_new_objs = {}
# Function to duplicate object with hierarchy
def duplicate_obj(obj, parent_obj):
# Duplicate the object and its data
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
# Append '_copy' to the duplicated object's name
new_obj.name += "_Copy"
if new_obj.data and hasattr(new_obj.data, 'name'):
new_obj.data.name += "_Copy"
# Unlink the new object from all current collections it's linked to
for col in new_obj.users_collection:
col.objects.unlink(new_obj)
# Link the new object only to the new collection
new_collection.objects.link(new_obj)
old_to_new_objs[obj] = new_obj
# Set the parent if it's already duplicated
if parent_obj is not None and parent_obj in old_to_new_objs:
new_obj.parent = old_to_new_objs[parent_obj]
# Sort objects by their depth in the hierarchy
def sort_objects_by_hierarchy(objects):
obj_depth = {}
def assign_depth(obj, depth=0):
if obj in obj_depth:
return obj_depth[obj]
if obj.parent is None or obj.parent not in objects:
obj_depth[obj] = depth
return depth
obj_depth[obj] = assign_depth(obj.parent, depth + 1) + 1
return obj_depth[obj]
# Assign depth to all objects
for obj in objects:
if obj not in obj_depth:
assign_depth(obj)
# Return objects sorted by their depth
return sorted(objects, key=lambda o: obj_depth[o])
# Sort and then duplicate objects
sorted_objects = sort_objects_by_hierarchy(list(source_collection.objects))
for obj in sorted_objects:
duplicate_obj(obj, obj.parent)
# Set the new collection as active if linking to the scene
if link_to_scene:
bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection.children[new_collection.name]
# Recursively duplicate child collections and their objects
for child_col in source_collection.children:
duplicate_collection_hierarchy(child_col, parent_collection=new_collection, link_to_scene=False)
return new_collection
def split_meshes_by_uv_islands(source_collection: bpy.types.Collection) -> None:
'''Split the Meshes by UV Islands.'''
for obj in source_collection.objects:
if obj.type == "MESH":
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(obj.data)
# old seams
old_seams = [e for e in bm.edges if e.seam]
# unmark
for e in old_seams:
e.seam = False
# mark seams from uv islands
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.seams_from_islands()
seams = [e for e in bm.edges if e.seam]
# split on seams
bmesh.ops.split_edges(bm, edges=seams)
# re instate old seams.. could clear new seams.
for e in old_seams:
e.seam = True
bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode='OBJECT')
def save_drs(operator, context, filepath="", use_apply_transform=True, split_mesh_by_uv_islands=False, keep_debug_collections=False):
'''Save the DRS File.'''
# Get the right Collection
source_collection: bpy.types.Collection = None
# The Collection has the Name: DRSModel_Name
for coll in bpy.data.collections:
if coll.name.find("DRSModel_") != -1:
source_collection = coll
break
if source_collection is None:
show_message_box("No DRSModel Collection found!", "Error", "ERROR")
return {"CANCELLED"}
# We dont want to modify the original Collection so we create a copy
source_collection = duplicate_collection_hierarchy(source_collection)
# Be sure that there are only triangles in the Meshes
triangulate(source_collection)
# Verify the Models
if not verify_models(source_collection):
return {"CANCELLED"}
# Split the Meshes by UV Islands
if split_mesh_by_uv_islands:
split_meshes_by_uv_islands(source_collection)
# What we need in every DRS File (*created by this sript): CGeoMesh*, CGeoOBBTree (empty)*, CDspJointMap*, CDspMeshFile, DrwResourceMeta*
# What we need in skinned DRS Files: CSkSkinInfo, CSkSkeleton, AnimationSet, AnimationTimings
# Models with Effects: EffectSet
# Static Objects need: CGeoPrimitiveContainer (empty), CollisionShape
# Destructable Objects need: StateBasedMeshSet, MeshSetGrid
# Check the model's type, based on the Collection's name: DRSModel_Name_Type
# Type can be: Static for now (later we can add Skinned, Destructable, Effect, etc.)
if source_collection.name.find("Static") != -1:
export_static_object(operator, context, filepath, source_collection, use_apply_transform)
# Remove the copied Collection
if not keep_debug_collections:
bpy.data.collections.remove(source_collection)
return {"FINISHED"}
# # CollisionShape if static
# elif LoadedDRSModels is not None:
# # If we only Edit the Model(s) we keep the whole structures and only update the neccecary parts
# for LoadedDRSModel in LoadedDRSModels:
# LoadedDRSModel = LoadedDRSModel[0]
# source_file: DRS = LoadedDRSModel[1]
# source_collection: bpy.types.Collection = LoadedDRSModel[2]
# SourceUsedTransform: bool = LoadedDRSModel[3]
# # Set the current Collection as Active
# bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection.children[source_collection.name]
# # Set the Active object to the first object in the Collection
# bpy.context.view_layer.objects.active = source_collection.objects[0]
# # Switch to Edit Mode
# bpy.ops.object.mode_set(mode='EDIT')
# # If the Source Mesh has been transformed, we need to revert the transformation before we can export it
# # Set the Global Matrix
# if SourceUsedTransform:
# # Get the Armature object if it exists
# ArmatureObject = None
# for object in source_collection.objects:
# if object.type == "ARMATURE":
# ArmatureObject = object
# break
# if ArmatureObject is not None:
# ArmatureObject.matrix_world = Matrix.Identity(4)
# else:
# for object in source_collection.objects:
# object.matrix_world = Matrix.Identity(4)
# # Update the Meshes
# update_cdspmeshfile(source_file, source_collection)