"""ReflectionProbe3D — Cubemap reflection capture probe for 3D scenes."""
from __future__ import annotations
import logging
from .descriptors import Property, Signal
from .math.types import Vec3
from .nodes_3d.node3d import Node3D
log = logging.getLogger(__name__)
[docs]
class ReflectionProbe3D(Node3D):
"""Cubemap reflection capture probe.
Captures a cubemap from a point in the scene and applies it as an
environment map to meshes within the probe's influence box. Supports
parallax-corrected box projection for accurate indoor reflections.
The probe influence volume is an axis-aligned box centred on
``world_position + origin_offset`` with half-extents given by ``size``.
Example::
probe = ReflectionProbe3D(
size=(5, 3, 5),
box_projection=True,
interior=True,
)
room.add_child(probe)
"""
# --- Influence volume ---
size = Property((10.0, 10.0, 10.0), hint="Half-extents of the probe influence box (Vec3)", group="Volume")
origin_offset = Property((0.0, 0.0, 0.0), hint="Capture origin offset from node position (Vec3)", group="Volume")
# --- Projection ---
box_projection = Property(False, hint="Enable parallax-corrected box projection", group="Volume")
# --- Intensity ---
intensity = Property(1.0, range=(0.0, 5.0), hint="Reflection intensity multiplier", group="Volume")
# --- Capture ---
max_distance = Property(0.0, range=(0.0, 16384.0), hint="Max capture distance (0 = infinite)", group="Capture")
update_mode = Property("once", enum=["once", "always"], hint="Cubemap update mode", group="Capture")
# --- Interior ---
interior = Property(False, hint="Indoor probe — disables skybox blending")
ambient_mode = Property(
"disabled", enum=["disabled", "environment", "constant"], hint="Ambient light contribution mode"
)
ambient_colour = Property((0.0, 0.0, 0.0, 1.0), hint="Ambient colour (RGBA, used when ambient_mode='constant')")
# --- Culling ---
cull_mask = Property(0xFFFFFFFF, range=(0, 0xFFFFFFFF), hint="Cull mask — which render layers are captured")
# --- Shadows ---
enable_shadows = Property(False, hint="Include shadow-casting in cubemap capture")
# --- Signals ---
cubemap_updated = Signal()
# --- Internal state ---
def __init__(self, size=None, origin_offset=None, **kwargs):
super().__init__(**kwargs)
if size is not None:
self.size = tuple(Vec3(size))
if origin_offset is not None:
self.origin_offset = tuple(Vec3(origin_offset))
self._update_requested: bool = False
self._cubemap_version: int = 0
# --- Public API ---
@property
def capture_position(self) -> Vec3:
"""World-space position where the cubemap is captured from."""
offset = Vec3(self.origin_offset)
return self.world_position + offset
[docs]
def request_update(self) -> None:
"""Mark this probe for cubemap recapture on the next frame.
The rendering backend checks ``_update_requested`` each frame and,
after capturing, increments ``_cubemap_version`` and emits
``cubemap_updated``.
"""
self._update_requested = True
# --- Cull mask helpers ---
[docs]
def set_cull_mask_layer(self, index: int, enabled: bool = True) -> None:
"""Enable or disable a specific cull mask layer (0-31)."""
if not 0 <= index < 32:
raise ValueError(f"Cull mask layer index must be 0-31, got {index}")
if enabled:
self.cull_mask = self.cull_mask | (1 << index)
else:
self.cull_mask = self.cull_mask & ~(1 << index)
[docs]
def is_cull_mask_layer_enabled(self, index: int) -> bool:
"""Check if a specific cull mask layer is enabled (0-31)."""
if not 0 <= index < 32:
raise ValueError(f"Cull mask layer index must be 0-31, got {index}")
return bool(self.cull_mask & (1 << index))
# --- Editor gizmo ---
[docs]
def get_gizmo_lines(self) -> list[tuple[Vec3, Vec3]]:
"""Return wireframe box lines for the probe influence volume.
Draws 12 edges of an axis-aligned box centred on ``capture_position``
with half-extents ``size``.
"""
c = self.capture_position
sx, sy, sz = (float(v) for v in Vec3(self.size))
# 8 corners of the AABB
corners = [
Vec3(c.x + dx * sx, c.y + dy * sy, c.z + dz * sz)
for dx in (-1, 1) for dy in (-1, 1) for dz in (-1, 1)
]
# 12 edges — connect corners that differ in exactly one axis
lines: list[tuple[Vec3, Vec3]] = []
for i, a in enumerate(corners):
for b in corners[i + 1:]:
diff = (
abs(float(a.x) - float(b.x)) > 1e-6,
abs(float(a.y) - float(b.y)) > 1e-6,
abs(float(a.z) - float(b.z)) > 1e-6,
)
if sum(diff) == 1:
lines.append((a, b))
return lines