Source code for simvx.core.nodes_3d.lights

"""Light3D, DirectionalLight3D, PointLight3D, SpotLight3D -- 3D light nodes."""

from __future__ import annotations

import math

import numpy as np

from ..descriptors import Property
from ..math.types import Quat, Vec3
from .node3d import Node3D


[docs] class Light3D(Node3D): """Base class for 3D light nodes.""" colour = Property((1.0, 1.0, 1.0), group="Light") intensity = Property(1.0, group="Light") shadows = Property(False, group="Light") light_cull_mask = Property( 0xFFFFFFFF, range=(0, 0xFFFFFFFF), hint="Light cull mask -- which render layers this light affects" )
[docs] def set_light_cull_mask_layer(self, index: int, enabled: bool = True) -> None: """Enable or disable a specific light cull mask layer (0-31).""" if not 0 <= index < 32: raise ValueError(f"Light cull mask layer index must be 0-31, got {index}") if enabled: self.light_cull_mask = self.light_cull_mask | (1 << index) else: self.light_cull_mask = self.light_cull_mask & ~(1 << index)
[docs] def is_light_cull_mask_layer_enabled(self, index: int) -> bool: """Check if a specific light cull mask layer is enabled (0-31).""" if not 0 <= index < 32: raise ValueError(f"Light cull mask layer index must be 0-31, got {index}") return bool(self.light_cull_mask & (1 << index))
[docs] class DirectionalLight3D(Light3D): """Directional light -- direction derived from node's forward vector.""" gizmo_colour = Property((1.0, 0.95, 0.5, 0.4), hint="Editor gizmo colour") @property def direction(self) -> Vec3: """Light direction (world-space). Derived from forward vector.""" return self.forward @direction.setter def direction(self, v): """Set light direction by rotating the node so forward matches *v*.""" d = Vec3(v).normalized() self.world_rotation = Quat.look_at(d)
[docs] def get_gizmo_lines(self) -> list[tuple[Vec3, Vec3]]: """Return direction arrow and parallel rays showing light direction.""" p = self.world_position fwd = self.forward lines: list[tuple[Vec3, Vec3]] = [(p, p + fwd * 2.5)] # Two parallel side rays up = Vec3(0, 1, 0) if abs(fwd.y) < 0.9 else Vec3(1, 0, 0) right = Vec3(*np.cross(fwd, up)) rn = np.linalg.norm(right) if rn > 1e-6: right = right * (0.4 / rn) lines.append((p + right, p + right + fwd * 2.0)) lines.append((p - right, p - right + fwd * 2.0)) return lines
[docs] class PointLight3D(Light3D): """Omnidirectional point light with range-based attenuation.""" range = Property(10.0, group="Light") gizmo_colour = Property((1.0, 0.95, 0.5, 0.4), hint="Editor gizmo colour")
[docs] def get_gizmo_lines(self) -> list[tuple[Vec3, Vec3]]: """Return 3 circles showing the light range sphere.""" from ..physics_nodes import _circle_lines_3d p = self.world_position r = float(self.range) lines: list[tuple[Vec3, Vec3]] = [] lines.extend(_circle_lines_3d(p, Vec3(1, 0, 0), Vec3(0, 1, 0), r)) lines.extend(_circle_lines_3d(p, Vec3(1, 0, 0), Vec3(0, 0, 1), r)) lines.extend(_circle_lines_3d(p, Vec3(0, 1, 0), Vec3(0, 0, 1), r)) return lines
[docs] class SpotLight3D(Light3D): """Spot light with inner/outer cone angles (degrees).""" range = Property(10.0, group="Light") inner_cone = Property(30.0, group="Light") outer_cone = Property(45.0, group="Light") gizmo_colour = Property((1.0, 0.95, 0.5, 0.4), hint="Editor gizmo colour")
[docs] def get_gizmo_lines(self) -> list[tuple[Vec3, Vec3]]: """Return cone wireframe showing spot light direction and angle.""" from ..physics_nodes import _circle_lines_3d p = self.world_position fwd = self.forward r = float(self.range) cone_angle = math.radians(float(self.outer_cone)) base_radius = r * math.tan(cone_angle * 0.5) base_center = p + fwd * r # Compute orthonormal basis up = Vec3(0, 1, 0) if abs(fwd.y) < 0.9 else Vec3(1, 0, 0) right_raw = np.cross(fwd, up) rn = np.linalg.norm(right_raw) if rn < 1e-6: return [(p, base_center)] right = Vec3(*(right_raw / rn)) u = Vec3(*np.cross(right, fwd)) lines: list[tuple[Vec3, Vec3]] = [] # Base circle lines.extend(_circle_lines_3d(base_center, right, u, base_radius, 16)) # 4 lines from apex to base circle for angle in [0.0, math.pi * 0.5, math.pi, math.pi * 1.5]: edge = base_center + right * (base_radius * math.cos(angle)) + u * (base_radius * math.sin(angle)) lines.append((p, edge)) return lines