|
"""Nodes, conforming to the glTF 2.0 standards as specified in |
|
https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node |
|
|
|
Author: Matthew Matl |
|
""" |
|
import numpy as np |
|
|
|
import trimesh.transformations as transformations |
|
|
|
from .camera import Camera |
|
from .mesh import Mesh |
|
from .light import Light |
|
|
|
|
|
class Node(object): |
|
"""A node in the node hierarchy. |
|
|
|
Parameters |
|
---------- |
|
name : str, optional |
|
The user-defined name of this object. |
|
camera : :class:`Camera`, optional |
|
The camera in this node. |
|
children : list of :class:`Node` |
|
The children of this node. |
|
skin : int, optional |
|
The index of the skin referenced by this node. |
|
matrix : (4,4) float, optional |
|
A floating-point 4x4 transformation matrix. |
|
mesh : :class:`Mesh`, optional |
|
The mesh in this node. |
|
rotation : (4,) float, optional |
|
The node's unit quaternion in the order (x, y, z, w), where |
|
w is the scalar. |
|
scale : (3,) float, optional |
|
The node's non-uniform scale, given as the scaling factors along the x, |
|
y, and z axes. |
|
translation : (3,) float, optional |
|
The node's translation along the x, y, and z axes. |
|
weights : (n,) float |
|
The weights of the instantiated Morph Target. Number of elements must |
|
match number of Morph Targets of used mesh. |
|
light : :class:`Light`, optional |
|
The light in this node. |
|
""" |
|
|
|
def __init__(self, |
|
name=None, |
|
camera=None, |
|
children=None, |
|
skin=None, |
|
matrix=None, |
|
mesh=None, |
|
rotation=None, |
|
scale=None, |
|
translation=None, |
|
weights=None, |
|
light=None): |
|
|
|
if children is None: |
|
children = [] |
|
|
|
self._matrix = None |
|
self._scale = None |
|
self._rotation = None |
|
self._translation = None |
|
if matrix is None: |
|
if rotation is None: |
|
rotation = np.array([0.0, 0.0, 0.0, 1.0]) |
|
if translation is None: |
|
translation = np.zeros(3) |
|
if scale is None: |
|
scale = np.ones(3) |
|
self.rotation = rotation |
|
self.translation = translation |
|
self.scale = scale |
|
else: |
|
self.matrix = matrix |
|
|
|
self.name = name |
|
self.camera = camera |
|
self.children = children |
|
self.skin = skin |
|
self.mesh = mesh |
|
self.weights = weights |
|
self.light = light |
|
|
|
@property |
|
def name(self): |
|
"""str : The user-defined name of this object. |
|
""" |
|
return self._name |
|
|
|
@name.setter |
|
def name(self, value): |
|
if value is not None: |
|
value = str(value) |
|
self._name = value |
|
|
|
@property |
|
def camera(self): |
|
""":class:`Camera` : The camera in this node. |
|
""" |
|
return self._camera |
|
|
|
@camera.setter |
|
def camera(self, value): |
|
if value is not None and not isinstance(value, Camera): |
|
raise TypeError('Value must be a camera') |
|
self._camera = value |
|
|
|
@property |
|
def children(self): |
|
"""list of :class:`Node` : The children of this node. |
|
""" |
|
return self._children |
|
|
|
@children.setter |
|
def children(self, value): |
|
self._children = value |
|
|
|
@property |
|
def skin(self): |
|
"""int : The skin index for this node. |
|
""" |
|
return self._skin |
|
|
|
@skin.setter |
|
def skin(self, value): |
|
self._skin = value |
|
|
|
@property |
|
def mesh(self): |
|
""":class:`Mesh` : The mesh in this node. |
|
""" |
|
return self._mesh |
|
|
|
@mesh.setter |
|
def mesh(self, value): |
|
if value is not None and not isinstance(value, Mesh): |
|
raise TypeError('Value must be a mesh') |
|
self._mesh = value |
|
|
|
@property |
|
def light(self): |
|
""":class:`Light` : The light in this node. |
|
""" |
|
return self._light |
|
|
|
@light.setter |
|
def light(self, value): |
|
if value is not None and not isinstance(value, Light): |
|
raise TypeError('Value must be a light') |
|
self._light = value |
|
|
|
@property |
|
def rotation(self): |
|
"""(4,) float : The xyzw quaternion for this node. |
|
""" |
|
return self._rotation |
|
|
|
@rotation.setter |
|
def rotation(self, value): |
|
value = np.asanyarray(value) |
|
if value.shape != (4,): |
|
raise ValueError('Quaternion must be a (4,) vector') |
|
if np.abs(np.linalg.norm(value) - 1.0) > 1e-3: |
|
raise ValueError('Quaternion must have norm == 1.0') |
|
self._rotation = value |
|
self._matrix = None |
|
|
|
@property |
|
def translation(self): |
|
"""(3,) float : The translation for this node. |
|
""" |
|
return self._translation |
|
|
|
@translation.setter |
|
def translation(self, value): |
|
value = np.asanyarray(value) |
|
if value.shape != (3,): |
|
raise ValueError('Translation must be a (3,) vector') |
|
self._translation = value |
|
self._matrix = None |
|
|
|
@property |
|
def scale(self): |
|
"""(3,) float : The scale for this node. |
|
""" |
|
return self._scale |
|
|
|
@scale.setter |
|
def scale(self, value): |
|
value = np.asanyarray(value) |
|
if value.shape != (3,): |
|
raise ValueError('Scale must be a (3,) vector') |
|
self._scale = value |
|
self._matrix = None |
|
|
|
@property |
|
def matrix(self): |
|
"""(4,4) float : The homogenous transform matrix for this node. |
|
|
|
Note that this matrix's elements are not settable, |
|
it's just a copy of the internal matrix. You can set the whole |
|
matrix, but not an individual element. |
|
""" |
|
if self._matrix is None: |
|
self._matrix = self._m_from_tqs( |
|
self.translation, self.rotation, self.scale |
|
) |
|
return self._matrix.copy() |
|
|
|
@matrix.setter |
|
def matrix(self, value): |
|
value = np.asanyarray(value) |
|
if value.shape != (4,4): |
|
raise ValueError('Matrix must be a 4x4 numpy ndarray') |
|
if not np.allclose(value[3,:], np.array([0.0, 0.0, 0.0, 1.0])): |
|
raise ValueError('Bottom row of matrix must be [0,0,0,1]') |
|
self.rotation = Node._q_from_m(value) |
|
self.scale = Node._s_from_m(value) |
|
self.translation = Node._t_from_m(value) |
|
self._matrix = value |
|
|
|
@staticmethod |
|
def _t_from_m(m): |
|
return m[:3,3] |
|
|
|
@staticmethod |
|
def _r_from_m(m): |
|
U = m[:3,:3] |
|
norms = np.linalg.norm(U.T, axis=1) |
|
return U / norms |
|
|
|
@staticmethod |
|
def _q_from_m(m): |
|
M = np.eye(4) |
|
M[:3,:3] = Node._r_from_m(m) |
|
q_wxyz = transformations.quaternion_from_matrix(M) |
|
return np.roll(q_wxyz, -1) |
|
|
|
@staticmethod |
|
def _s_from_m(m): |
|
return np.linalg.norm(m[:3,:3].T, axis=1) |
|
|
|
@staticmethod |
|
def _r_from_q(q): |
|
q_wxyz = np.roll(q, 1) |
|
return transformations.quaternion_matrix(q_wxyz)[:3,:3] |
|
|
|
@staticmethod |
|
def _m_from_tqs(t, q, s): |
|
S = np.eye(4) |
|
S[:3,:3] = np.diag(s) |
|
|
|
R = np.eye(4) |
|
R[:3,:3] = Node._r_from_q(q) |
|
|
|
T = np.eye(4) |
|
T[:3,3] = t |
|
|
|
return T.dot(R.dot(S)) |
|
|