# pyright: reportCallIssue=none
# Mitsuba 3 は variant ベースの動的属性解決を行うため Pylance/Pyright が
# mi.* を object 型と推論し reportCallIssue を誤検知する(coding-style.md の方針).
"""YAML 光源定義 → Mitsuba emitter dict 変換モジュール."""
from __future__ import annotations
from sm_tias_lightsim.runtime.llvm_init import ensure_drjit_libllvm_path
from sm_tias_lightsim.scene.materials import _to_rgb
from sm_tias_lightsim.scene.schema import (
AreaLight,
DirectionalLight,
LightConfig,
PointLight,
SpotLight,
)
# mitsuba を import した時点で Dr.Jit が LLVM の初期化を試みる.そのため
# DRJIT_LIBLLVM_PATH の自動探索は import より前に済ませておく必要がある.
# 本モジュールはモジュールレベルで mitsuba を import するので,
# render/runner.py の探索呼び出しでは手遅れになる(環境変数未設定の環境では
# レンダリングが ImportError で失敗し,slow マークのレンダテストが全滅していた).
ensure_drjit_libllvm_path()
import mitsuba as mi # noqa: E402
def _build_area_light_transform(
center: tuple[float, float, float],
half_extents: tuple[float, float],
direction: tuple[float, float, float],
position_scale: float,
) -> mi.ScalarTransform4f:
"""AreaLight の中心・半幅・方向から Mitsuba の to_world Transform を組み立てる.
Mitsuba の rectangle / disk 形状はどちらも XY 平面上の単位図形
(rectangle は ±1 の正方形,disk は半径 1 の円)で法線が +Z.
direction ベクトルが面の法線方向を指すように look_at を構成する.
Args:
center: 面中心位置(元単位).
half_extents: X / Y 方向の半幅 (hx, hy)(元単位).
rectangle は (width/2, height/2),disk は (radius, radius) を渡す.
direction: 放射方向ベクトル(単位ベクトルでなくても可).
position_scale: 座標スケール係数(mm→m なら 0.001).
Returns:
Mitsuba ScalarTransform4f.
"""
cx, cy, cz = [c * position_scale for c in center]
hx, hy = half_extents[0] * position_scale, half_extents[1] * position_scale
dx, dy, dz = direction
norm = (dx**2 + dy**2 + dz**2) ** 0.5
if norm == 0.0:
msg = "AreaLight direction vector must not be zero."
raise ValueError(msg)
dx, dy, dz = dx / norm, dy / norm, dz / norm
# direction が Y 軸に近い場合は up を Z 軸とし,それ以外は Y 軸を up に使う
# look_at の origin を center + direction(目線は放射方向の逆から見る)として構成
if abs(dy) < 0.9:
up = [0.0, 1.0, 0.0]
else:
up = [0.0, 0.0, 1.0]
# look_at: origin=center,target=center + direction,up=up
origin = [cx, cy, cz]
target = [cx + dx, cy + dy, cz + dz]
transform = mi.ScalarTransform4f().look_at(origin=origin, target=target, up=up)
# 単位図形(±1 / 半径 1)を半幅でスケールして実寸に合わせる
scale = mi.ScalarTransform4f().scale([hx, hy, 1.0])
return transform @ scale
def build_light_dict(light: LightConfig, position_scale: float) -> dict:
"""1 LightConfig を Mitsuba scene dict の emitter エントリに変換する.
Args:
light: SceneConfig.lights の 1 要素(PointLight / AreaLight / DirectionalLight).
position_scale: 座標スケール係数(例:mm→m なら 0.001).
位置・サイズに掛けて Mitsuba(m)に揃える.
Returns:
Mitsuba scene dict のキーに使える dict.
- PointLight → {"type": "point", "position": [...], "intensity": {...}}
- AreaLight → {"type": "rectangle" | "disk", "to_world": ...,
"emitter": {"type": "area", ...}}
- DirectionalLight → {"type": "directional", "direction": [...], "irradiance": {...}}
Raises:
ValueError: 未対応の type が指定された場合.
"""
if isinstance(light, PointLight):
pos = [c * position_scale for c in light.position]
return {
"type": "point",
"position": pos,
"intensity": {
"type": "rgb",
"value": _to_rgb(light.intensity),
},
}
if isinstance(light, AreaLight):
# 半幅: rectangle は size の半分,disk は radius(スキーマで必須検証済み)
if light.shape == "disk":
half_extents = (light.radius, light.radius)
else:
half_extents = (light.size[0] / 2.0, light.size[1] / 2.0)
to_world = _build_area_light_transform(
center=light.center,
half_extents=half_extents,
direction=light.direction,
position_scale=position_scale,
)
return {
"type": light.shape,
"to_world": to_world,
"emitter": {
"type": "area",
"radiance": {
"type": "rgb",
"value": _to_rgb(light.radiance),
},
},
}
if isinstance(light, DirectionalLight):
return {
"type": "directional",
"direction": list(light.direction),
"irradiance": {
"type": "rgb",
"value": _to_rgb(light.irradiance),
},
}
if isinstance(light, SpotLight):
# spot emitter は to_world で位置と光軸方向を指定する.
# Mitsuba の spot は to_world @ (origin=(0,0,0), target=(0,0,1)) を基準とし,
# to_world で世界座標へ変換するので look_at で組み立てる.
cx, cy, cz = [c * position_scale for c in light.position]
dx, dy, dz = light.direction
norm = (dx**2 + dy**2 + dz**2) ** 0.5
if norm == 0.0:
msg = "SpotLight direction vector must not be zero."
raise ValueError(msg)
dx, dy, dz = dx / norm, dy / norm, dz / norm
if abs(dy) < 0.9:
up = [0.0, 1.0, 0.0]
else:
up = [0.0, 0.0, 1.0]
origin = [cx, cy, cz]
target = [cx + dx, cy + dy, cz + dz]
to_world = mi.ScalarTransform4f().look_at(origin=origin, target=target, up=up)
return {
"type": "spot",
"to_world": to_world,
"cutoff_angle": light.cutoff_angle,
"beam_width": light.beam_width,
"intensity": {
"type": "rgb",
"value": _to_rgb(light.intensity),
},
}
msg = f"Unsupported light type: {type(light).__name__}"
raise ValueError(msg)