Spaces:
Runtime error
Runtime error
"""Matrix and Vector math. | |
This module provides Vector and Matrix objects, including Vec2, Vec3, | |
Vec4, Mat3, and Mat4. Most common matrix and vector operations are | |
supported. Helper methods are included for rotating, scaling, and | |
transforming. The :py:class:`~pyglet.matrix.Mat4` includes class methods | |
for creating orthographic and perspective projection matrixes. | |
Matrices behave just like they do in GLSL: they are specified in column-major | |
order and multiply on the left of vectors, which are treated as columns. | |
:note: For performance, Matrixes subclass the `tuple` type. They | |
are therefore immutable - all operations return a new object; | |
the object is not updated in-place. | |
""" | |
from __future__ import annotations | |
import math as _math | |
import typing as _typing | |
import warnings as _warnings | |
from operator import mul as _mul | |
from collections.abc import Iterable as _Iterable | |
from collections.abc import Iterator as _Iterator | |
number = _typing.Union[float, int] | |
Mat4T = _typing.TypeVar("Mat4T", bound="Mat4") | |
def clamp(num: float, min_val: float, max_val: float) -> float: | |
return max(min(num, max_val), min_val) | |
class Vec2: | |
__slots__ = 'x', 'y' | |
"""A two-dimensional vector represented as an X Y coordinate pair.""" | |
def __init__(self, x: number = 0.0, y: number = 0.0) -> None: | |
self.x = x | |
self.y = y | |
def __iter__(self) -> _Iterator[float]: | |
yield self.x | |
yield self.y | |
def __getitem__(self, item: int) -> float: | |
... | |
def __getitem__(self, item: slice) -> tuple[float, ...]: | |
... | |
def __getitem__(self, item): | |
return (self.x, self.y)[item] | |
def __setitem__(self, key, value): | |
if type(key) is slice: | |
for i, attr in enumerate(['x', 'y'][key]): | |
setattr(self, attr, value[i]) | |
else: | |
setattr(self, ['x', 'y'][key], value) | |
def __len__(self) -> int: | |
return 2 | |
def __add__(self, other: Vec2) -> Vec2: | |
return Vec2(self.x + other.x, self.y + other.y) | |
def __sub__(self, other: Vec2) -> Vec2: | |
return Vec2(self.x - other.x, self.y - other.y) | |
def __mul__(self, scalar: number) -> Vec2: | |
return Vec2(self.x * scalar, self.y * scalar) | |
def __truediv__(self, scalar: number) -> Vec2: | |
return Vec2(self.x / scalar, self.y / scalar) | |
def __floordiv__(self, scalar: number) -> Vec2: | |
return Vec2(self.x // scalar, self.y // scalar) | |
def __abs__(self) -> float: | |
return _math.sqrt(self.x ** 2 + self.y ** 2) | |
def __neg__(self) -> Vec2: | |
return Vec2(-self.x, -self.y) | |
def __round__(self, ndigits: int | None = None) -> Vec2: | |
return Vec2(*(round(v, ndigits) for v in self)) | |
def __radd__(self, other: Vec2 | int) -> Vec2: | |
"""Reverse add. Required for functionality with sum() | |
""" | |
if other == 0: | |
return self | |
else: | |
return self.__add__(_typing.cast(Vec2, other)) | |
def __eq__(self, other: object) -> bool: | |
return isinstance(other, Vec2) and self.x == other.x and self.y == other.y | |
def __ne__(self, other: object) -> bool: | |
return not isinstance(other, Vec2) or self.x != other.x or self.y != other.y | |
def from_polar(mag: float, angle: float) -> Vec2: | |
"""Create a new vector from the given polar coordinates. | |
:parameters: | |
`mag` : int or float : | |
The magnitude of the vector. | |
`angle` : int or float : | |
The angle of the vector in radians. | |
:returns: A new vector with the given angle and magnitude. | |
:rtype: Vec2 | |
""" | |
return Vec2(mag * _math.cos(angle), mag * _math.sin(angle)) | |
def from_magnitude(self, magnitude: float) -> Vec2: | |
"""Create a new Vector of the given magnitude by normalizing, | |
then scaling the vector. The heading remains unchanged. | |
:parameters: | |
`magnitude` : int or float : | |
The magnitude of the new vector. | |
:returns: A new vector with the magnitude. | |
:rtype: Vec2 | |
""" | |
return self.normalize() * magnitude | |
def from_heading(self, heading: float) -> Vec2: | |
"""Create a new vector of the same magnitude with the given heading. I.e. Rotate the vector to the heading. | |
:parameters: | |
`heading` : int or float : | |
The angle of the new vector in radians. | |
:returns: A new vector with the given heading. | |
:rtype: Vec2 | |
""" | |
mag = self.__abs__() | |
return Vec2(mag * _math.cos(heading), mag * _math.sin(heading)) | |
def heading(self) -> float: | |
"""The angle of the vector in radians. | |
:type: float | |
""" | |
return _math.atan2(self.y, self.x) | |
def mag(self) -> float: | |
"""The magnitude, or length of the vector. The distance between the coordinates and the origin. | |
Alias of abs(self). | |
:type: float | |
""" | |
return self.__abs__() | |
def limit(self, maximum: float) -> Vec2: | |
"""Limit the magnitude of the vector to the value used for the max parameter. | |
:parameters: | |
`maximum` : int or float : | |
The maximum magnitude for the vector. | |
:returns: Either self or a new vector with the maximum magnitude. | |
:rtype: Vec2 | |
""" | |
if self.x ** 2 + self.y ** 2 > maximum * maximum: | |
return self.from_magnitude(maximum) | |
return self | |
def lerp(self, other: Vec2, alpha: float) -> Vec2: | |
"""Create a new Vec2 linearly interpolated between this vector and another Vec2. | |
:parameters: | |
`other` : Vec2 : | |
The vector to linearly interpolate with. | |
`alpha` : float or int : | |
The amount of interpolation. | |
Some value between 0.0 (this vector) and 1.0 (other vector). | |
0.5 is halfway inbetween. | |
:returns: A new interpolated vector. | |
:rtype: Vec2 | |
""" | |
return Vec2(self.x + (alpha * (other.x - self.x)), | |
self.y + (alpha * (other.y - self.y))) | |
def reflect(self, normal: Vec2) -> Vec2: | |
"""Create a new Vec2 reflected (ricochet) from the given normal.""" | |
return self - normal * 2 * normal.dot(self) | |
def rotate(self, angle: float) -> Vec2: | |
"""Create a new Vector rotated by the angle. The magnitude remains unchanged. | |
:parameters: | |
`angle` : int or float : | |
The angle to rotate by | |
:returns: A new rotated vector of the same magnitude. | |
:rtype: Vec2 | |
""" | |
s = _math.sin(angle) | |
c = _math.cos(angle) | |
return Vec2(c * self.x - s * self.y, s * self.x + c * self.y) | |
def distance(self, other: Vec2) -> float: | |
"""Calculate the distance between this vector and another 2D vector.""" | |
return _math.sqrt(((other.x - self.x) ** 2) + ((other.y - self.y) ** 2)) | |
def normalize(self) -> Vec2: | |
"""Normalize the vector to have a magnitude of 1. i.e. make it a unit vector. | |
:returns: A unit vector with the same heading. | |
:rtype: Vec2 | |
""" | |
d = self.__abs__() | |
if d: | |
return Vec2(self.x / d, self.y / d) | |
return self | |
def clamp(self, min_val: float, max_val: float) -> Vec2: | |
"""Restrict the value of the X and Y components of the vector to be within the given values. | |
:parameters: | |
`min_val` : int or float : | |
The minimum value | |
`max_val` : int or float : | |
The maximum value | |
:returns: A new vector with clamped X and Y components. | |
:rtype: Vec2 | |
""" | |
return Vec2(clamp(self.x, min_val, max_val), clamp(self.y, min_val, max_val)) | |
def dot(self, other: Vec2) -> float: | |
"""Calculate the dot product of this vector and another 2D vector. | |
:parameters: | |
`other` : Vec2 : | |
The other vector. | |
:returns: The dot product of the two vectors. | |
:rtype: float | |
""" | |
return self.x * other.x + self.y * other.y | |
def __getattr__(self, attrs: str) -> Vec2 | Vec3 | Vec4: | |
try: | |
# Allow swizzled getting of attrs | |
vec_class = {2: Vec2, 3: Vec3, 4: Vec4}[len(attrs)] | |
return vec_class(*(self['xy'.index(c)] for c in attrs)) | |
except Exception: | |
raise AttributeError( | |
f"'{self.__class__.__name__}' object has no attribute '{attrs}'" | |
) from None | |
def __repr__(self) -> str: | |
return f"Vec2({self.x}, {self.y})" | |
class Vec3: | |
__slots__ = 'x', 'y', 'z' | |
"""A three-dimensional vector represented as X Y Z coordinates.""" | |
def __init__(self, x: number = 0.0, y: number = 0.0, z: number = 0.0) -> None: | |
self.x = x | |
self.y = y | |
self.z = z | |
def __iter__(self) -> _Iterator[float]: | |
yield self.x | |
yield self.y | |
yield self.z | |
def __getitem__(self, item: int) -> float: | |
... | |
def __getitem__(self, item: slice) -> tuple[float, ...]: | |
... | |
def __getitem__(self, item): | |
return (self.x, self.y, self.z)[item] | |
def __setitem__(self, key, value): | |
if type(key) is slice: | |
for i, attr in enumerate(['x', 'y', 'z'][key]): | |
setattr(self, attr, value[i]) | |
else: | |
setattr(self, ['x', 'y', 'z'][key], value) | |
def __len__(self) -> int: | |
return 3 | |
def mag(self) -> float: | |
"""The magnitude, or length of the vector. The distance between the coordinates and the origin. | |
Alias of abs(self). | |
:type: float | |
""" | |
return self.__abs__() | |
def __add__(self, other: Vec3) -> Vec3: | |
return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) | |
def __sub__(self, other: Vec3) -> Vec3: | |
return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) | |
def __mul__(self, scalar: number) -> Vec3: | |
return Vec3(self.x * scalar, self.y * scalar, self.z * scalar) | |
def __truediv__(self, scalar: number) -> Vec3: | |
return Vec3(self.x / scalar, self.y / scalar, self.z / scalar) | |
def __floordiv__(self, scalar: number) -> Vec3: | |
return Vec3(self.x // scalar, self.y // scalar, self.z // scalar) | |
def __abs__(self) -> float: | |
return _math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2) | |
def __neg__(self) -> Vec3: | |
return Vec3(-self.x, -self.y, -self.z) | |
def __round__(self, ndigits: int | None = None) -> Vec3: | |
return Vec3(*(round(v, ndigits) for v in self)) | |
def __radd__(self, other: Vec3 | int) -> Vec3: | |
"""Reverse add. Required for functionality with sum()""" | |
if other == 0: | |
return self | |
else: | |
return self.__add__(_typing.cast(Vec3, other)) | |
def __eq__(self, other: object) -> bool: | |
return isinstance(other, Vec3) and self.x == other.x and self.y == other.y and self.z == other.z | |
def __ne__(self, other: object) -> bool: | |
return not isinstance(other, Vec3) or self.x != other.x or self.y != other.y or self.z != other.z | |
def from_magnitude(self, magnitude: float) -> Vec3: | |
"""Create a new Vector of the given magnitude by normalizing, | |
then scaling the vector. The rotation remains unchanged. | |
:parameters: | |
`magnitude` : int or float : | |
The magnitude of the new vector. | |
:returns: A new vector with the magnitude. | |
:rtype: Vec3 | |
""" | |
return self.normalize() * magnitude | |
def limit(self, maximum: float) -> Vec3: | |
"""Limit the magnitude of the vector to the value used for the max parameter. | |
:parameters: | |
`maximum` : int or float : | |
The maximum magnitude for the vector. | |
:returns: Either self or a new vector with the maximum magnitude. | |
:rtype: Vec3 | |
""" | |
if self.x ** 2 + self.y ** 2 + self.z ** 2 > maximum * maximum * maximum: | |
return self.from_magnitude(maximum) | |
return self | |
def cross(self, other: Vec3) -> Vec3: | |
"""Calculate the cross product of this vector and another 3D vector. | |
:parameters: | |
`other` : Vec3 : | |
The other vector. | |
:returns: The cross product of the two vectors. | |
:rtype: float | |
""" | |
return Vec3((self.y * other.z) - (self.z * other.y), | |
(self.z * other.x) - (self.x * other.z), | |
(self.x * other.y) - (self.y * other.x)) | |
def dot(self, other: Vec3) -> float: | |
"""Calculate the dot product of this vector and another 3D vector. | |
:parameters: | |
`other` : Vec3 : | |
The other vector. | |
:returns: The dot product of the two vectors. | |
:rtype: float | |
""" | |
return self.x * other.x + self.y * other.y + self.z * other.z | |
def lerp(self, other: Vec3, alpha: float) -> Vec3: | |
"""Create a new Vec3 linearly interpolated between this vector and another Vec3. | |
:parameters: | |
`other` : Vec3 : | |
The vector to linearly interpolate with. | |
`alpha` : float or int : | |
The amount of interpolation. | |
Some value between 0.0 (this vector) and 1.0 (other vector). | |
0.5 is halfway inbetween. | |
:returns: A new interpolated vector. | |
:rtype: Vec3 | |
""" | |
return Vec3(self.x + (alpha * (other.x - self.x)), | |
self.y + (alpha * (other.y - self.y)), | |
self.z + (alpha * (other.z - self.z))) | |
def distance(self, other: Vec3) -> float: | |
"""Calculate the distance between this vector and another 3D vector. | |
:parameters: | |
`other` : Vec3 : | |
The other vector | |
:returns: The distance between the two vectors. | |
:rtype: float | |
""" | |
return _math.sqrt(((other.x - self.x) ** 2) + | |
((other.y - self.y) ** 2) + | |
((other.z - self.z) ** 2)) | |
def normalize(self) -> Vec3: | |
"""Normalize the vector to have a magnitude of 1. i.e. make it a unit vector. | |
:returns: A unit vector with the same rotation. | |
:rtype: Vec3 | |
""" | |
try: | |
d = self.__abs__() | |
return Vec3(self.x / d, self.y / d, self.z / d) | |
except ZeroDivisionError: | |
return self | |
def clamp(self, min_val: float, max_val: float) -> Vec3: | |
"""Restrict the value of the X, Y and Z components of the vector to be within the given values. | |
:parameters: | |
`min_val` : int or float : | |
The minimum value | |
`max_val` : int or float : | |
The maximum value | |
:returns: A new vector with clamped X, Y and Z components. | |
:rtype: Vec3 | |
""" | |
return Vec3(clamp(self.x, min_val, max_val), | |
clamp(self.y, min_val, max_val), | |
clamp(self.z, min_val, max_val)) | |
def __getattr__(self, attrs: str) -> Vec2 | Vec3 | Vec4: | |
try: | |
# Allow swizzled getting of attrs | |
vec_class = {2: Vec2, 3: Vec3, 4: Vec4}[len(attrs)] | |
return vec_class(*(self['xyz'.index(c)] for c in attrs)) | |
except Exception: | |
raise AttributeError( | |
f"'{self.__class__.__name__}' object has no attribute '{attrs}'" | |
) from None | |
def __repr__(self) -> str: | |
return f"Vec3({self.x}, {self.y}, {self.z})" | |
class Vec4: | |
__slots__ = 'x', 'y', 'z', 'w' | |
"""A four-dimensional vector represented as X Y Z W coordinates.""" | |
def __init__(self, x: number = 0.0, y: number = 0.0, z: number = 0.0, w: number = 0.0) -> None: | |
self.x = x | |
self.y = y | |
self.z = z | |
self.w = w | |
def __iter__(self) -> _Iterator[float]: | |
yield self.x | |
yield self.y | |
yield self.z | |
yield self.w | |
def __getitem__(self, item: int) -> float: | |
... | |
def __getitem__(self, item: slice) -> tuple[float, ...]: | |
... | |
def __getitem__(self, item): | |
return (self.x, self.y, self.z, self.w)[item] | |
def __setitem__(self, key, value): | |
if type(key) is slice: | |
for i, attr in enumerate(['x', 'y', 'z', 'w'][key]): | |
setattr(self, attr, value[i]) | |
else: | |
setattr(self, ['x', 'y', 'z', 'w'][key], value) | |
def __len__(self) -> int: | |
return 4 | |
def __add__(self, other: Vec4) -> Vec4: | |
return Vec4(self.x + other.x, self.y + other.y, self.z + other.z, self.w + other.w) | |
def __sub__(self, other: Vec4) -> Vec4: | |
return Vec4(self.x - other.x, self.y - other.y, self.z - other.z, self.w - other.w) | |
def __mul__(self, scalar: number) -> Vec4: | |
return Vec4(self.x * scalar, self.y * scalar, self.z * scalar, self.w * scalar) | |
def __truediv__(self, scalar: number) -> Vec4: | |
return Vec4(self.x / scalar, self.y / scalar, self.z / scalar, self.w / scalar) | |
def __floordiv__(self, scalar: number) -> Vec4: | |
return Vec4(self.x // scalar, self.y // scalar, self.z // scalar, self.w // scalar) | |
def __abs__(self) -> float: | |
return _math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2 + self.w ** 2) | |
def __neg__(self) -> Vec4: | |
return Vec4(-self.x, -self.y, -self.z, -self.w) | |
def __round__(self, ndigits: int | None = None) -> Vec4: | |
return Vec4(*(round(v, ndigits) for v in self)) | |
def __radd__(self, other: Vec4 | int) -> Vec4: | |
if other == 0: | |
return self | |
else: | |
return self.__add__(_typing.cast(Vec4, other)) | |
def __eq__(self, other: object) -> bool: | |
return ( | |
isinstance(other, Vec4) | |
and self.x == other.x | |
and self.y == other.y | |
and self.z == other.z | |
and self.w == other.w | |
) | |
def __ne__(self, other: object) -> bool: | |
return ( | |
not isinstance(other, Vec4) | |
or self.x != other.x | |
or self.y != other.y | |
or self.z != other.z | |
or self.w != other.w | |
) | |
def lerp(self, other: Vec4, alpha: float) -> Vec4: | |
"""Create a new Vec4 linearly interpolated between this one and another Vec4. | |
:parameters: | |
`other` : Vec4 : | |
The vector to linearly interpolate with. | |
`alpha` : float or int : | |
The amount of interpolation. | |
Some value between 0.0 (this vector) and 1.0 (other vector). | |
0.5 is halfway inbetween. | |
:returns: A new interpolated vector. | |
:rtype: Vec4 | |
""" | |
return Vec4(self.x + (alpha * (other.x - self.x)), | |
self.y + (alpha * (other.y - self.y)), | |
self.z + (alpha * (other.z - self.z)), | |
self.w + (alpha * (other.w - self.w))) | |
def distance(self, other: Vec4) -> float: | |
return _math.sqrt(((other.x - self.x) ** 2) + | |
((other.y - self.y) ** 2) + | |
((other.z - self.z) ** 2) + | |
((other.w - self.w) ** 2)) | |
def normalize(self) -> Vec4: | |
"""Normalize the vector to have a magnitude of 1. i.e. make it a unit vector.""" | |
d = self.__abs__() | |
if d: | |
return Vec4(self.x / d, self.y / d, self.z / d, self.w / d) | |
return self | |
def clamp(self, min_val: float, max_val: float) -> Vec4: | |
return Vec4(clamp(self.x, min_val, max_val), | |
clamp(self.y, min_val, max_val), | |
clamp(self.z, min_val, max_val), | |
clamp(self.w, min_val, max_val)) | |
def dot(self, other: Vec4) -> float: | |
return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w | |
def __getattr__(self, attrs: str) -> Vec2 | Vec3 | Vec4: | |
try: | |
# Allow swizzled getting of attrs | |
vec_class = {2: Vec2, 3: Vec3, 4: Vec4}[len(attrs)] | |
return vec_class(*(self['xyzw'.index(c)] for c in attrs)) | |
except Exception: | |
raise AttributeError( | |
f"'{self.__class__.__name__}' object has no attribute '{attrs}'" | |
) from None | |
def __repr__(self) -> str: | |
return f"Vec4({self.x}, {self.y}, {self.z}, {self.w})" | |
class Mat3(tuple): | |
"""A 3x3 Matrix class | |
`Mat3` is an immutable 3x3 Matrix, including most common | |
operators. Matrix multiplication must be performed using | |
the "@" operator. | |
""" | |
def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) -> Mat3: | |
"""Create a 3x3 Matrix | |
A Mat3 can be created with a list or tuple of 9 values. | |
If no values are provided, an "identity matrix" will be created | |
(1.0 on the main diagonal). Matrix objects are immutable, so | |
all operations return a new Mat3 object. | |
:Parameters: | |
`values` : tuple of float or int | |
A tuple or list containing 9 floats or ints. | |
""" | |
new = super().__new__(Mat3, values) | |
assert len(new) == 9, "A 3x3 Matrix requires 9 values" | |
return new | |
def scale(self, sx: float, sy: float) -> Mat3: | |
return self @ Mat3((1.0 / sx, 0.0, 0.0, 0.0, 1.0 / sy, 0.0, 0.0, 0.0, 1.0)) | |
def translate(self, tx: float, ty: float) -> Mat3: | |
return self @ Mat3((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -tx, ty, 1.0)) | |
def rotate(self, phi: float) -> Mat3: | |
s = _math.sin(_math.radians(phi)) | |
c = _math.cos(_math.radians(phi)) | |
return self @ Mat3((c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0)) | |
def shear(self, sx: float, sy: float) -> Mat3: | |
return self @ Mat3((1.0, sy, 0.0, sx, 1.0, 0.0, 0.0, 0.0, 1.0)) | |
def __add__(self, other: Mat3) -> Mat3: | |
if not isinstance(other, Mat3): | |
raise TypeError("Can only add to other Mat3 types") | |
return Mat3(s + o for s, o in zip(self, other)) | |
def __sub__(self, other: Mat3) -> Mat3: | |
if not isinstance(other, Mat3): | |
raise TypeError("Can only subtract from other Mat3 types") | |
return Mat3(s - o for s, o in zip(self, other)) | |
def __pos__(self) -> Mat3: | |
return self | |
def __neg__(self) -> Mat3: | |
return Mat3(-v for v in self) | |
def __round__(self, ndigits: int | None = None) -> Mat3: | |
return Mat3(round(v, ndigits) for v in self) | |
def __mul__(self, other: object) -> _typing.NoReturn: | |
raise NotImplementedError("Please use the @ operator for Matrix multiplication.") | |
def __matmul__(self, other: Vec3) -> Vec3: | |
... | |
def __matmul__(self, other: Mat3) -> Mat3: | |
... | |
def __matmul__(self, other): | |
if isinstance(other, Vec3): | |
# Rows: | |
r0 = self[0::3] | |
r1 = self[1::3] | |
r2 = self[2::3] | |
return Vec3(sum(map(_mul, r0, other)), | |
sum(map(_mul, r1, other)), | |
sum(map(_mul, r2, other))) | |
if not isinstance(other, Mat3): | |
raise TypeError("Can only multiply with Mat3 or Vec3 types") | |
# Rows: | |
r0 = self[0::3] | |
r1 = self[1::3] | |
r2 = self[2::3] | |
# Columns: | |
c0 = other[0:3] | |
c1 = other[3:6] | |
c2 = other[6:9] | |
# Multiply and sum rows * columns: | |
return Mat3((sum(map(_mul, c0, r0)), sum(map(_mul, c0, r1)), sum(map(_mul, c0, r2)), | |
sum(map(_mul, c1, r0)), sum(map(_mul, c1, r1)), sum(map(_mul, c1, r2)), | |
sum(map(_mul, c2, r0)), sum(map(_mul, c2, r1)), sum(map(_mul, c2, r2)))) | |
def __repr__(self) -> str: | |
return f"{self.__class__.__name__}{self[0:3]}\n {self[3:6]}\n {self[6:9]}" | |
class Mat4(tuple): | |
"""A 4x4 Matrix class | |
`Mat4` is an immutable 4x4 Matrix, including most common | |
operators. Matrix multiplication must be performed using | |
the "@" operator. | |
Class methods are available for creating orthogonal | |
and perspective projections matrixes. | |
""" | |
def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, | |
0.0, 1.0, 0.0, 0.0, | |
0.0, 0.0, 1.0, 0.0, | |
0.0, 0.0, 0.0, 1.0,)) -> Mat4: | |
"""Create a 4x4 Matrix | |
A Matrix can be created with a list or tuple of 16 values. | |
If no values are provided, an "identity matrix" will be created | |
(1.0 on the main diagonal). Matrix objects are immutable, so | |
all operations return a new Mat4 object. | |
:Parameters: | |
`values` : tuple of float or int | |
A tuple or list containing 16 floats or ints. | |
""" | |
new = super().__new__(Mat4, values) | |
assert len(new) == 16, "A 4x4 Matrix requires 16 values" | |
return new | |
def orthogonal_projection( | |
cls: type[Mat4T], | |
left: float, | |
right: float, | |
bottom: float, | |
top: float, | |
z_near: float, | |
z_far: float | |
) -> Mat4T: | |
"""Create a Mat4 orthographic projection matrix for use with OpenGL. | |
This matrix doesn't actually perform the projection; it transforms the | |
space so that OpenGL's vertex processing performs it. | |
""" | |
width = right - left | |
height = top - bottom | |
depth = z_far - z_near | |
sx = 2.0 / width | |
sy = 2.0 / height | |
sz = 2.0 / -depth | |
tx = -(right + left) / width | |
ty = -(top + bottom) / height | |
tz = -(z_far + z_near) / depth | |
return cls((sx, 0.0, 0.0, 0.0, | |
0.0, sy, 0.0, 0.0, | |
0.0, 0.0, sz, 0.0, | |
tx, ty, tz, 1.0)) | |
def perspective_projection( | |
cls: type[Mat4T], | |
aspect: float, | |
z_near: float, | |
z_far: float, | |
fov: float = 60 | |
) -> Mat4T: | |
""" | |
Create a Mat4 perspective projection matrix for use with OpenGL. | |
This matrix doesn't actually perform the projection; it transforms the | |
space so that OpenGL's vertex processing performs it. | |
:Parameters: | |
`aspect` : The aspect ratio as a `float` | |
`z_near` : The near plane as a `float` | |
`z_far` : The far plane as a `float` | |
`fov` : Field of view in degrees as a `float` | |
""" | |
xy_max = z_near * _math.tan(fov * _math.pi / 360) | |
y_min = -xy_max | |
x_min = -xy_max | |
width = xy_max - x_min | |
height = xy_max - y_min | |
depth = z_far - z_near | |
q = -(z_far + z_near) / depth | |
qn = -2 * z_far * z_near / depth | |
w = 2 * z_near / width | |
w = w / aspect | |
h = 2 * z_near / height | |
return cls((w, 0, 0, 0, | |
0, h, 0, 0, | |
0, 0, q, -1, | |
0, 0, qn, 0)) | |
def from_rotation(cls, angle: float, vector: Vec3) -> Mat4: | |
"""Create a rotation matrix from an angle and Vec3. | |
:Parameters: | |
`angle` : A `float` : | |
The angle as a float. | |
`vector` : A `Vec3`, or 3 component tuple of float or int : | |
Vec3 or tuple with x, y and z translation values | |
""" | |
return cls().rotate(angle, vector) | |
def from_scale(cls: type[Mat4T], vector: Vec3) -> Mat4T: | |
"""Create a scale matrix from a Vec3. | |
:Parameters: | |
`vector` : A `Vec3`, or 3 component tuple of float or int | |
Vec3 or tuple with x, y and z scale values | |
""" | |
return cls((vector[0], 0.0, 0.0, 0.0, | |
0.0, vector[1], 0.0, 0.0, | |
0.0, 0.0, vector[2], 0.0, | |
0.0, 0.0, 0.0, 1.0)) | |
def from_translation(cls: type[Mat4T], vector: Vec3) -> Mat4T: | |
"""Create a translation matrix from a Vec3. | |
:Parameters: | |
`vector` : A `Vec3`, or 3 component tuple of float or int | |
Vec3 or tuple with x, y and z translation values | |
""" | |
return cls((1.0, 0.0, 0.0, 0.0, | |
0.0, 1.0, 0.0, 0.0, | |
0.0, 0.0, 1.0, 0.0, | |
vector[0], vector[1], vector[2], 1.0)) | |
def look_at(cls: type[Mat4T], position: Vec3, target: Vec3, up: Vec3): | |
f = (target - position).normalize() | |
u = up.normalize() | |
s = f.cross(u).normalize() | |
u = s.cross(f) | |
return cls([s.x, u.x, -f.x, 0.0, | |
s.y, u.y, -f.y, 0.0, | |
s.z, u.z, -f.z, 0.0, | |
-s.dot(position), -u.dot(position), f.dot(position), 1.0]) | |
def row(self, index: int) -> tuple: | |
"""Get a specific row as a tuple.""" | |
return self[index::4] | |
def column(self, index: int) -> tuple: | |
"""Get a specific column as a tuple.""" | |
return self[index * 4: index * 4 + 4] | |
def rotate(self, angle: float, vector: Vec3) -> Mat4: | |
"""Get a rotation Matrix on x, y, or z axis.""" | |
if not all(abs(n) <= 1 for n in vector): | |
raise ValueError("vector must be normalized (<=1)") | |
x, y, z = vector | |
c = _math.cos(angle) | |
s = _math.sin(angle) | |
t = 1 - c | |
temp_x, temp_y, temp_z = t * x, t * y, t * z | |
ra = c + temp_x * x | |
rb = 0 + temp_x * y + s * z | |
rc = 0 + temp_x * z - s * y | |
re = 0 + temp_y * x - s * z | |
rf = c + temp_y * y | |
rg = 0 + temp_y * z + s * x | |
ri = 0 + temp_z * x + s * y | |
rj = 0 + temp_z * y - s * x | |
rk = c + temp_z * z | |
# ra, rb, rc, -- | |
# re, rf, rg, -- | |
# ri, rj, rk, -- | |
# --, --, --, -- | |
return Mat4(self) @ Mat4((ra, rb, rc, 0, re, rf, rg, 0, ri, rj, rk, 0, 0, 0, 0, 1)) | |
def scale(self, vector: Vec3) -> Mat4: | |
"""Get a scale Matrix on x, y, or z axis.""" | |
temp = list(self) | |
temp[0] *= vector[0] | |
temp[5] *= vector[1] | |
temp[10] *= vector[2] | |
return Mat4(temp) | |
def translate(self, vector: Vec3) -> Mat4: | |
"""Get a translation Matrix along x, y, and z axis.""" | |
return self @ Mat4((1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, *vector, 1)) | |
def transpose(self) -> Mat4: | |
"""Get a transpose of this Matrix.""" | |
return Mat4(self[0::4] + self[1::4] + self[2::4] + self[3::4]) | |
def __add__(self, other: Mat4) -> Mat4: | |
if not isinstance(other, Mat4): | |
raise TypeError("Can only add to other Mat4 types") | |
return Mat4(s + o for s, o in zip(self, other)) | |
def __sub__(self, other: Mat4) -> Mat4: | |
if not isinstance(other, Mat4): | |
raise TypeError("Can only subtract from other Mat4 types") | |
return Mat4(s - o for s, o in zip(self, other)) | |
def __pos__(self) -> Mat4: | |
return self | |
def __neg__(self) -> Mat4: | |
return Mat4(-v for v in self) | |
def __invert__(self) -> Mat4: | |
a = self[10] * self[15] - self[11] * self[14] | |
b = self[9] * self[15] - self[11] * self[13] | |
c = self[9] * self[14] - self[10] * self[13] | |
d = self[8] * self[15] - self[11] * self[12] | |
e = self[8] * self[14] - self[10] * self[12] | |
f = self[8] * self[13] - self[9] * self[12] | |
g = self[6] * self[15] - self[7] * self[14] | |
h = self[5] * self[15] - self[7] * self[13] | |
i = self[5] * self[14] - self[6] * self[13] | |
j = self[6] * self[11] - self[7] * self[10] | |
k = self[5] * self[11] - self[7] * self[9] | |
l = self[5] * self[10] - self[6] * self[9] | |
m = self[4] * self[15] - self[7] * self[12] | |
n = self[4] * self[14] - self[6] * self[12] | |
o = self[4] * self[11] - self[7] * self[8] | |
p = self[4] * self[10] - self[6] * self[8] | |
q = self[4] * self[13] - self[5] * self[12] | |
r = self[4] * self[9] - self[5] * self[8] | |
det = (self[0] * (self[5] * a - self[6] * b + self[7] * c) | |
- self[1] * (self[4] * a - self[6] * d + self[7] * e) | |
+ self[2] * (self[4] * b - self[5] * d + self[7] * f) | |
- self[3] * (self[4] * c - self[5] * e + self[6] * f)) | |
if det == 0: | |
_warnings.warn("Unable to calculate inverse of singular Matrix") | |
return self | |
pdet = 1 / det | |
ndet = -pdet | |
return Mat4((pdet * (self[5] * a - self[6] * b + self[7] * c), | |
ndet * (self[1] * a - self[2] * b + self[3] * c), | |
pdet * (self[1] * g - self[2] * h + self[3] * i), | |
ndet * (self[1] * j - self[2] * k + self[3] * l), | |
ndet * (self[4] * a - self[6] * d + self[7] * e), | |
pdet * (self[0] * a - self[2] * d + self[3] * e), | |
ndet * (self[0] * g - self[2] * m + self[3] * n), | |
pdet * (self[0] * j - self[2] * o + self[3] * p), | |
pdet * (self[4] * b - self[5] * d + self[7] * f), | |
ndet * (self[0] * b - self[1] * d + self[3] * f), | |
pdet * (self[0] * h - self[1] * m + self[3] * q), | |
ndet * (self[0] * k - self[1] * o + self[3] * r), | |
ndet * (self[4] * c - self[5] * e + self[6] * f), | |
pdet * (self[0] * c - self[1] * e + self[2] * f), | |
ndet * (self[0] * i - self[1] * n + self[2] * q), | |
pdet * (self[0] * l - self[1] * p + self[2] * r))) | |
def __round__(self, ndigits: int | None = None) -> Mat4: | |
return Mat4(round(v, ndigits) for v in self) | |
def __mul__(self, other: int) -> _typing.NoReturn: | |
raise NotImplementedError("Please use the @ operator for Matrix multiplication.") | |
def __matmul__(self, other: Vec4) -> Vec4: | |
... | |
def __matmul__(self, other: Mat4) -> Mat4: | |
... | |
def __matmul__(self, other): | |
if isinstance(other, Vec4): | |
# Rows: | |
r0 = self[0::4] | |
r1 = self[1::4] | |
r2 = self[2::4] | |
r3 = self[3::4] | |
return Vec4(sum(map(_mul, r0, other)), | |
sum(map(_mul, r1, other)), | |
sum(map(_mul, r2, other)), | |
sum(map(_mul, r3, other))) | |
if not isinstance(other, Mat4): | |
raise TypeError("Can only multiply with Mat4 or Vec4 types") | |
# Rows: | |
r0 = self[0::4] | |
r1 = self[1::4] | |
r2 = self[2::4] | |
r3 = self[3::4] | |
# Columns: | |
c0 = other[0:4] | |
c1 = other[4:8] | |
c2 = other[8:12] | |
c3 = other[12:16] | |
# Multiply and sum rows * columns: | |
return Mat4((sum(map(_mul, c0, r0)), sum(map(_mul, c0, r1)), sum(map(_mul, c0, r2)), sum(map(_mul, c0, r3)), | |
sum(map(_mul, c1, r0)), sum(map(_mul, c1, r1)), sum(map(_mul, c1, r2)), sum(map(_mul, c1, r3)), | |
sum(map(_mul, c2, r0)), sum(map(_mul, c2, r1)), sum(map(_mul, c2, r2)), sum(map(_mul, c2, r3)), | |
sum(map(_mul, c3, r0)), sum(map(_mul, c3, r1)), sum(map(_mul, c3, r2)), sum(map(_mul, c3, r3)))) | |
# def __getitem__(self, item): | |
# row = [slice(0, 4), slice(4, 8), slice(8, 12), slice(12, 16)][item] | |
# return super().__getitem__(row) | |
def __repr__(self) -> str: | |
return f"{self.__class__.__name__}{self[0:4]}\n {self[4:8]}\n {self[8:12]}\n {self[12:16]}" | |