CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-pyassimp

Python bindings for the Open Asset Import Library (ASSIMP) enabling 3D model loading, processing, and export

Pending
Overview
Eval results
Files

math-utilities.mddocs/

Matrix and Math Utilities

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.

Capabilities

Data Type Conversion

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)

Matrix Decomposition

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)

Vector and Matrix Types

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: float

Usage 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()

Advanced Mathematical Operations

Transformation Chains

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)

Vertex Transformation

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)

Geometric Calculations

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)

Performance Considerations

  1. NumPy Integration: Install NumPy for significantly better performance with mathematical operations
  2. Memory Usage: Mathematical operations create temporary arrays; be mindful of memory usage with large meshes
  3. In-place Operations: Use in-place NumPy operations when possible to reduce memory allocation
  4. Matrix Storage: ASSIMP uses row-major order; NumPy defaults to row-major which is compatible
  5. Precision: ASSIMP uses single-precision floats; consider precision requirements for your calculations

Common Mathematical Patterns

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_indices

Install with Tessl CLI

npx tessl i tessl/pypi-pyassimp

docs

index.md

materials.md

math-utilities.md

post-processing.md

scene-data.md

scene-loading.md

tile.json