Source code for fury.actor._billboard

"""
Billboard actor module.

Minimal isolated implementation of billboard support to reduce diffs in
existing planar actor module. Provides a Mesh-based world object and a
factory function plus shader registration.
"""

import numpy as np

from fury.actor import Mesh
from fury.geometry import buffer_to_geometry
from fury.lib import register_wgpu_render_function
from fury.material import (
    BillboardMaterial,
    BillboardSphereMaterial,
    validate_opacity,
)
from fury.shader import BillboardShader, BillboardSphereShader


def _create_billboard_actor(
    centers,
    colors,
    sizes,
    opacity,
    enable_picking,
    *,
    material_cls,
    material_kwargs=None,
):
    """
    Build a ``Billboard`` instance from broadcasted inputs.

    Parameters
    ----------
    centers : array_like
        Position of each billboard specified as an ``(N, 3)`` array or
        broadcastable equivalent.
    colors : array_like
        RGB or RGBA color per billboard. A single color is broadcast when
        needed.
    sizes : array_like
        Width and height per billboard. Accepts scalar, ``(2,)`` pair,
        ``(N,)`` radius (interpreted as square billboards), or ``(N, 2)`` data.
    opacity : float or None
        Global opacity multiplier. ``None`` keeps the material default.
    enable_picking : bool
        Whether the billboard should write picking information.
    material_cls : type[BillboardMaterial]
        Material class used to instantiate the billboard actor.
    material_kwargs : dict, optional
        Additional keyword arguments forwarded to ``material_cls``.

    Returns
    -------
    Billboard
        Configured billboard world object containing geometry, material and
        metadata about the generated billboards.
    """
    centers = np.asarray(centers, dtype=np.float32)
    if centers.ndim == 1:
        centers = centers.reshape(1, 3)
    n = len(centers)

    colors = np.asarray(colors, dtype=np.float32)
    if colors.ndim == 1:
        colors = np.tile(colors, (n, 1))
    elif colors.shape[0] != n:
        colors = np.tile(colors[0], (n, 1))

    sizes = np.asarray(sizes, dtype=np.float32)
    if sizes.ndim == 0:
        sizes = np.full((n, 2), float(sizes))
    elif sizes.ndim == 1:
        if sizes.size == 2:
            sizes = np.tile(sizes, (n, 1))
        elif sizes.size == n:
            sizes = np.column_stack([sizes, sizes])
        else:
            sizes = np.full((n, 2), sizes.flat[0])
    elif sizes.shape[0] != n:
        sizes = np.tile(sizes[0], (n, 1))

    opacity = validate_opacity(opacity)

    repeats = 6  # 2 triangles per quad
    pos = np.repeat(centers, repeats, axis=0).astype(np.float32)
    col = np.repeat(colors, repeats, axis=0).astype(np.float32)
    indices = np.arange(pos.shape[0], dtype=np.uint32)

    # Encode per-billboard size in normals so shaders can fetch dimensions
    normals = np.repeat(
        np.column_stack([sizes, np.ones((n, 1), dtype=np.float32)]),
        repeats,
        axis=0,
    ).astype(np.float32)

    geometry = buffer_to_geometry(
        positions=pos,
        colors=col,
        normals=normals,
        indices=indices,
    )

    material_kwargs = material_kwargs or {}
    material = material_cls(
        pick_write=enable_picking,
        opacity=opacity,
        color_mode="vertex",
        **material_kwargs,
    )

    obj = Billboard(geometry=geometry, material=material)
    obj.billboard_count = n
    obj.billboard_centers = centers.copy()
    obj.billboard_sizes = sizes.copy()
    return obj


[docs] class Billboard(Mesh): """ World object representing one or more billboards. Geometry buffers are duplicated per 6 vertices (two triangles) per billboard; the vertex shader reconstructs the quad via ``vertex_index`` math and uses camera right/up vectors to orient it. Size metadata is stored on ``billboard_sizes`` and reused by shaders for impostor variants. Parameters ---------- geometry : Geometry, optional The geometry object containing vertex data. material : Material, optional The material used to render the billboard. **kwargs : dict Additional keyword arguments forwarded to :class:`Mesh`. """
[docs] def __init__(self, geometry=None, material=None, **kwargs): """Initialize a Billboard actor.""" super().__init__(geometry=geometry, material=material, **kwargs) self._billboard_count = 0 self._billboard_centers = np.empty((0, 3), dtype=np.float32) self._billboard_sizes = np.empty((0, 2), dtype=np.float32)
[docs] def get_bounding_box(self): """ Compute the axis-aligned bounding box including billboard visual size. Returns ------- ndarray, shape (2, 3), or None Axis-aligned bounding box as ``[[min, min, min], [max, max, max]]``. """ centers = self._billboard_centers sizes = self._billboard_sizes if centers.size == 0 or sizes.size == 0: return super().get_bounding_box() half_extents = sizes.max(axis=1) / 2.0 expanded_min = centers - half_extents[:, np.newaxis] expanded_max = centers + half_extents[:, np.newaxis] aabb = np.empty((2, 3), dtype=np.float64) aabb[0] = expanded_min.min(axis=0) aabb[1] = expanded_max.max(axis=0) parent_aabb = super().get_bounding_box() if parent_aabb is not None: aabb[0] = np.minimum(aabb[0], parent_aabb[0]) aabb[1] = np.maximum(aabb[1], parent_aabb[1]) return aabb
@property def billboard_count(self): """ Get the number of billboards in this actor. Returns ------- int Number of billboards. """ return self._billboard_count @billboard_count.setter def billboard_count(self, value): """ Set the number of billboards in this actor. Parameters ---------- value : int Number of billboards. """ self._billboard_count = value @property def billboard_centers(self): """ Get the billboard center positions. Returns ------- ndarray, shape (N, 3) Billboard center positions. """ return self._billboard_centers @billboard_centers.setter def billboard_centers(self, value): """ Set the billboard center positions. Parameters ---------- value : array_like, shape (N, 3) Billboard center positions. """ self._billboard_centers = np.asarray(value, dtype=np.float32) @property def billboard_sizes(self): """ Get the billboard width/height pairs. Returns ------- ndarray, shape (N, 2) Billboard width/height pairs. """ return self._billboard_sizes @billboard_sizes.setter def billboard_sizes(self, value): """ Set the billboard width/height pairs. Parameters ---------- value : array_like, shape (N, 2) Billboard width/height pairs. """ self._billboard_sizes = np.asarray(value, dtype=np.float32)
[docs] def billboard( centers, *, colors=(1, 1, 1), sizes=(1, 1), opacity=None, enable_picking=True, ): """ Create a billboard world object. Parameters ---------- centers : (N,3) array_like Billboard positions. colors : (N,3|4) array_like or single color Per-billboard RGB(A) colors. sizes : (N,2) | (2,) | float | (N,) array_like Width/height per billboard. Scalar or single pair broadcast. opacity : float, optional Global opacity multiplier (0..1). enable_picking : bool Whether billboard is pickable. Returns ------- Billboard Billboard world object configured with the provided geometry and material. """ return _create_billboard_actor( centers, colors, sizes, opacity, enable_picking, material_cls=BillboardMaterial, )
[docs] def billboard_sphere( centers, *, colors=(1, 1, 1), radii=0.5, opacity=None, enable_picking=True, ): """ Create a billboard impostor sphere world object. Parameters ---------- centers : array_like Sphere centers provided as an ``(N, 3)`` array or broadcastable input. colors : array_like, optional RGB or RGBA color per sphere. Single color inputs are broadcast. radii : array_like, optional Scalar radii or per-sphere radii array. Used to compute billboard size. opacity : float, optional Opacity multiplier applied to the material. enable_picking : bool, optional Whether the impostor spheres support picking. Returns ------- Billboard Billboard actor configured to simulate spheres using impostor quads. """ sizes = np.asarray(radii, dtype=np.float32) * 2.0 obj = _create_billboard_actor( centers, colors, sizes, opacity, enable_picking, material_cls=BillboardSphereMaterial, ) obj.billboard_radii = obj.billboard_sizes[:, 0] * 0.5 obj.billboard_mode = "impostor" return obj
@register_wgpu_render_function(Billboard, BillboardMaterial) def register_billboard_render_function(wobject): """ Build the render pipeline for ``Billboard`` instances. Parameters ---------- wobject : Billboard Billboard world object to bind to the shader pipeline. Returns ------- tuple Tuple containing the configured shader instance. """ return (BillboardShader(wobject),) @register_wgpu_render_function(Billboard, BillboardSphereMaterial) def register_billboard_sphere_render_function(wobject): """ Register the pipeline for billboard-based sphere impostors. Parameters ---------- wobject : Billboard Billboard world object representing impostor spheres. Returns ------- tuple Tuple containing the configured :class:`~fury.shader.BillboardSphereShader`. """ return (BillboardSphereShader(wobject),)