Source code for dazpy.lighting

"""Domain-level lighting rigs built on the DazLight/DazScene primitives.

Provides :func:`apply_three_point_light_setup` for creating a conventional
key/fill/rim light rig around a target, either via angle/distance placement
or explicit world-space positions. Also provides :func:`apply_hdri_environment`
for image-based (HDRI/dome) lighting via :class:`HDRIEnvironment`, and
:func:`set_light_color` as a submodule-level entry point for the color-setting
already available via :meth:`~dazpy.DazLight.set_color`.
"""

from __future__ import annotations

import math
import os
from dataclasses import dataclass
from typing import TYPE_CHECKING

from .exceptions import RenderError
from .math3 import Vec3
from ._shot_geometry import spherical_offset as _spherical_offset
from ._shot_geometry import look_at_euler as _look_at_euler
from ._shot_geometry import resolve_target as _resolve_target

if TYPE_CHECKING:
    from ._light import DazLight
    from ._node import DazNode
    from ._render import DazRenderSettings
    from ._scene import DazScene


@dataclass(frozen=True)
class LightSpec:
    """One light's placement and output within a rig.

    Args:
        role: Informational label, e.g. ``"key"``, ``"fill"``, ``"rim"``.
        azimuth_deg: See :func:`_spherical_offset`. Ignored if *position* is set.
        elevation_deg: See :func:`_spherical_offset`. Ignored if *position* is set.
        distance: Distance from the target, in DAZ Studio units (cm).
            Ignored if *position* is set.
        intensity: Passed straight to :attr:`~dazpy.DazLight.intensity`.
        color: ``(r, g, b)`` in the 0-255 range, passed to
            :meth:`~dazpy.DazLight.set_color`.
        position: Explicit world-space override. When set, *azimuth_deg*,
            *elevation_deg*, and *distance* are ignored entirely.
    """

    role: str
    azimuth_deg: float
    elevation_deg: float
    distance: float
    intensity: float
    color: tuple[int, int, int] = (255, 255, 255)
    position: Vec3 | None = None


_DEFAULT_KEY = LightSpec(role="key", azimuth_deg=45.0, elevation_deg=30.0, distance=150.0, intensity=100.0)
_DEFAULT_FILL = LightSpec(role="fill", azimuth_deg=-45.0, elevation_deg=15.0, distance=150.0, intensity=50.0)
_DEFAULT_RIM = LightSpec(role="rim", azimuth_deg=180.0, elevation_deg=45.0, distance=150.0, intensity=75.0)


[docs] @dataclass(frozen=True) class ThreePointLightSetup: """Input spec for a three-point light rig. Args: target: The point to light, as a :class:`~dazpy.math3.Vec3` world position or a :class:`~dazpy.DazNode` (its :attr:`~dazpy.DazNode.position` is used). key: Key light placement/output. Defaults to a 45deg/30deg key light. fill: Fill light placement/output. Defaults to a -45deg/15deg fill light. rim: Rim light placement/output. Defaults to a 180deg/45deg rim light. light_type: Forwarded to :meth:`~dazpy.DazScene.create_light` for all three lights — one of ``"spot"``, ``"point"``, ``"distant"``. """ target: Vec3 | DazNode key: LightSpec = _DEFAULT_KEY fill: LightSpec = _DEFAULT_FILL rim: LightSpec = _DEFAULT_RIM light_type: str = "spot"
@dataclass(frozen=True) class ThreePointLightRig: """Handles to the three lights created by :func:`apply_three_point_light_setup`.""" key: DazLight fill: DazLight rim: DazLight def _resolve_light_position(target: Vec3, spec: LightSpec) -> Vec3: if spec.position is not None: return spec.position return _spherical_offset(target, spec.azimuth_deg, spec.elevation_deg, spec.distance) def _place_light(scene: DazScene, target: Vec3, spec: LightSpec, light_type: str) -> DazLight: light = scene.create_light(light_type, name=spec.role) pos = _resolve_light_position(target, spec) light.set_position(pos.x, pos.y, pos.z) pitch, yaw, roll = _look_at_euler(pos, target) light.set_rotation(pitch, yaw, roll) light.intensity = spec.intensity light.set_color(*spec.color) return light
[docs] def apply_three_point_light_setup(scene: DazScene, setup: ThreePointLightSetup) -> ThreePointLightRig: """Create and place a key/fill/rim light rig around *setup.target*. Args: scene: A :class:`~dazpy.DazScene`. setup: The rig configuration. Returns: A :class:`ThreePointLightRig` with handles to the three created lights. """ target = _resolve_target(setup.target) key_light = _place_light(scene, target, setup.key, setup.light_type) fill_light = _place_light(scene, target, setup.fill, setup.light_type) rim_light = _place_light(scene, target, setup.rim, setup.light_type) return ThreePointLightRig(key=key_light, fill=fill_light, rim=rim_light)
[docs] @dataclass(frozen=True) class HDRIEnvironment: """Image-based (HDRI/dome) lighting configuration. Args: image_path: Absolute path to an HDRI/environment map on disk. Must exist -- validated by :func:`apply_hdri_environment` before any DazScript call is made. intensity: Passed to the Iray "Environment Intensity" property. rotation_deg: Passed to the Iray "Dome Rotation" property. mode: One of ``"dome_only"``, ``"dome_and_scene"``, ``"scene_only"``. Maps to the DazScript "Environment Mode" enum. Procedural Sun-Sky mode is intentionally not exposed here. draw_dome: Whether the dome image is visible as a backdrop in the viewport/render (Iray "Draw Dome"), independent of whether it lights the scene. resolution: Iray "Environment Lighting Resolution" (IBL sampling quality). ``None`` leaves DAZ Studio's current value untouched. """ image_path: str intensity: float = 1.0 rotation_deg: float = 0.0 mode: str = "dome_only" draw_dome: bool = False resolution: int | None = None
_HDRI_MODE_LABELS = { "dome_only": "Dome Only", "dome_and_scene": "Dome and Scene", "scene_only": "Scene Only", }
[docs] def apply_hdri_environment(render_settings: DazRenderSettings, env: HDRIEnvironment) -> None: """Apply image-based (HDRI/dome) lighting via *render_settings*. Args: render_settings: A :class:`~dazpy.DazRenderSettings`. env: The environment configuration. Raises: FileNotFoundError: If ``env.image_path`` does not exist on disk. Checked before any DazScript call is made -- an invalid path passed to the underlying ``setMap()`` call can hang or crash DAZ Studio via a blocking file-not-found dialog. ValueError: If ``env.mode`` is not one of ``"dome_only"``, ``"dome_and_scene"``, ``"scene_only"``. RenderError: If a post-apply readback of "Environment Intensity" doesn't match *env.intensity*. All of this module's environment writes go through ``DazRenderSettings._environment_holder()``, which assumes ``getRenderElementObjects()[3]`` is always the Environment property holder -- confirmed on one live DAZ Studio 4.x instance only. If that index is wrong on a different DAZ Studio version, every write above silently no-ops (matching the existing ``if (!holder) return;`` pattern) with no exception, so this readback is what actually catches it. """ if not os.path.isfile(env.image_path): raise FileNotFoundError(f"HDRI/environment map not found: {env.image_path}") if env.mode not in _HDRI_MODE_LABELS: raise ValueError( f"Invalid HDRIEnvironment.mode {env.mode!r}; must be one of {sorted(_HDRI_MODE_LABELS)}" ) render_settings._set_environment_map(env.image_path) render_settings._set_environment_property("Environment Intensity", env.intensity) render_settings._set_environment_property("Dome Rotation", env.rotation_deg) render_settings._set_environment_property_from_string("Environment Mode", _HDRI_MODE_LABELS[env.mode]) render_settings._set_environment_property("Draw Dome", env.draw_dome) if env.resolution is not None: render_settings._set_environment_property("Environment Lighting Resolution", env.resolution) readback = render_settings._get_environment_property("Environment Intensity") if readback is None or not math.isclose(float(readback), env.intensity, rel_tol=1e-6, abs_tol=1e-6): raise RenderError( "HDRI environment apply failed verification: 'Environment Intensity' " f"readback ({readback!r}) does not match the requested value " f"({env.intensity!r}). The environment property holder " "(getRenderElementObjects()[3]) may be unavailable or at a " "different index on this DAZ Studio version/build -- the HDRI " "was likely never applied." )
def set_light_color(light: DazLight, r: float, g: float, b: float) -> None: """Set *light*'s diffuse color. A thin :mod:`dazpy.lighting`-level entry point for :meth:`~dazpy.DazLight.set_color`, so color-setting is discoverable alongside :func:`apply_three_point_light_setup` and :func:`apply_hdri_environment` without needing to know the lower-level :class:`~dazpy.DazLight` primitive lives in a different module. Args: light: The :class:`~dazpy.DazLight` to recolor. r: Red component (0-255). g: Green component (0-255). b: Blue component (0-255). """ light.set_color(r, g, b)