#
##
## SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.4 (Thu Nov 16 18:07:39 CET 2023)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: https://pyformex.org
## Project page: https://savannah.nongnu.org/projects/pyformex/
## Development: https://gitlab.com/bverheg/pyformex
## Distributed under the GNU General Public License version 3 or later.
##
## This program 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.
##
## This program 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 this program. If not, see http://www.gnu.org/licenses/.
##
"""opengl/matrix.py
Python OpenGL framework for pyFormex
"""
import numpy as np
import pyformex.arraytools as at
DTYPE = at.Float
[docs]class Matrix4(np.ndarray):
"""A 4x4 transformation matrix for homogeneous coordinates.
The matrix is to be used with post-multiplication on
row vectors (i.e. OpenGL convention).
Parameters
----------
data: array_like (4,4), optional
If specified, should be a (4,4) float array or compatible. Else
a 4x4 identity matrix is created.
Examples
--------
>>> I = Matrix4()
>>> print(I)
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
We can first scale and then rotate, or first rotate and then scale:
>>> a = Matrix4().scale([4.,4.,4.]).rotate(45.,[0.,0.,1.])
>>> a
Matrix4([[ 2.8284, 2.8284, 0. , 0. ],
[-2.8284, 2.8284, 0. , 0. ],
[ 0. , 0. , 4. , 0. ],
[ 0. , 0. , 0. , 1. ]])
>>> b = Matrix4().rotate(45.,[0.,0.,1.]).scale([4.,4.,4.])
>>> np.allclose(a,b)
True
"""
def __new__(clas, data=None):
"""Create a new Matrix instance"""
if data is None:
data = np.eye(4, 4, dtype=DTYPE)
else:
data = at.checkArray(data, (4, 4), 'f').astype(DTYPE)
ar = data.view(clas)
ar._gl = None
return ar
def __array_finalize__(self, obj):
"""Finalize the new Matrix object.
When a class is derived from numpy.ndarray and the constructor (the
:meth:`__new__` method) defines new attributes, these atttributes
need to be reset in this method.
"""
if obj is None:
return
self._gl = getattr(obj, '_gl', None)
def __array_wrap__(self, out_arr, context=None):
res = super().__array_wrap__(out_arr, context)
if type(res) == Matrix4 and (
res.dtype != DTYPE or res.shape != (4,4) ):
res = res.view(np.ndarray)
return res
[docs] def gl(self):
"""Get the transformation matrix as a 'ready-to-use'-gl version.
Returns the (4,4) Matrix as a rowwise flattened array of type float32.
Example:
>>> Matrix4().gl()
Matrix4([1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0.,
0., 0., 1.], dtype=float32)
"""
if self._gl is None:
self._gl = self.flatten().astype(np.float32)
return self._gl
@property
def rot(self):
"""Return the (3,3) rotation matrix"""
return self[:3, :3]
@rot.setter
def rot(self, value):
"""Set the rotation matrix to (3,3) value"""
self[:3, :3] = value
self._gl = None
@property
def trl(self):
"""Return the (3,) translation vector"""
return self[3, :3]
@trl.setter
def trl(self, value):
"""Set the translation vector to (3,) value"""
self[3, :3] = value
self._gl = None
[docs] def identity(self):
"""Reset the matrix to a 4x4 identity matrix."""
self = np.eye(4, 4)
self._gl = None
[docs] def translate(self, vector):
"""Translate a 4x4 matrix by a (3,) vector.
- `vector`: (3,) float array: the translation vector
Changes the Matrix in place and also returns the result
Example:
>>> Matrix4().translate([1.,2.,3.])
Matrix4([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[1., 2., 3., 1.]])
"""
vector = at.checkArray(vector, (3,), 'f')
self.trl += np.dot(vector, self.rot)
return self
[docs] def rotate(self, angle, axis=None):
"""Rotate a Matrix4.
The rotation can be specified by
- an angle and axis,
- a 3x3 rotation matrix,
- a 4x4 trtransformation matrix (Matrix4).
Parameters:
- `angle`: float: the rotation angle. A 3x3 or 4x4 matrix may be
give instead, to directly specify the roation matrix.
- `axis`: int or (3,) float: the axis to rotate around
Changes the Matrix in place and also returns the result.
Example:
>>> Matrix4().rotate(90.,[0.,1.,0.])
Matrix4([[ 0., 0., -1., 0.],
[ 0., 1., 0., 0.],
[ 1., 0., 0., 0.],
[ 0., 0., 0., 1.]])
"""
## !! TRANSPOSE!!
## x^2(1-c)+c xy(1-c)-zs xz(1-c)+ys 0
## yx(1-c)+zs y^2(1-c)+c yz(1-c)-xs 0
## xz(1-c)-ys yz(1-c)+xs z^2(1-c)+c 0
## 0 0 0 1
try:
rot = at.checkArray(angle, (4, 4), 'f')[:3, :3]
except Exception:
try:
rot = at.checkArray(angle, (3, 3), 'f')
except Exception:
angle = at.checkFloat(angle)
rot = at.rotationMatrix(angle, axis)
self.rot = rot @ self.rot
return self
[docs] def scale(self, vector):
"""Scale a 4x4 matrix by a (3,) vector.
- `vector`: (3,) float array: the scaling vector
Changes the Matrix in place and also returns the result
Example:
>>> Matrix4().scale([1.,2.,3.])
Matrix4([[1., 0., 0., 0.],
[0., 2., 0., 0.],
[0., 0., 3., 0.],
[0., 0., 0., 1.]])
"""
vector = at.checkArray(vector, (3,), 'f')
scale3 = np.diagflat(vector)
self[:3, :3] = self[:3, :3] @ scale3
self._gl = None
return self
# Do we need these?
[docs] def swapRows(self, row1, row2):
"""Swap two rows.
- `row1`, `row2`: index of the rows to swap
"""
temp = np.copy(self[row1])
self[row1] = self[row2]
self[row2] = temp
self._gl = None
[docs] def swapCols(self, col1, col2):
"""Swap two columns.
- `col1`, `col2`: index of the columns to swap
"""
temp = np.copy(self[:, col1])
self[:, col1] = self[:, col2]
self[:, col2] = temp
self._gl = None
[docs] def inverse(self):
"""Return the inverse matrix"""
return np.linalg.inv(self)
[docs] def transinv(self):
"""Return the transpose of the inverse."""
return self.inverse().transpose()
# End