-
Notifications
You must be signed in to change notification settings - Fork 58
Expand file tree
/
Copy pathosgdata.py
More file actions
1895 lines (1579 loc) · 81.7 KB
/
Copy pathosgdata.py
File metadata and controls
1895 lines (1579 loc) · 81.7 KB
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
# -*- python-indent: 4; mode: python -*-
# -*- coding: UTF-8 -*-
#
# Copyright (C) 2008-2012 Cedric Pinson
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Authors:
# Cedric Pinson <cedric@plopbyte.com>
# Jeremy Moles <jeremy@emperorlinux.com>
import bpy
import mathutils
import json
import math
import os
import shutil
import subprocess
import osg
from . import osglog
from . import osgconf
from .osgutils import *
from .osgconf import DEBUG
from . import osgbake
from . import osgobject
from .osgobject import *
osgobject.VERSION = osg.__version__
Euler = mathutils.Euler
Matrix = mathutils.Matrix
Vector = mathutils.Vector
Quaternion = mathutils.Quaternion
Log = osglog.log
def createAnimationUpdate(blender_object, callback, rotation_mode, prefix="", zero=False):
has_location_keys = False
has_scale_keys = False
has_rotation_keys = False
has_constraints = hasSolidConstraints(blender_object)
has_nla = hasNLATracks(blender_object)
# Use local transform matrix at t=0 to initialize stacked transforms.
scene = bpy.context.scene
backup_frame = scene.frame_current
scene.frame_set(0)
blender_object.update_tag(refresh={'OBJECT'})
scene.update()
lcl_transform = blender_object.matrix_local.copy()
scene.frame_set(backup_frame)
scene.update()
if blender_object.animation_data:
action = blender_object.animation_data.action
if action:
for curve in action.fcurves:
datapath = curve.data_path[len(prefix):]
Log("curve.data_path {} {} {}".format(curve.data_path, curve.array_index, datapath))
if datapath == "location":
has_location_keys = True
if datapath.startswith("rotation"):
has_rotation_keys = True
if datapath == "scale":
has_scale_keys = True
if not (has_location_keys or has_scale_keys or has_rotation_keys) and not has_constraints and not has_nla:
return None
if zero:
if has_location_keys:
tr = StackedTranslateElement()
tr.translate = Vector()
callback.stacked_transforms.append(tr)
if has_rotation_keys:
if rotation_mode in ["XYZ", "XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX"]:
rotation_keys = [StackedRotateAxisElement(name="euler_x", axis=Vector((1, 0, 0)), angle=0),
StackedRotateAxisElement(name="euler_y", axis=Vector((0, 1, 0)), angle=0),
StackedRotateAxisElement(name="euler_z", axis=Vector((0, 0, 1)), angle=0)]
callback.stacked_transforms.append(rotation_keys[ord(blender_object.rotation_mode[2]) - ord('X')])
callback.stacked_transforms.append(rotation_keys[ord(blender_object.rotation_mode[1]) - ord('X')])
callback.stacked_transforms.append(rotation_keys[ord(blender_object.rotation_mode[0]) - ord('X')])
if rotation_mode == "QUATERNION":
q = StackedQuaternionElement()
q.quaternion = Quaternion()
callback.stacked_transforms.append(q)
if rotation_mode == "AXIS_ANGLE":
callback.stacked_transforms.append(StackedRotateAxisElement(name="axis_angle",
axis=Vector((1, 0, 0)),
angle=0))
if has_scale_keys:
sc = StackedScaleElement()
sc.scale = Vector(blender_object.scale)
callback.stacked_transforms.append(sc)
else:
tr = StackedTranslateElement()
tr.translate = Vector(lcl_transform.to_translation())
callback.stacked_transforms.append(tr)
if rotation_mode in ["XYZ", "XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX"]:
lcl_rotation = lcl_transform.to_euler()
rotation_keys = [StackedRotateAxisElement(name="euler_x", axis=Vector((1, 0, 0)),
angle=lcl_rotation[0]),
StackedRotateAxisElement(name="euler_y", axis=Vector((0, 1, 0)),
angle=lcl_rotation[1]),
StackedRotateAxisElement(name="euler_z", axis=Vector((0, 0, 1)),
angle=lcl_rotation[2])]
callback.stacked_transforms.append(rotation_keys[ord(blender_object.rotation_mode[2]) - ord('X')])
callback.stacked_transforms.append(rotation_keys[ord(blender_object.rotation_mode[1]) - ord('X')])
callback.stacked_transforms.append(rotation_keys[ord(blender_object.rotation_mode[0]) - ord('X')])
if rotation_mode == "QUATERNION":
q = StackedQuaternionElement()
q.quaternion = lcl_transform.to_quaternion()
callback.stacked_transforms.append(q)
if rotation_mode == "AXIS_ANGLE":
lcl_axis_angle = lcl_transform.to_quaternion().to_axis_angle()
callback.stacked_transforms.append(StackedRotateAxisElement(name="axis_angle",
axis=lcl_axis_angle[0],
angle=lcl_axis_angle[1]))
sc = StackedScaleElement()
sc.scale = Vector(lcl_transform.to_scale())
callback.stacked_transforms.append(sc)
return callback
def createAnimationMaterialAndSetCallback(osg_node, blender_object, config, unique_objects):
Log("Warning: [[blender]] Update material animations are not yet supported")
return None
# return createAnimationsObject(osg_node, blender_object, config, UpdateMaterial(), uniq_anims)
class UniqueObject(object):
def __init__(self):
self.statesets = {}
self.textures = {}
self.objects = {}
def hasObject(self, obj):
return obj in self.objects
def getObject(self, obj):
if self.hasObject(obj):
return self.objects[obj]
return None
def registerObject(self, obj, reg):
self.objects[obj] = reg
def hasTexture(self, obj):
return obj in self.textures
def getTexture(self, obj):
if self.hasTexture(obj):
return self.textures[obj]
return None
def registerTexture(self, obj, reg):
self.textures[obj] = reg
def hasStateSet(self, obj):
return obj in self.statesets
def getStateSet(self, obj):
if self.hasStateSet(obj):
return self.statesets[obj]
return None
def registerStateSet(self, obj, reg):
self.statesets[obj] = reg
class Export(object):
def __init__(self, config=None):
object.__init__(self)
self.items = []
self.config = config
if self.config is None:
self.config = osgconf.Config()
self.config.defaultattr('scene', bpy.context.scene)
self.rest_armatures = []
self.animations = []
self.baked_actions = []
self.current_animation = None
self.images = set()
self.lights = {}
self.root = None
self.unique_objects = UniqueObject()
self.parse_all_actions = False # if only one object and several actions
def clean_generated_actions(self):
for action in self.baked_actions:
if action.users != 0:
Log('Warning: The exporter generated actions that are still attached to objects. Cleaning users')
action.user_clear()
try:
bpy.data.actions.remove(action)
except:
Log('Can''t remove generated action')
def isExcluded(self, blender_object):
return blender_object.name in self.config.exclude_objects
def setArmatureInRestMode(self):
self.rest_armatures = setArmaturesPosePosition(self.config.scene, 'REST')
def restoreArmaturePoseMode(self):
setArmaturesPosePosition(self.config.scene, 'POSE', self.rest_armatures)
self.rest_armatures = []
def exportItemAndChildren(self, blender_object):
item = self.exportChildrenRecursively(blender_object, None, None)
if item is not None:
self.items.append(item)
def evaluateGroup(self, blender_object, item, rootItem):
if blender_object.dupli_group is None or len(blender_object.dupli_group.objects) == 0:
return
Log("resolving {} for {} offset {}".format(blender_object.dupli_group.name,
blender_object.name,
blender_object.dupli_group.dupli_offset))
group = MatrixTransform()
group.matrix = Matrix.Translation(-blender_object.dupli_group.dupli_offset)
item.children.append(group)
# for group we disable the only visible
config_visible = self.config.only_visible
self.config.only_visible = False
for o in blender_object.dupli_group.objects:
Log("object {}".format(o))
self.exportChildrenRecursively(o, group, rootItem)
self.config.only_visible = config_visible
# and restore it after processing group
def getName(self, blender_object):
if hasattr(blender_object, "name"):
return blender_object.name
return "no name"
def isObjectVisible(self, blender_object):
return blender_object.is_visible(self.config.scene) or not self.config.only_visible
def createAnimationsObject(self,
osg_object,
blender_object,
config,
update_callback,
unique_objects,
parse_all_actions=False):
if not config.export_anim or len(bpy.data.actions) == 0:
return None
has_action = blender_object.animation_data and hasAction(blender_object)
has_constraints = hasSolidConstraints(blender_object) or hasExternalBoneConstraints(blender_object)
has_morph = hasShapeKeysAnimation(blender_object)
if blender_object.type != 'ARMATURE' and not has_morph and not update_callback:
return None
if not has_action and not has_constraints and not has_morph and not hasNLATracks(blender_object):
return None
if parse_all_actions and not has_action and not has_morph and not hasNLATracks(blender_object):
return None
if has_constraints and (blender_object.parent and blender_object.parent.type == 'ARMATURE'):
return None
action2animation = BlenderAnimationToAnimation(object=blender_object,
config=config,
unique_objects=unique_objects,
has_action=has_action,
has_constraints=has_constraints,
has_morph=has_morph)
if parse_all_actions:
self.animations = action2animation.parseAllActions()
else:
# must have only one animation here
if not self.current_animation:
self.current_animation = Animation()
self.current_animation.setName('Take 01')
self.animations.append(self.current_animation)
action2animation.handleAnimationBaking()
action2animation.addActionDataToAnimation(self.current_animation)
if has_morph:
# Bake morph animation
action2animation.handleMorphAnimationBaking()
action2animation.addActionDataToAnimation(self.current_animation, morph=True)
# Remove actions created by the exporter for baking
self.baked_actions.extend(action2animation.get_generated_actions())
if update_callback:
if blender_object.type == 'ARMATURE':
osg_object.update_callbacks = []
osg_object.update_callbacks.append(update_callback)
def exportChildrenRecursively(self, blender_object, parent, osg_root):
def parseArmature(blender_armature):
osg_object = self.createSkeleton(blender_object)
self.createAnimationsObject(osg_object, blender_object, self.config,
createAnimationUpdate(blender_object,
UpdateMatrixTransform(name=osg_object.name),
rotation_mode),
self.unique_objects,
self.parse_all_actions)
return osg_object
def parseLight(blender_light):
matrix = getDeltaMatrixFrom(blender_object.parent, blender_object)
osg_object = MatrixTransform()
osg_object.setName(blender_object.name)
osg_object.matrix = matrix
lightItem = self.createLight(blender_object)
self.createAnimationsObject(osg_object, blender_object, self.config,
createAnimationUpdate(blender_object,
UpdateMatrixTransform(name=osg_object.name),
rotation_mode),
self.unique_objects,
self.parse_all_actions)
osg_object.children.append(lightItem)
return osg_object
# Mesh, Camera and Empty objects
def parseBlenderObject(blender_object, is_visible):
# because it blender can insert inverse matrix, we have to recompute the parent child
# matrix for our use.
matrix = getDeltaMatrixFrom(blender_object.parent, blender_object)
osg_object = MatrixTransform()
osg_object.setName(blender_object.name)
osg_object.matrix = matrix.copy()
if self.config.zero_translations and parent is None:
if bpy.app.version[0] >= 2 and bpy.app.version[1] >= 62:
print("zero_translations option has not been converted to blender 2.62")
else:
osg_object.matrix[3].xyz = Vector()
self.createAnimationsObject(osg_object, blender_object, self.config,
createAnimationUpdate(blender_object,
UpdateMatrixTransform(name=osg_object.name),
rotation_mode),
self.unique_objects,
self.parse_all_actions)
if is_visible:
if blender_object.type == "MESH":
osg_geode = self.createGeodeFromObject(blender_object)
osg_object.children.append(osg_geode)
else:
self.evaluateGroup(blender_object, osg_object, osg_root)
return osg_object
def handleBoneChild(blender_object, osg_object):
bone = findBoneInHierarchy(osg_root, spaceSafe(blender_object.parent_bone + '_' +
str(blender_object.parent.name)))
if bone is None:
Log("Warning: [[blender]] {} not found".format(blender_object.parent_bone))
else:
armature = blender_object.parent
original_pose_position = armature.data.pose_position
armature.data.pose_position = 'REST'
boneInWorldSpace = armature.matrix_world \
* armature.pose.bones[blender_object.parent_bone].matrix
matrix = getDeltaMatrixFromMatrix(boneInWorldSpace, blender_object.matrix_world)
osg_object.matrix = matrix
bone.children.append(osg_object)
armature.data.pose_position = original_pose_position
# We skip the object if it is in the excluded objects list
if self.isExcluded(blender_object):
return None
# Check if the object is visible. The visibility will be used for meshes and lights
# to determine if we keep it or not. Other objects have to be taken into account even if they
# are not visible as they can be used as modifiers (avoiding some crashs during the export)
is_visible = self.isObjectVisible(blender_object)
Log("")
osg_object = None
rotation_mode = 'QUATERNION' if self.config.use_quaternions else blender_object.rotation_mode
if self.unique_objects.hasObject(blender_object):
Log("{} '{}' has already been parsed, reuse osg_object".format(blender_object.type, blender_object.name))
osg_object = self.unique_objects.getObject(blender_object)
else:
Log("Parsing object '{}' of type {}".format(blender_object.name, blender_object.type))
if blender_object.type == "ARMATURE":
osg_object = parseArmature(blender_object)
elif blender_object.type == "LAMP" and is_visible:
osg_object = parseLight(blender_object)
elif blender_object.type in ['MESH', 'EMPTY', 'CAMERA']:
osg_object = parseBlenderObject(blender_object, is_visible)
else:
Log("Warning: [[blender]] Skipping object {} (objects {} are not exported)"
.format(blender_object.name, blender_object.type))
return None
self.unique_objects.registerObject(blender_object, osg_object)
if osg_root is None:
osg_root = osg_object
# Handle parenting
if blender_object.parent_type == "BONE":
handleBoneChild(blender_object, osg_object)
elif parent:
parent.children.append(osg_object)
children = getChildrenOf(self.config.scene, blender_object)
for child in children:
self.exportChildrenRecursively(child, osg_object, osg_root)
return osg_object
def createSkeleton(self, blender_object):
Log("processing Armature {}".format(blender_object.name))
# if no animation, set it in pose mode to bake it
use_pose = not (hasAction(blender_object) or hasNLATracks(blender_object)) and not \
(hasExternalBoneConstraints(blender_object)) and not self.config.arm_rest
if use_pose and blender_object in self.rest_armatures:
setArmaturesPosePosition(self.config.scene, 'POSE', [blender_object])
roots = getRootBonesList(blender_object.data)
matrix = getDeltaMatrixFrom(blender_object.parent, blender_object)
skeleton = Skeleton(blender_object.name, matrix)
for bone in roots:
b = Bone(blender_object, bone)
b.buildBoneChildren(use_pose=use_pose)
skeleton.children.append(b)
skeleton.collectBones()
if use_pose and blender_object in self.rest_armatures:
setArmaturesPosePosition(self.config.scene, 'REST', [blender_object])
return skeleton
def preProcess(self):
def lookForAnimatedObjects():
scene = self.config.scene
nb_animated_objects = 0
for obj in scene.objects:
# FIXME not sure about the constraint check here
if hasAction(obj) or \
hasSolidConstraints(obj) or \
hasExternalBoneConstraints(obj) or \
hasNLATracks(obj) or \
hasShapeKeysAnimation(obj):
nb_animated_objects += 1
self.parse_all_actions = nb_animated_objects == 1
def checkNameEncoding(elements, label, renamed_count):
for element in elements:
try:
element.name
except UnicodeDecodeError:
element.name = 'renamed_{}_{}'.format(label, renamed_count)
renamed_count += 1
def resolveMisencodedNames(scene):
''' Replace misencoded object names to avoid errors '''
scene = bpy.context.scene
renamed_count = 0
renamed_count = checkNameEncoding(scene.objects, 'object', renamed_count)
for obj in scene.objects:
renamed_count = checkNameEncoding(obj.modifiers, 'modifier', renamed_count)
renamed_count = checkNameEncoding(obj.vertex_groups, 'vertex_group', renamed_count)
renamed_count = checkNameEncoding(bpy.data.armatures, 'armature', renamed_count)
for arm in bpy.data.armatures:
renamed_count = checkNameEncoding(arm.bones, 'bone', renamed_count)
renamed_count = checkNameEncoding(bpy.data.materials, 'material', renamed_count)
renamed_count = checkNameEncoding(bpy.data.textures, 'texture', renamed_count)
renamed_count = checkNameEncoding(bpy.data.images, 'image', renamed_count)
renamed_count = checkNameEncoding(bpy.data.curves, 'curve', renamed_count)
renamed_count = checkNameEncoding(bpy.data.cameras, 'camera', renamed_count)
renamed_count = checkNameEncoding(bpy.data.lamps, 'lamp', renamed_count)
renamed_count = checkNameEncoding(bpy.data.metaballs, 'metaball', renamed_count)
if renamed_count:
print('Warning: [[blender]] {} entities having misencoded names were renamed'.format(renamed_count))
def make_dupliverts_real(scene):
''' Duplicates vertex instances and makes them real'''
unselectAllObjects()
# Select all objects that use dupli_vertex mode
selectObjects([obj for obj in scene.objects if obj.dupli_type == 'VERTS' and obj.children])
# Duplicate all instances into real objects
if bpy.context.selected_objects:
bpy.ops.object.duplicates_make_real(use_base_parent=True, use_hierarchy=True)
print('Warning: [[blender]] Some instances (duplication at vertex) were duplicated as real objects')
# Clear selection
unselectAllObjects()
# The following process may alter object selection, so
# we need to save it
backup_selection = bpy.context.selected_objects
make_dupliverts_real(self.config.scene)
resolveMisencodedNames(self.config.scene)
lookForAnimatedObjects()
# restore the user's selection
unselectAllObjects()
selectObjects(backup_selection)
def process(self):
self.preProcess()
# Object.resetWriter()
self.scene_name = self.config.scene.name
Log("current scene {}".format(self.scene_name))
if self.config.validFilename() is False:
self.config.filename += self.scene_name
self.config.createLogfile()
self.setArmatureInRestMode()
try:
if self.config.object_selected is not None:
o = bpy.data.objects[self.config.object_selected]
try:
self.config.scene.objects.active = o
self.config.scene.objects.selected = [o]
except ValueError:
Log("Error, problem happens when assigning object {} to scene {}"
.format(o.name, self.config.scene.name))
raise
for obj in self.config.scene.objects:
Log("obj {}".format(obj.name))
if (self.config.selected == "SELECTED_ONLY_WITH_CHILDREN" and obj.select) or \
(self.config.selected == "ALL" and obj.parent is None):
self.exportItemAndChildren(obj)
finally:
self.restoreArmaturePoseMode()
self.clean_generated_actions()
self.postProcess()
# OSG requires that rig geometry be a child of the skeleton,
# but Blender does not. Move any meshes that are modified by
# an armature to be under the armature.
def reparentRiggedGeodes(self, item, parent):
if isinstance(item, MatrixTransform) \
and len(item.children) == 1 \
and isinstance(item.children[0], Geode) \
and not isinstance(parent, Skeleton):
geode = item.children[0]
Log("geode {}".format(geode.name))
# some blend files has a armature_modifier but a None object
# so we have to test armature_modifier and armature_modifier.object
if geode.armature_modifier is not None and geode.armature_modifier.object:
parent.children.remove(item)
modifier_object = item.children[0].armature_modifier.object
arm = self.unique_objects.getObject(modifier_object)
for (k, v) in self.unique_objects.objects.items():
if v == item:
meshobj = k
item.matrix = getDeltaMatrixFromMatrix(item.children[0].armature_modifier.object.matrix_world,
meshobj.matrix_world)
arm.children.append(item)
Log("NOTICE: Reparenting {} to {}".format(geode.name, arm.name))
if hasattr(item, "children"):
for c in list(item.children):
self.reparentRiggedGeodes(c, item)
def postProcess(self):
# set only one root to the scene
self.root = None
self.root = Group()
self.root.setName("Root")
self.root.children = self.items
self.root.getOrCreateUserData().append(StringValueObject("source", "blender"))
if len(self.animations) > 0:
animation_manager = BasicAnimationManager()
animation_manager.animations = self.animations
self.root.update_callbacks.append(animation_manager)
self.reparentRiggedGeodes(self.root, None)
# index light num for opengl use and enable them in a stateset
if len(self.lights) > 0:
st = StateSet()
self.root.stateset = st
if len(self.lights) > 8:
Log("Warning: [[blender]] The model has more than 8 lights")
# retrieve world to global ambient
lm = LightModel()
lm.ambient = (1.0, 1.0, 1.0, 1.0)
if self.config.scene.world is not None:
amb = self.config.scene.world.ambient_color
lm.ambient = (amb[0], amb[1], amb[2], 1.0)
st.attributes.append(lm)
# add by default
st.attributes.append(Material())
light_num = 0
for name, ls in self.lights.items():
ls.light.light_num = light_num
key = "GL_LIGHT{}".format(light_num)
st.modes[key] = "ON"
light_num += 1
for key in self.unique_objects.statesets.keys():
stateset = self.unique_objects.statesets[key]
if stateset is not None: # register images to unpack them at the end
images = getImageFilesFromStateSet(stateset)
for i in images:
self.images.add(i)
def write(self):
if len(self.items) == 0:
if self.config.log_file is not None:
self.config.closeLogfile()
return
filename = self.config.getFullName("osgt")
Log("write file to {}".format(filename))
with open(filename, "wb") as sfile:
# sfile.write(str(self.root).encode('utf-8'))
self.root.writeFile(sfile)
nativePath = os.path.join(os.path.abspath(self.config.getFullPath()), self.config.texture_prefix)
# blenderPath = bpy.path.relpath(nativePath)
if len(self.images) > 0:
try:
if not os.path.exists(nativePath):
os.mkdir(nativePath)
except:
Log("can't create textures directory {}".format(nativePath))
raise
copied_images = []
for i in self.images:
if i is not None:
imagename = bpy.path.basename(createImageFilename("", i))
try:
if i.packed_file:
original_filepath = i.filepath_raw
try:
if len(imagename.split('.')) == 1:
imagename += ".png"
filename = os.path.join(nativePath, imagename)
if not os.path.exists(filename):
# record which images that were newly copied and can be safely
# cleaned up
copied_images.append(filename)
i.filepath_raw = filename
Log("packed file, save it to {}"
.format(os.path.abspath(bpy.path.abspath(filename))))
i.save()
except:
Log("failed to save file {} to {}".format(imagename, nativePath))
i.filepath_raw = original_filepath
else:
filepath = os.path.abspath(bpy.path.abspath(i.filepath))
texturePath = os.path.join(nativePath, imagename)
if os.path.exists(filepath):
if not os.path.exists(texturePath):
# record which images that were newly copied and can be safely
# cleaned up
copied_images.append(texturePath)
shutil.copy(filepath, texturePath)
Log("copy file {} to {}".format(filepath, texturePath))
else:
Log("file {} not available".format(filepath))
except Exception as e:
Log("error while trying to copy file {} to {}: {}".format(imagename, nativePath, e))
filetoview = self.config.getFullName("osgt")
if self.config.osgconv_to_ive:
if self.config.osgconv_embed_textures:
r = [self.config.osgconv_path, "-O", "includeImageFileInIVEFile",
self.config.getFullName("osgt"), self.config.getFullName("ive")]
else:
r = [self.config.osgconv_path, "-O", "noTexturesInIVEFile",
self.config.getFullName("osgt"), self.config.getFullName("ive")]
try:
if subprocess.call(r) == 0:
filetoview = self.config.getFullName("ive")
if self.config.osgconv_cleanup:
os.unlink(self.config.getFullName("osgt"))
if self.config.osgconv_embed_textures:
for i in copied_images:
os.unlink(i)
except Exception as e:
print("Error running {}".format(r))
print(repr(e))
if self.config.run_viewer:
r = [self.config.viewer_path, filetoview]
try:
subprocess.Popen(r)
except Exception as e:
print("Error running {}".format(r))
print(repr(e))
if self.config.log_file is not None:
self.config.closeLogfile()
def createGeodeFromObject(self, mesh, skeleton=None):
Log("exporting object {}".format(mesh.name))
# check if the mesh has a armature modifier
# if no we don't write influence
exportInfluence = False
armature_modifier = None
has_non_armature_modifiers = False
for mod in mesh.modifiers:
if mod.type == "ARMATURE":
armature_modifier = mod
else:
has_non_armature_modifiers = True
# Consider a mesh child of armature as rigged only if it has no parent_bone. To get both bone parenting
# and riggin effect, an armature modifier has to be applied
if armature_modifier is not None or mesh.parent and mesh.parent.type == 'ARMATURE' and not mesh.parent_bone:
exportInfluence = True
# converting to mesh skips shape keys
if self.config.apply_modifiers and has_non_armature_modifiers and not hasShapeKeys(mesh):
mesh_object = mesh.to_mesh(self.config.scene, True, 'PREVIEW')
else:
mesh_object = mesh.data
Log("mesh_object is {}".format(mesh_object.name))
if self.unique_objects.hasObject(mesh_object):
return self.unique_objects.getObject(mesh_object)
hasVertexGroup = False
for vertex in mesh_object.vertices:
if len(vertex.groups) > 0:
hasVertexGroup = True
break
geometries = []
converter = BlenderObjectToGeometry(object=mesh,
mesh=mesh_object,
config=self.config,
unique_objects=self.unique_objects)
sources_geometries = converter.convert()
Log("vertex groups {} {} ".format(exportInfluence, hasVertexGroup))
if exportInfluence and hasVertexGroup:
for geom in sources_geometries:
rig_geom = RigGeometry()
rig_geom.sourcegeometry = geom
rig_geom.copyFrom(geom)
rig_geom.groups = geom.groups
geometries.append(rig_geom)
else:
geometries = sources_geometries
geode = Geode()
geode.setName(mesh_object.name)
geode.armature_modifier = armature_modifier
updateMorphs = {}
# Geometries are the result of splitting Blender multi-material mesh.
# We assume that we have as much geometries as the number of materials
# the original Blender mesh has. This mapping is used when renaming geometry
# targets in animation parsing code.
if len(geometries) > 0:
# Rename geometries to ensure that the order is kept bewteen MorphGeometry and UpdateMorphs
# Note: renaming geometries should not be a problem here since armature deform/animation doesn't use
# rig/source geometry names and solid animation is applied to geode (or upper osg node)
# The name of the morphGeometry is used for UpdateMorph callback, that is created after
# that so the link is not broken
for index, geom in enumerate(geometries):
geom.name = "{}_{}".format(geom.name, index)
if geom.className() == 'MorphGeometry':
updateMorphs.setdefault(geom.name, []).extend(map(lambda x: x.name, geom.morphTargets))
if geom.className() == 'RigGeometry' and geom.sourcegeometry.className() == 'MorphGeometry':
geom.sourcegeometry.name = "{}_{}".format(geom.sourcegeometry.name, index)
updateMorphs.setdefault(geom.name, []).extend(map(lambda x: x.name,
geom.sourcegeometry.morphTargets))
geode.drawables.append(geom)
# Material animations is not yet supported
# for name in converter.material_animations.keys():
# self.animations.append(converter.material_animations[name])
if updateMorphs:
# Important: to keep the same order in updateMorph than in drawables
names = [geom.name for geom in geometries]
update = None
for morphname in names:
callback = UpdateMorph()
callback.setName(morphname)
callback.targetNames.extend(updateMorphs[morphname])
if not update:
update = callback
else:
update.addNestedCallback(callback)
geode.update_callbacks.append(update)
self.unique_objects.registerObject(mesh_object, geode)
return geode
def createLight(self, obj):
converter = BlenderLightToLightSource(lamp=obj)
lightsource = converter.convert()
self.lights[lightsource.name] = lightsource # will be used to index lightnum at the end
return lightsource
class BlenderLightToLightSource(object):
def __init__(self, *args, **kwargs):
self.object = kwargs["lamp"]
self.lamp = self.object.data
def convert(self):
ls = LightSource()
ls.setName(self.object.name)
light = ls.light
energy = self.lamp.energy
light.ambient = (1.0, 1.0, 1.0, 1.0)
if self.lamp.use_diffuse:
light.diffuse = (self.lamp.color[0] * energy,
self.lamp.color[1] * energy,
self.lamp.color[2] * energy,
1.0)
else:
light.diffuse = (0, 0, 0, 1.0)
if self.lamp.use_specular:
light.specular = (energy, energy, energy, 1.0) # light.diffuse
else:
light.specular = (0, 0, 0, 1.0)
light.getOrCreateUserData().append(StringValueObject("source", "blender"))
light.getOrCreateUserData().append(StringValueObject("Energy", str(energy)))
light.getOrCreateUserData().append(StringValueObject("Color", "[{}, {}, {}]".format(self.lamp.color[0],
self.lamp.color[1],
self.lamp.color[2])))
if self.lamp.use_diffuse:
light.getOrCreateUserData().append(StringValueObject("UseDiffuse", "true"))
else:
light.getOrCreateUserData().append(StringValueObject("UseDiffuse", "false"))
if self.lamp.use_specular:
light.getOrCreateUserData().append(StringValueObject("UseSpecular", "true"))
else:
light.getOrCreateUserData().append(StringValueObject("UseSpecular", "false"))
light.getOrCreateUserData().append(StringValueObject("Distance", str(self.lamp.distance)))
if self.lamp.type == 'POINT' or self.lamp.type == "SPOT":
light.getOrCreateUserData().append(StringValueObject("FalloffType", str(self.lamp.falloff_type)))
light.getOrCreateUserData().append(StringValueObject("UseSphere", str(self.lamp.use_sphere).lower()))
light.getOrCreateUserData().append(StringValueObject("Type", str(self.lamp.type)))
# Lamp', 'Sun', 'Spot', 'Hemi', 'Area', or 'Photon
if self.lamp.type == 'POINT' or self.lamp.type == 'SPOT':
# position light
# Note DW - the distance may not be necessary anymore (blender 2.5)
light.position = (0, 0, 0, 1) # put light to vec3(0) it will inherit the position from parent transform
light.linear_attenuation = self.lamp.linear_attenuation / self.lamp.distance
light.quadratic_attenuation = self.lamp.quadratic_attenuation / self.lamp.distance
if self.lamp.falloff_type == 'CONSTANT':
light.quadratic_attenuation = 0
light.linear_attenuation = 0
if self.lamp.falloff_type == 'INVERSE_SQUARE':
light.constant_attenuation = 0
light.linear_attenuation = 0
if self.lamp.falloff_type == 'INVERSE_LINEAR':
light.constant_attenuation = 0
light.quadratic_attenuation = 0
elif self.lamp.type == 'SUN':
light.position = (0, 0, 1, 0) # put light to 0 it will inherit the position from parent transform
if self.lamp.type == 'SPOT':
light.spot_cutoff = math.degrees(self.lamp.spot_size * .5)
if light.spot_cutoff > 90:
light.spot_cutoff = 180
light.spot_exponent = 128.0 * self.lamp.spot_blend
light.getOrCreateUserData().append(StringValueObject("SpotSize", str(self.lamp.spot_size)))
light.getOrCreateUserData().append(StringValueObject("SpotBlend", str(self.lamp.spot_blend)))
return ls
class BlenderObjectToGeometry(object):
def __init__(self, *args, **kwargs):
self.object = kwargs["object"]
self.config = kwargs.get("config", osgconf.Config())
self.unique_objects = kwargs.get("unique_objects", UniqueObject())
self.geom_type = Geometry
self.mesh = kwargs.get("mesh", None)
# if self.config.apply_modifiers is False:
# self.mesh = self.object.data
# else:
# self.mesh = self.object.to_mesh(self.config.scene, True, 'PREVIEW')
self.material_animations = {}
def createTexture2D(self, mtex):
image_object = None
try:
image_object = mtex.texture.image
except:
image_object = None
if image_object is None:
Log("Warning: [[blender]] The texture {} is skipped since it has no Image".format(mtex.name))
return None
if self.unique_objects.hasTexture(mtex.texture):
return self.unique_objects.getTexture(mtex.texture)
texture = Texture2D()
texture.name = mtex.texture.name
# reference texture relative to export path
filename = createImageFilename(self.config.texture_prefix, image_object)
texture.file = filename
texture.source_image = image_object
self.unique_objects.registerTexture(mtex.texture, texture)
return texture
def createTexture2DFromNode(self, node):
image_object = None
try:
image_object = node.image
except:
image_object = None
if image_object is None:
Log("Warning: [[blender]] The texture node {} is skipped since it has no Image".format(node))
return None
if self.unique_objects.hasTexture(node):
return self.unique_objects.getTexture(node)
texture = Texture2D()
texture.name = node.image.name
# reference texture relative to export path
filename = createImageFilename(self.config.texture_prefix, image_object)
texture.file = filename
texture.source_image = image_object
self.unique_objects.registerTexture(node, texture)
return texture