0
# Matrix and Math Utilities
1
2
Mathematical operations for 3D transformations, matrix decomposition, and data type conversions between ASSIMP and Python formats. PyAssimp provides utilities for working with transformation matrices, vectors, and converting C data structures to Python-friendly formats.
3
4
## Capabilities
5
6
### Data Type Conversion
7
8
Convert ASSIMP C structures to Python tuples or NumPy arrays for mathematical operations.
9
10
```python { .api }
11
def make_tuple(ai_obj, type=None):
12
"""
13
Convert ASSIMP objects to Python tuples/numpy arrays.
14
15
Automatically detects object type and converts appropriately:
16
- Matrix4x4 → 4x4 array/nested list
17
- Matrix3x3 → 3x3 array/nested list
18
- Vector types → 1D array/list
19
- Other structures → list of field values
20
21
Parameters:
22
- ai_obj: ASSIMP structure object (Matrix4x4, Matrix3x3, Vector3D, etc.)
23
- type: Optional type hint (usually auto-detected)
24
25
Returns:
26
- numpy.ndarray if numpy is available, otherwise nested Python lists
27
- For matrices: 2D array with proper dimensions
28
- For vectors: 1D array with component values
29
"""
30
```
31
32
Usage examples:
33
34
```python
35
import pyassimp
36
import numpy as np
37
38
scene = pyassimp.load("model.dae")
39
40
# Convert node transformation matrices
41
node = scene.rootnode
42
if hasattr(node, 'transformation'):
43
# Transformation is already converted to array/list by PyAssimp
44
transform = node.transformation
45
46
if isinstance(transform, np.ndarray):
47
print(f"Transform matrix shape: {transform.shape}")
48
print(f"Matrix type: numpy array")
49
50
# Extract components
51
translation = transform[:3, 3]
52
rotation_part = transform[:3, :3]
53
54
print(f"Translation: {translation}")
55
print(f"Rotation matrix:\n{rotation_part}")
56
else:
57
print(f"Transform matrix: {len(transform)}x{len(transform[0]) if transform else 0}")
58
print(f"Matrix type: nested list")
59
60
# Manual conversion if needed
61
for mesh in scene.meshes:
62
if hasattr(mesh, 'vertices') and mesh.vertices:
63
# Vertices are already converted
64
vertices = mesh.vertices
65
print(f"Vertices shape: {vertices.shape if hasattr(vertices, 'shape') else len(vertices)}")
66
67
pyassimp.release(scene)
68
```
69
70
### Matrix Decomposition
71
72
Decompose 4x4 transformation matrices into translation, rotation, and scaling components.
73
74
```python { .api }
75
def decompose_matrix(matrix):
76
"""
77
Decompose 4x4 transformation matrix into components.
78
79
Uses ASSIMP's aiDecomposeMatrix function to accurately decompose
80
transformation matrices into their constituent parts.
81
82
Parameters:
83
- matrix: Matrix4x4 object (ASSIMP structure, not numpy array)
84
85
Returns:
86
Tuple of (scaling, rotation, position):
87
- scaling: Vector3D with scale factors for X, Y, Z axes
88
- rotation: Quaternion representing rotation
89
- position: Vector3D with translation values
90
91
Raises:
92
AssimpError: If matrix is not a Matrix4x4 structure
93
"""
94
```
95
96
Usage examples:
97
98
```python
99
import pyassimp
100
from pyassimp.structs import Matrix4x4
101
102
scene = pyassimp.load("animated_model.dae")
103
104
def analyze_transformation(node):
105
"""Analyze transformation matrix of a scene node."""
106
107
print(f"Node: {node.name}")
108
109
# PyAssimp automatically converts transformations to arrays/lists
110
# For decomposition, we need the original Matrix4x4 structure
111
# This is typically available through lower-level access
112
113
if hasattr(node, 'transformation'):
114
transform = node.transformation
115
116
# If we have a numpy array, we can manually extract components
117
if hasattr(transform, 'shape') and transform.shape == (4, 4):
118
# Extract translation (last column, first 3 rows)
119
translation = transform[:3, 3]
120
121
# Extract rotation/scale matrix (upper-left 3x3)
122
rotation_scale = transform[:3, :3]
123
124
# Extract scale (length of each column vector)
125
scale_x = np.linalg.norm(rotation_scale[:, 0])
126
scale_y = np.linalg.norm(rotation_scale[:, 1])
127
scale_z = np.linalg.norm(rotation_scale[:, 2])
128
129
print(f" Translation: ({translation[0]:.3f}, {translation[1]:.3f}, {translation[2]:.3f})")
130
print(f" Scale: ({scale_x:.3f}, {scale_y:.3f}, {scale_z:.3f})")
131
132
# Extract rotation matrix (normalized)
133
rotation_matrix = rotation_scale / [scale_x, scale_y, scale_z]
134
print(f" Rotation matrix:\n{rotation_matrix}")
135
136
# Recursively process children
137
for child in node.children:
138
analyze_transformation(child)
139
140
analyze_transformation(scene.rootnode)
141
pyassimp.release(scene)
142
```
143
144
### Vector and Matrix Types
145
146
Core mathematical data types used throughout PyAssimp.
147
148
```python { .api }
149
class Vector2D:
150
"""
151
2D vector with X and Y components.
152
153
Attributes:
154
- x: float, X coordinate
155
- y: float, Y coordinate
156
"""
157
x: float
158
y: float
159
160
class Vector3D:
161
"""
162
3D vector with X, Y, and Z components.
163
164
Attributes:
165
- x: float, X coordinate
166
- y: float, Y coordinate
167
- z: float, Z coordinate
168
"""
169
x: float
170
y: float
171
z: float
172
173
class Matrix3x3:
174
"""
175
3x3 transformation matrix.
176
177
Matrix elements stored in row-major order:
178
[a1 a2 a3]
179
[b1 b2 b3]
180
[c1 c2 c3]
181
182
Attributes:
183
- a1, a2, a3: float, first row elements
184
- b1, b2, b3: float, second row elements
185
- c1, c2, c3: float, third row elements
186
"""
187
a1: float; a2: float; a3: float
188
b1: float; b2: float; b3: float
189
c1: float; c2: float; c3: float
190
191
class Matrix4x4:
192
"""
193
4x4 transformation matrix for 3D transformations.
194
195
Stores translation, rotation, and scaling transformations
196
in homogeneous coordinates.
197
"""
198
# Matrix elements (implementation details in C structure)
199
200
class Quaternion:
201
"""
202
Quaternion for representing rotations.
203
204
Attributes:
205
- w: float, scalar component
206
- x: float, X component of vector part
207
- y: float, Y component of vector part
208
- z: float, Z component of vector part
209
"""
210
w: float
211
x: float
212
y: float
213
z: float
214
215
class Color4D:
216
"""
217
RGBA color representation.
218
219
Attributes:
220
- r: float, red component (0.0-1.0)
221
- g: float, green component (0.0-1.0)
222
- b: float, blue component (0.0-1.0)
223
- a: float, alpha component (0.0-1.0)
224
"""
225
r: float
226
g: float
227
b: float
228
a: float
229
```
230
231
Usage examples:
232
233
```python
234
import pyassimp
235
import math
236
237
def vector_operations_example():
238
"""Example of working with vector data."""
239
240
scene = pyassimp.load("model.obj")
241
242
for mesh in scene.meshes:
243
if mesh.vertices:
244
vertices = mesh.vertices
245
246
# Calculate mesh bounds
247
if hasattr(vertices, 'shape'): # NumPy array
248
min_bounds = np.min(vertices, axis=0)
249
max_bounds = np.max(vertices, axis=0)
250
center = (min_bounds + max_bounds) / 2
251
size = max_bounds - min_bounds
252
253
print(f"Mesh bounds: {min_bounds} to {max_bounds}")
254
print(f"Mesh center: {center}")
255
print(f"Mesh size: {size}")
256
257
else: # Python list
258
if vertices:
259
# Manual calculation for list format
260
min_x = min(v[0] for v in vertices)
261
max_x = max(v[0] for v in vertices)
262
min_y = min(v[1] for v in vertices)
263
max_y = max(v[1] for v in vertices)
264
min_z = min(v[2] for v in vertices)
265
max_z = max(v[2] for v in vertices)
266
267
center = [(min_x + max_x)/2, (min_y + max_y)/2, (min_z + max_z)/2]
268
size = [max_x - min_x, max_y - min_y, max_z - min_z]
269
270
print(f"Mesh center: {center}")
271
print(f"Mesh size: {size}")
272
273
pyassimp.release(scene)
274
275
vector_operations_example()
276
```
277
278
## Advanced Mathematical Operations
279
280
### Transformation Chains
281
282
```python
283
import pyassimp
284
import numpy as np
285
286
def compute_world_transform(node, parent_transform=None):
287
"""Compute world transformation for a node."""
288
289
if parent_transform is None:
290
parent_transform = np.eye(4) # Identity matrix
291
292
# Get node's local transformation
293
local_transform = node.transformation
294
295
# Ensure it's a numpy array
296
if not isinstance(local_transform, np.ndarray):
297
local_transform = np.array(local_transform)
298
299
# Compute world transformation
300
world_transform = parent_transform @ local_transform
301
302
return world_transform
303
304
def traverse_with_transforms(node, parent_transform=None):
305
"""Traverse scene graph computing world transforms."""
306
307
world_transform = compute_world_transform(node, parent_transform)
308
309
print(f"Node '{node.name}' world transform:")
310
print(world_transform)
311
312
# Process node's meshes with world transform
313
for mesh_ref in node.meshes:
314
mesh = mesh_ref if hasattr(mesh_ref, 'vertices') else scene.meshes[mesh_ref]
315
print(f" Mesh: {len(mesh.vertices) if mesh.vertices else 0} vertices")
316
317
# Recursively process children
318
for child in node.children:
319
traverse_with_transforms(child, world_transform)
320
321
# Usage
322
scene = pyassimp.load("hierarchical_model.dae")
323
traverse_with_transforms(scene.rootnode)
324
pyassimp.release(scene)
325
```
326
327
### Vertex Transformation
328
329
```python
330
import pyassimp
331
import numpy as np
332
333
def transform_vertices(vertices, transformation_matrix):
334
"""Transform vertex positions by a 4x4 matrix."""
335
336
if not isinstance(vertices, np.ndarray):
337
vertices = np.array(vertices)
338
339
# Add homogeneous coordinate (w=1) for position vectors
340
ones = np.ones((vertices.shape[0], 1))
341
homogeneous_vertices = np.hstack([vertices, ones])
342
343
# Apply transformation
344
transformed = (transformation_matrix @ homogeneous_vertices.T).T
345
346
# Return 3D coordinates (drop homogeneous coordinate)
347
return transformed[:, :3]
348
349
def transform_normals(normals, transformation_matrix):
350
"""Transform normal vectors (use inverse transpose for correct transformation)."""
351
352
if not isinstance(normals, np.ndarray):
353
normals = np.array(normals)
354
355
# Use inverse transpose of upper-left 3x3 for normals
356
rotation_part = transformation_matrix[:3, :3]
357
normal_transform = np.linalg.inv(rotation_part).T
358
359
# Transform normals
360
transformed_normals = (normal_transform @ normals.T).T
361
362
# Normalize
363
norms = np.linalg.norm(transformed_normals, axis=1, keepdims=True)
364
return transformed_normals / norms
365
366
# Usage example
367
scene = pyassimp.load("model.obj")
368
369
# Create a transformation (e.g., rotate 45 degrees around Y axis)
370
angle = np.radians(45)
371
rotation_y = np.array([
372
[np.cos(angle), 0, np.sin(angle), 0],
373
[0, 1, 0, 0],
374
[-np.sin(angle), 0, np.cos(angle), 0],
375
[0, 0, 0, 1]
376
])
377
378
for mesh in scene.meshes:
379
if mesh.vertices:
380
# Transform vertices
381
transformed_vertices = transform_vertices(mesh.vertices, rotation_y)
382
print(f"Transformed {len(transformed_vertices)} vertices")
383
384
# Transform normals if available
385
if mesh.normals:
386
transformed_normals = transform_normals(mesh.normals, rotation_y)
387
print(f"Transformed {len(transformed_normals)} normals")
388
389
pyassimp.release(scene)
390
```
391
392
### Geometric Calculations
393
394
```python
395
import pyassimp
396
import numpy as np
397
398
def calculate_mesh_properties(mesh):
399
"""Calculate various geometric properties of a mesh."""
400
401
if not mesh.vertices:
402
return {}
403
404
vertices = mesh.vertices
405
if not isinstance(vertices, np.ndarray):
406
vertices = np.array(vertices)
407
408
properties = {}
409
410
# Bounding box
411
properties['bbox_min'] = np.min(vertices, axis=0)
412
properties['bbox_max'] = np.max(vertices, axis=0)
413
properties['bbox_center'] = (properties['bbox_min'] + properties['bbox_max']) / 2
414
properties['bbox_size'] = properties['bbox_max'] - properties['bbox_min']
415
416
# Centroid
417
properties['centroid'] = np.mean(vertices, axis=0)
418
419
# Bounding sphere (approximate)
420
center = properties['centroid']
421
distances = np.linalg.norm(vertices - center, axis=1)
422
properties['bounding_sphere_radius'] = np.max(distances)
423
424
# Surface area (approximate, assuming triangulated mesh)
425
if mesh.faces:
426
total_area = 0
427
faces = mesh.faces
428
429
for face in faces:
430
indices = face.indices if hasattr(face, 'indices') else face
431
if len(indices) >= 3:
432
# Triangle area using cross product
433
v0, v1, v2 = vertices[indices[0]], vertices[indices[1]], vertices[indices[2]]
434
edge1 = v1 - v0
435
edge2 = v2 - v0
436
area = 0.5 * np.linalg.norm(np.cross(edge1, edge2))
437
total_area += area
438
439
properties['surface_area'] = total_area
440
441
# Volume (for closed meshes, using divergence theorem)
442
if mesh.faces:
443
volume = 0
444
faces = mesh.faces
445
446
for face in faces:
447
indices = face.indices if hasattr(face, 'indices') else face
448
if len(indices) >= 3:
449
v0, v1, v2 = vertices[indices[0]], vertices[indices[1]], vertices[indices[2]]
450
# Contribution to volume
451
volume += np.dot(v0, np.cross(v1, v2)) / 6.0
452
453
properties['volume'] = abs(volume)
454
455
return properties
456
457
# Usage
458
scene = pyassimp.load("model.stl")
459
460
for i, mesh in enumerate(scene.meshes):
461
props = calculate_mesh_properties(mesh)
462
463
print(f"Mesh {i} properties:")
464
for key, value in props.items():
465
if isinstance(value, np.ndarray):
466
print(f" {key}: {value}")
467
else:
468
print(f" {key}: {value:.6f}")
469
470
pyassimp.release(scene)
471
```
472
473
## Performance Considerations
474
475
1. **NumPy Integration**: Install NumPy for significantly better performance with mathematical operations
476
2. **Memory Usage**: Mathematical operations create temporary arrays; be mindful of memory usage with large meshes
477
3. **In-place Operations**: Use in-place NumPy operations when possible to reduce memory allocation
478
4. **Matrix Storage**: ASSIMP uses row-major order; NumPy defaults to row-major which is compatible
479
5. **Precision**: ASSIMP uses single-precision floats; consider precision requirements for your calculations
480
481
## Common Mathematical Patterns
482
483
```python
484
import pyassimp
485
import numpy as np
486
487
# Pattern 1: Mesh analysis pipeline
488
def analyze_mesh_geometry(filename):
489
scene = pyassimp.load(filename)
490
491
for mesh in scene.meshes:
492
if mesh.vertices:
493
vertices = np.array(mesh.vertices) if not isinstance(mesh.vertices, np.ndarray) else mesh.vertices
494
495
# Quick statistics
496
stats = {
497
'vertex_count': len(vertices),
498
'bounds': (np.min(vertices, axis=0), np.max(vertices, axis=0)),
499
'centroid': np.mean(vertices, axis=0)
500
}
501
502
yield stats
503
504
pyassimp.release(scene)
505
506
# Pattern 2: Transformation application
507
def apply_transform_to_scene(scene, transform_matrix):
508
"""Apply transformation to all meshes in scene."""
509
510
for mesh in scene.meshes:
511
if mesh.vertices:
512
mesh.vertices = transform_vertices(mesh.vertices, transform_matrix)
513
if mesh.normals:
514
mesh.normals = transform_normals(mesh.normals, transform_matrix)
515
516
# Pattern 3: Data format conversion
517
def convert_to_numpy(mesh):
518
"""Ensure mesh data is in NumPy format."""
519
520
if mesh.vertices and not isinstance(mesh.vertices, np.ndarray):
521
mesh.vertices = np.array(mesh.vertices, dtype=np.float32)
522
523
if mesh.normals and not isinstance(mesh.normals, np.ndarray):
524
mesh.normals = np.array(mesh.normals, dtype=np.float32)
525
526
if mesh.faces:
527
# Convert faces to consistent format
528
face_indices = []
529
for face in mesh.faces:
530
indices = face.indices if hasattr(face, 'indices') else face
531
face_indices.append(indices)
532
mesh.faces = face_indices
533
```