pyrender-maintained 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrender/__init__.py +24 -0
- pyrender/camera.py +435 -0
- pyrender/constants.py +149 -0
- pyrender/font.py +272 -0
- pyrender/light.py +382 -0
- pyrender/material.py +705 -0
- pyrender/mesh.py +328 -0
- pyrender/node.py +263 -0
- pyrender/offscreen.py +160 -0
- pyrender/platforms/__init__.py +6 -0
- pyrender/platforms/base.py +73 -0
- pyrender/platforms/egl.py +219 -0
- pyrender/platforms/osmesa.py +59 -0
- pyrender/platforms/pyglet_platform.py +90 -0
- pyrender/primitive.py +489 -0
- pyrender/renderer.py +1328 -0
- pyrender/sampler.py +102 -0
- pyrender/scene.py +585 -0
- pyrender/shader_program.py +283 -0
- pyrender/texture.py +259 -0
- pyrender/trackball.py +216 -0
- pyrender/utils.py +115 -0
- pyrender/version.py +1 -0
- pyrender/viewer.py +1157 -0
- pyrender_maintained-1.0.0.dist-info/METADATA +55 -0
- pyrender_maintained-1.0.0.dist-info/RECORD +29 -0
- pyrender_maintained-1.0.0.dist-info/WHEEL +5 -0
- pyrender_maintained-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyrender_maintained-1.0.0.dist-info/top_level.txt +1 -0
pyrender/mesh.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Meshes, conforming to the glTF 2.0 standards as specified in
|
|
2
|
+
https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-mesh
|
|
3
|
+
|
|
4
|
+
Author: Matthew Matl
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import trimesh
|
|
10
|
+
|
|
11
|
+
from .primitive import Primitive
|
|
12
|
+
from .constants import GLTF
|
|
13
|
+
from .material import MetallicRoughnessMaterial
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Mesh(object):
|
|
17
|
+
"""A set of primitives to be rendered.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
name : str
|
|
22
|
+
The user-defined name of this object.
|
|
23
|
+
primitives : list of :class:`Primitive`
|
|
24
|
+
The primitives associated with this mesh.
|
|
25
|
+
weights : (k,) float
|
|
26
|
+
Array of weights to be applied to the Morph Targets.
|
|
27
|
+
is_visible : bool
|
|
28
|
+
If False, the mesh will not be rendered.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, primitives, name=None, weights=None, is_visible=True):
|
|
32
|
+
self.primitives = primitives
|
|
33
|
+
self.name = name
|
|
34
|
+
self.weights = weights
|
|
35
|
+
self.is_visible = is_visible
|
|
36
|
+
|
|
37
|
+
self._bounds = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def name(self):
|
|
41
|
+
"""str : The user-defined name of this object.
|
|
42
|
+
"""
|
|
43
|
+
return self._name
|
|
44
|
+
|
|
45
|
+
@name.setter
|
|
46
|
+
def name(self, value):
|
|
47
|
+
if value is not None:
|
|
48
|
+
value = str(value)
|
|
49
|
+
self._name = value
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def primitives(self):
|
|
53
|
+
"""list of :class:`Primitive` : The primitives associated
|
|
54
|
+
with this mesh.
|
|
55
|
+
"""
|
|
56
|
+
return self._primitives
|
|
57
|
+
|
|
58
|
+
@primitives.setter
|
|
59
|
+
def primitives(self, value):
|
|
60
|
+
self._primitives = value
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def weights(self):
|
|
64
|
+
"""(k,) float : Weights to be applied to morph targets.
|
|
65
|
+
"""
|
|
66
|
+
return self._weights
|
|
67
|
+
|
|
68
|
+
@weights.setter
|
|
69
|
+
def weights(self, value):
|
|
70
|
+
self._weights = value
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def is_visible(self):
|
|
74
|
+
"""bool : Whether the mesh is visible.
|
|
75
|
+
"""
|
|
76
|
+
return self._is_visible
|
|
77
|
+
|
|
78
|
+
@is_visible.setter
|
|
79
|
+
def is_visible(self, value):
|
|
80
|
+
self._is_visible = value
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def bounds(self):
|
|
84
|
+
"""(2,3) float : The axis-aligned bounds of the mesh.
|
|
85
|
+
"""
|
|
86
|
+
if self._bounds is None:
|
|
87
|
+
bounds = np.array([[np.inf, np.inf, np.inf],
|
|
88
|
+
[-np.inf, -np.inf, -np.inf]])
|
|
89
|
+
for p in self.primitives:
|
|
90
|
+
bounds[0] = np.minimum(bounds[0], p.bounds[0])
|
|
91
|
+
bounds[1] = np.maximum(bounds[1], p.bounds[1])
|
|
92
|
+
self._bounds = bounds
|
|
93
|
+
return self._bounds
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def centroid(self):
|
|
97
|
+
"""(3,) float : The centroid of the mesh's axis-aligned bounding box
|
|
98
|
+
(AABB).
|
|
99
|
+
"""
|
|
100
|
+
return np.mean(self.bounds, axis=0)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def extents(self):
|
|
104
|
+
"""(3,) float : The lengths of the axes of the mesh's AABB.
|
|
105
|
+
"""
|
|
106
|
+
return np.diff(self.bounds, axis=0).reshape(-1)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def scale(self):
|
|
110
|
+
"""(3,) float : The length of the diagonal of the mesh's AABB.
|
|
111
|
+
"""
|
|
112
|
+
return np.linalg.norm(self.extents)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_transparent(self):
|
|
116
|
+
"""bool : If True, the mesh is partially-transparent.
|
|
117
|
+
"""
|
|
118
|
+
for p in self.primitives:
|
|
119
|
+
if p.is_transparent:
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def from_points(points, colors=None, normals=None,
|
|
125
|
+
is_visible=True, poses=None):
|
|
126
|
+
"""Create a Mesh from a set of points.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
points : (n,3) float
|
|
131
|
+
The point positions.
|
|
132
|
+
colors : (n,3) or (n,4) float, optional
|
|
133
|
+
RGB or RGBA colors for each point.
|
|
134
|
+
normals : (n,3) float, optionals
|
|
135
|
+
The normal vectors for each point.
|
|
136
|
+
is_visible : bool
|
|
137
|
+
If False, the points will not be rendered.
|
|
138
|
+
poses : (x,4,4)
|
|
139
|
+
Array of 4x4 transformation matrices for instancing this object.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
mesh : :class:`Mesh`
|
|
144
|
+
The created mesh.
|
|
145
|
+
"""
|
|
146
|
+
primitive = Primitive(
|
|
147
|
+
positions=points,
|
|
148
|
+
normals=normals,
|
|
149
|
+
color_0=colors,
|
|
150
|
+
mode=GLTF.POINTS,
|
|
151
|
+
poses=poses
|
|
152
|
+
)
|
|
153
|
+
mesh = Mesh(primitives=[primitive], is_visible=is_visible)
|
|
154
|
+
return mesh
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def from_trimesh(mesh, material=None, is_visible=True,
|
|
158
|
+
poses=None, wireframe=False, smooth=True):
|
|
159
|
+
"""Create a Mesh from a :class:`~trimesh.base.Trimesh`.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
mesh : :class:`~trimesh.base.Trimesh` or list of them
|
|
164
|
+
A triangular mesh or a list of meshes.
|
|
165
|
+
material : :class:`Material`
|
|
166
|
+
The material of the object. Overrides any mesh material.
|
|
167
|
+
If not specified and the mesh has no material, a default material
|
|
168
|
+
will be used.
|
|
169
|
+
is_visible : bool
|
|
170
|
+
If False, the mesh will not be rendered.
|
|
171
|
+
poses : (n,4,4) float
|
|
172
|
+
Array of 4x4 transformation matrices for instancing this object.
|
|
173
|
+
wireframe : bool
|
|
174
|
+
If `True`, the mesh will be rendered as a wireframe object
|
|
175
|
+
smooth : bool
|
|
176
|
+
If `True`, the mesh will be rendered with interpolated vertex
|
|
177
|
+
normals. Otherwise, the mesh edges will stay sharp.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
mesh : :class:`Mesh`
|
|
182
|
+
The created mesh.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
if isinstance(mesh, (list, tuple, set, np.ndarray)):
|
|
186
|
+
meshes = list(mesh)
|
|
187
|
+
elif isinstance(mesh, trimesh.Trimesh):
|
|
188
|
+
meshes = [mesh]
|
|
189
|
+
else:
|
|
190
|
+
raise TypeError('Expected a Trimesh or a list, got a {}'
|
|
191
|
+
.format(type(mesh)))
|
|
192
|
+
|
|
193
|
+
primitives = []
|
|
194
|
+
for m in meshes:
|
|
195
|
+
positions = None
|
|
196
|
+
normals = None
|
|
197
|
+
indices = None
|
|
198
|
+
|
|
199
|
+
# Compute positions, normals, and indices
|
|
200
|
+
if smooth:
|
|
201
|
+
positions = m.vertices.copy()
|
|
202
|
+
normals = m.vertex_normals.copy()
|
|
203
|
+
indices = m.faces.copy()
|
|
204
|
+
else:
|
|
205
|
+
positions = m.vertices[m.faces].reshape((3 * len(m.faces), 3))
|
|
206
|
+
normals = np.repeat(m.face_normals, 3, axis=0)
|
|
207
|
+
|
|
208
|
+
# Compute colors, texture coords, and material properties
|
|
209
|
+
color_0, texcoord_0, primitive_material = Mesh._get_trimesh_props(m, smooth=smooth, material=material)
|
|
210
|
+
|
|
211
|
+
# Override if material is given.
|
|
212
|
+
if material is not None:
|
|
213
|
+
#primitive_material = copy.copy(material)
|
|
214
|
+
primitive_material = copy.deepcopy(material) # TODO
|
|
215
|
+
|
|
216
|
+
if primitive_material is None:
|
|
217
|
+
# Replace material with default if needed
|
|
218
|
+
primitive_material = MetallicRoughnessMaterial(
|
|
219
|
+
alphaMode='BLEND',
|
|
220
|
+
baseColorFactor=[0.3, 0.3, 0.3, 1.0],
|
|
221
|
+
metallicFactor=0.2,
|
|
222
|
+
roughnessFactor=0.8
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
primitive_material.wireframe = wireframe
|
|
226
|
+
|
|
227
|
+
# Create the primitive
|
|
228
|
+
primitives.append(Primitive(
|
|
229
|
+
positions=positions,
|
|
230
|
+
normals=normals,
|
|
231
|
+
texcoord_0=texcoord_0,
|
|
232
|
+
color_0=color_0,
|
|
233
|
+
indices=indices,
|
|
234
|
+
material=primitive_material,
|
|
235
|
+
mode=GLTF.TRIANGLES,
|
|
236
|
+
poses=poses
|
|
237
|
+
))
|
|
238
|
+
|
|
239
|
+
return Mesh(primitives=primitives, is_visible=is_visible)
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _get_trimesh_props(mesh, smooth=False, material=None):
|
|
243
|
+
"""Gets the vertex colors, texture coordinates, and material properties
|
|
244
|
+
from a :class:`~trimesh.base.Trimesh`.
|
|
245
|
+
"""
|
|
246
|
+
colors = None
|
|
247
|
+
texcoords = None
|
|
248
|
+
|
|
249
|
+
# If the trimesh visual is undefined, return none for both
|
|
250
|
+
if not mesh.visual.defined:
|
|
251
|
+
return colors, texcoords, material
|
|
252
|
+
|
|
253
|
+
# Process vertex colors
|
|
254
|
+
if material is None:
|
|
255
|
+
if mesh.visual.kind == 'vertex':
|
|
256
|
+
vc = mesh.visual.vertex_colors.copy()
|
|
257
|
+
if smooth:
|
|
258
|
+
colors = vc
|
|
259
|
+
else:
|
|
260
|
+
colors = vc[mesh.faces].reshape(
|
|
261
|
+
(3 * len(mesh.faces), vc.shape[1])
|
|
262
|
+
)
|
|
263
|
+
material = MetallicRoughnessMaterial(
|
|
264
|
+
alphaMode='BLEND',
|
|
265
|
+
baseColorFactor=[1.0, 1.0, 1.0, 1.0],
|
|
266
|
+
metallicFactor=0.2,
|
|
267
|
+
roughnessFactor=0.8
|
|
268
|
+
)
|
|
269
|
+
# Process face colors
|
|
270
|
+
elif mesh.visual.kind == 'face':
|
|
271
|
+
if smooth:
|
|
272
|
+
raise ValueError('Cannot use face colors with a smooth mesh')
|
|
273
|
+
else:
|
|
274
|
+
colors = np.repeat(mesh.visual.face_colors, 3, axis=0)
|
|
275
|
+
|
|
276
|
+
material = MetallicRoughnessMaterial(
|
|
277
|
+
alphaMode='BLEND',
|
|
278
|
+
baseColorFactor=[1.0, 1.0, 1.0, 1.0],
|
|
279
|
+
metallicFactor=0.2,
|
|
280
|
+
roughnessFactor=0.8
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Process texture colors
|
|
284
|
+
if mesh.visual.kind == 'texture':
|
|
285
|
+
# Configure UV coordinates
|
|
286
|
+
if mesh.visual.uv is not None and len(mesh.visual.uv) != 0:
|
|
287
|
+
uv = mesh.visual.uv.copy()
|
|
288
|
+
if smooth:
|
|
289
|
+
texcoords = uv
|
|
290
|
+
else:
|
|
291
|
+
texcoords = uv[mesh.faces].reshape(
|
|
292
|
+
(3 * len(mesh.faces), uv.shape[1])
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if material is None:
|
|
296
|
+
# Configure mesh material
|
|
297
|
+
mat = mesh.visual.material
|
|
298
|
+
|
|
299
|
+
if isinstance(mat, trimesh.visual.texture.PBRMaterial):
|
|
300
|
+
material = MetallicRoughnessMaterial(
|
|
301
|
+
normalTexture=mat.normalTexture,
|
|
302
|
+
occlusionTexture=mat.occlusionTexture,
|
|
303
|
+
emissiveTexture=mat.emissiveTexture,
|
|
304
|
+
emissiveFactor=mat.emissiveFactor,
|
|
305
|
+
alphaMode='BLEND',
|
|
306
|
+
baseColorFactor=mat.baseColorFactor,
|
|
307
|
+
baseColorTexture=mat.baseColorTexture,
|
|
308
|
+
metallicFactor=mat.metallicFactor,
|
|
309
|
+
roughnessFactor=mat.roughnessFactor,
|
|
310
|
+
metallicRoughnessTexture=mat.metallicRoughnessTexture,
|
|
311
|
+
doubleSided=mat.doubleSided,
|
|
312
|
+
alphaCutoff=mat.alphaCutoff
|
|
313
|
+
)
|
|
314
|
+
elif isinstance(mat, trimesh.visual.texture.SimpleMaterial):
|
|
315
|
+
glossiness = mat.kwargs.get('Ns', 1.0)
|
|
316
|
+
if isinstance(glossiness, list):
|
|
317
|
+
glossiness = float(glossiness[0])
|
|
318
|
+
roughness = (2 / (glossiness + 2)) ** (1.0 / 4.0)
|
|
319
|
+
material = MetallicRoughnessMaterial(
|
|
320
|
+
alphaMode='BLEND',
|
|
321
|
+
roughnessFactor=roughness,
|
|
322
|
+
baseColorFactor=mat.diffuse,
|
|
323
|
+
baseColorTexture=mat.image,
|
|
324
|
+
)
|
|
325
|
+
elif isinstance(mat, MetallicRoughnessMaterial):
|
|
326
|
+
material = mat
|
|
327
|
+
|
|
328
|
+
return colors, texcoords, material
|
pyrender/node.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Nodes, conforming to the glTF 2.0 standards as specified in
|
|
2
|
+
https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-node
|
|
3
|
+
|
|
4
|
+
Author: Matthew Matl
|
|
5
|
+
"""
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
import trimesh.transformations as transformations
|
|
9
|
+
|
|
10
|
+
from .camera import Camera
|
|
11
|
+
from .mesh import Mesh
|
|
12
|
+
from .light import Light
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Node(object):
|
|
16
|
+
"""A node in the node hierarchy.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
name : str, optional
|
|
21
|
+
The user-defined name of this object.
|
|
22
|
+
camera : :class:`Camera`, optional
|
|
23
|
+
The camera in this node.
|
|
24
|
+
children : list of :class:`Node`
|
|
25
|
+
The children of this node.
|
|
26
|
+
skin : int, optional
|
|
27
|
+
The index of the skin referenced by this node.
|
|
28
|
+
matrix : (4,4) float, optional
|
|
29
|
+
A floating-point 4x4 transformation matrix.
|
|
30
|
+
mesh : :class:`Mesh`, optional
|
|
31
|
+
The mesh in this node.
|
|
32
|
+
rotation : (4,) float, optional
|
|
33
|
+
The node's unit quaternion in the order (x, y, z, w), where
|
|
34
|
+
w is the scalar.
|
|
35
|
+
scale : (3,) float, optional
|
|
36
|
+
The node's non-uniform scale, given as the scaling factors along the x,
|
|
37
|
+
y, and z axes.
|
|
38
|
+
translation : (3,) float, optional
|
|
39
|
+
The node's translation along the x, y, and z axes.
|
|
40
|
+
weights : (n,) float
|
|
41
|
+
The weights of the instantiated Morph Target. Number of elements must
|
|
42
|
+
match number of Morph Targets of used mesh.
|
|
43
|
+
light : :class:`Light`, optional
|
|
44
|
+
The light in this node.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self,
|
|
48
|
+
name=None,
|
|
49
|
+
camera=None,
|
|
50
|
+
children=None,
|
|
51
|
+
skin=None,
|
|
52
|
+
matrix=None,
|
|
53
|
+
mesh=None,
|
|
54
|
+
rotation=None,
|
|
55
|
+
scale=None,
|
|
56
|
+
translation=None,
|
|
57
|
+
weights=None,
|
|
58
|
+
light=None):
|
|
59
|
+
# Set defaults
|
|
60
|
+
if children is None:
|
|
61
|
+
children = []
|
|
62
|
+
|
|
63
|
+
self._matrix = None
|
|
64
|
+
self._scale = None
|
|
65
|
+
self._rotation = None
|
|
66
|
+
self._translation = None
|
|
67
|
+
if matrix is None:
|
|
68
|
+
if rotation is None:
|
|
69
|
+
rotation = np.array([0.0, 0.0, 0.0, 1.0])
|
|
70
|
+
if translation is None:
|
|
71
|
+
translation = np.zeros(3)
|
|
72
|
+
if scale is None:
|
|
73
|
+
scale = np.ones(3)
|
|
74
|
+
self.rotation = rotation
|
|
75
|
+
self.translation = translation
|
|
76
|
+
self.scale = scale
|
|
77
|
+
else:
|
|
78
|
+
self.matrix = matrix
|
|
79
|
+
|
|
80
|
+
self.name = name
|
|
81
|
+
self.camera = camera
|
|
82
|
+
self.children = children
|
|
83
|
+
self.skin = skin
|
|
84
|
+
self.mesh = mesh
|
|
85
|
+
self.weights = weights
|
|
86
|
+
self.light = light
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def name(self):
|
|
90
|
+
"""str : The user-defined name of this object.
|
|
91
|
+
"""
|
|
92
|
+
return self._name
|
|
93
|
+
|
|
94
|
+
@name.setter
|
|
95
|
+
def name(self, value):
|
|
96
|
+
if value is not None:
|
|
97
|
+
value = str(value)
|
|
98
|
+
self._name = value
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def camera(self):
|
|
102
|
+
""":class:`Camera` : The camera in this node.
|
|
103
|
+
"""
|
|
104
|
+
return self._camera
|
|
105
|
+
|
|
106
|
+
@camera.setter
|
|
107
|
+
def camera(self, value):
|
|
108
|
+
if value is not None and not isinstance(value, Camera):
|
|
109
|
+
raise TypeError('Value must be a camera')
|
|
110
|
+
self._camera = value
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def children(self):
|
|
114
|
+
"""list of :class:`Node` : The children of this node.
|
|
115
|
+
"""
|
|
116
|
+
return self._children
|
|
117
|
+
|
|
118
|
+
@children.setter
|
|
119
|
+
def children(self, value):
|
|
120
|
+
self._children = value
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def skin(self):
|
|
124
|
+
"""int : The skin index for this node.
|
|
125
|
+
"""
|
|
126
|
+
return self._skin
|
|
127
|
+
|
|
128
|
+
@skin.setter
|
|
129
|
+
def skin(self, value):
|
|
130
|
+
self._skin = value
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def mesh(self):
|
|
134
|
+
""":class:`Mesh` : The mesh in this node.
|
|
135
|
+
"""
|
|
136
|
+
return self._mesh
|
|
137
|
+
|
|
138
|
+
@mesh.setter
|
|
139
|
+
def mesh(self, value):
|
|
140
|
+
if value is not None and not isinstance(value, Mesh):
|
|
141
|
+
raise TypeError('Value must be a mesh')
|
|
142
|
+
self._mesh = value
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def light(self):
|
|
146
|
+
""":class:`Light` : The light in this node.
|
|
147
|
+
"""
|
|
148
|
+
return self._light
|
|
149
|
+
|
|
150
|
+
@light.setter
|
|
151
|
+
def light(self, value):
|
|
152
|
+
if value is not None and not isinstance(value, Light):
|
|
153
|
+
raise TypeError('Value must be a light')
|
|
154
|
+
self._light = value
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def rotation(self):
|
|
158
|
+
"""(4,) float : The xyzw quaternion for this node.
|
|
159
|
+
"""
|
|
160
|
+
return self._rotation
|
|
161
|
+
|
|
162
|
+
@rotation.setter
|
|
163
|
+
def rotation(self, value):
|
|
164
|
+
value = np.asanyarray(value)
|
|
165
|
+
if value.shape != (4,):
|
|
166
|
+
raise ValueError('Quaternion must be a (4,) vector')
|
|
167
|
+
if np.abs(np.linalg.norm(value) - 1.0) > 1e-3:
|
|
168
|
+
raise ValueError('Quaternion must have norm == 1.0')
|
|
169
|
+
self._rotation = value
|
|
170
|
+
self._matrix = None
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def translation(self):
|
|
174
|
+
"""(3,) float : The translation for this node.
|
|
175
|
+
"""
|
|
176
|
+
return self._translation
|
|
177
|
+
|
|
178
|
+
@translation.setter
|
|
179
|
+
def translation(self, value):
|
|
180
|
+
value = np.asanyarray(value)
|
|
181
|
+
if value.shape != (3,):
|
|
182
|
+
raise ValueError('Translation must be a (3,) vector')
|
|
183
|
+
self._translation = value
|
|
184
|
+
self._matrix = None
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def scale(self):
|
|
188
|
+
"""(3,) float : The scale for this node.
|
|
189
|
+
"""
|
|
190
|
+
return self._scale
|
|
191
|
+
|
|
192
|
+
@scale.setter
|
|
193
|
+
def scale(self, value):
|
|
194
|
+
value = np.asanyarray(value)
|
|
195
|
+
if value.shape != (3,):
|
|
196
|
+
raise ValueError('Scale must be a (3,) vector')
|
|
197
|
+
self._scale = value
|
|
198
|
+
self._matrix = None
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def matrix(self):
|
|
202
|
+
"""(4,4) float : The homogenous transform matrix for this node.
|
|
203
|
+
|
|
204
|
+
Note that this matrix's elements are not settable,
|
|
205
|
+
it's just a copy of the internal matrix. You can set the whole
|
|
206
|
+
matrix, but not an individual element.
|
|
207
|
+
"""
|
|
208
|
+
if self._matrix is None:
|
|
209
|
+
self._matrix = self._m_from_tqs(
|
|
210
|
+
self.translation, self.rotation, self.scale
|
|
211
|
+
)
|
|
212
|
+
return self._matrix.copy()
|
|
213
|
+
|
|
214
|
+
@matrix.setter
|
|
215
|
+
def matrix(self, value):
|
|
216
|
+
value = np.asanyarray(value)
|
|
217
|
+
if value.shape != (4,4):
|
|
218
|
+
raise ValueError('Matrix must be a 4x4 numpy ndarray')
|
|
219
|
+
if not np.allclose(value[3,:], np.array([0.0, 0.0, 0.0, 1.0])):
|
|
220
|
+
raise ValueError('Bottom row of matrix must be [0,0,0,1]')
|
|
221
|
+
self.rotation = Node._q_from_m(value)
|
|
222
|
+
self.scale = Node._s_from_m(value)
|
|
223
|
+
self.translation = Node._t_from_m(value)
|
|
224
|
+
self._matrix = value
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _t_from_m(m):
|
|
228
|
+
return m[:3,3]
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _r_from_m(m):
|
|
232
|
+
U = m[:3,:3]
|
|
233
|
+
norms = np.linalg.norm(U.T, axis=1)
|
|
234
|
+
return U / norms
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _q_from_m(m):
|
|
238
|
+
M = np.eye(4)
|
|
239
|
+
M[:3,:3] = Node._r_from_m(m)
|
|
240
|
+
q_wxyz = transformations.quaternion_from_matrix(M)
|
|
241
|
+
return np.roll(q_wxyz, -1)
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _s_from_m(m):
|
|
245
|
+
return np.linalg.norm(m[:3,:3].T, axis=1)
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def _r_from_q(q):
|
|
249
|
+
q_wxyz = np.roll(q, 1)
|
|
250
|
+
return transformations.quaternion_matrix(q_wxyz)[:3,:3]
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _m_from_tqs(t, q, s):
|
|
254
|
+
S = np.eye(4)
|
|
255
|
+
S[:3,:3] = np.diag(s)
|
|
256
|
+
|
|
257
|
+
R = np.eye(4)
|
|
258
|
+
R[:3,:3] = Node._r_from_q(q)
|
|
259
|
+
|
|
260
|
+
T = np.eye(4)
|
|
261
|
+
T[:3,3] = t
|
|
262
|
+
|
|
263
|
+
return T.dot(R.dot(S))
|