Python bindings for the Open Asset Import Library (ASSIMP) enabling 3D model loading, processing, and export
—
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.
Convert ASSIMP C structures to Python tuples or NumPy arrays for mathematical operations.
def make_tuple(ai_obj, type=None):
"""
Convert ASSIMP objects to Python tuples/numpy arrays.
Automatically detects object type and converts appropriately:
- Matrix4x4 → 4x4 array/nested list
- Matrix3x3 → 3x3 array/nested list
- Vector types → 1D array/list
- Other structures → list of field values
Parameters:
- ai_obj: ASSIMP structure object (Matrix4x4, Matrix3x3, Vector3D, etc.)
- type: Optional type hint (usually auto-detected)
Returns:
- numpy.ndarray if numpy is available, otherwise nested Python lists
- For matrices: 2D array with proper dimensions
- For vectors: 1D array with component values
"""Usage examples:
import pyassimp
import numpy as np
scene = pyassimp.load("model.dae")
# Convert node transformation matrices
node = scene.rootnode
if hasattr(node, 'transformation'):
# Transformation is already converted to array/list by PyAssimp
transform = node.transformation
if isinstance(transform, np.ndarray):
print(f"Transform matrix shape: {transform.shape}")
print(f"Matrix type: numpy array")
# Extract components
translation = transform[:3, 3]
rotation_part = transform[:3, :3]
print(f"Translation: {translation}")
print(f"Rotation matrix:\n{rotation_part}")
else:
print(f"Transform matrix: {len(transform)}x{len(transform[0]) if transform else 0}")
print(f"Matrix type: nested list")
# Manual conversion if needed
for mesh in scene.meshes:
if hasattr(mesh, 'vertices') and mesh.vertices:
# Vertices are already converted
vertices = mesh.vertices
print(f"Vertices shape: {vertices.shape if hasattr(vertices, 'shape') else len(vertices)}")
pyassimp.release(scene)Decompose 4x4 transformation matrices into translation, rotation, and scaling components.
def decompose_matrix(matrix):
"""
Decompose 4x4 transformation matrix into components.
Uses ASSIMP's aiDecomposeMatrix function to accurately decompose
transformation matrices into their constituent parts.
Parameters:
- matrix: Matrix4x4 object (ASSIMP structure, not numpy array)
Returns:
Tuple of (scaling, rotation, position):
- scaling: Vector3D with scale factors for X, Y, Z axes
- rotation: Quaternion representing rotation
- position: Vector3D with translation values
Raises:
AssimpError: If matrix is not a Matrix4x4 structure
"""Usage examples:
import pyassimp
from pyassimp.structs import Matrix4x4
scene = pyassimp.load("animated_model.dae")
def analyze_transformation(node):
"""Analyze transformation matrix of a scene node."""
print(f"Node: {node.name}")
# PyAssimp automatically converts transformations to arrays/lists
# For decomposition, we need the original Matrix4x4 structure
# This is typically available through lower-level access
if hasattr(node, 'transformation'):
transform = node.transformation
# If we have a numpy array, we can manually extract components
if hasattr(transform, 'shape') and transform.shape == (4, 4):
# Extract translation (last column, first 3 rows)
translation = transform[:3, 3]
# Extract rotation/scale matrix (upper-left 3x3)
rotation_scale = transform[:3, :3]
# Extract scale (length of each column vector)
scale_x = np.linalg.norm(rotation_scale[:, 0])
scale_y = np.linalg.norm(rotation_scale[:, 1])
scale_z = np.linalg.norm(rotation_scale[:, 2])
print(f" Translation: ({translation[0]:.3f}, {translation[1]:.3f}, {translation[2]:.3f})")
print(f" Scale: ({scale_x:.3f}, {scale_y:.3f}, {scale_z:.3f})")
# Extract rotation matrix (normalized)
rotation_matrix = rotation_scale / [scale_x, scale_y, scale_z]
print(f" Rotation matrix:\n{rotation_matrix}")
# Recursively process children
for child in node.children:
analyze_transformation(child)
analyze_transformation(scene.rootnode)
pyassimp.release(scene)Core mathematical data types used throughout PyAssimp.
class Vector2D:
"""
2D vector with X and Y components.
Attributes:
- x: float, X coordinate
- y: float, Y coordinate
"""
x: float
y: float
class Vector3D:
"""
3D vector with X, Y, and Z components.
Attributes:
- x: float, X coordinate
- y: float, Y coordinate
- z: float, Z coordinate
"""
x: float
y: float
z: float
class Matrix3x3:
"""
3x3 transformation matrix.
Matrix elements stored in row-major order:
[a1 a2 a3]
[b1 b2 b3]
[c1 c2 c3]
Attributes:
- a1, a2, a3: float, first row elements
- b1, b2, b3: float, second row elements
- c1, c2, c3: float, third row elements
"""
a1: float; a2: float; a3: float
b1: float; b2: float; b3: float
c1: float; c2: float; c3: float
class Matrix4x4:
"""
4x4 transformation matrix for 3D transformations.
Stores translation, rotation, and scaling transformations
in homogeneous coordinates.
"""
# Matrix elements (implementation details in C structure)
class Quaternion:
"""
Quaternion for representing rotations.
Attributes:
- w: float, scalar component
- x: float, X component of vector part
- y: float, Y component of vector part
- z: float, Z component of vector part
"""
w: float
x: float
y: float
z: float
class Color4D:
"""
RGBA color representation.
Attributes:
- r: float, red component (0.0-1.0)
- g: float, green component (0.0-1.0)
- b: float, blue component (0.0-1.0)
- a: float, alpha component (0.0-1.0)
"""
r: float
g: float
b: float
a: floatUsage examples:
import pyassimp
import math
def vector_operations_example():
"""Example of working with vector data."""
scene = pyassimp.load("model.obj")
for mesh in scene.meshes:
if mesh.vertices:
vertices = mesh.vertices
# Calculate mesh bounds
if hasattr(vertices, 'shape'): # NumPy array
min_bounds = np.min(vertices, axis=0)
max_bounds = np.max(vertices, axis=0)
center = (min_bounds + max_bounds) / 2
size = max_bounds - min_bounds
print(f"Mesh bounds: {min_bounds} to {max_bounds}")
print(f"Mesh center: {center}")
print(f"Mesh size: {size}")
else: # Python list
if vertices:
# Manual calculation for list format
min_x = min(v[0] for v in vertices)
max_x = max(v[0] for v in vertices)
min_y = min(v[1] for v in vertices)
max_y = max(v[1] for v in vertices)
min_z = min(v[2] for v in vertices)
max_z = max(v[2] for v in vertices)
center = [(min_x + max_x)/2, (min_y + max_y)/2, (min_z + max_z)/2]
size = [max_x - min_x, max_y - min_y, max_z - min_z]
print(f"Mesh center: {center}")
print(f"Mesh size: {size}")
pyassimp.release(scene)
vector_operations_example()import pyassimp
import numpy as np
def compute_world_transform(node, parent_transform=None):
"""Compute world transformation for a node."""
if parent_transform is None:
parent_transform = np.eye(4) # Identity matrix
# Get node's local transformation
local_transform = node.transformation
# Ensure it's a numpy array
if not isinstance(local_transform, np.ndarray):
local_transform = np.array(local_transform)
# Compute world transformation
world_transform = parent_transform @ local_transform
return world_transform
def traverse_with_transforms(node, parent_transform=None):
"""Traverse scene graph computing world transforms."""
world_transform = compute_world_transform(node, parent_transform)
print(f"Node '{node.name}' world transform:")
print(world_transform)
# Process node's meshes with world transform
for mesh_ref in node.meshes:
mesh = mesh_ref if hasattr(mesh_ref, 'vertices') else scene.meshes[mesh_ref]
print(f" Mesh: {len(mesh.vertices) if mesh.vertices else 0} vertices")
# Recursively process children
for child in node.children:
traverse_with_transforms(child, world_transform)
# Usage
scene = pyassimp.load("hierarchical_model.dae")
traverse_with_transforms(scene.rootnode)
pyassimp.release(scene)import pyassimp
import numpy as np
def transform_vertices(vertices, transformation_matrix):
"""Transform vertex positions by a 4x4 matrix."""
if not isinstance(vertices, np.ndarray):
vertices = np.array(vertices)
# Add homogeneous coordinate (w=1) for position vectors
ones = np.ones((vertices.shape[0], 1))
homogeneous_vertices = np.hstack([vertices, ones])
# Apply transformation
transformed = (transformation_matrix @ homogeneous_vertices.T).T
# Return 3D coordinates (drop homogeneous coordinate)
return transformed[:, :3]
def transform_normals(normals, transformation_matrix):
"""Transform normal vectors (use inverse transpose for correct transformation)."""
if not isinstance(normals, np.ndarray):
normals = np.array(normals)
# Use inverse transpose of upper-left 3x3 for normals
rotation_part = transformation_matrix[:3, :3]
normal_transform = np.linalg.inv(rotation_part).T
# Transform normals
transformed_normals = (normal_transform @ normals.T).T
# Normalize
norms = np.linalg.norm(transformed_normals, axis=1, keepdims=True)
return transformed_normals / norms
# Usage example
scene = pyassimp.load("model.obj")
# Create a transformation (e.g., rotate 45 degrees around Y axis)
angle = np.radians(45)
rotation_y = np.array([
[np.cos(angle), 0, np.sin(angle), 0],
[0, 1, 0, 0],
[-np.sin(angle), 0, np.cos(angle), 0],
[0, 0, 0, 1]
])
for mesh in scene.meshes:
if mesh.vertices:
# Transform vertices
transformed_vertices = transform_vertices(mesh.vertices, rotation_y)
print(f"Transformed {len(transformed_vertices)} vertices")
# Transform normals if available
if mesh.normals:
transformed_normals = transform_normals(mesh.normals, rotation_y)
print(f"Transformed {len(transformed_normals)} normals")
pyassimp.release(scene)import pyassimp
import numpy as np
def calculate_mesh_properties(mesh):
"""Calculate various geometric properties of a mesh."""
if not mesh.vertices:
return {}
vertices = mesh.vertices
if not isinstance(vertices, np.ndarray):
vertices = np.array(vertices)
properties = {}
# Bounding box
properties['bbox_min'] = np.min(vertices, axis=0)
properties['bbox_max'] = np.max(vertices, axis=0)
properties['bbox_center'] = (properties['bbox_min'] + properties['bbox_max']) / 2
properties['bbox_size'] = properties['bbox_max'] - properties['bbox_min']
# Centroid
properties['centroid'] = np.mean(vertices, axis=0)
# Bounding sphere (approximate)
center = properties['centroid']
distances = np.linalg.norm(vertices - center, axis=1)
properties['bounding_sphere_radius'] = np.max(distances)
# Surface area (approximate, assuming triangulated mesh)
if mesh.faces:
total_area = 0
faces = mesh.faces
for face in faces:
indices = face.indices if hasattr(face, 'indices') else face
if len(indices) >= 3:
# Triangle area using cross product
v0, v1, v2 = vertices[indices[0]], vertices[indices[1]], vertices[indices[2]]
edge1 = v1 - v0
edge2 = v2 - v0
area = 0.5 * np.linalg.norm(np.cross(edge1, edge2))
total_area += area
properties['surface_area'] = total_area
# Volume (for closed meshes, using divergence theorem)
if mesh.faces:
volume = 0
faces = mesh.faces
for face in faces:
indices = face.indices if hasattr(face, 'indices') else face
if len(indices) >= 3:
v0, v1, v2 = vertices[indices[0]], vertices[indices[1]], vertices[indices[2]]
# Contribution to volume
volume += np.dot(v0, np.cross(v1, v2)) / 6.0
properties['volume'] = abs(volume)
return properties
# Usage
scene = pyassimp.load("model.stl")
for i, mesh in enumerate(scene.meshes):
props = calculate_mesh_properties(mesh)
print(f"Mesh {i} properties:")
for key, value in props.items():
if isinstance(value, np.ndarray):
print(f" {key}: {value}")
else:
print(f" {key}: {value:.6f}")
pyassimp.release(scene)import pyassimp
import numpy as np
# Pattern 1: Mesh analysis pipeline
def analyze_mesh_geometry(filename):
scene = pyassimp.load(filename)
for mesh in scene.meshes:
if mesh.vertices:
vertices = np.array(mesh.vertices) if not isinstance(mesh.vertices, np.ndarray) else mesh.vertices
# Quick statistics
stats = {
'vertex_count': len(vertices),
'bounds': (np.min(vertices, axis=0), np.max(vertices, axis=0)),
'centroid': np.mean(vertices, axis=0)
}
yield stats
pyassimp.release(scene)
# Pattern 2: Transformation application
def apply_transform_to_scene(scene, transform_matrix):
"""Apply transformation to all meshes in scene."""
for mesh in scene.meshes:
if mesh.vertices:
mesh.vertices = transform_vertices(mesh.vertices, transform_matrix)
if mesh.normals:
mesh.normals = transform_normals(mesh.normals, transform_matrix)
# Pattern 3: Data format conversion
def convert_to_numpy(mesh):
"""Ensure mesh data is in NumPy format."""
if mesh.vertices and not isinstance(mesh.vertices, np.ndarray):
mesh.vertices = np.array(mesh.vertices, dtype=np.float32)
if mesh.normals and not isinstance(mesh.normals, np.ndarray):
mesh.normals = np.array(mesh.normals, dtype=np.float32)
if mesh.faces:
# Convert faces to consistent format
face_indices = []
for face in mesh.faces:
indices = face.indices if hasattr(face, 'indices') else face
face_indices.append(indices)
mesh.faces = face_indicesInstall with Tessl CLI
npx tessl i tessl/pypi-pyassimp