Skip to content

File: ShaderFlow/Modules/Camera.py

ShaderFlow.Modules.Camera

The Camera requires some prior knowledge of a fun piece of math called Quaternions.

They are 4D complex numbers that perfectly represents rotations in 3D space without the need of 3D rotation matrices (which are ugly!)*, and are pretty intuitive to use.

Great resources for understanding Quaternions:

• "Quaternions and 3d rotation, explained interactively" by @3blue1brown - https://www.youtube.com/watch?v=d4EgbgTm0Bg

• "Visualizing quaternions (4d numbers) with stereographic projection" by @3blue1brown - https://www.youtube.com/watch?v=zjMuIxRvygQ

• "Visualizing quaternion, an explorable video series" by Ben Eater and @3blue1brown - https://eater.net/quaternions

Useful resources on Linear Algebra and Coordinate Systems:

• "The Essence of Linear Algebra" by @3blue1brown - https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab

• "here, have a coordinate system chart~" by @FreyaHolmer - https://twitter.com/FreyaHolmer/status/1325556229410861056

Quaternion

Quaternion: TypeAlias = quaternion.quaternion

Vector3D

Vector3D: TypeAlias = numpy.ndarray

GlobalBasis

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
57
58
59
60
61
62
63
64
65
class GlobalBasis:
    Origin   = numpy.array(( 0,  0,  0), dtype=_dtype)
    Null     = numpy.array(( 0,  0,  0), dtype=_dtype)
    Up       = numpy.array(( 0,  1,  0), dtype=_dtype)
    Down     = numpy.array(( 0, -1,  0), dtype=_dtype)
    Left     = numpy.array((-1,  0,  0), dtype=_dtype)
    Right    = numpy.array(( 1,  0,  0), dtype=_dtype)
    Forward  = numpy.array(( 0,  0,  1), dtype=_dtype)
    Backward = numpy.array(( 0,  0, -1), dtype=_dtype)

Origin

Origin = numpy.array((0, 0, 0), dtype=_dtype)

Null

Null = numpy.array((0, 0, 0), dtype=_dtype)

Up

Up = numpy.array((0, 1, 0), dtype=_dtype)

Down

Down = numpy.array((0, -1, 0), dtype=_dtype)

Left

Left = numpy.array((-1, 0, 0), dtype=_dtype)

Right

Right = numpy.array((1, 0, 0), dtype=_dtype)

Forward

Forward = numpy.array((0, 0, 1), dtype=_dtype)

Backward

Backward = numpy.array((0, 0, -1), dtype=_dtype)

CameraProjection

Bases: BrokenEnum

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class CameraProjection(BrokenEnum):
    Perspective = 0
    """
    Project from a Plane A at the position to a Plane B at a distance of one
    - The plane is always perpendicular to the camera's direction
    - Plane A is multiplied by isometric, Plane B by Zoom
    """

    VirtualReality = 1
    """Two halves of the screen, one for each eye, with a separation between them"""

    Equirectangular = 2
    """The 360° videos of platforms like YouTube, it's a simples sphere projected to the screen
    where X defines the azimuth and Y the inclination, ranging such that they sweep the sphere"""

Perspective

Perspective = 0

Project from a Plane A at the position to a Plane B at a distance of one - The plane is always perpendicular to the camera's direction - Plane A is multiplied by isometric, Plane B by Zoom

VirtualReality

VirtualReality = 1

Two halves of the screen, one for each eye, with a separation between them

Equirectangular

Equirectangular = 2

The 360° videos of platforms like YouTube, it's a simples sphere projected to the screen where X defines the azimuth and Y the inclination, ranging such that they sweep the sphere

CameraMode

Bases: BrokenEnum

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
84
85
86
87
88
89
90
91
92
class CameraMode(BrokenEnum):
    FreeCamera = 0
    """Free to rotate in any direction - do not ensure the 'up' direction matches the zenith"""

    Camera2D = 1
    """Fixed direction, drag moves position on the plane of the screen"""

    Spherical = 2
    """Always correct such that the camera orthonormal base is pointing 'UP'"""

FreeCamera

FreeCamera = 0

Free to rotate in any direction - do not ensure the 'up' direction matches the zenith

Camera2D

Camera2D = 1

Fixed direction, drag moves position on the plane of the screen

Spherical

Spherical = 2

Always correct such that the camera orthonormal base is pointing 'UP'

Algebra

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
 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
class Algebra:

    def quaternion(axis: Vector3D, angle: Degrees) -> Quaternion:
        """Builds a quaternion that represents an rotation around an axis for an angle"""
        return Quaternion(math.cos(theta := math.radians(angle/2)), *(math.sin(theta)*axis))

    def rotate_vector(vector: Vector3D, R: Quaternion) -> Vector3D:
        """Applies a Quaternion rotation to a vector"""
        return quaternion.as_vector_part(R * quaternion.quaternion(0, *vector) * R.conjugate())

    def angle(A: Vector3D, B: Vector3D) -> Degrees:
        """
        Returns the angle between two vectors by the linear algebra formula:
        • Theta(A, B) = arccos( (A·B) / (|A|*|B|) )
        • Safe for zero vector norm divisions
        • Clips the arccos domain to [-1, 1] to avoid NaNs
        """
        A, B = DynamicNumber.extract(A, B)

        # Avoid zero divisions
        if not (LA := numpy.linalg.norm(A)):
            return 0.0
        if not (LB := numpy.linalg.norm(B)):
            return 0.0

        # Avoid NaNs by clipping domain
        cos = numpy.clip(numpy.dot(A, B)/(LA*LB), -1, 1)
        return numpy.degrees(numpy.arccos(cos))

    def unit_vector(vector: Vector3D) -> Vector3D:
        """Returns the unit vector of a given vector, safely"""
        if (magnitude := numpy.linalg.norm(vector)):
            return (vector/magnitude)
        return vector

    @staticmethod
    def safe(
        *vector: Union[numpy.ndarray, tuple[float], float, int],
        dimensions: int=3,
        dtype: numpy.dtype=_dtype
    ) -> numpy.ndarray:
        """
        Returns a safe numpy array from a given vector, with the correct dimensions and dtype
        """
        return numpy.array(vector, dtype=dtype).reshape(dimensions)

quaternion

quaternion(axis: Vector3D, angle: Degrees) -> Quaternion

Builds a quaternion that represents an rotation around an axis for an angle

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
 98
 99
100
def quaternion(axis: Vector3D, angle: Degrees) -> Quaternion:
    """Builds a quaternion that represents an rotation around an axis for an angle"""
    return Quaternion(math.cos(theta := math.radians(angle/2)), *(math.sin(theta)*axis))

rotate_vector

rotate_vector(vector: Vector3D, R: Quaternion) -> Vector3D

Applies a Quaternion rotation to a vector

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
102
103
104
def rotate_vector(vector: Vector3D, R: Quaternion) -> Vector3D:
    """Applies a Quaternion rotation to a vector"""
    return quaternion.as_vector_part(R * quaternion.quaternion(0, *vector) * R.conjugate())

angle

angle(A: Vector3D, B: Vector3D) -> Degrees

Returns the angle between two vectors by the linear algebra formula: • Theta(A, B) = arccos( (A·B) / (|A|*|B|) ) • Safe for zero vector norm divisions • Clips the arccos domain to [-1, 1] to avoid NaNs

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def angle(A: Vector3D, B: Vector3D) -> Degrees:
    """
    Returns the angle between two vectors by the linear algebra formula:
    • Theta(A, B) = arccos( (A·B) / (|A|*|B|) )
    • Safe for zero vector norm divisions
    • Clips the arccos domain to [-1, 1] to avoid NaNs
    """
    A, B = DynamicNumber.extract(A, B)

    # Avoid zero divisions
    if not (LA := numpy.linalg.norm(A)):
        return 0.0
    if not (LB := numpy.linalg.norm(B)):
        return 0.0

    # Avoid NaNs by clipping domain
    cos = numpy.clip(numpy.dot(A, B)/(LA*LB), -1, 1)
    return numpy.degrees(numpy.arccos(cos))

unit_vector

unit_vector(vector: Vector3D) -> Vector3D

Returns the unit vector of a given vector, safely

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
125
126
127
128
129
def unit_vector(vector: Vector3D) -> Vector3D:
    """Returns the unit vector of a given vector, safely"""
    if (magnitude := numpy.linalg.norm(vector)):
        return (vector/magnitude)
    return vector

safe

safe(
    *vector: Union[numpy.ndarray, tuple[float], float, int],
    dimensions: int = 3,
    dtype: numpy.dtype = _dtype
) -> numpy.ndarray

Returns a safe numpy array from a given vector, with the correct dimensions and dtype

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
131
132
133
134
135
136
137
138
139
140
@staticmethod
def safe(
    *vector: Union[numpy.ndarray, tuple[float], float, int],
    dimensions: int=3,
    dtype: numpy.dtype=_dtype
) -> numpy.ndarray:
    """
    Returns a safe numpy array from a given vector, with the correct dimensions and dtype
    """
    return numpy.array(vector, dtype=dtype).reshape(dimensions)

ShaderCamera

Bases: ShaderModule

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
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
@define
class ShaderCamera(ShaderModule):
    name:       str = "iCamera"
    mode:       CameraMode       = CameraMode.Camera2D.field()
    projection: CameraProjection = CameraProjection.Perspective.field()
    separation: ShaderDynamics = None
    rotation:   ShaderDynamics = None
    position:   ShaderDynamics = None
    zenith:     ShaderDynamics = None
    zoom:       ShaderDynamics = None
    isometric:  ShaderDynamics = None
    orbital:    ShaderDynamics = None
    dolly:      ShaderDynamics = None

    def build(self):
        self.position = ShaderDynamics(scene=self.scene,
            name=f"{self.name}Position", real=True,
            frequency=4, zeta=1, response=0,
            value=numpy.copy(GlobalBasis.Origin)
        )
        self.separation = ShaderDynamics(scene=self.scene,
            name=f"{self.name}VRSeparation", real=True,
            frequency=0.5, zeta=1, response=0, value=0.05
        )
        self.rotation = ShaderDynamics(scene=self.scene,
            name=f"{self.name}Rotation", real=True, primary=False,
            frequency=5, zeta=1, response=0,
            value=Quaternion(1, 0, 0, 0)
        )
        self.zenith = ShaderDynamics(scene=self.scene,
            name=f"{self.name}Zenith", real=True,
            frequency=1, zeta=1, response=0,
            value=numpy.copy(GlobalBasis.Up)
        )
        self.zoom = ShaderDynamics(scene=self.scene,
            name=f"{self.name}Zoom", real=True,
            frequency=3, zeta=1, response=0, value=1
        )
        self.isometric = ShaderDynamics(scene=self.scene,
            name=f"{self.name}Isometric", real=True,
            frequency=1, zeta=1, response=0, value=0
        )
        self.focal_length = ShaderDynamics(scene=self.scene,
            name=f"{self.name}FocalLength", real=True,
            frequency=1, zeta=1, response=0, value=1
        )
        self.orbital = ShaderDynamics(scene=self.scene,
            name=f"{self.name}Orbital", real=True,
            frequency=1, zeta=1, response=0, value=0
        )
        self.dolly = ShaderDynamics(scene=self.scene,
            name=f"{self.name}Dolly", real=True,
            frequency=1, zeta=1, response=0, value=0
        )

    @property
    def fov(self) -> Degrees:
        """The vertical field of view angle, considers the isometric factor"""
        return 2.0 * math.degrees(math.atan(self.zoom.value - self.isometric.value))

    @fov.setter
    def fov(self, value: Degrees):
        self.zoom.target = math.tan(math.radians(value)/2.0) + self.isometric.value

    def pipeline(self) -> Iterable[ShaderVariable]:
        yield Uniform("int",  f"{self.name}Mode",       value=self.mode)
        yield Uniform("int",  f"{self.name}Projection", value=self.projection)
        yield Uniform("vec3", f"{self.name}Right",      value=self.right)
        yield Uniform("vec3", f"{self.name}Upward",     value=self.up)
        yield Uniform("vec3", f"{self.name}Forward",    value=self.forward)

    def includes(self) -> Iterable[str]:
        yield SHADERFLOW.RESOURCES.SHADERS_INCLUDE/"Camera.glsl"

    # ---------------------------------------------------------------------------------------------|
    # Actions with vectors

    def move(self, *direction: Vector3D, absolute: bool=False) -> Self:
        """Move the camera in a direction relative to the camera's position"""
        self.position.target += Algebra.safe(direction) - (self.position.target * absolute)
        return self

    def rotate(self, direction: Vector3D=GlobalBasis.Null, angle: Degrees=0.0) -> Self:
        """Adds a cumulative rotation to the camera. Use "look" for absolute rotation"""
        self.rotation.target  = Algebra.quaternion(direction, angle) * self.rotation.target
        self.rotation.target /= numpy.linalg.norm(quaternion.as_float_array(self.rotation.target))
        return self

    def rotate2d(self, angle: Degrees=0.0) -> Self:
        """Aligns the UP vector rotated on FORWARD direction. Same math angle on a cartesian plane"""
        target = Algebra.rotate_vector(self.zenith.value, Algebra.quaternion(self.forward_target, angle))
        return self.align(self.up_target, target)

    def align(self, A: Vector3D, B: Vector3D, angle: Degrees=0.0) -> Self:
        """Rotate the camera as if we were to align these two vectors"""
        A, B = DynamicNumber.extract(A, B)
        return self.rotate(
            Algebra.unit_vector(numpy.cross(A, B)),
            Algebra.angle(A, B) - angle
        )

    def look(self, *target: Vector3D) -> Self:
        """Rotate the camera to look at some target point"""
        return self.align(self.forward_target, Algebra.safe(target) - self.position.target)

    # ---------------------------------------------------------------------------------------------|
    # Interaction

    def update(self):
        dt = abs(self.scene.dt or self.scene.rdt)

        # Movement on keys
        move = numpy.copy(GlobalBasis.Null)

        # WASD Shift Spacebar movement
        if self.mode == CameraMode.Camera2D:
            if self.scene.keyboard(ShaderKeyboard.Keys.W): move += GlobalBasis.Up
            if self.scene.keyboard(ShaderKeyboard.Keys.A): move += GlobalBasis.Left
            if self.scene.keyboard(ShaderKeyboard.Keys.S): move += GlobalBasis.Down
            if self.scene.keyboard(ShaderKeyboard.Keys.D): move += GlobalBasis.Right
        else:
            if self.scene.keyboard(ShaderKeyboard.Keys.W): move += GlobalBasis.Forward
            if self.scene.keyboard(ShaderKeyboard.Keys.A): move += GlobalBasis.Left
            if self.scene.keyboard(ShaderKeyboard.Keys.S): move += GlobalBasis.Backward
            if self.scene.keyboard(ShaderKeyboard.Keys.D): move += GlobalBasis.Right
            if self.scene.keyboard(ShaderKeyboard.Keys.SPACE): move += GlobalBasis.Up
            if self.scene.keyboard(ShaderKeyboard.Keys.LEFT_SHIFT): move += GlobalBasis.Down

        if move.any():
            move = Algebra.rotate_vector(move, self.rotation.target)
            self.move(2 * Algebra.unit_vector(move) * self.zoom.value * dt)

        # Rotation on Q and E
        rotate = numpy.copy(GlobalBasis.Null)
        if self.scene.keyboard(ShaderKeyboard.Keys.Q): rotate += GlobalBasis.Forward
        if self.scene.keyboard(ShaderKeyboard.Keys.E): rotate += GlobalBasis.Backward
        if rotate.any(): self.rotate(Algebra.rotate_vector(rotate, self.rotation.target), 45*dt)

        # Alignment with the "UP" direction
        if self.mode == CameraMode.Spherical:
            self.align(self.right_target, self.zenith.target, 90)

        # Isometric on T and G
        if (self.scene.keyboard(ShaderKeyboard.Keys.T)):
            self.isometric.target = clamp(self.isometric.target + 0.5*dt, 0, 1)
        if (self.scene.keyboard(ShaderKeyboard.Keys.G)):
            self.isometric.target = clamp(self.isometric.target - 0.5*dt, 0, 1)

    def apply_zoom(self, value: float) -> None:
        # Note: Ensures a zoom in then out returns to the same value
        if (value > 0):
            self.zoom.target *= (1 + value)
        else:
            self.zoom.target /= (1 - value)

    def handle(self, message: ShaderMessage):

        # Movement on Drag
        if any([
            isinstance(message, ShaderMessage.Mouse.Position) and self.scene.exclusive,
            isinstance(message, ShaderMessage.Mouse.Drag)
        ]):
            if not (self.scene.mouse_buttons[1] or self.scene.exclusive):
                return

            # Rotate around the camera basis itself
            if (self.mode == CameraMode.FreeCamera):
                self.rotate(direction=self.up*self.zoom.value, angle= message.du*100)
                self.rotate(direction=self.right*self.zoom.value, angle=-message.dv*100)

            # Rotate relative to the XY plane
            elif (self.mode == CameraMode.Camera2D):
                move = (message.du*GlobalBasis.Right) + (message.dv*GlobalBasis.Up)
                move = Algebra.rotate_vector(move, self.rotation.target)
                self.move(move*(1 if self.scene.exclusive else -1)*self.zoom.value)

            elif (self.mode == CameraMode.Spherical):
                up = 1 if (Algebra.angle(self.up_target, self.zenith) < 90) else -1
                self.rotate(direction=self.zenith*up *self.zoom.value, angle= message.du*100)
                self.rotate(direction=self.right*self.zoom.value, angle=-message.dv*100)

        # Wheel Scroll Zoom
        elif isinstance(message, ShaderMessage.Mouse.Scroll):
            self.apply_zoom(-0.05*message.dy)

        # Camera alignments and modes
        elif isinstance(message, ShaderMessage.Keyboard.Press) and (message.action == 1):

            # Switch camera modes
            for _ in range(1):
                if (message.key == ShaderKeyboard.Keys.NUMBER_1):
                    self.mode = CameraMode.FreeCamera
                elif (message.key == ShaderKeyboard.Keys.NUMBER_2):
                    self.align(self.right_target,  GlobalBasis.Right)
                    self.align(self.up_target, GlobalBasis.Up)
                    self.mode = CameraMode.Camera2D
                    self.position.target[2] = 0
                    self.isometric.target = 0
                    self.zoom.target = 1
                elif (message.key == ShaderKeyboard.Keys.NUMBER_3):
                    self.mode = CameraMode.Spherical
                else: break
            else:
                self.log_info(f"• Set mode to {self.mode}")

            # What is "UP", baby don't hurt me
            for _ in range(1):
                if (message.key == ShaderKeyboard.Keys.I):
                    self.zenith.target = GlobalBasis.Right
                elif (message.key == ShaderKeyboard.Keys.J):
                    self.zenith.target = GlobalBasis.Up
                elif (message.key == ShaderKeyboard.Keys.K):
                    self.zenith.target = GlobalBasis.Forward
                else: break
            else:
                self.log_info(f"• Set zenith to {self.zenith.target}")
                self.align(self.forward_target, self.zenith.target)
                self.align(self.up_target, self.zenith.target, 90)
                self.align(self.right_target, self.zenith.target, 90)

            # Switch Projection
            if (message.key == ShaderKeyboard.Keys.P):
                self.projection = next(self.projection)
                self.log_info(f"• Set projection to {self.projection}")

    # ---------------------------------------------------------------------------------------------|
    # Bases and directions

    @property
    def right(self) -> Vector3D:
        """The current 'right' direction relative to the camera"""
        return Algebra.rotate_vector(GlobalBasis.Right, self.rotation.value)

    @property
    def right_target(self) -> Vector3D:
        """The target 'right' direction the camera is aligning to"""
        return Algebra.rotate_vector(GlobalBasis.Right, self.rotation.target)

    @property
    def left(self) -> Vector3D:
        """The current 'left' direction relative to the camera"""
        return (-1) * self.right

    @property
    def left_target(self) -> Vector3D:
        """The target 'left' direction the camera is aligning to"""
        return (-1) * self.right_target

    @property
    def up(self) -> Vector3D:
        """The current 'upwards' direction relative to the camera"""
        return Algebra.rotate_vector(GlobalBasis.Up, self.rotation.value)

    @property
    def up_target(self) -> Vector3D:
        """The target 'upwards' direction the camera is aligning to"""
        return Algebra.rotate_vector(GlobalBasis.Up, self.rotation.target)

    @property
    def down(self) -> Vector3D:
        """The current 'downwards' direction relative to the camera"""
        return (-1) * self.up

    @property
    def down_target(self) -> Vector3D:
        """The target 'downwards' direction the camera is aligning to"""
        return (-1) * self.up_target

    @property
    def forward(self) -> Vector3D:
        """The current 'forward' direction relative to the camera"""
        return Algebra.rotate_vector(GlobalBasis.Forward, self.rotation.value)

    @property
    def forward_target(self) -> Vector3D:
        """The target 'forward' direction the camera is aligning to"""
        return Algebra.rotate_vector(GlobalBasis.Forward, self.rotation.target)

    @property
    def backward(self) -> Vector3D:
        """The current 'backward' direction relative to the camera"""
        return (-1) * self.forward

    @property
    def backward_target(self) -> Vector3D:
        """The target 'backward' direction the camera is aligning to"""
        return (-1) * self.forward_target

    # # Positions

    @property
    def x(self) -> float:
        """The current X position of the camera"""
        return self.position.value[0]

    @x.setter
    def x(self, value: float):
        self.position.target[0] = value

    @property
    def y(self) -> float:
        """The current Y position of the camera"""
        return self.position.value[1]

    @y.setter
    def y(self, value: float):
        self.position.target[1] = value

    @property
    def z(self) -> float:
        """The current Z position of the camera"""
        return self.position.value[2]

    @z.setter
    def z(self, value: float):
        self.position.target[2] = value

name

name: str = 'iCamera'

mode

mode: CameraMode = CameraMode.Camera2D.field()

projection

projection: CameraProjection = (
    CameraProjection.Perspective.field()
)

separation

separation: ShaderDynamics = None

rotation

rotation: ShaderDynamics = None

position

position: ShaderDynamics = None

zenith

zenith: ShaderDynamics = None

zoom

zoom: ShaderDynamics = None

isometric

isometric: ShaderDynamics = None

orbital

orbital: ShaderDynamics = None

dolly

dolly: ShaderDynamics = None

build

build()
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
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
def build(self):
    self.position = ShaderDynamics(scene=self.scene,
        name=f"{self.name}Position", real=True,
        frequency=4, zeta=1, response=0,
        value=numpy.copy(GlobalBasis.Origin)
    )
    self.separation = ShaderDynamics(scene=self.scene,
        name=f"{self.name}VRSeparation", real=True,
        frequency=0.5, zeta=1, response=0, value=0.05
    )
    self.rotation = ShaderDynamics(scene=self.scene,
        name=f"{self.name}Rotation", real=True, primary=False,
        frequency=5, zeta=1, response=0,
        value=Quaternion(1, 0, 0, 0)
    )
    self.zenith = ShaderDynamics(scene=self.scene,
        name=f"{self.name}Zenith", real=True,
        frequency=1, zeta=1, response=0,
        value=numpy.copy(GlobalBasis.Up)
    )
    self.zoom = ShaderDynamics(scene=self.scene,
        name=f"{self.name}Zoom", real=True,
        frequency=3, zeta=1, response=0, value=1
    )
    self.isometric = ShaderDynamics(scene=self.scene,
        name=f"{self.name}Isometric", real=True,
        frequency=1, zeta=1, response=0, value=0
    )
    self.focal_length = ShaderDynamics(scene=self.scene,
        name=f"{self.name}FocalLength", real=True,
        frequency=1, zeta=1, response=0, value=1
    )
    self.orbital = ShaderDynamics(scene=self.scene,
        name=f"{self.name}Orbital", real=True,
        frequency=1, zeta=1, response=0, value=0
    )
    self.dolly = ShaderDynamics(scene=self.scene,
        name=f"{self.name}Dolly", real=True,
        frequency=1, zeta=1, response=0, value=0
    )

fov

fov: Degrees

The vertical field of view angle, considers the isometric factor

pipeline

pipeline() -> Iterable[ShaderVariable]
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
208
209
210
211
212
213
def pipeline(self) -> Iterable[ShaderVariable]:
    yield Uniform("int",  f"{self.name}Mode",       value=self.mode)
    yield Uniform("int",  f"{self.name}Projection", value=self.projection)
    yield Uniform("vec3", f"{self.name}Right",      value=self.right)
    yield Uniform("vec3", f"{self.name}Upward",     value=self.up)
    yield Uniform("vec3", f"{self.name}Forward",    value=self.forward)

includes

includes() -> Iterable[str]
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
215
216
def includes(self) -> Iterable[str]:
    yield SHADERFLOW.RESOURCES.SHADERS_INCLUDE/"Camera.glsl"

move

move(*direction: Vector3D, absolute: bool = False) -> Self

Move the camera in a direction relative to the camera's position

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
221
222
223
224
def move(self, *direction: Vector3D, absolute: bool=False) -> Self:
    """Move the camera in a direction relative to the camera's position"""
    self.position.target += Algebra.safe(direction) - (self.position.target * absolute)
    return self

rotate

rotate(
    direction: Vector3D = GlobalBasis.Null,
    angle: Degrees = 0.0,
) -> Self

Adds a cumulative rotation to the camera. Use "look" for absolute rotation

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
226
227
228
229
230
def rotate(self, direction: Vector3D=GlobalBasis.Null, angle: Degrees=0.0) -> Self:
    """Adds a cumulative rotation to the camera. Use "look" for absolute rotation"""
    self.rotation.target  = Algebra.quaternion(direction, angle) * self.rotation.target
    self.rotation.target /= numpy.linalg.norm(quaternion.as_float_array(self.rotation.target))
    return self

rotate2d

rotate2d(angle: Degrees = 0.0) -> Self

Aligns the UP vector rotated on FORWARD direction. Same math angle on a cartesian plane

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
232
233
234
235
def rotate2d(self, angle: Degrees=0.0) -> Self:
    """Aligns the UP vector rotated on FORWARD direction. Same math angle on a cartesian plane"""
    target = Algebra.rotate_vector(self.zenith.value, Algebra.quaternion(self.forward_target, angle))
    return self.align(self.up_target, target)

align

align(
    A: Vector3D, B: Vector3D, angle: Degrees = 0.0
) -> Self

Rotate the camera as if we were to align these two vectors

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
237
238
239
240
241
242
243
def align(self, A: Vector3D, B: Vector3D, angle: Degrees=0.0) -> Self:
    """Rotate the camera as if we were to align these two vectors"""
    A, B = DynamicNumber.extract(A, B)
    return self.rotate(
        Algebra.unit_vector(numpy.cross(A, B)),
        Algebra.angle(A, B) - angle
    )

look

look(*target: Vector3D) -> Self

Rotate the camera to look at some target point

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
245
246
247
def look(self, *target: Vector3D) -> Self:
    """Rotate the camera to look at some target point"""
    return self.align(self.forward_target, Algebra.safe(target) - self.position.target)

update

update()
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
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
def update(self):
    dt = abs(self.scene.dt or self.scene.rdt)

    # Movement on keys
    move = numpy.copy(GlobalBasis.Null)

    # WASD Shift Spacebar movement
    if self.mode == CameraMode.Camera2D:
        if self.scene.keyboard(ShaderKeyboard.Keys.W): move += GlobalBasis.Up
        if self.scene.keyboard(ShaderKeyboard.Keys.A): move += GlobalBasis.Left
        if self.scene.keyboard(ShaderKeyboard.Keys.S): move += GlobalBasis.Down
        if self.scene.keyboard(ShaderKeyboard.Keys.D): move += GlobalBasis.Right
    else:
        if self.scene.keyboard(ShaderKeyboard.Keys.W): move += GlobalBasis.Forward
        if self.scene.keyboard(ShaderKeyboard.Keys.A): move += GlobalBasis.Left
        if self.scene.keyboard(ShaderKeyboard.Keys.S): move += GlobalBasis.Backward
        if self.scene.keyboard(ShaderKeyboard.Keys.D): move += GlobalBasis.Right
        if self.scene.keyboard(ShaderKeyboard.Keys.SPACE): move += GlobalBasis.Up
        if self.scene.keyboard(ShaderKeyboard.Keys.LEFT_SHIFT): move += GlobalBasis.Down

    if move.any():
        move = Algebra.rotate_vector(move, self.rotation.target)
        self.move(2 * Algebra.unit_vector(move) * self.zoom.value * dt)

    # Rotation on Q and E
    rotate = numpy.copy(GlobalBasis.Null)
    if self.scene.keyboard(ShaderKeyboard.Keys.Q): rotate += GlobalBasis.Forward
    if self.scene.keyboard(ShaderKeyboard.Keys.E): rotate += GlobalBasis.Backward
    if rotate.any(): self.rotate(Algebra.rotate_vector(rotate, self.rotation.target), 45*dt)

    # Alignment with the "UP" direction
    if self.mode == CameraMode.Spherical:
        self.align(self.right_target, self.zenith.target, 90)

    # Isometric on T and G
    if (self.scene.keyboard(ShaderKeyboard.Keys.T)):
        self.isometric.target = clamp(self.isometric.target + 0.5*dt, 0, 1)
    if (self.scene.keyboard(ShaderKeyboard.Keys.G)):
        self.isometric.target = clamp(self.isometric.target - 0.5*dt, 0, 1)

apply_zoom

apply_zoom(value: float) -> None
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
292
293
294
295
296
297
def apply_zoom(self, value: float) -> None:
    # Note: Ensures a zoom in then out returns to the same value
    if (value > 0):
        self.zoom.target *= (1 + value)
    else:
        self.zoom.target /= (1 - value)

handle

handle(message: ShaderMessage)
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Camera.py
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
def handle(self, message: ShaderMessage):

    # Movement on Drag
    if any([
        isinstance(message, ShaderMessage.Mouse.Position) and self.scene.exclusive,
        isinstance(message, ShaderMessage.Mouse.Drag)
    ]):
        if not (self.scene.mouse_buttons[1] or self.scene.exclusive):
            return

        # Rotate around the camera basis itself
        if (self.mode == CameraMode.FreeCamera):
            self.rotate(direction=self.up*self.zoom.value, angle= message.du*100)
            self.rotate(direction=self.right*self.zoom.value, angle=-message.dv*100)

        # Rotate relative to the XY plane
        elif (self.mode == CameraMode.Camera2D):
            move = (message.du*GlobalBasis.Right) + (message.dv*GlobalBasis.Up)
            move = Algebra.rotate_vector(move, self.rotation.target)
            self.move(move*(1 if self.scene.exclusive else -1)*self.zoom.value)

        elif (self.mode == CameraMode.Spherical):
            up = 1 if (Algebra.angle(self.up_target, self.zenith) < 90) else -1
            self.rotate(direction=self.zenith*up *self.zoom.value, angle= message.du*100)
            self.rotate(direction=self.right*self.zoom.value, angle=-message.dv*100)

    # Wheel Scroll Zoom
    elif isinstance(message, ShaderMessage.Mouse.Scroll):
        self.apply_zoom(-0.05*message.dy)

    # Camera alignments and modes
    elif isinstance(message, ShaderMessage.Keyboard.Press) and (message.action == 1):

        # Switch camera modes
        for _ in range(1):
            if (message.key == ShaderKeyboard.Keys.NUMBER_1):
                self.mode = CameraMode.FreeCamera
            elif (message.key == ShaderKeyboard.Keys.NUMBER_2):
                self.align(self.right_target,  GlobalBasis.Right)
                self.align(self.up_target, GlobalBasis.Up)
                self.mode = CameraMode.Camera2D
                self.position.target[2] = 0
                self.isometric.target = 0
                self.zoom.target = 1
            elif (message.key == ShaderKeyboard.Keys.NUMBER_3):
                self.mode = CameraMode.Spherical
            else: break
        else:
            self.log_info(f"• Set mode to {self.mode}")

        # What is "UP", baby don't hurt me
        for _ in range(1):
            if (message.key == ShaderKeyboard.Keys.I):
                self.zenith.target = GlobalBasis.Right
            elif (message.key == ShaderKeyboard.Keys.J):
                self.zenith.target = GlobalBasis.Up
            elif (message.key == ShaderKeyboard.Keys.K):
                self.zenith.target = GlobalBasis.Forward
            else: break
        else:
            self.log_info(f"• Set zenith to {self.zenith.target}")
            self.align(self.forward_target, self.zenith.target)
            self.align(self.up_target, self.zenith.target, 90)
            self.align(self.right_target, self.zenith.target, 90)

        # Switch Projection
        if (message.key == ShaderKeyboard.Keys.P):
            self.projection = next(self.projection)
            self.log_info(f"• Set projection to {self.projection}")

right

right: Vector3D

The current 'right' direction relative to the camera

right_target

right_target: Vector3D

The target 'right' direction the camera is aligning to

left

left: Vector3D

The current 'left' direction relative to the camera

left_target

left_target: Vector3D

The target 'left' direction the camera is aligning to

up

The current 'upwards' direction relative to the camera

up_target

up_target: Vector3D

The target 'upwards' direction the camera is aligning to

down

down: Vector3D

The current 'downwards' direction relative to the camera

down_target

down_target: Vector3D

The target 'downwards' direction the camera is aligning to

forward

forward: Vector3D

The current 'forward' direction relative to the camera

forward_target

forward_target: Vector3D

The target 'forward' direction the camera is aligning to

backward

backward: Vector3D

The current 'backward' direction relative to the camera

backward_target

backward_target: Vector3D

The target 'backward' direction the camera is aligning to

x

x: float

The current X position of the camera

y

y: float

The current Y position of the camera

z

z: float

The current Z position of the camera