Source code for simvx.core.nodes_3d.mesh

"""MeshInstance3D -- visible 3D mesh node."""

import numpy as np

from ..descriptors import Property
from ..math.matrices import mat4_from_trs
from .node3d import Node3D


[docs] class MeshInstance3D(Node3D): """Visible 3D object. Holds a Mesh and Material for the renderer. Set ``skin`` to a :class:`~simvx.core.skeleton.Skeleton` node to enable skeletal animation. The renderer reads ``skin.joint_matrices`` each frame to upload bone transforms for vertex skinning. The ``pivot`` Property controls where the mesh's local origin sits relative to its bounding box. ``"center"`` (default) treats the mesh-local origin as the geometric centre: convenient for free- floating objects. ``"bottom"`` lifts the mesh by half its height along local +Y, so foot-aligned entities (characters, props, obstacles) place naturally on a ground plane at ``local_position.y = 0``. The shift is applied at draw time via the model matrix; the mesh data itself is untouched. Usage: from simvx.core.graphics.mesh import Mesh from simvx.core.graphics.material import Material mi = MeshInstance3D(mesh=Mesh.cube(), material=Material(colour=(1, 0, 0))) # Foot-aligned cube: position.y = 0 puts the bottom on the floor: mi = MeshInstance3D(mesh=Mesh.cube(), pivot="bottom") # Skeletal mesh: mi.skin = skeleton_node """ lod_bias = Property(0.0, range=(-10.0, 10.0), hint="LOD distance bias (positive = prefer coarser)") pivot = Property( "center", enum=["center", "bottom"], hint="Pivot point relative to the mesh bounding box (\"center\" or \"bottom\").", ) def __init__(self, mesh=None, material=None, skin=None, **kwargs): super().__init__(**kwargs) self.mesh = mesh self.material = material # defaults to white in renderer if None self._skin = None if skin is not None: self.skin = skin @property def skin(self): """Skeleton node providing joint matrices for vertex skinning. Accepts a :class:`~simvx.core.skeleton.Skeleton` instance (or ``None`` to disable skinning). Assigning a skeleton does *not* reparent it; the skeleton should already be part of the scene tree. """ return self._skin
[docs] @skin.setter def skin(self, value): from ..skeleton import Skeleton if value is not None and not isinstance(value, Skeleton): raise TypeError(f"skin must be a Skeleton node or None, got {type(value).__name__}") self._skin = value
[docs] @property def model_matrix(self) -> np.ndarray: """Model transform matrix from global position/rotation/scale. When ``pivot == "bottom"``, the mesh is pre-translated along its local +Y by half its bounding-box height so the bottom of the mesh sits at ``local_position.y``. The shift is applied in the node's local frame so it rotates / scales with the node. """ offset = self._pivot_offset() if offset is None: return mat4_from_trs(self.world_position, self.world_rotation, self.world_scale) # Apply pivot offset in local space: post-multiply by a translation # that shifts the mesh before the node's TRS is applied. trs = mat4_from_trs(self.world_position, self.world_rotation, self.world_scale) offset_mat = np.eye(4, dtype=np.float32) offset_mat[0, 3] = offset[0] offset_mat[1, 3] = offset[1] offset_mat[2, 3] = offset[2] return (trs @ offset_mat).astype(np.float32)
def _pivot_offset(self) -> np.ndarray | None: """Return the local-space shift to apply for the current pivot mode.""" if self.pivot == "center" or self.mesh is None: return None if self.pivot == "bottom": try: lo, _ = self.mesh.bounding_box() except (AttributeError, ValueError): return None # Shift the mesh up so its lowest Y vertex lands at the node's # local origin. ``-lo[1]`` works for any mesh: centred or not. return np.array([0.0, -float(lo[1]), 0.0], dtype=np.float32) return None