-
-
Notifications
You must be signed in to change notification settings - Fork 322
/
entity.py
1306 lines (983 loc) · 46.3 KB
/
entity.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
import ursina
from pathlib import Path
from panda3d.core import NodePath
from ursina.vec2 import Vec2
from ursina.vec3 import Vec3
from ursina.vec4 import Vec4
from panda3d.core import Quat
from panda3d.core import TransparencyAttrib
from panda3d.core import Shader
from panda3d.core import TexGenAttrib
from ursina.texture import Texture
from panda3d.core import MovieTexture
from panda3d.core import TextureStage
from panda3d.core import CullFaceAttrib
from ursina import application
from ursina.collider import Collider, BoxCollider, SphereCollider, MeshCollider, CapsuleCollider
from ursina.mesh import Mesh
from ursina.sequence import Sequence, Func, Wait
from ursina.ursinamath import lerp
from ursina import curve
from ursina.curve import CubicBezier
from ursina import mesh_importer
from ursina.mesh_importer import load_model
from ursina.texture_importer import load_texture
from ursina.string_utilities import camel_to_snake
from textwrap import dedent
from panda3d.core import Shader as Panda3dShader
from ursina import shader
from ursina.shader import Shader
from ursina.string_utilities import print_info, print_warning
from ursina.ursinamath import Bounds
from ursina.ursinastuff import invoke, PostInitCaller
from ursina import color
from ursina.color import Color
try:
from ursina.scene import instance as scene
except:
pass
_Ursina_instance = None
_warn_if_ursina_not_instantiated = True # gets set to True after Ursina.__init__() to ensure the correct order.
from ursina.scripts.property_generator import generate_properties_for_class
@generate_properties_for_class()
class Entity(NodePath, metaclass=PostInitCaller):
rotation_directions = (-1,-1,1)
default_shader = None
default_values = {
# 'parent':scene,
'name':'entity', 'enabled':True, 'eternal':False, 'position':Vec3(0,0,0), 'rotation':Vec3(0,0,0), 'scale':Vec3(1,1,1), 'model':None, 'origin':Vec3(0,0,0),
'shader':None, 'texture':None, 'texture_scale':Vec2(1,1), 'color':color.white, 'collider':None}
def __init__(self, add_to_scene_entities=True, enabled=True, **kwargs):
self._children = []
super().__init__(self.__class__.__name__)
self.name = camel_to_snake(self.__class__.__name__)
self.ignore = False # if True, will not try to run code.
self.ignore_paused = False # if True, will still run when application is paused. useful when making a pause menu for example.
self.ignore_input = False
self.parent = scene # default parent is scene, which means it's in 3d space. to use UI space, set the parent to camera.ui instead.
self.add_to_scene_entities = add_to_scene_entities # set to False to be ignored by the engine, but still get rendered.
if add_to_scene_entities:
scene.entities.append(self)
self._shader_inputs = {}
if Entity.default_shader:
self.shader = Entity.default_shader
self.setPythonTag('Entity', self) # for the raycast to get the Entity and not just the NodePath
self.scripts = [] # add with add_script(class_instance). will assign an 'entity' variable to the script.
self.animations = []
self.hovered = False # will return True if mouse hovers entity.
self.line_definition = None # returns a Traceback(filename, lineno, function, code_context, index).
if application.trace_entity_definition and add_to_scene_entities or (not _Ursina_instance and _warn_if_ursina_not_instantiated and add_to_scene_entities):
from inspect import getframeinfo, stack
_stack = stack()
caller = getframeinfo(_stack[1][0])
if len(_stack) > 2 and _stack[1].code_context and 'super().__init__()' in _stack[1].code_context[0]:
caller = getframeinfo(_stack[2][0])
self.line_definition = caller
if caller.code_context:
self.code_context = caller.code_context[0]
if (self.code_context.count('(') == self.code_context.count(')') and
' = ' in self.code_context and 'name=' not in self.code_context
and 'Ursina()' not in self.code_context):
self.name = self.code_context.split(' = ')[0].strip().replace('self.', '')
# print('set name to:', self.code_context.split(' = ')[0].strip().replace('self.', ''))
if application.print_entity_definition:
print(f'{Path(caller.filename).name} -> {caller.lineno} -> {caller.code_context}')
if not _Ursina_instance and _warn_if_ursina_not_instantiated and add_to_scene_entities:
print_warning('Tried to instantiate Entity before Ursina. Please create an instance of Ursina first (app = Ursina())', self.line_definition)
# make sure things get set in the correct order. both colliders and texture need the model to be set first.
for key in ('model', 'origin', 'origin_x', 'origin_y', 'origin_z', 'collider', 'shader', 'texture', 'texture_scale', 'texture_offset'):
if key in kwargs:
setattr(self, key, kwargs[key])
del kwargs[key]
for key, value in kwargs.items():
setattr(self, key, value)
self.enabled = enabled
# look for @every decorator and start a looping Sequence for decorated method
from ursina.scripts.every_decorator import every, get_class_name
for method in every.decorated_methods:
if get_class_name(method._func) == self.types[0]:
self.animations.append(Sequence(Func(method, self), Wait(method._every.interval), loop=True, started=True, entity=self))
print('append to animations:', self)
def __post_init__(self):
if self.enabled and hasattr(self, 'on_enable'):
self.on_enable()
elif not self.enabled and hasattr(self, 'on_disable'):
self.on_disable()
def _list_to_vec(self, value):
if isinstance(value, (int, float, complex)):
return Vec3(value, value, value)
if len(value) % 2 == 0:
new_value = Vec2()
for i in range(0, len(value), 2):
new_value.add_x(value[i])
new_value.add_y(value[i+1])
if len(value) % 3 == 0:
new_value = Vec3()
for i in range(0, len(value), 3):
new_value.add_x(value[i])
new_value.add_y(value[i+1])
new_value.add_z(value[i+2])
return new_value
def enable(self): # same as .enabled = True
self.enabled = True
def disable(self): # same as .enabled = False
self.enabled = False
def enabled_getter(self):
return getattr(self, '_enabled', True)
def enabled_setter(self, value): # disabled entities will not be visible nor run code.
original_value = self.enabled
self._enabled = value
if value and not original_value and hasattr(self, 'on_enable'):
self.on_enable()
if not value and original_value and hasattr(self, 'on_disable'):
self.on_disable()
if value:
if hasattr(self, 'is_singleton') and not self.is_singleton():
self.unstash()
else:
if hasattr(self, 'is_singleton') and not self.is_singleton():
self.stash()
for loose_child in self.loose_children:
loose_child.enabled = value
def model_setter(self, value): # set model with model='model_name' (without file type extension)
if value is None:
if self.model:
self.model.removeNode()
# print('removed model')
self._model = value
return
if isinstance(value, NodePath): # pass procedural model
if self.model and value != self.model:
self.model.detachNode()
self._model = value
elif isinstance(value, str): # pass model asset name
m = load_model(value, application.asset_folder)
if not m:
m = load_model(value, application.internal_models_compressed_folder)
if m:
if self.model is not None:
self.model.removeNode()
m.name = value
m.setPos(Vec3(0,0,0))
self._model = m
# if not value in mesh_importer.imported_meshes:
# print_info('loaded model successfully:', value)
else:
if application.raise_exception_on_missing_model:
raise ValueError(f"missing model: '{value}'")
print_warning(f"missing model: '{value}'")
return
if self._model:
self._model.reparentTo(self)
self._model.setTransparency(TransparencyAttrib.M_dual)
self.color = self.color # reapply color after changing model
self.texture = self.texture # reapply texture after changing model
self._vert_cache = None
if isinstance(value, Mesh):
if hasattr(value, 'on_assign'):
value.on_assign(assigned_to=self)
def color_getter(self):
return getattr(self, '_color', color.white)
def color_setter(self, value):
if isinstance(value, str):
value = color.hex(value)
if not isinstance(value, Vec4):
value = Vec4(value[0], value[1], value[2], value[3])
self._color = value
if self.model:
# print('SET COLOR TO', value, self.name)
self.model.setColorScaleOff() # prevent inheriting color from parent
self.model.setColorScale(value)
def eternal_getter(self):
return getattr(self, '_eternal', False)
def eternal_setter(self, value): # eternal entities does not get destroyed on scene.clear()
self._eternal = value
for c in self.children + self.loose_children:
c.eternal = value
def double_sided_setter(self, value):
self._double_sided = value
self.setTwoSided(value)
def render_queue_getter(self):
return getattr(self, '_render_queue', 0)
def render_queue_setter(self, value): # for custom sorting in case of conflict. To sort things in 2d, set .z instead of using this.
self._render_queue = value
if self.model:
self.model.setBin('fixed', value)
def parent_setter(self, value):
if hasattr(self, '_parent') and self._parent and hasattr(self._parent, '_children') and self in self._parent._children:
self._parent._children.remove(self)
if hasattr(value, '_children') and self not in value._children:
value._children.append(self)
self._parent = value
if value is None:
self.enabled = False
return
# value = scene
self.reparent_to(value)
self.enabled = self.enabled # parenting will undo the .stash() done when setting .enabled to False, so reapply it here
def loose_parent_getter(self):
return getattr(self, '_loose_parent', None)
def loose_parent_setter(self, value):
if hasattr(self, '_loose_parent') and self._loose_parent and hasattr(self._loose_parent, '_loose_children') and self in self._loose_parent._loose_children:
self._loose_parent._loose_children.remove(self)
if not hasattr(value, '_loose_children'):
value.loose_children = []
value._loose_children.append(self)
self._loose_parent = value
def world_parent_getter(self):
return getattr(self, '_parent', None)
def world_parent_setter(self, value): # change the parent, but keep position, rotation and scale
if hasattr(self, '_parent') and self._parent and hasattr(self._parent, '_children') and self in self._parent._children:
self._parent._children.remove(self)
if hasattr(value, '_children') and self not in value._children:
value._children.append(self)
self.wrtReparentTo(value)
self.enabled = self._enabled # parenting will undo the .stash() done when setting .enabled to False, so reapply it here
self._parent = value
@property
def types(self): # get all class names including those this inhertits from.
from inspect import getmro
return [c.__name__ for c in getmro(self.__class__)]
def visible_getter(self):
return getattr(self, '_visible', True)
def visible_setter(self, value):
self._visible = value
if value:
self.show()
else:
self.hide()
def visible_self_getter(self): # set visibility of self, without affecting children.
return getattr(self, '_visible_self', True)
def visible_self_setter(self, value):
self._visible_self = value
if not self.model:
return
if value:
self.model.show()
else:
self.model.hide()
def collider_getter(self):
return getattr(self, '_collider', None)
def collider_setter(self, value): # set to 'box'/'sphere'/'capsule'/'mesh' for auto fitted collider.
if value is None and self.collider:
self._collider.remove()
self._collider = None
self.collision = False
return
# destroy existing collider
if value and self.collider:
self._collider.remove()
if isinstance(value, Collider):
self._collider = value
elif value == 'box':
if self.model:
_bounds = self.model_bounds
self._collider = BoxCollider(entity=self, center=_bounds.center, size=_bounds.size)
else:
self._collider = BoxCollider(entity=self)
self._collider.name = value
elif value == 'sphere':
self._collider = SphereCollider(entity=self, center=-self.origin)
self._collider.name = value
elif value == 'capsule':
self._collider = CapsuleCollider(entity=self, center=-self.origin)
self._collider.name = value
elif value == 'mesh' and self.model:
self._collider = MeshCollider(entity=self, mesh=self.model, center=-self.origin)
self._collider.name = value
elif isinstance(value, Mesh):
self._collider = MeshCollider(entity=self, mesh=value, center=-self.origin)
elif isinstance(value, str):
m = load_model(value)
if not m:
self._collider = None
self._collision = False
return
self._collider = MeshCollider(entity=self, mesh=m, center=-self.origin)
self._collider.name = value
self.collision = bool(self.collider)
return
def collision_getter(self):
return getattr(self, '_collision', False)
def collision_setter(self, value): # toggle collision without changing collider.
self._collision = value
if not hasattr(self, 'collider') or not self.collider:
if self in scene.collidables:
scene.collidables.remove(self)
return
if value:
self.collider.node_path.unstash()
scene.collidables.add(self)
else:
self.collider.node_path.stash()
if self in scene.collidables:
scene.collidables.remove(self)
def on_click_getter(self):
return getattr(self, '_on_click', None)
def on_click_setter(self, value):
if not callable(value):
raise TypeError(f'on_click must be a callabe, not {type(value)}')
self._on_click = value
def origin_getter(self):
return getattr(self, '_origin', Vec3.zero)
def origin_setter(self, value):
if not isinstance(value, (Vec2, Vec3)):
value = self._list_to_vec(value)
if isinstance(value, Vec2):
value = Vec3(*value, self.origin_z)
self._origin = value
if self.model:
self.model.setPos(-value[0], -value[1], -value[2])
def origin_x_getter(self):
return self.origin[0]
def origin_x_setter(self, value):
self.origin = Vec3(value, self.origin_y, self.origin_z)
def origin_y_getter(self):
return self.origin[1]
def origin_y_setter(self, value):
self.origin = Vec3(self.origin_x, value, self.origin_z)
def origin_z_getter(self):
return self.origin[2]
def origin_z_setter(self, value):
self.origin = Vec3(self.origin_x, self.origin_y, value)
def world_position_getter(self):
return Vec3(self.get_position(scene))
def world_position_setter(self, value):
if not isinstance(value, (Vec2, Vec3)):
value = self._list_to_vec(value)
if isinstance(value, Vec2):
value = Vec3(*value, self.z)
self.setPos(scene, Vec3(value[0], value[1], value[2]))
def world_x_getter(self):
return self.getX(scene)
def world_y_getter(self):
return self.getY(scene)
def world_z_getter(self):
return self.getZ(scene)
def world_x_setter(self, value):
self.setX(scene, value)
def world_y_setter(self, value):
self.setY(scene, value)
def world_z_setter(self, value):
self.setZ(scene, value)
def position_getter(self):
return Vec3(*self.getPos())
def position_setter(self, value): # right, up, forward. can also set self.x, self.y, self.z
if not isinstance(value, (Vec2, Vec3)):
value = self._list_to_vec(value)
if isinstance(value, Vec2):
value = Vec3(*value, self.z)
self.setPos(value[0], value[1], value[2])
def x_getter(self):
return self.getX()
def x_setter(self, value):
self.setX(value)
def y_getter(self):
return self.getY()
def y_setter(self, value):
self.setY(value)
def z_getter(self):
return self.getZ()
def z_setter(self, value):
self.setZ(value)
@property
def X(self): # shortcut for int(entity.x)
return int(self.x)
@property
def Y(self): # shortcut for int(entity.y)
return int(self.y)
@property
def Z(self): # shortcut for int(entity.z)
return int(self.z)
def world_rotation_getter(self):
rotation = self.getHpr(scene)
return Vec3(rotation[1], rotation[0], rotation[2]) * Entity.rotation_directions
def world_rotation_setter(self, value):
self.setHpr(scene, Vec3(value[1], value[0], value[2]) * Entity.rotation_directions)
def world_rotation_x_getter(self):
return self.world_rotation[0]
def world_rotation_x_setter(self, value):
self.world_rotation = Vec3(value, self.world_rotation[1], self.world_rotation[2])
def world_rotation_y_getter(self):
return self.world_rotation[1]
def world_rotation_y_setter(self, value):
self.world_rotation = Vec3(self.world_rotation[0], value, self.world_rotation[2])
def world_rotation_z_getter(self):
return self.world_rotation[2]
def world_rotation_z_setter(self, value):
self.world_rotation = Vec3(self.world_rotation[0], self.world_rotation[1], value)
def rotation_getter(self):
rotation = self.getHpr()
return Vec3(rotation[1], rotation[0], rotation[2]) * Entity.rotation_directions
def rotation_setter(self, value): # can also set self.rotation_x, self.rotation_y, self.rotation_z
if not isinstance(value, (Vec2, Vec3)):
value = self._list_to_vec(value)
if isinstance(value, Vec2):
value = Vec3(*value, self.rotation_z)
self.setHpr(Vec3(value[1], value[0], value[2]) * Entity.rotation_directions)
def rotation_x_getter(self):
return self.rotation.x
def rotation_x_setter(self, value):
self.rotation = Vec3(value, self.rotation[1], self.rotation[2])
def rotation_y_getter(self):
return self.rotation.y
def rotation_y_setter(self, value):
self.rotation = Vec3(self.rotation[0], value, self.rotation[2])
def rotation_z_getter(self):
return self.rotation.z
def rotation_z_setter(self, value):
self.rotation = Vec3(self.rotation[0], self.rotation[1], value)
def quaternion_getter(self):
return self.get_quat()
def quaternion_setter(self, value):
self.set_quat(value)
def world_scale_getter(self):
return Vec3(*self.getScale(scene))
def world_scale_setter(self, value):
if not isinstance(value, (Vec2, Vec3)):
value = self._list_to_vec(value)
if isinstance(value, Vec2):
value = Vec3(*value, self.scale_z)
self.setScale(scene, value)
def world_scale_x_getter(self):
return self.getScale(scene)[0]
def world_scale_x_setter(self, value):
self.setScale(scene, Vec3(value, self.world_scale_y, self.world_scale_z))
def world_scale_y_getter(self):
return self.getScale(scene)[1]
def world_scale_y_setter(self, value):
self.setScale(scene, Vec3(self.world_scale_x, value, self.world_scale_z))
def world_scale_z_getter(self):
return self.getScale(scene)[2]
def world_scale_z_setter(self, value):
self.setScale(scene, Vec3(self.world_scale_x, self.world_scale_y, value))
def scale_getter(self):
scale = self.getScale()
return Vec3(scale[0], scale[1], scale[2])
def scale_setter(self, value): # can also set self.scale_x, self.scale_y, self.scale_z
if not isinstance(value, (Vec2, Vec3)):
value = self._list_to_vec(value)
if isinstance(value, Vec2):
value = Vec3(*value, self.scale_z)
value = [e if e!=0 else .001 for e in value]
self.setScale(value[0], value[1], value[2])
def scale_x_getter(self):
return self.scale[0]
def scale_x_setter(self, value):
self.setScale(value, self.scale_y, self.scale_z)
def scale_y_getter(self):
return self.scale[1]
def scale_y_setter(self, value):
self.setScale(self.scale_x, value, self.scale_z)
def scale_z_getter(self):
return self.scale[2]
def scale_z_setter(self, value):
self.setScale(self.scale_x, self.scale_y, value)
def transform_getter(self): # get/set position, rotation and scale
return (self.position, self.rotation, self.scale)
def transform_setter(self, value):
self.position, self.rotation, self.scale = value
def world_transform_getter(self): # get/set world_position, world_rotation and world_scale
return (self.world_position, self.world_rotation, self.world_scale)
def world_transform_setter(self, value):
self.world_position, self.world_rotation, self.world_scale = value
@property
def forward(self): # get forward direction.
return Vec3(*scene.getRelativeVector(self, (0, 0, 1)))
@property
def back(self): # get backwards direction.
return -self.forward
@property
def right(self): # get right direction.
return Vec3(*scene.getRelativeVector(self, (1, 0, 0)))
@property
def left(self): # get left direction.
return -self.right
@property
def up(self): # get up direction.
return Vec3(*scene.getRelativeVector(self, (0, 1, 0)))
@property
def down(self): # get down direction.
return -self.up
@property
def screen_position(self): # get screen position(ui space) from world space.
from ursina import camera
p3d = camera.getRelativePoint(self, Vec3.zero)
full = camera.lens.getProjectionMat().xform(Vec4(*p3d, 1))
recip_full3 = 1
if full[3] > 0:
recip_full3 = 1 / full[3]
p2d = full * recip_full3
return Vec2(p2d[0]*camera.aspect_ratio/2, p2d[1]/2)
def shader_setter(self, value):
if value is None:
self._shader = value
self.setShaderAuto()
return
if isinstance(value, Panda3dShader): #panda3d shader
self._shader = value
self.setShader(value)
return
if isinstance(value, str):
if not value in shader.imported_shaders:
print_warning('missing shader:', value)
return
value = shader.imported_shaders[value]
if isinstance(value, Shader):
self._shader = value
if not value.compiled:
value.compile()
self.setShader(value._shader)
value.entity = self
for key, value in value.default_input.items():
if callable(value):
value = value()
self.set_shader_input(key, value)
return
raise ValueError(f'{value} is not a Shader')
def get_shader_input(self, name):
return self._shader_inputs.get(name, None)
def set_shader_input(self, name, value):
self._shader_inputs[name] = value
if isinstance(value, str):
value = load_texture(value)
if isinstance(value, Texture):
value = value._texture # make sure to send the panda3d texture to the shader
super().set_shader_input(name, value)
def shader_input_getter(self):
return self._shader_inputs
def shader_input_setter(self, value):
for key, value in value.items():
self.set_shader_input(key, value)
def material_setter(self, value): # a way to set shader, texture, texture_scale, texture_offset and shader inputs in one go
for name in ('shader', 'texture', 'texture_scale', 'texture_offset', 'color'):
if name in value:
setattr(self, name, value[name])
self.shader_input = {key: value for key, value in value.items() if key not in ('shader', 'texture', 'texture_scale', 'texture_offset', 'color')}
def texture_setter(self, value): # set model with texture='texture_name'. requires a model to be set beforehand.
if value is None and self.texture:
# print('remove texture')
self._texture = None
if self.model:
self.model.clearTexture()
return
if isinstance(value, str):
texture_name = value
value = load_texture(value)
# print('loaded texture:', texture)
if value is None:
if application.raise_exception_on_missing_texture:
raise ValueError(f"missing texture: '{texture_name}'")
print_warning(f"missing texture: '{texture_name}'")
return
if self.model:
self.model.setTextureOff(False)
if value.__class__ is MovieTexture:
self._texture = value
if self.model:
self.model.setTexture(value, 1)
return
self._texture = value
if self.model and value is not None:
self.model.setTexture(value._texture, 1)
def texture_scale_getter(self):
if 'texture_scale' in self._shader_inputs:
return self._shader_inputs['texture_scale']
else:
return Vec2(1,1)
def texture_scale_setter(self, value): # how many times the texture should repeat, eg. texture_scale=(8,8).
value = Vec2(*value)
if self.model and self.texture:
self.model.setTexScale(TextureStage.getDefault(), value[0], value[1])
self.set_shader_input('texture_scale', value)
def texture_offset_getter(self):
return getattr(self, '_texture_offset', Vec2(0,0))
def texture_offset_setter(self, value):
value = Vec2(*value)
if self.model and self.texture:
self.model.setTexOffset(TextureStage.getDefault(), value[0], value[1])
self.texture = self.texture
self.set_shader_input('texture_offset', value)
self._texture_offset = value
def tileset_size_getter(self): # if the texture is a tileset, say how many tiles there are so it only use one tile of the texture, e.g. tileset_size=[8,4]
return self._tileset_size
def tileset_size_setter(self, value):
self._tileset_size = value
self.texture_scale = Vec2(1/value[0], 1/value[1])
def tile_coordinate_getter(self): # set the tile coordinate, starts in the lower left.
return self._tile_coordinate
def tile_coordinate_setter(self, value):
self._tile_coordinate = value
self.texture_offset = Vec2(value[0] / self.tileset_size[0], value[1] / self.tileset_size[1])
def alpha_getter(self):
return self.color[3]
def alpha_setter(self, value): # shortcut for setting color's transparency/opacity
if value > 1:
value = value / 255
self.color = color.hsv(self.color.h, self.color.s, self.color.v, value)
def always_on_top_setter(self, value):
self._always_on_top = value
self.set_bin("fixed", 0)
self.set_depth_write(not value)
self.set_depth_test(not value)
def unlit_setter(self, value): # set to True to ignore light and not cast shadows
self._unlit = value
self.setLightOff(value)
if value:
self.hide(0b0001)
else:
self.show(0b0001)
def billboard_setter(self, value): # set to True to make this Entity always face the camera.
self._billboard = value
if value:
self.setBillboardPointEye(value)
def wireframe_setter(self, value): # set to True to render model as wireframe
self._wireframe = value
self.setRenderModeWireframe(value)
def generate_sphere_map(self, size=512, name=f'sphere_map_{len(scene.entities)}'):
from ursina import camera
_name = 'textures/' + name + '.jpg'
org_pos = camera.position
camera.position = self.position
base.saveSphereMap(_name, size=size)
camera.position = org_pos
# print('saved sphere map:', name)
self.model.setTexGen(TextureStage.getDefault(), TexGenAttrib.MEyeSphereMap)
self.reflection_map = name
def generate_cube_map(self, size=512, name=f'cube_map_{len(scene.entities)}'):
from ursina import camera
_name = 'textures/' + name
org_pos = camera.position
camera.position = self.position
base.saveCubeMap(_name+'.jpg', size=size)
camera.position = org_pos
# print('saved cube map:', name + '.jpg')
self.model.setTexGen(TextureStage.getDefault(), TexGenAttrib.MWorldCubeMap)
self.reflection_map = _name + '#.jpg'
self.model.setTexture(loader.loadCubeMap(_name + '#.jpg'), 1)
@property
def model_bounds(self):
if self.model:
if not self.model.getTightBounds():
return Bounds(start=self.world_position, end=self.world_position, center=Vec3.zero, size=Vec3.zero)
start, end = self.model.getTightBounds()
start = Vec3(start)
end = Vec3(end)
center = (start + end) / 2
size = end - start
return Bounds(start=start, end=end, center=center, size=size)
return Vec3(0,0,0)
@property
def bounds(self):
_bounds = self.model_bounds
return Bounds(start=_bounds.start*self.scale, end=_bounds.end*self.scale, center=_bounds.center, size=_bounds.size*self.scale)
def get_position(self, relative_to=scene): # get position relative to on other Entity. In most cases, use .position instead.
return Vec3(*self.getPos(relative_to))
def set_position(self, value, relative_to=scene): # set position relative to on other Entity. In most cases, use .position instead.
self.setPos(relative_to, Vec3(value[0], value[1], value[2]))
def rotate(self, value, relative_to=None, duration=0, fps=60): # rotate around local axis.
if not relative_to:
relative_to = self
if duration:
rotation_sequence = Sequence()
for i in range(60*duration):
rotation_sequence.append(Func(self.rotate, rotation_step))
rotation_sequence.append(Wait(time_step))
self.setHpr(relative_to, Vec3(value[1], value[0], value[2]) * Entity.rotation_directions)
def add_script(self, class_instance):
if isinstance(class_instance, object) and not isinstance(class_instance, str):
class_instance.entity = self
class_instance.enabled = True
self.scripts.append(class_instance)
if hasattr(class_instance, 'on_script_added') and callable(class_instance.on_script_added):
class_instance.on_script_added()
# print('added script:', camel_to_snake(name.__class__.__name__))
return class_instance
def combine(self, analyze=False, auto_destroy=True, ignore=[]):
from ursina.scripts.combine import combine
self.model = combine(self, analyze, auto_destroy, ignore)
return self.model
def flipped_faces_setter(self, value):
self._flipped_faces = value
if value:
self.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullClockwise))
else:
self.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullCounterClockwise))
def look_at(self, target, axis='forward', up=None): # up defaults to self.up
from panda3d.core import Quat
if isinstance(target, Entity):
target = Vec3(*target.world_position)
elif not isinstance(target, Vec3):
target = Vec3(*target)
up_axis = self.up
if up:
up_axis = up
self.lookAt(target, up_axis)
if axis == 'forward':
return
rotation_offset = {
'back' : Quat(0,0,1,0),
'down' : Quat(-.707,.707,0,0),
'up' : Quat(-.707,-.707,0,0),
'right' : Quat(-.707,0,.707,0),
'left' : Quat(-.707,0,-.707,0),
}[axis]
self.setQuat(rotation_offset * self.getQuat())
def look_at_2d(self, target, axis='z'):
from math import degrees, atan2
if isinstance(target, Entity):
target = Vec3(target.world_position)
pos = target - self.world_position
if axis == 'z':
self.rotation_z = degrees(atan2(pos[0], pos[1]))
elif axis == 'y':
self.rotation_y = degrees(atan2(pos[0], pos[2]))
elif axis == 'x':
self.rotation_x = degrees(atan2(pos[1], pos[2]))
def look_at_xy(self, target):
self.look_at_2d(target)
def look_at_xz(self, target):
self.look_at_2d(target, 'y')
def has_ancestor(self, possible_ancestor):
if self.parent == possible_ancestor:
return True
p = self
if isinstance(possible_ancestor, Entity):
for i in range(100):
if p.parent:
if p.parent == possible_ancestor:
return True
p = p.parent
return False
def has_disabled_ancestor(self):
p = self
for i in range(100):
if not p.parent:
return False