Source code for simvx.graphics.renderer.transparency

"""Transparency sorting and instance splitting for correct alpha blending.

Transparent objects must be rendered back-to-front after all opaque geometry,
with alpha blending enabled and depth writes disabled.
"""


from __future__ import annotations

import logging
from typing import Any

import numpy as np

from .._types import ALPHA_OPAQUE

__all__ = ["compute_sort_key", "split_instances", "sort_transparent"]

log = logging.getLogger(__name__)


[docs] def compute_sort_key(transform: np.ndarray, camera_pos: np.ndarray) -> float: """Return negative squared distance from camera for back-to-front sorting. Negative so that ``sorted()`` produces back-to-front order (farthest first). Uses squared distance to avoid the sqrt. Args: transform: 4x4 model matrix (row-major numpy). camera_pos: Camera world-space position as (3,) array. """ obj_pos = transform[:3, 3] if transform.shape == (4, 4) else transform[3, :3] diff = obj_pos - camera_pos return -float(diff @ diff)
[docs] def split_instances( instances: list[tuple[Any, np.ndarray, int, int]], materials: np.ndarray, ) -> tuple[ list[tuple[Any, np.ndarray, int, int, int]], list[tuple[Any, np.ndarray, int, int, int]], list[tuple[Any, np.ndarray, int, int, int]], ]: """Split instance list into opaque, double-sided opaque, and transparent. Each returned entry is ``(mesh_handle, transform, material_id, viewport_id, original_index)`` where ``original_index`` is the position in the original ``instances`` list (needed to reference the correct SSBO slot). Args: instances: List of (mesh_handle, transform, material_id, viewport_id) tuples. materials: Numpy array with MATERIAL_DTYPE, indexed by material_id. Returns: (opaque, double_sided_opaque, transparent) with original indices appended. """ opaque: list[tuple[Any, np.ndarray, int, int, int]] = [] double_sided: list[tuple[Any, np.ndarray, int, int, int]] = [] transparent: list[tuple[Any, np.ndarray, int, int, int]] = [] mat_count = len(materials) for i, (mesh_handle, transform, material_id, viewport_id) in enumerate(instances): if material_id < mat_count and materials[material_id]["alpha_mode"] != ALPHA_OPAQUE: transparent.append((mesh_handle, transform, material_id, viewport_id, i)) elif material_id < mat_count and materials[material_id]["double_sided"]: double_sided.append((mesh_handle, transform, material_id, viewport_id, i)) else: opaque.append((mesh_handle, transform, material_id, viewport_id, i)) return opaque, double_sided, transparent
[docs] def sort_transparent( instances: list[tuple[Any, np.ndarray, int, int, int]], camera_pos: np.ndarray, ) -> list[tuple[Any, np.ndarray, int, int, int]]: """Sort transparent instances back-to-front relative to camera position. Args: instances: List of (mesh_handle, transform, material_id, viewport_id, original_index). camera_pos: Camera world-space position as (3,) array. Returns: Sorted list (farthest from camera first). """ return sorted(instances, key=lambda inst: compute_sort_key(inst[1], camera_pos))
[docs] def extract_camera_position(view_matrix: np.ndarray) -> np.ndarray: """Extract world-space camera position from a view matrix. For a view matrix V, the camera position is ``-R^T * t`` where R is the upper-left 3x3 rotation and t is the translation column. Args: view_matrix: 4x4 view matrix (row-major numpy). Returns: (3,) float32 array of the camera world position. """ r = view_matrix[:3, :3] t = view_matrix[:3, 3] return (-r.T @ t).astype(np.float32)