-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvisual_base.py
More file actions
2534 lines (2065 loc) · 104 KB
/
Copy pathvisual_base.py
File metadata and controls
2534 lines (2065 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
#/usr/bin/python3
# -*- coding: utf-8 -*-
#
# (c) 2019 by Rob Knop
#
# This file is part of physvis
#
# physvis 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 3 of the License, or
# (at your option) any later version.
# physvis 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 physvis. If not, see <https://www.gnu.org/licenses/>.
"""visual_base is a basic framework and scene graph for easily creating,
displaying, and updating simple 3d objects useful for visualization of physics.
"""
import sys
import math
import time
import queue
import threading
# import multiprocessing
import random
import ctypes
import itertools
import numpy
import numpy.linalg
from quaternions import *
from rater import Rater
# from physvis_observer import Subject, Observer # comes in grcontext
from grcontext import *
from object_collection import *
_first_context = None
# ======================================================================
# rate()
def rate(fps):
"""Call this in the main loop of your program to have it run at most every 1/fps seconds."""
rater = Rater.get()
rater.rate(fps)
def exit_whole_program():
"""Call this to have the program quit next time you call rate()."""
Rater.exit_whole_program()
# ======================================================================
# A special case 3-element numpy.ndarray. Can't create them as
# views or slices of other things. This might not be exactly what
# I want; not 100% sure. I need to experiment with old VPython to
# find out.
class vector(numpy.ndarray):
"""A 3-element vector of floats that represents a physical vector quantity (displacement, velocity, etc.).
Create a new vector with just:
v = vector()
(which initializes it to (0, 0, 0)), or with:
v = vector( (0, 1, 0) )
The argument you pass to vector() must be a sequence, i.e. a tuple,
list, numpy array, vector, or something else that has three
elements.
"""
def __new__(subtype, vals=(0., 0., 0.), copyvector=None):
if copyvector is not None:
return super().__new__(subtype, 3, float, copyvector)
if len(vals) != 3:
err = "Need 3 values to initialize a vector, got {}\n".format(len(vals))
raise IndexError(err)
# I *hope* that when I call __new__, it doesn't copy the data again.
# I don't think it does.
tmp = numpy.array(vals, dtype=float)
return super().__new__(subtype, (3), float, tmp)
@property
def mag(self):
"""The magnitude of the vector."""
return math.sqrt(numpy.square(self).sum())
@mag.setter
def mag(self, val):
self /= self.mag
self *= val
@property
def mag2(self):
"""The square of the magnitude of the vector.
Faster than getting vector.mag and then squaring.
"""
return numpy.square(self).sum()
def norm(self):
"""Returns a new vector that is the unit vector in the same direction."""
vec = vector(self)/self.mag
return vec
def cross(self, B, **unused_kwargs):
"""Returns the cross product of this vector and B."""
if type(B) is not vector:
B = vector(B)
return vector(copyvector = numpy.cross(self, B))
def proj(self, B, **unused_kwargs):
"""Returns this vector projected on to B.
A.proj(B) = A.dot(B.norm()) * B.norm()
"""
if type(B) is not vector:
B = vector(B)
Bn = B.norm()
return vector(copyvector = Bn * self.dot(Bn))
def comp(self, B, **unused_kwargs):
"""Returns the component of this vector along B.
A.comp(B) = A.dot(B.norm())
"""
if type(B) is not vector:
B = vector(B)
return self.dot(B.norm())
def diff_angle(self, B, **unused_kwargs):
"""Returns the angle between this vector and V."""
if type(B) is not vector:
B = vector(B)
return math.acos( self.dot(B) / (self.mag * B.mag) )
def rotate(self, theta, B, **unused_kwargs):
"""Rotates this vector by angle theta about vecytor B."""
if type(B) is not vector:
B = vector(B)
B = B.norm()
st = math.sin(theta/2.)
ct = math.cos(theta/2.)
roted = quaternion_rotate(self, numpy.array( [st*B[0], st*B[1], st*B[2], ct] ))
return vector(copyvector = roted)
# I don't know if this is really faster than astype(self)
def astuple(self):
"""Return a 3-element tuple of the vector components."""
return ( self[0], self[1], self[2] )
# ======================================================================
class color(object):
"""A helper class for creating colors (which are just 3-element numpy arrays.
Defined colors:
color.red — [1, 0, 0]
color.green — [0, 1, 0]
color.blue — [0, 0, 1]
color.yellow — [1, 1, 0]
color.cyan — [0, 1, 1]
color.magenta — [1, 0, 1]
color.orange — [1, 0.5, 0]
color.black — [0, 0, 0]
color.white — [1, 1, 1]
"""
red = numpy.array( [1., 0., 0.] )
green = numpy.array( [0., 1., 0.] )
blue = numpy.array( [0., 0., 1.] )
yellow = numpy.array( [1., 1., 0.] )
cyan = numpy.array( [0., 1., 1.] )
magenta = numpy.array( [1., 0., 1.] )
orange = numpy.array( [1., 0.5, 0.] )
black = numpy.array( [0., 0. ,0.] )
white = numpy.array( [1., 1., 1.] )
def gray(val):
"""Returns a grey color; val=0 is black, val=1 is white."""
return numpy.array( [val, val, val] )
def grey(val):
"""Returns a grey color; val=0 is black, val=1 is white."""
return gray(val)
# ======================================================================
# ======================================================================
# ======================================================================
class GrObject(Subject):
"""Base class for all graphical objects (Box, Sphere, etc.)"""
def __init__(self, context=None, pos=None, axis=None, up=None, scale=None,
color=None, opacity=None, make_trail=False, interval=10, retain=50,
trail_radius = 0.02, trail_color=None, *args, **kwargs):
"""Parameters:
context — the context in which this object will exist
pos — The position of the object (vector)
axis — The orientation of the object, and, if it's not normalized, the scale along its standard axis
up — not implemented
scale — How much to scale the object (interactis with the amgnitude of axis)
color — The color of the object (r, g, b) or (r, g, b, a)
make_trail — True to leave behind a trail
interval — Only add a trail segment after the object has moved this many times (default: 10)
retain — Only keep this many trail segments (the most recent ones) (Default: 50)
trail_radius — radius of trail cross-section (def: 0.02)
trail_color — color of trail (def: same as object)
"""
super().__init__(*args, **kwargs)
self._object_type = GLObjectCollection._OBJ_TYPE_SIMPLE
self._make_trail = False
self._trail = None
self.num_triangles = 0
self._visible = True
# sys.stderr.write("Starting GrObject.__init__")
if context is None:
self.context = GrContext.get_default_instance()
else:
self.context = context
self.draw_as_lines = False
self._rotation = numpy.array( [0., 0., 0., 1.] ) # Identity quaternion
if pos is None:
self._pos = vector([0., 0., 0.])
else:
self._pos = vector(pos)
if scale is None:
self._scale = numpy.array([1., 1., 1.])
else:
self._scale = scale
self.colordata = None
if color is None and opacity is None:
self._color = numpy.array( self.context.default_color, dtype=numpy.float32 )
elif color is None:
self._color = numpy.empty(4, dtype=numpy.float32)
self._color[0:3] = context.default_color[0:3]
self._color[3] = opacity
else:
self._color = numpy.empty(4, dtype=numpy.float32)
self._color[0:3] = numpy.array(color)[0:3]
if opacity is None:
self._color[3] = 1.
else:
self._color[3] = opacity
# ROB, write an interface for these
self._specular_strength = 0.75
self._specular_pow = 32
self.model_matrix = numpy.array( [ [ 1., 0., 0., 0. ],
[ 0., 1., 0., 0. ],
[ 0., 0., 1., 0. ],
[ 0., 0., 0., 1. ] ], dtype=numpy.float32)
self.inverse_model_matrix = numpy.array( [ [ 1., 0., 0., 0.],
[ 0., 1., 0., 0.],
[ 0., 0., 1., 0.] ], dtype=numpy.float32)
self.vertexdata = None
self.normaldata = None
self.matrixdata = None
self.normalmatrixdata = None
self._axis = vector([1., 0., 0.])
self._up = vector([0., 1., 0.])
if axis is not None:
self.axis = vector(axis)
if up is not None:
self.up = numpy.array(up)
self._interval = interval
self._nexttrail = interval
self._retain = retain
self._trail_radius = trail_radius
self._trail_color = trail_color
self.make_trail = make_trail
def finish_init(self):
self.update_model_matrix()
self.context.add_object(self)
@property
def pos(self):
"""The position of the object (a vector). Set this to move the object."""
return self._pos
@pos.setter
def pos(self, value):
self._pos = vector(value)
self.update_model_matrix()
self.update_trail()
@property
def x(self):
"""The x-component of object position."""
return self._pos[0]
@x.setter
def x(self, value):
self._pos[0] = value
self.update_model_matrix()
self.update_trail()
@property
def y(self):
"""The y-component of object position."""
return self._pos[1]
@y.setter
def y(self, value):
self._pos[1] = value
self.update_model_matrix()
self.update_trail()
@property
def z(self):
"""The z-component of object position."""
return self._pos[2]
@z.setter
def z(self, value):
self._pos[2] = value
self.update_model_matrix()
self.update_trail()
@property
def scale(self):
return self._scale
@scale.setter
def scale(self, value):
if len(value) != 3:
sys.stderr.write("ERROR, scale must have 3 elements.")
sys.exit(20)
self._scale = numpy.array(value)
self.update_model_matrix()
@property
def sx(self):
return self._scale[0]
@sx.setter
def sx(self, value):
self._scale[0] = value
self.update_model_matrix()
@property
def sy(self):
return self._scale[1]
@sy.setter
def sy(self, value):
self._scale[1] = value
self.update_model_matrix()
@property
def sz(self):
return self._scale[2]
@sz.setter
def sz(self, value):
self._scale[2] = value
self.update_model_matrix()
@property
def axis(self):
"""The orientation of the object. Set this to rotate (and maybe stretch) the object.
Objects by default have an axis along the x-axis, so if you pass
[1,0,0], you'll get the default object orientation. The
magnitude of axis scales the object along it's primary axis.
(The meaning of the primary axis depends on the type of object.)
"""
return self._axis
@axis.setter
def axis(self, value):
if len(value) != 3:
raise Exception("axis must have 3 values")
newaxis = numpy.array( value, dtype=float )
axismag = math.sqrt( newaxis[0]*newaxis[0] + newaxis[1]*newaxis[1] + newaxis[2]*newaxis[2] )
if axismag < 1e-8:
raise Exception("axis too short")
newaxis /= axismag
# # This code will figure out the orientation from scratch.
# # The problem is that it doesn't lead to smooth rotations.
# self._axis = newaxis
# self._scale[0] = axismag
# self.set_object_rotation()
# Figure out the direct rotation to go from self._axis to
# newaxis, and add that on top of current rotation. (I'm
# a little worried about accumulating precision errors.)
# Rotate about self._axis × newaxis (normalized)
# The dot product self._axis · newaxis gives the cos of the angle to rotate (both vectors are normalized)
# But, because of floating point inefficiencies, I still gotta clip it
cosrot = self._axis[0]*newaxis[0] + self._axis[1]*newaxis[1] + self._axis[2]*newaxis[2]
if cosrot > 1.: cosrot = 1.
elif cosrot < -1.: cosrot = -1.
# If the new axis is parallel or antiparallel, then we can't use
# axis cross newaxis as the rotation axis
if 1-math.fabs(cosrot) < 1e-8:
if cosrot > 0.:
# No actual rotation (well, dinky)
if self._scale[0] != axismag:
self._scale[0] = axismag
self.update_model_matrix()
return
else:
# newaxis is opposite self._axis. Try crossing with zhat to get a rotaxis
rotax = numpy.array( [ self._axis[1], -self._axis[0], 0. ] )
# If that didn't work, then use yhat
rotaxmag = math.sqrt( rotax[0]*rotax[0] + rotax[1]*rotax[1] + rotax[2]*rotax[2] )
if rotaxmag < 1e-10:
rotax = numpy.array( [ -self._axis[2], 0., self._axis[0] ] )
rotaxmag = math.sqrt( rotax[0]*rotax[0] + rotax[1]*rotax[1] + rotax[2]*rotax[2] )
else:
rotax = numpy.array( [ self._axis[1]*newaxis[2] - self._axis[2]*newaxis[1],
self._axis[2]*newaxis[0] - self._axis[0]*newaxis[2],
self._axis[0]*newaxis[1] - self._axis[1]*newaxis[0] ] )
rotaxmag = math.sqrt( numpy.square(rotax).sum() )
rotax /= rotaxmag
cosrot_2 = math.sqrt( (1+cosrot) / 2. )
sinrot_2 = math.sqrt( (1-cosrot) / 2. )
self._rotation = quaternion_multiply( [ sinrot_2 * rotax[0],
sinrot_2 * rotax[1],
sinrot_2 * rotax[2],
cosrot_2 ] , self._rotation )
self._axis = numpy.array( newaxis )
self._scale[0] = axismag
self.update_model_matrix()
@property
def up(self):
"""A vector in the object's frame tries to be up on the screen.
If you read this, the results could be meaningless.
Pass a vector in the object's frame; the object will be rotated
around its axis in an attempt to make that vector "up" on the
screen. Only the y- and z- components of up matter, as the x
component in the object's frame points along the object's axis.
"""
return self._up
@up.setter
def up(self, value):
if len(value) != 3:
sys.stderr.write("ERROR, up must have 3 elements.")
# Only the component in the yz plane matters. Normalize.
yzmag = math.sqrt(value[1]**2 + value[2]**2)
# Punt if this is zero
if yzmag < 1e-8:
self._up = numpy.array( [0., 1., 0.] )
else:
self._up = numpy.array( [ 0., value[1]/yzmag, value[2]/yzmag ] )
self.set_object_rotation()
@property
def trail_radius(self):
return self._trail_radius
@trail_radius.setter
def trail_radius(self, val):
self._trail_radius = val
if self._trail is not None:
self._trail.radius = self._trail_radius
@property
def rotation(self):
"""Returns (for now) a quaternion representing the rotation of the object away from [1, 0, 0]"""
return self._rotation
@rotation.setter
def rotation(self, value):
if len(value) != 4:
sys.sderr.write("rotation is a quaternion, needs 4 values\n")
sys.exit(20)
self._rotation = numpy.array(value)
self.update_model_matrix()
def rotate(self, angle, axis=None, origin=None):
"""Rotate the object by angle angle about axis axis."""
if axis is None:
axis = self.axis
axis = numpy.array(axis)
axis /= math.sqrt(numpy.square(axis).sum())
s = math.sin(angle/2.)
c = math.cos(angle/2.)
q = numpy.array( [axis[0]*s, axis[1]*s, axis[2]*s, c] )
if origin is not None:
if len(origin) != 3:
raise Exception("Error, origin must have 3 values.")
origin = numpy.array(origin)
relpos = self._pos - origin
relpos = quaternion_rotate(relpos, q)
self.pos = origin + relpos
self.rotation = quaternion_multiply(q, self.rotation)
def set_object_rotation(self):
"""Figures out what the quaternion self._rotation should be from self._axis and self._up.
(Both self._axis and self._up must be normalized
"""
# θ is the angle off of the x-axis
# φ is the angle in the y-z plane off of y towards z (i.e. about x)
# costheta = math.sqrt(1 - self._axis[1])
try:
costheta = self._axis[0]
costheta_2 = math.sqrt( (1+costheta) / 2. )
sintheta_2 = math.sqrt( (1-costheta) / 2. )
except ValueError:
import pdb; pdb.set_trace()
# Make sure we aren't going to divide by (close to) 0
yzmag = math.sqrt( self._axis[1]**2 + self._axis[2]**2 )
if yzmag < 1e-12:
# Object is oriented effectively along ±x
if self._axis[1] < 0.:
baserot = numpy.array( [0., 1., 0., 0.] ) # rot by π about y
else:
baserot = numpy.array( [0., 0., 0., 1.] ) # no rotation
else:
cosphi = self._axis[1] / yzmag
cosphi_2 = math.sqrt( (1+cosphi) / 2. )
sinphi_2 = math.sqrt( (1-cosphi) / 2. )
if self._axis[2] < 0.:
# gotta rotate about -x
xrot = -1.
else:
xrot = 1.
# This is the quaternion for a rotation of θ about z followed by a rotation of φ about ±x (I HOPE!)
baserot = numpy.array( [ costheta_2 * sinphi_2 * xrot,
-sintheta_2 * sinphi_2 * xrot,
sintheta_2 * cosphi_2,
costheta_2 * cosphi_2 ] )
# Finally, rotate around axis by an angle determined by up
rotup = quaternion_rotate( self._up, baserot )
if rotup[2] < 0.:
psirot = 1
else:
psirot = -1.
cospsi = rotup[1]
# sys.stderr.write("up={}, rotup={}, cospsi={}\n".format(self._up, rotup, cospsi))
cospsi_2 = math.sqrt( (1+cospsi)/2. )
sinpsi_2 = math.sqrt( (1-cospsi)/2. )
self._rotation = quaternion_multiply( [ sinpsi_2 * self._axis[0] * psirot,
sinpsi_2 * self._axis[1] * psirot,
sinpsi_2 * self._axis[2] * psirot,
cospsi_2 ] , baserot )
self.update_model_matrix()
def update_model_matrix(self):
"""(Internal function to update stuff needed by OpenGL.)"""
q = self._rotation
s = 1./( q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3] )
q0 = q[0]
q1 = q[1]
q2 = q[2]
q3 = q[3]
rot = numpy.array(
[[ 1.-2*s*(q1*q1+q2*q2) , 2*s*(q0*q1-q2*q3) , 2*s*(q0*q2+q1*q3)],
[ 2*s*(q0*q1+q2*q3) , 1.-2*s*(q0*q0+q2*q2) , 2*s*(q1*q2-q0*q3)],
[ 2*s*(q0*q2-q1*q3) , 2*s*(q1*q2+q0*q3) , 1.-2*s*(q0*q0+q1*q1)]],
dtype=numpy.float32)
# Inverse quaternion, just flip the sign on elements 0, 1, 2
invrot = numpy.array(
[[ 1.-2*s*(q1*q1+q2*q2) , 2*s*(q0*q1+q2*q3) , 2*s*(q0*q2-q1*q3)],
[ 2*s*(q0*q1-q2*q3) , 1.-2*s*(q0*q0+q2*q2) , 2*s*(q1*q2+q0*q3)],
[ 2*s*(q0*q2+q1*q3) , 2*s*(q1*q2-q0*q3) , 1.-2*s*(q0*q0+q1*q1)]],
dtype=numpy.float32)
sca = numpy.array( [[ self._scale[0], 0., 0., 0. ],
[ 0., self._scale[1], 0., 0. ],
[ 0., 0., self._scale[2], 0. ],
[ 0., 0., 0., 1.]], dtype=numpy.float32 )
invsca = numpy.array( [[ 1./self._scale[0], 0., 0., 0. ],
[ 0., 1./self._scale[1], 0., 0. ],
[ 0., 0., 1./self._scale[2], 0. ],
[ 0., 0., 0., 1.]], dtype=numpy.float32 )
# Turns out this is *slightly* faster tha numpy.identity(4, dtype=numpy.float32)
rotation = numpy.array( [ [1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1] ], dtype=numpy.float32)
rotation[0:3, 0:3] = rot.T
invrotation = numpy.array( [ [1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1] ], dtype=numpy.float32)
invrotation[0:3, 0:3] = invrot.T
translation = numpy.array( [ [1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1] ], dtype=numpy.float32)
translation[3, 0:3] = self._pos
invtrans = numpy.array( [ [1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1] ], dtype=numpy.float32)
invtrans[3, 0:3] = -self._pos
mat = numpy.matmul(sca, rotation)
mat = numpy.matmul(mat, translation)
self.model_matrix[:] = mat
mat = numpy.matmul(invtrans, invrotation)
mat = numpy.matmul(mat, invsca)
self.inverse_model_matrix[0:3, 0:3] = mat[0:3, 0:3].T
# It was faster to construct the inverse manually here
# self.inverse_model_matrix[0:3, 0:3] = numpy.linalg.inv(mat[0:3, 0:3]).T
self.broadcast("update matrix")
@property
def visible(self):
"""Set to False to remove an object from the display, True to put it back."""
return self._visible
@visible.setter
def visible(self, value):
# sys.stderr.write("In visible setter\n")
value = bool(value)
if value == self._visible: return
self._visible = value
if value == True:
self.context.add_object(self)
if self._trail is not None:
self._trail.visible = True
else:
# import pdb; pdb.set_trace()
self.context.remove_object(self)
if self._trail is not None:
self._trail.visible = False
@property
def color(self):
"""The color of an object: (red, green, blue)"""
return self._color[0:3]
@color.setter
def color(self, rgb):
if len(rgb) != 3:
sys.stderr.write("ERROR! Need all of r, g, and b for color.\n")
sys.exit(20)
self._color[0:3] = numpy.array(rgb)
self.broadcast("update color")
@property
def opacity(self):
"""Opacity is not implemented."""
return self.color[3]
@opacity.setter
def opacity(self, alpha):
self._color[3] = alpha
self.update_colordata()
self.broadcast("update color")
@property
def make_trail(self):
"""Set this to True to start leaving a thin trail behind the object as you move it."""
return self._make_trail
@make_trail.setter
def make_trail(self, val):
if not val:
if self._make_trail:
self.kill_trail()
self._make_trail = False
if val:
if not self._make_trail:
self.initialize_trail()
self._make_trail = True
@property
def interval(self):
"""Only add a new trail segment after the object has been moved this many times."""
return self._interval
@interval.setter
def interval(self, val):
self._interval = val
if self._nexttrail > self._interval:
self._nexttrail = self._interval
@property
def retain(self):
"""Number of trail segments to keep."""
return self._retain
@retain.setter
def retain(self, val):
if val != self._retain:
self._retain = val
if self._make_trail:
self.initialize_trail()
def kill_trail(self):
"""(Internal, do not call.)"""
if self._trail is not None:
self._trail.visible = False
self._trail = None
def initialize_trail(self):
"""(Internal, do not call.)"""
self.kill_trail()
if self._trail_color is None:
color = self.color
else:
color = self._trail_color
self._trail = Curve( color=color, retain=self._retain, points=[ self._pos ],
radius=self._trail_radius)
self._nexttrail = self._interval
def clear_trail(self):
self._trail.points = []
def update_trail(self):
"""(Internal, do not call.)"""
if not self._make_trail: return
self._nexttrail -= 1
if self._nexttrail <= 0:
self._nexttrail = self._interval
self._trail.add_point(self._pos)
# self._trail.push_point(self._pos)
def __del__(self):
raise Exception("Rob, you really need to think about object deletion.")
self.visible = False
self.destroy()
def destroy(self):
pass
# =====================================================================
class Faces(GrObject):
"""A set of triangles.
vertices must be a [3*faces, 3] numpy array. Each of the elements
along axis 0 is one vertex (with x, y, z indexed along axis 1).
Three elements in a row specify a triangle. Try to make it so that
the outward face of the triangle is what you'd get from the
right-hand-rule crossing the second minus first vertices with the
third minus second.
"""
def __init__(self, vertices, normals=None, smooth=False, *args, **kwargs):
"""Parameters:
vertices — The vertices of all the triangles; must be [3*nfaces, 3]
normals — An array of normals, the same shape as vertices. Optional.
"""
super().__init__(*args, **kwargs)
self.num_triangles = vertices.shape[0]//3
if ( len(vertices.shape) != 2 or
vertices.shape[0] % 3 != 0 or
vertices.shape[0] == 0 or
vertices.shape[1] != 3 ):
raise Exception("Faces requires a (3n, 3) numpy array.")
self.vertexdata = numpy.ones(self.num_triangles * 3 * 4, dtype=numpy.float32)
self.normaldata = numpy.zeros(self.num_triangles * 3 * 3, dtype=numpy.float32)
if normals is not None:
if normals.shape != vertices.shape:
raise Exception("Faces must have (3n, 3) numpy array for both vertices and normals.")
for i in range(3*self.num_triangles):
self.vertexdata[4*i : 4*i+3] = vertices[i : i+3, :]
self.normaldata[3*i : 3*i+3] = normaldata[i : i+3, :]
else:
if smooth:
raise Exception("Faces: smooth isn't implemented.")
else:
for i in range(self.num_triangles):
self.vertexdata[3*4*i+0 : 3*4*i+3 ] = vertices[3*i , :]
self.vertexdata[3*4*i+4 : 3*4*i+7 ] = vertices[3*i+1, :]
self.vertexdata[3*4*i+8 : 3*4*i+11] = vertices[3*i+2, :]
l1 = vertices[3*i+1, :] - vertices[3*i, :]
l2 = vertices[3*i+2, :] - vertices[3*i+1, :]
norm = numpy.array( [ l1[1]*l2[2] - l1[2]*l2[1],
l1[2]*l2[0] - l1[0]*l2[2],
l1[0]*l2[1] - l1[1]*l2[0] ],
dtype=numpy.float32 )
normnorm = math.sqrt( norm[0]*norm[0] + norm[1]*norm[1] + norm[2]*norm[2] )
if normnorm < 1e-6:
import pdb; pdb.set_trace()
raise Exception("Faces error: degenerate triangle.")
norm /= normnorm
self.normaldata[3*3*i : 3*3*i+3] = norm
self.normaldata[3*3*i+3 : 3*3*i+6] = norm
self.normaldata[3*3*i+6 : 3*3*i+9] = norm
self.finish_init()
def destroy(self):
raise Exception("OMG ROB! You need to figure out how to destroy things!")
# ======================================================================
class Box(GrObject):
"""A rectangular solid with dimenions (x,y,z) = (length,height,width)"""
@staticmethod
def make_box_buffers(context):
with Subject._threadlock:
if not hasattr(Box, "_box_vertices"):
Box._box_vertices = numpy.array( [ -0.5, -0.5, 0.5, 1.,
-0.5, -0.5, -0.5, 1.,
0.5, -0.5, 0.5, 1.,
0.5, -0.5, 0.5, 1.,
-0.5, -0.5, -0.5, 1.,
0.5, -0.5, -0.5, 1.,
-0.5, 0.5, 0.5, 1.,
0.5, 0.5, 0.5, 1.,
-0.5, 0.5, -0.5, 1.,
-0.5, 0.5, -0.5, 1.,
0.5, 0.5, 0.5, 1.,
0.5, 0.5, -0.5, 1.,
-0.5, -0.5, -0.5, 1.,
-0.5, -0.5, 0.5, 1.,
-0.5, 0.5, -0.5, 1.,
-0.5, 0.5, -0.5, 1.,
-0.5, -0.5, 0.5, 1.,
-0.5, 0.5, 0.5, 1.,
0.5, 0.5, -0.5, 1.,
0.5, 0.5, 0.5, 1.,
0.5, -0.5, -0.5, 1.,
0.5, -0.5, -0.5, 1.,
0.5, 0.5, 0.5, 1.,
0.5, -0.5, 0.5, 1.,
-0.5, -0.5, 0.5, 1.,
0.5, -0.5, 0.5, 1.,
-0.5, 0.5, 0.5, 1.,
-0.5, 0.5, 0.5, 1.,
0.5, -0.5, 0.5, 1.,
0.5, 0.5, 0.5, 1.,
0.5, -0.5, -0.5, 1.,
-0.5, -0.5, -0.5, 1.,
0.5, 0.5, -0.5, 1.,
0.5, 0.5, -0.5, 1.,
-0.5, -0.5, -0.5, 1.,
-0.5, 0.5, -0.5, 1. ],
dtype = numpy.float32 )
Box._box_normals = numpy.array( [ 0., -1., 0., 0., -1., 0., 0., -1., 0.,
0., -1., 0., 0., -1., 0., 0., -1., 0.,
0., 1., 0., 0., 1., 0., 0., 1., 0.,
0., 1., 0., 0., 1., 0., 0., 1., 0.,
-1., 0., 0., -1., 0., 0., -1., 0., 0.,
-1., 0., 0., -1., 0., 0., -1., 0., 0.,
1., 0., 0., 1., 0., 0., 1., 0., 0.,
1., 0., 0., 1., 0., 0., 1., 0., 0.,
0., 0., 1., 0., 0., 1., 0., 0., 1.,
0., 0., 1., 0., 0., 1., 0., 0., 1.,
0., 0., -1., 0., 0., -1., 0., 0., -1.,
0., 0., -1., 0., 0., -1., 0., 0., -1. ],
dtype = numpy.float32 )
def __init__(self, length=1., width=1., height=1., *args, **kwargs):
"""Parameters:
length — The size of the box along the x-axis (left-right with default camera orientation)
height — The size of the box along the y-axis (up-down with default camera orientation)
width — The size of the box along the z-axis (in-out with default camera orientatino)
Plus standard GrObject parameters: context, pos, axis, up, scale, color, etc. from GrObject.
"""
super().__init__(*args, **kwargs)
Box.make_box_buffers(self.context)
self.num_triangles = 12
self.vertexdata = Box._box_vertices
self.normaldata = Box._box_normals
self.length = length
self.width = width
self.height = height
self.finish_init()
@property
def length(self):
return self.sx
@length.setter
def length(self, value):
self.sx = value
@property
def width(self):
return self.sz
@width.setter
def width(self, value):
self.sz = value
@property
def height(self):
return self.sy
@height.setter
def height(self, value):
self.sy = value
def destroy(self):
raise Exception("OMG ROB! You need to figure out how to destroy things!")
# ======================================================================
class Tetrahedron(Faces):
"""A tetrahedron"""
def __init__(self, *args, **kwargs):
"""Creates a tetrahedron centered (I think) on the origin.
Uses standard GrObject parameters: context, pos, axis, up, scale, color, etc. from GrObject.
"""
norm = 2*math.sqrt(2/3)
sqrt6 = math.sqrt(6)
sqrt3 = math.sqrt(3)
sqrt2 = math.sqrt(2)
verts = numpy.array( [ [ norm*sqrt3/(2*sqrt2) , 0 , 0, ],
[ -norm/(2*sqrt6), norm/2, -norm/(2*sqrt3), ],
[ -norm/(2*sqrt6), 0, norm/sqrt3, ],
[ norm*sqrt3/(2*sqrt2) , 0 , 0, ],
[ -norm/(2*sqrt6), 0, norm/sqrt3, ],
[ -norm/(2*sqrt6), -norm/2., -norm/(2*sqrt3), ],
[ norm*sqrt3/(2*sqrt2) , 0 , 0, ],
[ -norm/(2*sqrt6), -norm/2., -norm/(2*sqrt3), ],
[ -norm/(2*sqrt6), norm/2, -norm/(2*sqrt3), ],
[ -norm/(2*sqrt6), norm/2, -norm/(2*sqrt3), ],
[ -norm/(2*sqrt6), -norm/2, -norm/(2*sqrt3), ],
[ -norm/(2*sqrt6), 0, norm/sqrt3 ] ]
,dtype=numpy.float32 )
super().__init__(verts, smooth=False, *args, **kwargs)
# ======================================================================
class Octahedron(Faces):
"""An octahedron"""
def __init__(self, *args, **kwargs):
verts = numpy.array( [ [ 0., 1., 0.,], [ 0., 0., 1.], [ 1., 0., 0.],
[ 0., 1., 0.,], [-1., 0., 0.], [ 0., 0., 1.],
[ 0., 1., 0.,], [ 0., 0., -1.], [-1., 0., 0.],
[ 0., 1., 0.,], [ 1., 0., 0.], [ 0., 0., -1.],
[ 0., -1., 0.,], [ 1., 0., 0.], [ 0., 0., 1.],
[ 0., -1., 0.,], [ 0., 0., 1.], [-1., 0., 0.],
[ 0., -1., 0.,], [-1., 0., 0.], [ 0., 0., -1.],
[ 0., -1., 0.,], [ 0., 0., -1.], [ 1., 0., 0.] ],
dtype=numpy.float32 )