forked from reattiva/Urho3D-Blender
-
Notifications
You must be signed in to change notification settings - Fork 4
/
decompose.py
2535 lines (2180 loc) · 105 KB
/
decompose.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
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# This script is licensed as public domain.
# Based on "Export Inter-Quake Model (.iqm/.iqe)" by Lee Salzman
#
# http://www.blender.org/documentation/blender_python_api_2_63_2/info_best_practice.html
# http://www.blender.org/documentation/blender_python_api_2_63_2/info_gotcha.html
# Blender types:
# http://www.blender.org/documentation/blender_python_api_2_63_7/bpy.types.Mesh.html
# http://www.blender.org/documentation/blender_python_api_2_63_7/bpy.types.MeshTessFace.html
# http://www.blender.org/documentation/blender_python_api_2_63_7/bpy.types.Material.html
# UV:
# http://www.blender.org/documentation/blender_python_api_2_63_2/bpy.types.MeshTextureFaceLayer.html
# http://www.blender.org/documentation/blender_python_api_2_63_2/bpy.types.MeshTextureFace.html
# Skeleton:
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Armature.html
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Bone.html
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Pose.html
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.PoseBone.html
# Animations:
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Action.html
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.AnimData.html
# Vertex color:
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.MeshColor.html
# Morphs (Shape keys):
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Key.html
# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.ShapeKey.html
# Inverse transpose for normals
# http://www.arcsynthesis.org/gltut/Illumination/Tut09%20Normal%20Transformation.html
# Python binary writing:
# http://docs.python.org/2/library/struct.html
DEBUG = 0
import bpy
import bmesh
import math
import time as ostime
from mathutils import Vector, Matrix, Quaternion, Euler, Color
from collections import OrderedDict
import os
import operator
import heapq
import logging
import re
from bpy_extras import io_utils, node_shader_utils
log = logging.getLogger("ExportLogger")
#------------------
# Geometry classes
#------------------
# Vertex class
class TVertex:
def __init__(self):
# Index of the vertex in the Blender buffer
self.blenderIndex = None
# Position of the vertex: Vector((0.0, 0.0, 0.0))
self.pos = None
# Normal of the vertex: Vector((0.0, 0.0, 0.0))
self.normal = None
# Color of the vertex: (0, 0, 0, 0)...(255, 255, 255, 255)
self.color = None
# UV coordinates of the vertex: Vector((0.0, 0.0))..Vector((1.0, 1.0))
self.uv = None
# UV2 coordinates of the vertex: Vector((0.0, 0.0))..Vector((1.0, 1.0))
self.uv2 = None
# Tangent of the vertex: Vector((0.0, 0.0, 0.0, 0.0))
self.tangent = None
# Bitangent of the vertex: Vector((0.0, 0.0, 0.0))
self.bitangent = None
# Bones weights: list of tuple(boneIndex, weight)
self.weights = None
# returns True is this vertex is a changed morph of vertex 'other'
def isMorphed(self, other):
# TODO: compare floats with a epsilon margin?
if other.pos is None:
return True
if self.pos and self.pos != other.pos:
return True
if self.normal and self.normal != other.normal:
return True
# We cannot use tangent, it is not calculated yet
if self.uv and self.uv != other.uv:
return True
return False
# used by the function index() of lists
def __eq__(self, other):
# TODO: can we do without weights?
# TODO: compare floats with a epsilon margin?
#return (self.__dict__ == other.__dict__)
return (self.pos == other.pos and
self.normal == other.normal and
self.uv == other.uv and
self.color == other.color)
def isEqual(self, other):
# TODO: compare floats with a epsilon margin?
return self == other
def __hash__(self):
hashValue = 0
if self.pos:
hashValue ^= hash(self.pos.x) ^ hash(self.pos.y) ^ hash(self.pos.z)
if self.normal:
hashValue ^= hash(self.normal.x) ^ hash(self.normal.y) ^ hash(self.normal.z)
if self.uv:
hashValue ^= hash(self.uv.x) ^ hash(self.uv.y)
if self.color:
hashValue ^= hash((self.color[3] << 24) | (self.color[2] << 16) | (self.color[1] << 8) | self.color[0])
return hashValue
def __str__(self):
s = " coords: {: .3f} {: .3f} {: .3f}".format(self.pos.x, self.pos.y, self.pos.z)
s += "\n normals: {: .3f} {: .3f} {: .3f}".format(self.normal.x, self.normal.y, self.normal.z)
if self.color:
s += "\n color: {:3d} {:3d} {:3d} {:3d}".format(self.color[0], self.color[1], self.color[2], self.color[3])
if self.uv:
s += "\n uv: {: .3f} {: .3f}".format(self.uv[0], self.uv[1])
if self.uv2:
s += "\n uv2: {: .3f} {: .3f}".format(self.uv2[0], self.uv2[1])
if self.tangent:
s += "\n tangent: {: .3f} {: .3f} {: .3f}".format(self.tangent.x, self.tangent.y, self.tangent.z)
if self.weights:
s += "\n weights: "
for w in self.weights:
s += "{:d} {:.3f} ".format(w[0],w[1])
return s
# Geometry LOD level class
class TLodLevel:
def __init__(self):
self.distance = 0.0
# Set of all vertex indices use by this LOD
self.indexSet = set()
# List of triangles of the LOD (triples of vertex indices)
self.triangleList = []
def __str__(self):
s = " distance: {:.3f}\n".format(self.distance)
s += " triangles: "
for i, t in enumerate(self.triangleList):
if i and (i % 5) == 0:
s += "\n "
s += "{:3d} {:3d} {:3d} |".format(t[0],t[1],t[2])
return s
# Geometry class
class TGeometry:
def __init__(self):
# List of TLodLevel
self.lodLevels = []
# Name of the Blender material associated
self.materialName = None
def __str__(self):
s = ""
for i, l in enumerate(self.lodLevels):
s += " {:d}\n".format(i) + str(l)
return s
#------------------
# Morph classes
#------------------
class TMorph:
def __init__(self, name):
# Morph name
self.name = name
# Set of all vertex indices used by this morph
self.indexSet = set()
# List of triangles of the morph (triples of vertex indices)
self.triangleList = []
# Maps vertex index to morphed TVertex
self.vertexMap = {}
def __str__(self):
s = " name: {:s}\n".format(self.name)
s += " Vertices: "
for k, v in sorted(self.vertices.items()):
s += "\n index: {:d}".format(k)
s += "\n" + str(v)
return s
#-------------------
# Materials classes
#-------------------
# NOTE: in Blender images names are unique
class TMaterial:
def __init__(self, name):
# Material name
self.name = name
# Diffuse color (0.0, 0.0, 0.0)
self.diffuseColor = None
# Diffuse intensity (0.0)
self.diffuseIntensity = None
# Specular color (0.0, 0.0, 0.0)
self.specularColor = None
# Specular intensity (0.0)
self.specularIntensity = None
# Specular hardness (1.0)
self.specularHardness = None
# Emit color (0.0, 0.0, 0.0)
self.emitColor = None
# Emit factor (1.0)
self.emitIntensity = None
# Opacity (1.0)
self.opacity = None
# Alpha Mask
self.alphaMask = False
# Material is two sided
self.twoSided = False
'''
# Diffuse color texture filename (no path)
self.diffuseTexName = None
# Normal texture filename (no path)
self.normalTexName = None
# Specular texture filename (no path)
self.specularTexName = None
# Emit texture filename (no path)
self.emitTexName = None
# Light map texture filename (no path)
self.lightmapTexName = None
# Ambient light map texture filename (light map modulated by ambient color)(no path)
self.ambientLightTexName = None
'''
# Material is shadeless
self.shadeless = False
# Textures names (no path)
# keys: diffuse, specular, normal, emissive, ao, lightmap
self.texturesNames = {}
def __eq__(self, other):
if hasattr(other, 'name'):
return (self.name == other.name)
return (self.name == other)
def __str__(self):
return (" name: {:s}\n".format(self.name) )
#--------------------
# Animations classes
#--------------------
class TBone:
def __init__(self, index, parentName, position, rotation, scale, transform, length):
# Position of the bone in the OrderedDict
self.index = index
# Name of the parent bone
self.parentName = parentName
# Bone position in the parent bone tail space (you first apply this)
self.bindPosition = position
# Bone rotation in the parent bone tail space (and then this)
self.bindRotation = rotation
# Bone scale
self.bindScale = scale
# Bone transformation in object space
self.worldTransform = transform
# Bone length
self.length = length
def __str__(self):
s = " bind pos " + str(self.bindPosition)
s += "\n bind rot " + str(self.bindRotation) #+ "\n" + str(self.bindRotation.to_axis_angle())
#s += "\n" + str(self.worldTransform.inverted())
s += "\n" + str(self.worldTransform)
return s
class TFrame:
def __init__(self, time, position, rotation, scale):
self.time = time
self.position = position
self.rotation = rotation
self.scale = scale
def hasMoved(self, other):
return (self.position != other.position or self.rotation != other.rotation or self.scale != other.scale)
class TTrack:
def __init__(self, name):
self.name = name
self.frames = []
class TTrigger:
def __init__(self, name):
# Trigger name
self.name = name
# Time in seconds
self.time = None
# Time as ratio
self.ratio = None
# Event data (variant, see typeNames[] in Variant.cpp)
self.data = None
class TAnimation:
def __init__(self, name):
self.name = name
self.tracks = []
self.triggers = []
#---------------------
# Export data classes
#---------------------
class TData:
def __init__(self):
self.objectName = None
self.blenderObjectName = None
# List of all the TVertex of all the geometries
self.verticesList = []
# List of TGeometry, they contains triangles, triangles are made of vertex indices
self.geometriesList = []
# List of TMorph: a subset of the vertices list with modified position
self.morphsList = []
# List of TMaterial
self.materialsList = []
# Material name to geometry index map
self.materialGeometryMap = {}
# Ordered dictionary of TBone: bone name to TBone
self.bonesMap = OrderedDict()
# List of TAnimation, one animation per object
self.animationsList = []
# Common TAnimation, one track per object
self.commonAnimation = None
class TOptions:
def __init__(self):
self.lodUpdatedGeometryIndices = set()
self.lodDistance = None
self.doForceElements = False
self.mergeObjects = False
self.mergeNotMaterials = False
self.useLods = False
self.onlySelected = False
self.orientation = Quaternion((1.0, 0.0, 0.0, 0.0))
self.scale = 1.0
self.globalOrigin = True
self.bonesGlobalOrigin = False #useless
self.actionsGlobalOrigin = False
self.applyModifiers = False
self.doBones = True
self.doOnlyKeyedBones = False
self.doOnlyDeformBones = False
self.doOnlyVisibleBones = False
self.actionsByFcurves = False
self.skinBoneParent = False
self.derigifyArmature = False
self.doAnimations = True
self.doObjAnimations = False
self.doAllActions = True
self.doCurrentAction = False
self.doUsedActions = False
self.doSelectedActions = False
self.doSelectedStrips = False
self.doSelectedTracks = False
self.doStrips = False
self.doTracks = False
self.doTimeline = False
self.doTriggers = False
self.doAnimationExtraFrame = True
self.doAnimationPos = True
self.doAnimationRot = True
self.doAnimationSca = True
self.filterSingleKeyFrames = False
self.doGeometries = True
self.doGeometryPos = True
self.doGeometryNor = True
self.doGeometryCol = True
self.doGeometryColAlpha = False
self.doGeometryUV = True
self.doGeometryUV2 = False
self.doGeometryTan = True
self.doGeometryWei = True
self.doMorphs = True
self.doMorphNor = True
self.doMorphTan = True
self.doMorphUV = True
self.doOptimizeIndices = True
self.doMaterials = True
#--------------------
# “Computing Tangent Space Basis Vectors for an Arbitrary Mesh” by Lengyel, Eric.
# Terathon Software 3D Graphics Library, 2001.
# http://www.terathon.com/code/tangent.html
#--------------------
def GenerateTangents(tLodLevels, tVertexList, errorsMem):
if not tVertexList:
log.warning("No vertices, tangent generation cancelled.")
return
nullUvIndices = None
incompleteUvIndices = None
if errorsMem:
nullUvIndices = errorsMem.Get("null UV area", set() )
incompleteUvIndices = errorsMem.Get("incomplete UV", set() )
# Init the values
tangentOverwritten = 0
for tLodLevel in reversed(tLodLevels):
if not tLodLevel.indexSet or not tLodLevel.triangleList:
log.warning("Empty LOD, tangent generation skipped.")
tLodLevels.remove(tLodLevel)
continue
for vertexIndex in tLodLevel.indexSet:
vertex = tVertexList[vertexIndex]
# Check if the tangent was already calculated (4 components) for this vertex and we're overwriting it
if vertex.tangent and len(vertex.tangent) == 4:
tangentOverwritten += 1
# Check if we have all the needed data to do the calculations
if vertex.pos is None:
if incompleteUvIndices is not None:
incompleteUvIndices.add(vertex.blenderIndex)
log.warning("Missing position on vertex {:d}, tangent generation cancelled.".format(vertex.blenderIndex[1]))
return
if vertex.normal is None:
if incompleteUvIndices is not None:
incompleteUvIndices.add(vertex.blenderIndex)
log.warning("Missing normal on vertex {:d}, tangent generation cancelled.".format(vertex.blenderIndex[1]))
return
if vertex.uv is None:
if incompleteUvIndices is not None:
incompleteUvIndices.add(vertex.blenderIndex)
log.warning("Missing UV on vertex {:d}, tangent generation cancelled.".format(vertex.blenderIndex[1]))
return
# Init tangent (3 components) and bitangent vectors
vertex.tangent = Vector((0.0, 0.0, 0.0))
vertex.bitangent = Vector((0.0, 0.0, 0.0))
if tangentOverwritten:
log.warning("Overwriting {:d} tangents".format(tangentOverwritten))
# Calculate tangent and bitangent
invalidUV = False
for tLodLevel in tLodLevels:
for i, triangle in enumerate(tLodLevel.triangleList):
# For each triangle, we have 3 vertices vertex1, vertex2, vertex3, each of the have their UV coordinates, we want to
# find two unit orthogonal vectors (tangent and bitangent) such as we can express each vertex position as a function
# of the vertex UV:
# VertexPosition = Tangent * f'(VertexUV) + BiTangent * f"(VertexUV)
# Actually we are going to express them relatively to a vertex chosen as origin (vertex1):
# vertex - vertex1 = Tangent * (vertex.u - vertex1.u) + BiTangent * (vertex.v - vertex1.v)
# We have two equations, one for vertex2-vertex1 and one for vertex3-vertex1, if we put them in a system and solve it
# we can obtain Tangent and BiTangent:
# [T; B] = [u1, v1; u2, v2]^-1 * [V2-V1; V3-V1]
vertex1 = tVertexList[triangle[0]]
vertex2 = tVertexList[triangle[1]]
vertex3 = tVertexList[triangle[2]]
# First equation: [x1, y1, z1] = Tangent * u1 + BiTangent * v1
x1 = vertex2.pos.x - vertex1.pos.x
y1 = vertex2.pos.y - vertex1.pos.y
z1 = vertex2.pos.z - vertex1.pos.z
u1 = vertex2.uv.x - vertex1.uv.x
v1 = vertex2.uv.y - vertex1.uv.y
# Second equation: [x2, y2, z2] = Tangent * u2 + BiTangent * v2
x2 = vertex3.pos.x - vertex1.pos.x
y2 = vertex3.pos.y - vertex1.pos.y
z2 = vertex3.pos.z - vertex1.pos.z
u2 = vertex3.uv.x - vertex1.uv.x
v2 = vertex3.uv.y - vertex1.uv.y
# Determinant of the matrix [u1 v1; u2 v2]
d = u1 * v2 - u2 * v1
# If the determinant is zero then the points (0,0), (u1,v1), (u2,v2) are in line, this means
# the area on the UV map of this triangle is null. This is an error, we must skip this triangle.
if d == 0:
if nullUvIndices is not None:
nullUvIndices.add(vertex1.blenderIndex)
nullUvIndices.add(vertex2.blenderIndex)
nullUvIndices.add(vertex3.blenderIndex)
invalidUV = True
continue
t = Vector( ((v2 * x1 - v1 * x2) / d, (v2 * y1 - v1 * y2) / d, (v2 * z1 - v1 * z2) / d) )
b = Vector( ((u1 * x2 - u2 * x1) / d, (u1 * y2 - u2 * y1) / d, (u1 * z2 - u2 * z1) / d) )
vertex1.tangent += t;
vertex2.tangent += t;
vertex3.tangent += t;
vertex1.bitangent += b;
vertex2.bitangent += b;
vertex3.bitangent += b;
if invalidUV:
log.error("Invalid UV, the area in the UV map is too small.")
# Gram-Schmidt orthogonalize normal, tangent and bitangent
for tLodLevel in tLodLevels:
for vertexIndex in tLodLevel.indexSet:
vertex = tVertexList[vertexIndex]
# Skip already calculated vertices
if len(vertex.tangent) == 4:
continue
# Unit vector perpendicular to normal and in the same plane of normal and tangent
tOrtho = ( vertex.tangent - vertex.normal * vertex.normal.dot(vertex.tangent) ).normalized()
# Unit vector perpendicular to the plane of normal and tangent
bOrtho = vertex.normal.cross(vertex.tangent).normalized()
# Calculate handedness: if bOrtho and bitangent have the different directions, save the verse
# in tangent.w, so we can reconstruct bitangent by: tangent.w * normal.cross(tangent)
w = 1.0 if bOrtho.dot(vertex.bitangent) >= 0.0 else -1.0
vertex.bitangent = bOrtho
vertex.tangent = Vector((tOrtho.x, tOrtho.y, tOrtho.z, w))
#--------------------
# Linear-Speed Vertex Cache Optimisation algorithm by Tom Forsyth
# https://home.comcast.net/~tom_forsyth/papers/fast_vert_cache_opt.html
#--------------------
# This is an optimized version, but it is still slow.
# (on an average pc, 5 minutes for 30K smooth vertices)
# We try to sort triangles in the index buffer so that we gain an optimal use
# of the hardware vertices cache.
# We assign a score to each triangle, we find the best and save it in a new
# ordered list.
# The score of each triangle is the sum of the score of its vertices, and the
# score of a vertex is higher if it is:
# - used recently (it is still in the cache) but we also try to avoid the last
# triangle added (n this way we get better result),
# - lonely isolated vertices (otherwise the will be keep for last and drawing
# them will require an higher cost)
# The order of vertices in the triangle does not matter.
# We'll apply this optimization to each lod of each geometry.
# These are the constants used in the algorithm:
VERTEX_CACHE_SIZE = 32
CACHE_DECAY_POWER = 1.5
LAST_TRI_SCORE = 0.75
VALENCE_BOOST_SCALE = 2.0
VALENCE_BOOST_POWER = 0.5
def CalculateScore(rank):
if rank.useCount == 0:
rank.score = -1.0
return
score = 0.0
cachePosition = rank.cachePosition
if cachePosition < 0:
# Vertex is not in FIFO cache - no score
pass
elif cachePosition < 3:
# This vertex was used in the last triangle,
# so it has a fixed score, whichever of the three
# it's in. Otherwise, you can get very different
# answers depending on whether you add
# the triangle 1,2,3 or 3,1,2 - which is silly.
score = LAST_TRI_SCORE
else:
# Points for being high in the cache
score = 1.0 - float(rank.cachePosition - 3) / (VERTEX_CACHE_SIZE - 3)
score = pow(score, CACHE_DECAY_POWER)
# Bonus points for having a low number of tris still to
# use the vert, so we get rid of lone verts quickly
valenceBoost = VALENCE_BOOST_SCALE * pow(rank.useCount, -VALENCE_BOOST_POWER);
rank.score = score + valenceBoost;
# Triangles score list sizes
TRIANGLERANK_SIZE = 500
TRIANGLERANK_MAX_SIZE = 505
def OptimizeIndices(lodLevel):
# Ranks are used to store data for each vertex
class Rank:
def __init__(self):
self.score = 0.0
self.useCount = 1
self.cachePosition = -1
# Create a map: vertex index to its corresponding Rank
ranking = {}
# This list contains the original triangles (not in optimal order), we'll move them
# one by one in a new list following the optimal order
oldTriangles = lodLevel.triangleList
# For each vertex index of each triangle increment the use counter
# (we can find the same vertex index more than once)
for triangle in oldTriangles:
for index in triangle:
try:
ranking[index].useCount += 1
except KeyError:
ranking[index] = Rank()
# Calculate the first round of scores
# (Rank is mutable, so CalculateScore will be able to modify it)
for rank in ranking.values():
CalculateScore(rank)
# Ths list will contain the triangles sorted in optimal order
newTriangles = []
# Cache of vertex indices
vertexCache = []
# The original algorithm was:
# - scan all the old triangles and find the one with the best score;
# - move it to the new triangles;
# - move its vertices in the cache;
# - recalculate the score on all the vertices on the cache.
# The slowest part is the first step, scanning all the old triangles,
# but in the last step we update only a little subset of these triangles,
# and it is a waste to recalculate the triangle score of each old triangle.
# So we do this:
# - create a map 'trianglesMap': vertex index to triangles;
# - keep a list 'trianglesRanking' of the best triangles;
# - at first this list is empty, we start adding triangles; we add tuples like
# (score, triangle) and we keep track of the min score, we don't add triangles
# with score lower than the min; for now we add triangles without bothering
# about order; if the triangle is already present in the list we only update
# its score (even if it is lower);
# - when the list is a little too big (TRIANGLERANK_MAX_SIZE), we sort the list
# by score and we only keep the best TRIANGLERANK_SIZE triangles, we update
# the min score;
# - after scanning all the old triangles, we take out from the list the best
# triangle;
# - move it to the new triangles and remove it from the map;
# - move its vertices in the cache;
# - recalculate the score on all the vertices in the cache, if the score of one
# vertex is changed, we use the map to find what triangles are affected and
# we add them to the list (unordered and only if their score is > min);
# - now when we repeat we have the list already populated, so we don't need to
# recalculate all old triangles scores, we only need to sort the list and take
# out the best triangle.
# Vertex index to triangle indices list map
trianglesMap = {}
# Populate the map
for triangle in oldTriangles:
for vertexIndex in triangle:
try:
triangleList = trianglesMap[vertexIndex]
except KeyError:
triangleList = []
trianglesMap[vertexIndex] = triangleList
triangleList.append(triangle)
class TrianglesRanking:
def __init__(self):
self.ranklist = []
self.min = None
self.isSorted = True
def update(self, triangle):
# Sum the score of all its vertex.
# >> This is the slowest part of the algorithm <<
triangleScore = ranking[triangle[0]].score + ranking[triangle[1]].score + ranking[triangle[2]].score
# If needed, add it to the list
if not self.ranklist:
self.ranklist.append( (triangleScore, triangle) )
self.min = triangleScore
else:
# We add only triangles with score > min
if triangleScore > self.min:
found = False
# Search of the triangle is already present in the list
for i, rank in enumerate(self.ranklist):
if triangle == rank[1]:
if triangleScore != rank[0]:
self.ranklist[i] = (triangleScore, triangle)
self.isSorted = False
found = True
break
# It is a new triangle
if not found:
self.ranklist.append( (triangleScore, triangle) )
self.isSorted = False
def sort(self):
if self.isSorted:
return
#self.ranklist = sorted(self.ranklist, key=operator.itemgetter(0), reverse=True)[:TRIANGLERANK_SIZE]
self.ranklist = heapq.nlargest(TRIANGLERANK_SIZE, self.ranklist, key = operator.itemgetter(0))
self.min = self.ranklist[-1][0]
self.isSorted = True
def popBest(self):
bestTriangle = self.ranklist[0][1]
del self.ranklist[0]
return bestTriangle
trianglesRanking = TrianglesRanking()
# Progress counter
progressCur = 0
progressTot = 0.01 * len(oldTriangles)
if DEBUG: ttt = ostime.time() #!TIME
# While there still are unsorted triangles
while oldTriangles:
# Print progress
if (progressCur & 0x7F) == 0:
print("{:.3f}%\r".format(progressCur / progressTot), end='' )
progressCur += 1
# When the list is empty, we need to scan all the old triangles
if not trianglesRanking.ranklist:
for triangle in oldTriangles:
# We add the triangle but we don't search for the best one
trianglesRanking.update(triangle)
# If the list is too big, sort and truncate it
if len(trianglesRanking.ranklist) > TRIANGLERANK_MAX_SIZE:
trianglesRanking.sort()
if trianglesRanking:
# Only if needed, we sort and truncate
trianglesRanking.sort()
# We take the best triangles out of the list
bestTriangle = trianglesRanking.popBest()
else:
log.error("Could not find next triangle")
return
# Move the best triangle to the output list
oldTriangles.remove(bestTriangle)
newTriangles.append(bestTriangle)
# Model the LRU cache behaviour
# Recreate the cache removing the vertices of the best triangle
vertexCache = [i for i in vertexCache if i not in bestTriangle]
for vertexIndex in bestTriangle:
# Then push them to the front
vertexCache.insert(0, vertexIndex)
# Decrement the use counter of its vertices
ranking[vertexIndex].useCount -= 1
# Remove best triangle from the map
triangleList = trianglesMap[vertexIndex]
triangleList.remove(bestTriangle)
# Update positions & scores of all vertices in the cache
# Give position -1 if vertex is going to be erased
for i, vertexIndex in enumerate(vertexCache):
rank = ranking[vertexIndex]
if (i > VERTEX_CACHE_SIZE):
rank.cachePosition = -1
else:
rank.cachePosition = i
# Calculate the new score
oldScore = rank.score
CalculateScore(rank)
# If the score is changed
if oldScore != rank.score:
# Add to the list all the triangles affected
triangleList = trianglesMap[vertexIndex]
for triangle in triangleList:
trianglesRanking.update(triangle)
# Finally erase the extra vertices
vertexCache[:] = vertexCache[:VERTEX_CACHE_SIZE]
if DEBUG: print("[TIME2] {:.4f}".format(ostime.time() - ttt) ) #!TIME
# Rewrite the index data now
lodLevel.triangleList = newTriangles
#--------------------
# Decompose armatures
#--------------------
def SetRestPosePosition(context, armatureObj):
if not armatureObj:
return None
# Force the armature in the rest position (warning: https://developer.blender.org/T24674)
# This should reset bones matrices ok, but for sure it is not resetting the mesh tessfaces
# positions
savedPosePositionAndVisibility = [armatureObj.data.pose_position, armatureObj.hide_viewport]
armatureObj.data.pose_position = 'REST'
armatureObj.hide_viewport = False
# This should help to recalculate all the mesh vertices, it is needed by decomposeMesh
# and maybe it helps decomposeArmature (but no problem was seen there)
# TODO: find the correct way, for sure it is not this
objects = context.view_layer.objects
savedObjectActive = objects.active
objects.active = armatureObj
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
objects.active = savedObjectActive
return savedPosePositionAndVisibility
def RestorePosePosition(armatureObj, savedValue):
if not armatureObj:
return
armatureObj.data.pose_position = savedValue[0]
armatureObj.hide_viewport = savedValue[1]
# -- Rigify --
# In a Rigify system there are ORG bones and DEF bones. The DEF bones causes the mesh deformation
# and from their corresponding vertex groups we'll get all the vertices weights. The ORG bones
# are used to reconstruct the DEF bones hierarchy (they have deform off).
# Normally you can find the DEF bones in the third to last armature layer (29) and the ORG bones
# in last layer (31).
# The association between ORG and DEF bones is done by the names with a pair "ORG-<name>" and
# "DEF-<name>" (e.g. ORG-SPINE and DEF-spine). Sometimes a ORG bone covers multiple DEF bones,
# in this case the DEF bones have name "DEF-<name>.<number>" (e.g. ORG-thigh.L, DEF-thigh.L.01,
# DEF-thigh.L.02) the lowest number is the root of the group and it is linked to the ORG bone.
# -- Derigify --
# We need a list of deform bones ('defbones') and their hierarchy ('defparent', 'defchildren').
# We start by searching all the DEF and ORG bones. Then we try to associate the DEF bones to
# their corresponding ORG bones, we need it in both ways: DEF to ORG and ORG to DEF.
# An ORG bone can be associated with multiple DEF bones, in this case the DEF bones have a number
# in their name. We order this list of DEF bones by name, so we can get their correct structure,
# the lowest number (01) is the root then follows their children.
# Now we can reconstruct the hierarchy. For each DEF bone X we get the list of DEF bones of the
# associate ORG bone Y. X will be in the list, if it is the first (it name can be "DEF-<name>"
# or "DEF-<name>.01") we get the parent P of the ORG bone Y, we get the list of DEF bones of P,
# we set the last bone in this list (lower child) as the parent of X. If X is not the first we
# set the previous DEF bone in the list as the parent of X ('defparent').
# Now for each bone X with a parent Y, we set X as a child of Y ('defchildren').
# Finally for each DEF bone with no parent (root DEF bone) we go down to all its hierarchy and
# for each bone we traverse we store the tuple 'bone, parent bone' in the list 'boneList'.
# -- Note --
# To make it worse from some version of Rigify (<=0.4) the name convention was changed from
# DEF/ORG-<name>.L/R.<number> to DEF/ORG-<name>.<number>.L/R
def DerigifyArmature(armature, tOptions):
# Map {ORG bone name: Blender ORG bone}
orgbones = {}
# Map {DEF bone name: Blender DEF bone}
defbones = {}
# Map {ORG bone name: list of DEF bones names}
org2defs = {}
# Map {DEF bone name: ORG bone name}
def2org = {}
# Map {DEF bone name: list of children DEF bones}
defchildren = {}
# Map {DEF bone name: its parent DEF bone name}
defparent = {}
# List of names of bad DEF bones
badbones = []
# Experimental extended search for the Sintel model (does not work)
extended = False
# Flag for bad Rigify rig
badrig = False
# Scan the armature and collect ORG bones and DEF bones in the maps by their names,
# we remove ORG- or DEF- from names
for bone in armature.bones.values():
if bone.name.startswith('ORG-'):
orgbones[bone.name[4:]] = bone
org2defs[bone.name[4:]] = []
elif bone.name.startswith('DEF-'):
if tOptions.doOnlyVisibleBones and not any(al and bl for al,bl in zip(armature.layers, bone.layers)):
continue
if tOptions.doOnlyDeformBones and not bone.use_deform:
continue
defbones[bone.name[4:]] = bone
defchildren[bone.name[4:]] = []
# Populate the org2defs with all the DEF bones corresponding to a ORG bone and def2org
# with the unique ORG bone corresponding to a DEF bone.
# For each DEF bone in the map get its name and Blender bone
for name, bone in defbones.items():
orgname = name
# Search if exist an ORG bone with the same name of this DEF bone (None if not found)
orgbone = orgbones.get(orgname)
# If this ORG bone does not exist, then the DEF bone name could be DEF-<name>.<number>,
# so we remove .<number> and search for the ORG bone again
if not orgbone:
# Search with new format: <name>.<number>.L/R (e.g. "DEF-thigh.02.L")
prog = re.compile("^(.+)\.\d+(\.[L,R])$")
mo = prog.match(name)
# Search with old format: <name>.L/R.<number> (e.g. "DEF-thigh.L.02")
if not mo:
prog = re.compile("^(.+)(\.[L,R])\.\d+$")
mo = prog.match(name)
# If the format is correct try to find the ORG bone
if mo:
orgname = mo.group(1) + mo.group(2)
orgbone = orgbones.get(orgname)
# Still no ORG bone, is it a chain of DEF bones till a ORG bone ?
if not orgbone and mo and extended:
pbone = bone.parent
while pbone:
# If the parent is a ORG bone, we have found it
if pbone.name.startswith('ORG-'):
orgname = pbone.name[4:]
orgbone = orgbones.get(orgname)
break
# Fail if the parent is not a DEF bone
if not pbone.name.startswith('DEF-'):
break
# Fail if the parent has a different format
pmo = prog.match(pbone.name[4:])
if not pmo or pmo.groups() != mo.groups():
break
pbone = pbone.parent
# If we cannot find a ORG bone for the DEF bone, this is a bad rig
if not orgbone:
badbones.append(name)
badrig = True
continue
# Map the ORG name (can be None) to the DEF name (one to many)
org2defs[orgname].append(name)
# Map the DEF name to the ORG name (can be None) (one to one)
def2org[name] = orgname
# Delete bad DEF bones
if badbones:
log.warning("Bad DEF bones with no ORG skipped: {:s}".format( ", ".join(badbones) ))
for name in badbones:
del defbones[name]
badbones.clear()
# Sort DEF bones names in the ORG to DEF map, so we get: <name>.0, <name>.1, <name>.2 ...
for defs in org2defs.values():
defs.sort()
# Populate the parent map of each DEF bone
for name, bone in defbones.items():
# Get the relative ORG bone name (can be None)
orgname = def2org[name]
# Get the ORG bone
orgbone = orgbones.get(orgname)
# Get the list (sorted by number) of DEF bones associated to the ORG bone
defs = org2defs[orgname]
if orgbone:
# Get the index of the DEF bone in the list
i = defs.index(name)
# If it is the first (it has the lowest number, e.g. <name>.0)
if i == 0:
orgparent = orgbone.parent
# If the ORG parent bone exists and it is an ORG bone
while orgparent and orgparent.name.startswith('ORG-'):
orgpname = orgparent.name[4:]
# Map this DEF bone to the last DEF bone of the ORG parent bone
pdefs = org2defs[orgpname]
if pdefs:
defparent[name] = pdefs[-1]
break
# If the ORG has no DEF bones we can try with its parent
if not extended:
badbones.append(orgbone.parent.name)
badrig = True
break
orgparent = orgparent.parent
else:
# Map this DEF bone to the previous DEF bone in the list (it has a lower number)
defparent[name] = defs[i-1]
# Populate the children list of each DEF bone
for name in defbones.keys():
# If this DEF bone has a parent, append it as a child of the parent
if name in defparent:
defchildren[defparent[name]].append(name)
# List bad ORG bones
if badbones:
log.warning("Bad ORG bones with no DEF children: {:s}".format( ", ".join(badbones) ))
badbones.clear()
bonesList = []
# Warning for not standard rig
if badrig:
log.warning("Incompatible Rigify rig")
##return bonesList
# Recursively add children