"""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))