forked from panda3d/panda3d
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathActor.py
More file actions
2555 lines (2174 loc) · 104 KB
/
Actor.py
File metadata and controls
2555 lines (2174 loc) · 104 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
"""Actor module: contains the Actor class"""
__all__ = ['Actor']
from panda3d.core import *
from direct.showbase.DirectObject import DirectObject
from direct.directnotify import DirectNotifyGlobal
import types
class Actor(DirectObject, NodePath):
"""
Actor class: Contains methods for creating, manipulating
and playing animations on characters
"""
notify = DirectNotifyGlobal.directNotify.newCategory("Actor")
partPrefix = "__Actor_"
modelLoaderOptions = LoaderOptions(LoaderOptions.LFSearch |
LoaderOptions.LFReportErrors |
LoaderOptions.LFConvertSkeleton)
animLoaderOptions = LoaderOptions(LoaderOptions.LFSearch |
LoaderOptions.LFReportErrors |
LoaderOptions.LFConvertAnim)
validateSubparts = ConfigVariableBool('validate-subparts', True)
class PartDef:
"""Instances of this class are stored within the
PartBundleDict to track all of the individual PartBundles
associated with the Actor. In general, each separately loaded
model file is a different PartBundle. This can include the
multiple different LOD's, as well as the multiple different
pieces of a multipart Actor. """
def __init__(self, partBundleNP, partBundleHandle, partModel):
# We also save the ModelRoot node along with the
# PartBundle, so that the reference count in the ModelPool
# will be accurate.
self.partBundleNP = partBundleNP
self.partBundleHandle = partBundleHandle
self.partModel = partModel
def getBundle(self):
return self.partBundleHandle.getBundle()
def __repr__(self):
return 'Actor.PartDef(%s, %s)' % (repr(self.partBundleNP), repr(self.partModel))
class AnimDef:
"""Instances of this class are stored within the
AnimControlDict to track all of the animations associated with
the Actor. This includes animations that have already been
bound (these have a valid AnimControl) as well as those that
have not yet been bound (for these, self.animControl is None).
There is a different AnimDef for each different part or
sub-part, times each different animation in the AnimDict. """
def __init__(self, filename = None, animBundle = None):
self.filename = filename
self.animBundle = None
self.animControl = None
def makeCopy(self):
return Actor.AnimDef(self.filename, self.animBundle)
def __repr__(self):
return 'Actor.AnimDef(%s)' % (repr(self.filename))
class SubpartDef:
"""Instances of this class are stored within the SubpartDict
to track the existance of arbitrary sub-parts. These are
designed to appear to the user to be identical to true "part"
of a multi-part Actor, but in fact each subpart represents a
subset of the joints of an existing part (which is accessible
via a different name). """
def __init__(self, truePartName, subset = PartSubset()):
self.truePartName = truePartName
self.subset = subset
def makeCopy(self):
return Actor.SubpartDef(self.truePartName, PartSubset(self.subset))
def __repr__(self):
return 'Actor.SubpartDef(%s, %s)' % (repr(self.truePartName), repr(self.subset))
def __init__(self, models=None, anims=None, other=None, copy=True,
lodNode = None, flattenable = True, setFinal = False,
mergeLODBundles = None, allowAsyncBind = None,
okMissing = None):
"""__init__(self, string | string:string{}, string:string{} |
string:(string:string{}){}, Actor=None)
Actor constructor: can be used to create single or multipart
actors. If another Actor is supplied as an argument this
method acts like a copy constructor. Single part actors are
created by calling with a model and animation dictionary
(animName:animPath{}) as follows:
a = Actor("panda-3k.egg", {"walk":"panda-walk.egg" \
"run":"panda-run.egg"})
This could be displayed and animated as such:
a.reparentTo(render)
a.loop("walk")
a.stop()
Multipart actors expect a dictionary of parts and a dictionary
of animation dictionaries (partName:(animName:animPath{}){}) as
below:
a = Actor(
# part dictionary
{"head":"char/dogMM/dogMM_Shorts-head-mod", \
"torso":"char/dogMM/dogMM_Shorts-torso-mod", \
"legs":"char/dogMM/dogMM_Shorts-legs-mod"}, \
# dictionary of anim dictionaries
{"head":{"walk":"char/dogMM/dogMM_Shorts-head-walk", \
"run":"char/dogMM/dogMM_Shorts-head-run"}, \
"torso":{"walk":"char/dogMM/dogMM_Shorts-torso-walk", \
"run":"char/dogMM/dogMM_Shorts-torso-run"}, \
"legs":{"walk":"char/dogMM/dogMM_Shorts-legs-walk", \
"run":"char/dogMM/dogMM_Shorts-legs-run"} \
})
In addition multipart actor parts need to be connected together
in a meaningful fashion:
a.attach("head", "torso", "joint-head")
a.attach("torso", "legs", "joint-hips")
#
# ADD LOD COMMENT HERE!
#
Other useful Actor class functions:
#fix actor eye rendering
a.drawInFront("joint-pupil?", "eyes*")
#fix bounding volumes - this must be done after drawing
#the actor for a few frames, otherwise it has no effect
a.fixBounds()
"""
try:
self.Actor_initialized
return
except:
self.Actor_initialized = 1
# initialize our NodePath essence
NodePath.__init__(self)
# Set the mergeLODBundles flag. If this is true, all
# different LOD's will be merged into a single common bundle
# (joint hierarchy). All LOD's will thereafter share the same
# skeleton, even though they may have been loaded from
# different egg files. If this is false, LOD's will be kept
# completely isolated, and each LOD will have its own
# skeleton.
# When this flag is true, __animControlDict has only one key,
# ['common']; when it is false, __animControlDict has one key
# per each LOD name.
if mergeLODBundles == None:
# If this isn't specified, it comes from the Config.prc
# file.
self.mergeLODBundles = base.config.GetBool('merge-lod-bundles', True)
else:
self.mergeLODBundles = mergeLODBundles
# Set the allowAsyncBind flag. If this is true, it enables
# asynchronous animation binding. This requires that you have
# run "egg-optchar -preload" on your animation and models to
# generate the appropriate AnimPreloadTable.
if allowAsyncBind == None:
self.allowAsyncBind = base.config.GetBool('allow-async-bind', True)
else:
self.allowAsyncBind = allowAsyncBind
# create data structures
self.__commonBundleHandles = {}
self.__partBundleDict = {}
self.__subpartDict = {}
self.__sortedLODNames = []
self.__animControlDict = {}
self.__subpartsComplete = False
self.__LODNode = None
self.__LODAnimation = None
self.__LODCenter = Point3(0, 0, 0)
self.switches = None
if (other == None):
# act like a normal constructor
# create base hierarchy
self.gotName = 0
if flattenable:
# If we want a flattenable Actor, don't create all
# those ModelNodes, and the GeomNode is the same as
# the root.
root = PandaNode('actor')
self.assign(NodePath(root))
self.setGeomNode(NodePath(self))
else:
# A standard Actor has a ModelNode at the root, and
# another ModelNode to protect the GeomNode.
root = ModelNode('actor')
root.setPreserveTransform(1)
self.assign(NodePath(root))
self.setGeomNode(self.attachNewNode(ModelNode('actorGeom')))
self.__hasLOD = 0
# load models
#
# four cases:
#
# models, anims{} = single part actor
# models{}, anims{} = single part actor w/ LOD
# models{}, anims{}{} = multi-part actor
# models{}{}, anims{}{} = multi-part actor w/ LOD
#
# make sure we have models
if (models):
# do we have a dictionary of models?
if (type(models)==type({})):
# if this is a dictionary of dictionaries
if (type(models[models.keys()[0]]) == type({})):
# then it must be a multipart actor w/LOD
self.setLODNode(node = lodNode)
# preserve numerical order for lod's
# this will make it easier to set ranges
sortedKeys = models.keys()
sortedKeys.sort()
for lodName in sortedKeys:
# make a node under the LOD switch
# for each lod (just because!)
self.addLOD(str(lodName))
# iterate over both dicts
for modelName in models[lodName].keys():
self.loadModel(models[lodName][modelName],
modelName, lodName, copy = copy,
okMissing = okMissing)
# then if there is a dictionary of dictionaries of anims
elif (type(anims[anims.keys()[0]])==type({})):
# then this is a multipart actor w/o LOD
for partName in models.keys():
# pass in each part
self.loadModel(models[partName], partName,
copy = copy, okMissing = okMissing)
else:
# it is a single part actor w/LOD
self.setLODNode(node = lodNode)
# preserve order of LOD's
sortedKeys = models.keys()
sortedKeys.sort()
for lodName in sortedKeys:
self.addLOD(str(lodName))
# pass in dictionary of parts
self.loadModel(models[lodName], lodName=lodName,
copy = copy, okMissing = okMissing)
else:
# else it is a single part actor
self.loadModel(models, copy = copy, okMissing = okMissing)
# load anims
# make sure the actor has animations
if (anims):
if (len(anims) >= 1):
# if so, does it have a dictionary of dictionaries?
if (type(anims[anims.keys()[0]])==type({})):
# are the models a dict of dicts too?
if (type(models)==type({})):
if (type(models[models.keys()[0]]) == type({})):
# then we have a multi-part w/ LOD
sortedKeys = models.keys()
sortedKeys.sort()
for lodName in sortedKeys:
# iterate over both dicts
for partName in anims.keys():
self.loadAnims(
anims[partName], partName, lodName)
else:
# then it must be multi-part w/o LOD
for partName in anims.keys():
self.loadAnims(anims[partName], partName)
elif (type(models)==type({})):
# then we have single-part w/ LOD
sortedKeys = models.keys()
sortedKeys.sort()
for lodName in sortedKeys:
self.loadAnims(anims, lodName=lodName)
else:
# else it is single-part w/o LOD
self.loadAnims(anims)
else:
self.copyActor(other, True) # overwrite everything
if setFinal:
# If setFinal is true, the Actor will set its top bounding
# volume to be the "final" bounding volume: the bounding
# volumes below the top volume will not be tested. If a
# cull test passes the top bounding volume, the whole
# Actor is rendered.
# We do this partly because an Actor is likely to be a
# fairly small object relative to the scene, and is pretty
# much going to be all onscreen or all offscreen anyway;
# and partly because of the Character bug that doesn't
# update the bounding volume for pieces that animate away
# from their original position. It's disturbing to see
# someone's hands disappear; better to cull the whole
# object or none of it.
self.__geomNode.node().setFinal(1)
def delete(self):
try:
self.Actor_deleted
return
except:
self.Actor_deleted = 1
self.cleanup()
def copyActor(self, other, overwrite=False):
# act like a copy constructor
self.gotName = other.gotName
# copy the scene graph elements of other
if (overwrite):
otherCopy = other.copyTo(NodePath())
otherCopy.detachNode()
# assign these elements to ourselve (overwrite)
self.assign(otherCopy)
else:
# just copy these to ourselves
otherCopy = other.copyTo(self)
# masad: check if otherCopy has a geomNode as its first child
# if actor is initialized with flattenable, then otherCopy, not
# its first child, is the geom node; check __init__, for reference
if other.getGeomNode().getName() == other.getName():
self.setGeomNode(otherCopy)
else:
self.setGeomNode(otherCopy.getChild(0))
# copy the switches for lods
self.switches = other.switches
self.__LODNode = self.find('**/+LODNode')
self.__hasLOD = 0
if (not self.__LODNode.isEmpty()):
self.__hasLOD = 1
# copy the part dictionary from other
self.__copyPartBundles(other)
self.__copySubpartDict(other)
self.__subpartsComplete = other.__subpartsComplete
# copy the anim dictionary from other
self.__copyAnimControls(other)
def __cmp__(self, other):
# Actor inherits from NodePath, which inherits a definition of
# __cmp__ from FFIExternalObject that uses the NodePath's
# compareTo() method to compare different NodePaths. But we
# don't want this behavior for Actors; Actors should only be
# compared pointerwise. A NodePath that happens to reference
# the same node is still different from the Actor.
if self is other:
return 0
else:
return 1
def __str__(self):
"""
Actor print function
"""
return "Actor %s, parts = %s, LODs = %s, anims = %s" % \
(self.getName(), self.getPartNames(), self.getLODNames(), self.getAnimNames())
def listJoints(self, partName="modelRoot", lodName="lodRoot"):
"""Handy utility function to list the joint hierarchy of the
actor. """
if self.mergeLODBundles:
partBundleDict = self.__commonBundleHandles
else:
partBundleDict = self.__partBundleDict.get(lodName)
if not partBundleDict:
Actor.notify.error("no lod named: %s" % (lodName))
subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
partDef = partBundleDict.get(subpartDef.truePartName)
if partDef == None:
Actor.notify.error("no part named: %s" % (partName))
self.__doListJoints(0, partDef.getBundle(),
subpartDef.subset.isIncludeEmpty(), subpartDef.subset)
def __doListJoints(self, indentLevel, part, isIncluded, subset):
name = part.getName()
if subset.matchesInclude(name):
isIncluded = True
elif subset.matchesExclude(name):
isIncluded = False
if isIncluded:
value = ''
if hasattr(part, 'outputValue'):
lineStream = LineStream()
part.outputValue(lineStream)
value = lineStream.getLine()
print ' ' * indentLevel, part.getName(), value
for i in range(part.getNumChildren()):
self.__doListJoints(indentLevel + 2, part.getChild(i),
isIncluded, subset)
def getActorInfo(self):
"""
Utility function to create a list of information about an actor.
Useful for iterating over details of an actor.
"""
lodInfo = []
for lodName, partDict in self.__animControlDict.items():
if self.mergeLODBundles:
lodName = self.__sortedLODNames[0]
partInfo = []
for partName in partDict.keys():
subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
partBundleDict = self.__partBundleDict.get(lodName)
partDef = partBundleDict.get(subpartDef.truePartName)
partBundle = partDef.getBundle()
animDict = partDict[partName]
animInfo = []
for animName in animDict.keys():
file = animDict[animName].filename
animControl = animDict[animName].animControl
animInfo.append([animName, file, animControl])
partInfo.append([partName, partBundle, animInfo])
lodInfo.append([lodName, partInfo])
return lodInfo
def getAnimNames(self):
animNames = []
for lodName, lodInfo in self.getActorInfo():
for partName, bundle, animInfo in lodInfo:
for animName, file, animControl in animInfo:
if animName not in animNames:
animNames.append(animName)
return animNames
def pprint(self):
"""
Pretty print actor's details
"""
for lodName, lodInfo in self.getActorInfo():
print 'LOD:', lodName
for partName, bundle, animInfo in lodInfo:
print ' Part:', partName
print ' Bundle:', repr(bundle)
for animName, file, animControl in animInfo:
print ' Anim:', animName
print ' File:', file
if animControl == None:
print ' (not loaded)'
else:
print (' NumFrames: %d PlayRate: %0.2f' %
(animControl.getNumFrames(),
animControl.getPlayRate()))
def cleanup(self):
"""
Actor cleanup function
"""
self.stop(None)
self.clearPythonData()
self.flush()
if(self.__geomNode):
self.__geomNode.removeNode()
self.__geomNode = None
if not self.isEmpty():
self.removeNode()
def removeNode(self):
if self.__geomNode and (self.__geomNode.getNumChildren() > 0):
assert self.notify.warning("called actor.removeNode() on %s without calling cleanup()" % self.getName())
NodePath.removeNode(self)
def clearPythonData(self):
self.__commonBundleHandles = {}
self.__partBundleDict = {}
self.__subpartDict = {}
self.__sortedLODNames = []
self.__animControlDict = {}
def flush(self):
"""
Actor flush function
"""
self.clearPythonData()
if self.__LODNode and (not self.__LODNode.isEmpty()):
self.__LODNode.removeNode()
self.__LODNode = None
# remove all its children
if(self.__geomNode):
self.__geomNode.removeChildren()
self.__hasLOD = 0
# accessing
def getAnimControlDict(self):
return self.__animControlDict
def removeAnimControlDict(self):
self.__animControlDict = {}
def getPartBundleDict(self):
return self.__partBundleDict
def getPartBundles(self, partName = None):
""" Returns a list of PartBundle objects for the entire Actor,
or for the indicated part only. """
bundles = []
for lodName, partBundleDict in self.__partBundleDict.items():
if partName == None:
for partDef in partBundleDict.values():
bundles.append(partDef.getBundle())
else:
subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
partDef = partBundleDict.get(subpartDef.truePartName)
if partDef != None:
bundles.append(partDef.getBundle())
else:
Actor.notify.warning("Couldn't find part: %s" % (partName))
return bundles
def __updateSortedLODNames(self):
# Cache the sorted LOD names so we don't have to grab them
# and sort them every time somebody asks for the list
self.__sortedLODNames = self.__partBundleDict.keys()
# Reverse sort the doing a string->int
def sortKey(x):
if not str(x).isdigit():
smap = {'h':3,
'm':2,
'l':1,
'f':0}
"""
sx = smap.get(x[0], None)
if sx is None:
self.notify.error('Invalid lodName: %s' % x)
"""
return smap[x[0]]
else:
return int(x)
self.__sortedLODNames.sort(key=sortKey, reverse=True)
def getLODNames(self):
"""
Return list of Actor LOD names. If not an LOD actor,
returns 'lodRoot'
Caution - this returns a reference to the list - not your own copy
"""
return self.__sortedLODNames
def getPartNames(self):
"""
Return list of Actor part names. If not an multipart actor,
returns 'modelRoot' NOTE: returns parts of arbitrary LOD
"""
partNames = []
if self.__partBundleDict:
partNames = self.__partBundleDict.values()[0].keys()
return partNames + self.__subpartDict.keys()
def getGeomNode(self):
"""
Return the node that contains all actor geometry
"""
return self.__geomNode
def setGeomNode(self, node):
"""
Set the node that contains all actor geometry
"""
self.__geomNode = node
def getLODNode(self):
"""
Return the node that switches actor geometry in and out"""
return self.__LODNode.node()
def setLODNode(self, node=None):
"""
Set the node that switches actor geometry in and out.
If one is not supplied as an argument, make one
"""
if (node == None):
node = LODNode.makeDefaultLod("lod")
if self.__LODNode:
self.__LODNode = node
else:
self.__LODNode = self.__geomNode.attachNewNode(node)
self.__hasLOD = 1
self.switches = {}
def useLOD(self, lodName):
"""
Make the Actor ONLY display the given LOD
"""
# make sure we don't call this twice in a row
# and pollute the the switches dictionary
## sortedKeys = self.switches.keys()
## sortedKeys.sort()
child = self.__LODNode.find(str(lodName))
index = self.__LODNode.node().findChild(child.node())
self.__LODNode.node().forceSwitch(index)
def printLOD(self):
## sortedKeys = self.switches.keys()
## sortedKeys.sort()
sortedKeys = self.__sortedLODNames
for eachLod in sortedKeys:
print "python switches for %s: in: %d, out %d" % (eachLod,
self.switches[eachLod][0],
self.switches[eachLod][1])
switchNum = self.__LODNode.node().getNumSwitches()
for eachSwitch in range(0, switchNum):
print "c++ switches for %d: in: %d, out: %d" % (eachSwitch,
self.__LODNode.node().getIn(eachSwitch),
self.__LODNode.node().getOut(eachSwitch))
def resetLOD(self):
"""
Restore all switch distance info (usually after a useLOD call)"""
self.__LODNode.node().clearForceSwitch()
## sortedKeys = self.switches.keys()
## sortedKeys.sort()
## for eachLod in sortedKeys:
## index = sortedKeys.index(eachLod)
## self.__LODNode.node().setSwitch(index, self.switches[eachLod][0],
## self.switches[eachLod][1])
def addLOD(self, lodName, inDist=0, outDist=0, center=None):
"""addLOD(self, string)
Add a named node under the LODNode to parent all geometry
of a specific LOD under.
"""
self.__LODNode.attachNewNode(str(lodName))
# save the switch distance info
self.switches[lodName] = [inDist, outDist]
# add the switch distance info
self.__LODNode.node().addSwitch(inDist, outDist)
if center != None:
self.setCenter(center)
def setLOD(self, lodName, inDist=0, outDist=0):
"""setLOD(self, string)
Set the switch distance for given LOD
"""
# save the switch distance info
self.switches[lodName] = [inDist, outDist]
# add the switch distance info
## sortedKeys = self.switches.keys()
## sortedKeys.sort()
self.__LODNode.node().setSwitch(self.getLODIndex(lodName), inDist, outDist)
def getLODIndex(self, lodName):
"""getLODIndex(self)
safe method (but expensive) for retrieving the child index
"""
return list(self.__LODNode.getChildren()).index(self.getLOD(lodName))
def getLOD(self, lodName):
"""getLOD(self, string)
Get the named node under the LOD to which we parent all LOD
specific geometry to. Returns 'None' if not found
"""
if self.__LODNode:
lod = self.__LODNode.find(str(lodName))
if lod.isEmpty():
return None
else:
return lod
else:
return None
def hasLOD(self):
"""
Return 1 if the actor has LODs, 0 otherwise
"""
return self.__hasLOD
def setCenter(self, center):
if center == None:
center = Point3(0, 0, 0)
self.__LODCenter = center
if self.__LODNode:
self.__LODNode.node().setCenter(self.__LODCenter)
if self.__LODAnimation:
self.setLODAnimation(*self.__LODAnimation)
def setLODAnimation(self, farDistance, nearDistance, delayFactor):
""" Activates a special mode in which the Actor animates less
frequently as it gets further from the camera. This is
intended as a simple optimization to minimize the effort of
computing animation for lots of characters that may not
necessarily be very important to animate every frame.
If the character is closer to the camera than near_distance,
then it is animated its normal rate, every frame. If the
character is exactly far_distance away, it is animated only
every delay_factor seconds (which should be a number greater
than 0). If the character is between near_distance and
far_distance, its animation rate is linearly interpolated
according to its distance between the two. The interpolation
function continues beyond far_distance, so that the character
is animated increasingly less frequently as it gets farther
away. """
self.__LODAnimation = (farDistance, nearDistance, delayFactor)
for lodData in self.__partBundleDict.values():
for partData in lodData.values():
char = partData.partBundleNP
char.node().setLodAnimation(self.__LODCenter, farDistance, nearDistance, delayFactor)
def clearLODAnimation(self):
""" Description: Undoes the effect of a recent call to
set_lod_animation(). Henceforth, the character will animate
every frame, regardless of its distance from the camera.
"""
self.__LODAnimation = None
for lodData in self.__partBundleDict.values():
for partData in lodData.values():
char = partData.partBundleNP
char.node().clearLodAnimation()
def update(self, lod=0, partName=None, lodName=None, force=False):
""" Updates all of the Actor's joints in the indicated LOD.
The LOD may be specified by name, or by number, where 0 is the
highest level of detail, 1 is the next highest, and so on.
If force is True, this will update every joint, even if we
don't believe it's necessary.
Returns True if any joint has changed as a result of this,
False otherwise. """
if lodName == None:
lodNames = self.getLODNames()
else:
lodNames = [lodName]
anyChanged = False
if lod < len(lodNames):
lodName = lodNames[lod]
if partName == None:
partBundleDict = self.__partBundleDict[lodName]
partNames = partBundleDict.keys()
else:
partNames = [partName]
for partName in partNames:
partBundle = self.getPartBundle(partName, lodNames[lod])
if force:
if partBundle.forceUpdate():
anyChanged = True
else:
if partBundle.update():
anyChanged = True
else:
self.notify.warning('update() - no lod: %d' % lod)
return anyChanged
def getFrameRate(self, animName=None, partName=None):
"""getFrameRate(self, string, string=None)
Return actual frame rate of given anim name and given part.
If no anim specified, use the currently playing anim.
If no part specified, return anim durations of first part.
NOTE: returns info only for an arbitrary LOD
"""
lodName = self.__animControlDict.keys()[0]
controls = self.getAnimControls(animName, partName)
if len(controls) == 0:
return None
return controls[0].getFrameRate()
def getBaseFrameRate(self, animName=None, partName=None):
"""getBaseFrameRate(self, string, string=None)
Return frame rate of given anim name and given part, unmodified
by any play rate in effect.
"""
lodName = self.__animControlDict.keys()[0]
controls = self.getAnimControls(animName, partName)
if len(controls) == 0:
return None
return controls[0].getAnim().getBaseFrameRate()
def getPlayRate(self, animName=None, partName=None):
"""
Return the play rate of given anim for a given part.
If no part is given, assume first part in dictionary.
If no anim is given, find the current anim for the part.
NOTE: Returns info only for an arbitrary LOD
"""
if self.__animControlDict:
# use the first lod
lodName = self.__animControlDict.keys()[0]
controls = self.getAnimControls(animName, partName)
if controls:
return controls[0].getPlayRate()
return None
def setPlayRate(self, rate, animName, partName=None):
"""setPlayRate(self, float, string, string=None)
Set the play rate of given anim for a given part.
If no part is given, set for all parts in dictionary.
It used to be legal to let the animName default to the
currently-playing anim, but this was confusing and could lead
to the wrong anim's play rate getting set. Better to insist
on this parameter.
NOTE: sets play rate on all LODs"""
for control in self.getAnimControls(animName, partName):
control.setPlayRate(rate)
def getDuration(self, animName=None, partName=None,
fromFrame=None, toFrame=None):
"""
Return duration of given anim name and given part.
If no anim specified, use the currently playing anim.
If no part specified, return anim duration of first part.
NOTE: returns info for arbitrary LOD
"""
lodName = self.__animControlDict.keys()[0]
controls = self.getAnimControls(animName, partName)
if len(controls) == 0:
return None
animControl = controls[0]
if fromFrame is None:
fromFrame = 0
if toFrame is None:
toFrame = animControl.getNumFrames()-1
return ((toFrame+1)-fromFrame) / animControl.getFrameRate()
def getNumFrames(self, animName=None, partName=None):
lodName = self.__animControlDict.keys()[0]
controls = self.getAnimControls(animName, partName)
if len(controls) == 0:
return None
return controls[0].getNumFrames()
def getFrameTime(self, anim, frame, partName=None):
numFrames = self.getNumFrames(anim,partName)
animTime = self.getDuration(anim,partName)
frameTime = animTime * float(frame) / numFrames
return frameTime
def getCurrentAnim(self, partName=None):
"""
Return the anim currently playing on the actor. If part not
specified return current anim of an arbitrary part in dictionary.
NOTE: only returns info for an arbitrary LOD
"""
if len(self.__animControlDict.items()) == 0:
return
lodName, animControlDict = self.__animControlDict.items()[0]
if partName == None:
partName, animDict = animControlDict.items()[0]
else:
animDict = animControlDict.get(partName)
if animDict == None:
# part was not present
Actor.notify.warning("couldn't find part: %s" % (partName))
return None
# loop through all anims for named part and find if any are playing
for animName, anim in animDict.items():
if anim.animControl and anim.animControl.isPlaying():
return animName
# we must have found none, or gotten an error
return None
def getCurrentFrame(self, animName=None, partName=None):
"""
Return the current frame number of the named anim, or if no
anim is specified, then the anim current playing on the
actor. If part not specified return current anim of first part
in dictionary. NOTE: only returns info for an arbitrary LOD
"""
lodName, animControlDict = self.__animControlDict.items()[0]
if partName == None:
partName, animDict = animControlDict.items()[0]
else:
animDict = animControlDict.get(partName)
if animDict == None:
# part was not present
Actor.notify.warning("couldn't find part: %s" % (partName))
return None
if animName:
anim = animDict.get(animName)
if not anim:
Actor.notify.warning("couldn't find anim: %s" % (animName))
elif anim.animControl:
return anim.animControl.getFrame()
else:
# loop through all anims for named part and find if any are playing
for animName, anim in animDict.items():
if anim.animControl and anim.animControl.isPlaying():
return anim.animControl.getFrame()
# we must have found none, or gotten an error
return None
# arranging
def getPart(self, partName, lodName="lodRoot"):
"""
Find the named part in the optional named lod and return it, or
return None if not present
"""
partBundleDict = self.__partBundleDict.get(lodName)
if not partBundleDict:
Actor.notify.warning("no lod named: %s" % (lodName))
return None
subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
partDef = partBundleDict.get(subpartDef.truePartName)
if partDef != None:
return partDef.partBundleNP
return None
def getPartBundle(self, partName, lodName="lodRoot"):
"""
Find the named part in the optional named lod and return its
associated PartBundle, or return None if not present
"""
partBundleDict = self.__partBundleDict.get(lodName)
if not partBundleDict:
Actor.notify.warning("no lod named: %s" % (lodName))
return None
subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
partDef = partBundleDict.get(subpartDef.truePartName)
if partDef != None:
return partDef.getBundle()
return None
def removePart(self, partName, lodName="lodRoot"):
"""
Remove the geometry and animations of the named part of the
optional named lod if present.
NOTE: this will remove child geometry also!
"""
# find the corresponding part bundle dict
partBundleDict = self.__partBundleDict.get(lodName)