shaderflow 0.4.0.dev1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shaderflow might be problematic. Click here for more details.

Files changed (51) hide show
  1. ShaderFlow/Common/Imgui.py +0 -0
  2. ShaderFlow/Common/Notes.py +134 -0
  3. ShaderFlow/Common/__init__.py +0 -0
  4. ShaderFlow/Message.py +147 -0
  5. ShaderFlow/Module.py +162 -0
  6. ShaderFlow/Modules/Audio.py +431 -0
  7. ShaderFlow/Modules/Bouncing.py +94 -0
  8. ShaderFlow/Modules/Camera.py +451 -0
  9. ShaderFlow/Modules/Depth.py +0 -0
  10. ShaderFlow/Modules/Dynamics.py +288 -0
  11. ShaderFlow/Modules/Frametimer.py +79 -0
  12. ShaderFlow/Modules/Keyboard.py +43 -0
  13. ShaderFlow/Modules/Noise.py +80 -0
  14. ShaderFlow/Modules/OpticalFlow.py +1 -0
  15. ShaderFlow/Modules/Piano.py +362 -0
  16. ShaderFlow/Modules/Spectrogram.py +315 -0
  17. ShaderFlow/Modules/Video.py +234 -0
  18. ShaderFlow/Modules/Waveform.py +100 -0
  19. ShaderFlow/Modules/__init__.py +0 -0
  20. ShaderFlow/Resources/Images/ShaderFlow.png +0 -0
  21. ShaderFlow/Resources/Images/ShaderFlow.svg +1 -0
  22. ShaderFlow/Resources/Scenes/Examples/Demo.py +284 -0
  23. ShaderFlow/Resources/Scenes/Examples/GLSL/Bars.frag +20 -0
  24. ShaderFlow/Resources/Scenes/Examples/GLSL/Bouncing.frag +50 -0
  25. ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Simulation.glsl +49 -0
  26. ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Visuals.glsl +31 -0
  27. ShaderFlow/Resources/Scenes/Examples/GLSL/MotionBlur.frag +20 -0
  28. ShaderFlow/Resources/Scenes/Examples/GLSL/Multipass.frag +43 -0
  29. ShaderFlow/Resources/Scenes/Examples/GLSL/RayMarch.frag +59 -0
  30. ShaderFlow/Resources/Scenes/Examples/GLSL/ShaderToy.frag +65 -0
  31. ShaderFlow/Resources/Scenes/Examples/GLSL/Video.frag +5 -0
  32. ShaderFlow/Resources/Scenes/Examples/GLSL/Visualizer.frag +75 -0
  33. ShaderFlow/Resources/Scenes/Examples/GLSL/Waveform.frag +18 -0
  34. ShaderFlow/Resources/Shaders/Fragment/Default.glsl +65 -0
  35. ShaderFlow/Resources/Shaders/Fragment/Final.glsl +7 -0
  36. ShaderFlow/Resources/Shaders/Fragment/Loading.frag +40 -0
  37. ShaderFlow/Resources/Shaders/Fragment/Missing.glsl +23 -0
  38. ShaderFlow/Resources/Shaders/Include/Camera.glsl +166 -0
  39. ShaderFlow/Resources/Shaders/Include/Complex.glsl +0 -0
  40. ShaderFlow/Resources/Shaders/Include/ShaderFlow.glsl +444 -0
  41. ShaderFlow/Resources/Shaders/Vertex/Default.glsl +21 -0
  42. ShaderFlow/Scene.py +1124 -0
  43. ShaderFlow/Shader.py +391 -0
  44. ShaderFlow/Texture.py +528 -0
  45. ShaderFlow/Variable.py +128 -0
  46. ShaderFlow/__init__.py +11 -0
  47. ShaderFlow/__main__.py +22 -0
  48. shaderflow-0.4.0.dev1.dist-info/METADATA +79 -0
  49. shaderflow-0.4.0.dev1.dist-info/RECORD +51 -0
  50. shaderflow-0.4.0.dev1.dist-info/WHEEL +5 -0
  51. shaderflow-0.4.0.dev1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,451 @@
1
+ """
2
+ The Camera requires some prior knowledge of a fun piece of math called Quaternions.
3
+
4
+ They are a 4D "imaginary" number that inherently represents rotations in 3D space without the
5
+ need of 3D rotation matrices (which are ugly!)*, and are pretty intuitive to use.
6
+
7
+ * https://github.com/moble/quaternion/wiki/Euler-angles-are-horrible
8
+
9
+
10
+ Great resources for understanding Quaternions:
11
+
12
+ • "Quaternions and 3d rotation, explained interactively" by 3blue1brown
13
+ - https://www.youtube.com/watch?v=d4EgbgTm0Bg
14
+
15
+ • "Visualizing quaternions (4d numbers) with stereographic projection" by 3blue1brown
16
+ - https://www.youtube.com/watch?v=zjMuIxRvygQ
17
+
18
+ • "Visualizing quaternion, an explorable video series" by Ben Eater and 3blue1brown
19
+ - https://eater.net/quaternions
20
+
21
+
22
+ Useful resources on Linear Algebra and Coordinate Systems:
23
+
24
+ • "The Essence of Linear Algebra" by 3blue1brown
25
+ - https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab
26
+
27
+ • "here, have a coordinate system chart~" by @FreyaHolmer
28
+ - https://twitter.com/FreyaHolmer/status/1325556229410861056
29
+ """
30
+
31
+ import math
32
+ from typing import Iterable, Self, Tuple, Union
33
+
34
+ import numpy
35
+ import quaternion
36
+ from attr import define
37
+
38
+ from Broken import BrokenEnum, clamp, log
39
+ from Broken.Types import Degrees
40
+ from ShaderFlow import SHADERFLOW
41
+ from ShaderFlow.Message import ShaderMessage
42
+ from ShaderFlow.Module import ShaderModule
43
+ from ShaderFlow.Modules.Dynamics import DynamicNumber, ShaderDynamics
44
+ from ShaderFlow.Modules.Keyboard import ShaderKeyboard
45
+ from ShaderFlow.Variable import ShaderVariable
46
+
47
+ # -------------------------------------------------------------------------------------------------|
48
+
49
+ Quaternion = quaternion.quaternion
50
+ Vector3D = numpy.ndarray
51
+ _dtype = numpy.float32
52
+
53
+ class GlobalBasis:
54
+ Origin = numpy.array((0, 0, 0), dtype=_dtype)
55
+ Null = numpy.array((0, 0, 0), dtype=_dtype)
56
+ X = numpy.array((1, 0, 0), dtype=_dtype)
57
+ Y = numpy.array((0, 1, 0), dtype=_dtype)
58
+ Z = numpy.array((0, 0, 1), dtype=_dtype)
59
+
60
+ # -------------------------------------------------------------------------------------------------|
61
+
62
+ class CameraProjection(BrokenEnum):
63
+ """
64
+ # Perspective
65
+ Project from a Plane A at the position to a Plane B at a distance of one
66
+ - The plane is always perpendicular to the camera's direction
67
+ - Plane A is multiplied by isometric, Plane B by Zoom
68
+
69
+ # VirtualReality
70
+ Two halves of the screen, one for each eye, with a separation between them
71
+
72
+ # Equirectangular
73
+ The "360°" videos we see on platforms like YouTube, it's a simples sphere projected to the
74
+ screen where X defines the azimuth and Y the inclination, ranging such that they sweep the sphere
75
+ """
76
+ Perspective = 0
77
+ VirtualReality = 1
78
+ Equirectangular = 2
79
+
80
+ class CameraMode(BrokenEnum):
81
+ """
82
+ How to deal with Rotations and actions on 3D or 2D space
83
+ - FreeCamera: Apply quaternion rotation and don't care of roll changing the "UP" direction
84
+ - Camera2D: Fixed direction, drag moves position on the plane of the screen, becomes isometric
85
+ - Spherical: Always correct such that the camera orthonormal base is pointing "UP"
86
+ """
87
+ FreeCamera = 0
88
+ Camera2D = 1
89
+ Spherical = 2
90
+
91
+ # -------------------------------------------------------------------------------------------------|
92
+
93
+ class Algebra:
94
+
95
+ def rotate_vector(vector: Vector3D, R: Quaternion) -> Vector3D:
96
+ """
97
+ Applies a Quaternion rotation to a vector.
98
+
99
+ • Permalink: https://github.com/moble/quaternion/blob/2286c479016097b156682eddaf927036c192c22e/src/quaternion/__init__.py#L654
100
+
101
+ As numpy-quaternion documentation says, we should avoid quaternion.rotate_vectors
102
+ when we don't have multiple vectors to rotate, and we mean a lot of vectors.
103
+
104
+ Args:
105
+ vector (Vector3D): Vector to rotate
106
+ R (Quaternion): Rotation quaternion
107
+
108
+ Returns:
109
+ Vector3D: Rotated vector
110
+ """
111
+ # Potential speed gains, need to verify
112
+ # if sum(quaternion.as_float_array(R)[1:]) < 1e-6:
113
+ # return vector
114
+ return quaternion.as_vector_part(R * quaternion.quaternion(0, *vector) * R.conjugate())
115
+
116
+ def quaternion(axis: Vector3D, angle: Degrees) -> Quaternion:
117
+ """Builds a quaternion that represents an rotation around an axis for an angle"""
118
+ theta = math.radians(angle/2)
119
+ return Quaternion(math.cos(theta), *(math.sin(theta)*axis))
120
+
121
+ def angle(A: Vector3D, B: Vector3D) -> Degrees:
122
+ """
123
+ Returns the angle between two vectors by the linear algebra formula:
124
+ • Theta(A, B) = arccos( (A·B) / (|A|*|B|) )
125
+ • Safe for zero vector norm divisions
126
+ • Clips the arccos domain to [-1, 1] to avoid NaNs
127
+ """
128
+ A, B = DynamicNumber.extract(A, B)
129
+
130
+ # Avoid zero divisions
131
+ if not (LA := numpy.linalg.norm(A)):
132
+ return 0.0
133
+ if not (LB := numpy.linalg.norm(B)):
134
+ return 0.0
135
+
136
+ # Inner cosine; avoid NaNs
137
+ cos = numpy.clip(numpy.dot(A, B)/(LA*LB), -1, 1)
138
+ return numpy.degrees(numpy.arccos(cos))
139
+
140
+ def unit_vector(vector: Vector3D) -> Vector3D:
141
+ """Returns the unit vector of a given vector, safely"""
142
+ if (factor := numpy.linalg.norm(vector)):
143
+ return vector/factor
144
+ return vector
145
+
146
+ @staticmethod
147
+ def safe(
148
+ *vector: Union[numpy.ndarray, Tuple[float], float, int],
149
+ dimensions: int=3,
150
+ dtype: numpy.dtype=_dtype
151
+ ) -> numpy.ndarray:
152
+ """
153
+ Returns a safe numpy array from a given vector, with the correct dimensions and dtype
154
+ """
155
+ return numpy.array(vector, dtype=dtype).reshape(dimensions)
156
+
157
+ # -------------------------------------------------------------------------------------------------|
158
+
159
+ @define
160
+ class ShaderCamera(ShaderModule):
161
+ name: str = "iCamera"
162
+ mode: CameraMode = CameraMode.Camera2D.field()
163
+ projection: CameraProjection = CameraProjection.Perspective.field()
164
+ separation: ShaderDynamics = None
165
+ rotation: ShaderDynamics = None
166
+ position: ShaderDynamics = None
167
+ up: ShaderDynamics = None
168
+ zoom: ShaderDynamics = None
169
+ isometric: ShaderDynamics = None
170
+ orbital: ShaderDynamics = None
171
+ dolly: ShaderDynamics = None
172
+
173
+ def __post__(self):
174
+ self.position = ShaderDynamics(scene=self.scene,
175
+ name=f"{self.name}Position", real=True,
176
+ frequency=7, zeta=1, response=1,
177
+ value=numpy.copy(GlobalBasis.Origin)
178
+ )
179
+ self.separation = ShaderDynamics(scene=self.scene,
180
+ name=f"{self.name}VRSeparation", real=True,
181
+ frequency=0.5, zeta=1, response=0, value=0.1
182
+ )
183
+ self.rotation = ShaderDynamics(scene=self.scene,
184
+ name=f"{self.name}Rotation", real=True,
185
+ frequency=5, zeta=1, response=0,
186
+ value=Quaternion(1, 0, 0, 0)
187
+ )
188
+ self.up = ShaderDynamics(scene=self.scene,
189
+ name=f"{self.name}UP", real=True,
190
+ frequency=1, zeta=1, response=0,
191
+ value=numpy.copy(GlobalBasis.Y)
192
+ )
193
+ self.zoom = ShaderDynamics(scene=self.scene,
194
+ name=f"{self.name}Zoom", real=True,
195
+ frequency=3, zeta=1, response=0, value=1
196
+ )
197
+ self.isometric = ShaderDynamics(scene=self.scene,
198
+ name=f"{self.name}Isometric", real=True,
199
+ frequency=1, zeta=1, response=0, value=0
200
+ )
201
+ self.orbital = ShaderDynamics(scene=self.scene,
202
+ name=f"{self.name}Orbital", real=True,
203
+ frequency=1, zeta=1, response=0, value=0
204
+ )
205
+ self.dolly = ShaderDynamics(scene=self.scene,
206
+ name=f"{self.name}Dolly", real=True,
207
+ frequency=1, zeta=1, response=0, value=0
208
+ )
209
+
210
+ def pipeline(self) -> Iterable[ShaderVariable]:
211
+ yield ShaderVariable("uniform", "int", f"{self.name}Mode", value=self.mode.value)
212
+ yield ShaderVariable("uniform", "int", f"{self.name}Projection", value=self.projection.value)
213
+ yield ShaderVariable("uniform", "vec3", f"{self.name}X", value=self.base_x)
214
+ yield ShaderVariable("uniform", "vec3", f"{self.name}Y", value=self.base_y)
215
+ yield ShaderVariable("uniform", "vec3", f"{self.name}Z", value=self.base_z)
216
+
217
+ def includes(self) -> Iterable[str]:
218
+ yield SHADERFLOW.RESOURCES.SHADERS_INCLUDE/"Camera.glsl"
219
+
220
+ # ---------------------------------------------------------------------------------------------|
221
+ # Actions with vectors
222
+
223
+ def move(self, *direction: Vector3D, absolute: bool=False) -> Self:
224
+ """
225
+ Move the camera in a direction relative to the camera's position
226
+
227
+ Args:
228
+ direction: Direction to move
229
+
230
+ Returns:
231
+ Self: Fluent interface
232
+ """
233
+ if not absolute:
234
+ self.position.target += Algebra.safe(direction)
235
+ else:
236
+ self.position.target = Algebra.safe(direction)
237
+ return self
238
+
239
+ def rotate(self, direction: Vector3D=GlobalBasis.Null, angle: Degrees=0.0) -> Self:
240
+ """
241
+ Adds a cumulative rotation to the camera. Use "look" for absolute rotation
242
+
243
+ Args:
244
+ direction: Perpendicular axis to rotate around, following the right-hand rule
245
+ angle: Angle to rotate
246
+
247
+ Returns:
248
+ Self: Fluent interface
249
+ """
250
+ self.rotation.target = Algebra.quaternion(direction, angle) * self.rotation.target
251
+ self.rotation.target /= numpy.linalg.norm(quaternion.as_float_array(self.rotation.target))
252
+ return self
253
+
254
+ def rotate2d(self, angle: Degrees=0.0) -> Self:
255
+ """Aligns the UP vector rotated on FORWARD direction. Same math angle on a cartesian plane"""
256
+ target = Algebra.rotate_vector(self.up.value, Algebra.quaternion(self.base_z_target, angle))
257
+ return self.align(self.base_y_target, target)
258
+
259
+ def align(self, A: Vector3D, B: Vector3D, angle: Degrees=0) -> Self:
260
+ """
261
+ Rotate the camera as if we were to align these two vectors
262
+ """
263
+ A, B = DynamicNumber.extract(A, B)
264
+ return self.rotate(
265
+ Algebra.unit_vector(numpy.cross(A, B)),
266
+ Algebra.angle(A, B) - angle
267
+ )
268
+
269
+ def look(self, *target: Vector3D) -> Self:
270
+ """
271
+ Rotate the camera to look at some target point
272
+
273
+ Args:
274
+ target: Target point to look at
275
+
276
+ Returns:
277
+ Self: Fluent interface
278
+ """
279
+ return self.align(self.base_z_target, Algebra.safe(target) - self.position.target)
280
+
281
+ # ---------------------------------------------------------------------------------------------|
282
+ # Bases and directions
283
+
284
+ @property
285
+ def base_x(self) -> Vector3D:
286
+ return Algebra.rotate_vector(GlobalBasis.X, self.rotation.value)
287
+ @property
288
+ def base_x_target(self) -> Vector3D:
289
+ return Algebra.rotate_vector(GlobalBasis.X, self.rotation.target)
290
+
291
+ @property
292
+ def base_y(self) -> Vector3D:
293
+ return Algebra.rotate_vector(GlobalBasis.Y, self.rotation.value)
294
+ @property
295
+ def base_y_target(self) -> Vector3D:
296
+ return Algebra.rotate_vector(GlobalBasis.Y, self.rotation.target)
297
+
298
+ @property
299
+ def base_z(self) -> Vector3D:
300
+ return Algebra.rotate_vector(GlobalBasis.Z, self.rotation.value)
301
+ @property
302
+ def base_z_target(self) -> Vector3D:
303
+ return Algebra.rotate_vector(GlobalBasis.Z, self.rotation.target)
304
+
305
+ @property
306
+ def x(self) -> float:
307
+ return self.position.value[0]
308
+ @x.setter
309
+ def x(self, value: float):
310
+ self.position.target[0] = value
311
+
312
+ @property
313
+ def y(self) -> float:
314
+ return self.position.value[1]
315
+ @y.setter
316
+ def y(self, value: float):
317
+ self.position.target[1] = value
318
+
319
+ @property
320
+ def z(self) -> float:
321
+ return self.position.value[2]
322
+ @z.setter
323
+ def z(self, value: float):
324
+ self.position.target[2] = value
325
+
326
+ # ---------------------------------------------------------------------------------------------|
327
+ # Interaction
328
+
329
+ def update(self):
330
+ dt = abs(self.scene.dt or self.scene.rdt)
331
+
332
+ # Movement on keys
333
+ move = numpy.copy(GlobalBasis.Null)
334
+
335
+ # WASD Shift Spacebar movement
336
+ if self.mode == CameraMode.Camera2D:
337
+ if self.scene.keyboard(ShaderKeyboard.Keys.W): move += GlobalBasis.Y
338
+ if self.scene.keyboard(ShaderKeyboard.Keys.A): move -= GlobalBasis.X
339
+ if self.scene.keyboard(ShaderKeyboard.Keys.S): move -= GlobalBasis.Y
340
+ if self.scene.keyboard(ShaderKeyboard.Keys.D): move += GlobalBasis.X
341
+ else:
342
+ if self.scene.keyboard(ShaderKeyboard.Keys.W): move += GlobalBasis.Z
343
+ if self.scene.keyboard(ShaderKeyboard.Keys.A): move -= GlobalBasis.X
344
+ if self.scene.keyboard(ShaderKeyboard.Keys.S): move -= GlobalBasis.Z
345
+ if self.scene.keyboard(ShaderKeyboard.Keys.D): move += GlobalBasis.X
346
+ if self.scene.keyboard(ShaderKeyboard.Keys.SPACE): move += GlobalBasis.Y
347
+ if self.scene.keyboard(ShaderKeyboard.Keys.LEFT_SHIFT): move -= GlobalBasis.Y
348
+
349
+ if move.any():
350
+ move = Algebra.rotate_vector(move, self.rotation.target)
351
+ self.move(2 * Algebra.unit_vector(move) * dt / self.zoom.value)
352
+
353
+ # Rotation on Q and E
354
+ rotate = numpy.copy(GlobalBasis.Null)
355
+ if self.scene.keyboard(ShaderKeyboard.Keys.Q): rotate += GlobalBasis.Z
356
+ if self.scene.keyboard(ShaderKeyboard.Keys.E): rotate -= GlobalBasis.Z
357
+ if rotate.any(): self.rotate(Algebra.rotate_vector(rotate, self.rotation.target), 45*dt)
358
+
359
+ # Alignment with the "UP" direction
360
+ if self.mode == CameraMode.Spherical:
361
+ self.align(self.base_x_target, self.up, 90)
362
+
363
+ # Isometric on T and G
364
+ self.apply_isometric(+0.5*self.scene.keyboard(ShaderKeyboard.Keys.T)*dt)
365
+ self.apply_isometric(-0.5*self.scene.keyboard(ShaderKeyboard.Keys.G)*dt)
366
+
367
+ def apply_isometric(self, value: float, absolute: bool=False) -> None:
368
+ if value == 0:
369
+ return
370
+ if not absolute:
371
+ self.isometric.target += value
372
+ else:
373
+ self.isometric.target = value
374
+ self.isometric.target = clamp(self.isometric.target, 0, 1)
375
+
376
+ def apply_zoom(self, value: float) -> None:
377
+ # Note: Need to separate multiply and divide to return to the original value
378
+ if value > 0:
379
+ self.zoom.target *= (1 + value)
380
+ else:
381
+ self.zoom.target /= (1 - value)
382
+
383
+ def handle(self, message: ShaderMessage):
384
+
385
+ # Movement on Drag
386
+ if any([
387
+ isinstance(message, ShaderMessage.Mouse.Position) and self.scene.exclusive,
388
+ isinstance(message, ShaderMessage.Mouse.Drag)
389
+ ]):
390
+ if not (self.scene.mouse_buttons[1] or self.scene.exclusive):
391
+ return
392
+
393
+ # Rotate around the camera basis itself
394
+ if (self.mode == CameraMode.FreeCamera):
395
+ self.rotate(direction=self.base_y/self.zoom.value, angle= message.du*100)
396
+ self.rotate(direction=self.base_x/self.zoom.value, angle=-message.dv*100)
397
+
398
+ # Rotate relative to the XY plane
399
+ elif (self.mode == CameraMode.Camera2D):
400
+ move = (message.du*GlobalBasis.X) + (message.dv*GlobalBasis.Y)
401
+ move = Algebra.rotate_vector(move, self.rotation.target)
402
+ self.move(move*(1 if self.scene.exclusive else -1)/self.zoom.value)
403
+
404
+ elif (self.mode == CameraMode.Spherical):
405
+ up = 1 if (Algebra.angle(self.base_y_target, self.up) < 90) else -1
406
+ self.rotate(direction=self.up*up /self.zoom.value, angle= message.du*100)
407
+ self.rotate(direction=self.base_x/self.zoom.value, angle=-message.dv*100)
408
+
409
+ # Wheel Scroll Zoom
410
+ elif isinstance(message, ShaderMessage.Mouse.Scroll):
411
+ self.apply_zoom(0.05*message.dy)
412
+
413
+ # Camera alignments and modes
414
+ elif isinstance(message, ShaderMessage.Keyboard.Press) and (message.action == 1):
415
+
416
+ # Switch camera modes
417
+ for _ in range(1):
418
+ if (message.key == ShaderKeyboard.Keys.NUMBER_1):
419
+ self.mode = CameraMode.FreeCamera
420
+ elif (message.key == ShaderKeyboard.Keys.NUMBER_2):
421
+ self.align(self.base_x_target, GlobalBasis.X)
422
+ self.align(self.base_y_target, GlobalBasis.Y)
423
+ self.mode = CameraMode.Camera2D
424
+ self.position.target[2] = 0
425
+ self.isometric.target = 0
426
+ self.zoom.target = 1
427
+ elif (message.key == ShaderKeyboard.Keys.NUMBER_3):
428
+ self.mode = CameraMode.Spherical
429
+ else: break
430
+ else:
431
+ log.info(f"{self.who} • Set mode to {self.mode}")
432
+
433
+ # What is "UP", baby don't hurt me
434
+ for _ in range(1):
435
+ if (message.key == ShaderKeyboard.Keys.I):
436
+ self.up.target = GlobalBasis.X
437
+ elif (message.key == ShaderKeyboard.Keys.J):
438
+ self.up.target = GlobalBasis.Y
439
+ elif (message.key == ShaderKeyboard.Keys.K):
440
+ self.up.target = GlobalBasis.Z
441
+ else: break
442
+ else:
443
+ log.info(f"{self.who} • Set up to {self.up.target}")
444
+ self.align(self.base_z_target, self.up.target)
445
+ self.align(self.base_y_target, self.up.target, 90)
446
+ self.align(self.base_x_target, self.up.target, 90)
447
+
448
+ # Switch Projection
449
+ if (message.key == ShaderKeyboard.Keys.P):
450
+ self.projection = next(self.projection)
451
+ log.info(f"{self.who} • Set projection to {self.projection}")
File without changes