def shear_matrix(angle: float, direction: np.ndarray, point: np.ndarray, normal: np.ndarray): """Return matrix to shear by angle along direction vector on shear plane. The shear plane is defined by a point and normal vector. The direction vector must be orthogonal to the plane's normal vector. A point P is transformed by the shear matrix into P" such that the vector P-P" is parallel to the direction vector and its extent is given by the angle of P-P'-P", where P' is the orthogonal projection of P onto the shear plane. >>> angle = (np.random.random() - 0.5) * 4*pi >>> direct = np.random.random(3) - 0.5 >>> point = np.random.random(3) - 0.5 >>> normal = np.cross(direct, np.random.random(3)) >>> S = shear_matrix(angle, direct, point, normal) >>> np.allclose(1, np.linalg.det(S)) True """ normal = unit_vector(normal[:3]) direction = unit_vector(direction[:3]) if abs(np.dot(normal, direction)) > 1e-6: raise ValueError("direction and normal vectors are not orthogonal") angle = tan(angle) M = np.identity(4) M[:3, :3] += angle * np.outer(direction, normal) M[:3, 3] = -angle * np.dot(point[:3], normal) * direction return M
def scale_matrix(factor: float, origin: np.ndarray = _origin, direction: np.ndarray = _origin): """Return matrix to scale by factor around origin in direction. Use factor -1 for point symmetry. >>> v = (np.random.rand(4, 5) - 0.5) * 20 >>> v[3] = 1 >>> S = scale_matrix(-1.234) >>> np.allclose(np.dot(S, v)[:3], -1.234*v[:3]) True >>> factor = np.random.random() * 10 - 5 >>> origin = np.random.random(3) - 0.5 >>> direct = np.random.random(3) - 0.5 >>> S = scale_matrix(factor, origin) >>> S = scale_matrix(factor, origin, direct) """ if (direction == _origin).all(): # uniform scaling M = np.diag(np.array((factor, factor, factor, 1.0), dtype=np.float64)) if np.any(origin != _origin): M[:3, 3] = origin[:3] M[:3, 3] *= 1.0 - factor else: # nonuniform scaling direction = unit_vector(direction[:3]) factor = 1.0 - factor M = np.identity(4) M[:3, :3] -= factor * np.outer(direction, direction) if (origin == _origin).all(): M[:3, 3] = (factor * np.dot(origin[:3], direction)) * direction return M
def arcball_constrain_to_axis(point, axis): """Return sphere point perpendicular to axis.""" v = np.array(point, dtype=np.float64, copy=True) a = np.array(axis, dtype=np.float64, copy=True) v -= a * np.dot(a, v) # on plane n = norm(v) if n > EPS: if v[2] < 0.0: np.negative(v, v) v /= n return v if a[2] == 1.0: return vector(1.0, 0.0, 0.0) return unit_vector(vector(-a[1], a[0], 0.0))
def rotation_matrix(angle: float, direction: np.ndarray, point: np.ndarray = _origin) -> np.ndarray: """Return matrix to rotate about axis defined by point and direction. :param angle: the angle for rotation :param direction: the direction vector of the rotation :param point: the reference point of rotation. By default origin is used. :returns: the resulting rotation matrix (4x4) >>> R = rotation_matrix(pi/2, vector(0, 0, 1), vector(1, 0, 0)) >>> np.allclose(np.dot(R, [0, 0, 0, 1]), [1, -1, 0, 1]) True >>> angle = (np.random.random() - 0.5) * (2*pi) >>> direc = np.random.random(3) - 0.5 >>> point = np.random.random(3) - 0.5 >>> R0 = rotation_matrix(angle, direc, point) >>> R1 = rotation_matrix(angle-2*pi, direc, point) >>> is_same_transform(R0, R1) True >>> R0 = rotation_matrix(angle, direc, point) >>> R1 = rotation_matrix(-angle, -direc, point) >>> is_same_transform(R0, R1) True >>> I = np.identity(4, np.float64) >>> np.allclose(I, rotation_matrix(pi*2, direc)) True >>> np.allclose(2, np.trace(rotation_matrix(pi/2, ... direc, point))) True """ sina: float = sin(angle) cosa: float = cos(angle) direction = unit_vector(direction[:3]) # rotation matrix around unit vector R = np.diag(np.array((cosa, cosa, cosa), dtype=double)) R += np.outer(direction, direction) * (1.0 - cosa) direction *= sina R += np.array([[0.0, -direction[2], direction[1]], [direction[2], 0.0, -direction[0]], [-direction[1], direction[0], 0.0]]) M = np.identity(4) M[:3, :3] = R if np.any(point != _origin): # rotation not around origin M[:3, 3] = point - np.dot(R, point) return M
def reflection_matrix(point: np.ndarray, normal: np.ndarray) -> np.ndarray: """Return matrix to mirror at plane defined by point and normal vector. >>> v0 = np.random.random(4) - 0.5 >>> v0[3] = 1. >>> v1 = np.random.random(3) - 0.5 >>> R = reflection_matrix(v0, v1) >>> np.allclose(2, np.trace(R)) True >>> np.allclose(v0, np.dot(R, v0)) True >>> v2 = v0.copy() >>> v2[:3] += v1 >>> v3 = v0.copy() >>> v2[:3] -= v1 >>> np.allclose(v2, np.dot(R, v3)) True """ normal = unit_vector(normal[:3]) M = np.identity(4) M[:3, :3] -= 2.0 * np.outer(normal, normal) M[:3, 3] = (2.0 * np.dot(point[:3], normal)) * normal return M
def projection_matrix(point: np.ndarray, normal: np.ndarray, direction: np.ndarray = None, perspective: np.ndarray = None, pseudo: bool = False) -> np.ndarray: """Return matrix to project onto plane defined by point and normal. Using either perspective point, projection direction, or none of both. If pseudo is True, perspective projections will preserve relative depth such that Perspective = dot(Orthogonal, PseudoPerspective). :param point: the point defining center of projection plane :param normal: the normal vector defining projection plane. should be unit vector :param direction: ??? :param perspective: if given, is the point of observation, i.e. where eye is :param pseudo: ??? :return: projection matrix 4x4 >>> P = projection_matrix(vector(0, 0, 0), vector(1, 0, 0)) >>> np.allclose(P[1:, 1:], np.identity(4)[1:, 1:]) True >>> point = np.random.random(3) - 0.5 >>> normal = np.random.random(3) - 0.5 >>> direct = np.random.random(3) - 0.5 >>> persp = np.random.random(3) - 0.5 >>> P0 = projection_matrix(point, normal) >>> P1 = projection_matrix(point, normal, direction=direct) >>> P2 = projection_matrix(point, normal, perspective=persp) >>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True) >>> is_same_transform(P2, np.dot(P0, P3)) True >>> P = projection_matrix(vector(3, 0, 0), vector(1, 1, 0), vector(1, 0, 0)) >>> v0 = (np.random.rand(4, 5) - 0.5) * 20 >>> v0[3] = 1 >>> v1 = np.dot(P, v0) >>> np.allclose(v1[1], v0[1]) True >>> np.allclose(v1[0], 3-v1[1]) True """ M = np.identity(4) point = np.copy(point[:3]) normal = unit_vector(normal[:3]) if perspective is not None: # perspective projection perspective = np.copy(perspective[:3]) M[0, 0] = M[1, 1] = M[2, 2] = np.dot(perspective - point, normal) M[:3, :3] -= np.outer(perspective, normal) if pseudo: # preserve relative depth M[:3, :3] -= np.outer(normal, normal) M[:3, 3] = np.dot(point, normal) * (perspective + normal) else: M[:3, 3] = np.dot(point, normal) * perspective M[3, :3] = -normal M[3, 3] = np.dot(perspective, normal) elif direction is not None: # parallel projection direction = np.copy(direction[:3]) # noinspection PyTypeChecker scale: float = np.dot(direction, normal) M[:3, :3] -= np.outer(direction, normal) / scale M[:3, 3] = direction * (np.dot(point, normal) / scale) else: # orthogonal projection M[:3, :3] -= np.outer(normal, normal) M[:3, 3] = np.dot(point, normal) * normal return M
def setaxes(self, *axes): """Set axes to constrain rotations.""" if axes is None: self._axes = None else: self._axes = [unit_vector(axis) for axis in axes]