"""YAML 材料定義 → Mitsuba BSDF dict 変換モジュール."""
from __future__ import annotations
from sm_tias_lightsim.scene.schema import (
BlendBSDFConfig,
DielectricMaterial,
DiffuseMaterial,
MaterialConfig,
RoughConductorBSDFConfig,
ThinDielectricMaterial,
)
def _to_rgb(value: float | tuple[float, float, float]) -> list[float]:
"""スカラまたは RGB タプルを 3 要素の float リストに正規化する.
lights.py / materials.py 共通のヘルパ.このモジュールが正本で,
lights.py はここから import して重複を回避する.
Args:
value: スカラ(全チャンネル同値)または RGB タプル.
Returns:
3 要素の float リスト.
"""
if isinstance(value, (int, float)):
return [float(value)] * 3
return list(value)
def _build_bsdf_dict(material: object) -> dict:
"""1 つの BSDF config を Mitsuba BSDF dict に再帰変換する(内部ヘルパ).
BlendBSDFConfig の components は再帰的に変換する.
Args:
material: DiffuseMaterial / DielectricMaterial / ThinDielectricMaterial /
RoughConductorBSDFConfig / BlendBSDFConfig のいずれか.
Returns:
Mitsuba BSDF dict.
Raises:
ValueError: 未対応の材料タイプが指定された場合.
"""
if isinstance(material, DiffuseMaterial):
return {
"type": "diffuse",
"reflectance": {
"type": "rgb",
"value": _to_rgb(material.reflectance),
},
}
if isinstance(material, DielectricMaterial):
return {
"type": "dielectric",
"int_ior": material.int_ior,
"ext_ior": material.ext_ior,
}
if isinstance(material, ThinDielectricMaterial):
return {
"type": "thindielectric",
"int_ior": material.int_ior,
"ext_ior": material.ext_ior,
}
if isinstance(material, RoughConductorBSDFConfig):
# material="none" で eta/k なし(完全反射体)として扱う
return {
"type": "roughconductor",
"alpha": material.alpha,
"distribution": material.distribution,
"material": "none",
}
if isinstance(material, BlendBSDFConfig):
# components は必ず 2 要素(スキーマで min_length=2, max_length=2 を保証)
return {
"type": "blendbsdf",
"weight": material.weight,
"bsdf_0": _build_bsdf_dict(material.components[0]),
"bsdf_1": _build_bsdf_dict(material.components[1]),
}
msg = f"Unsupported material type: {type(material).__name__}"
raise ValueError(msg)
def build_material_dict(material: MaterialConfig) -> dict:
"""1 MaterialConfig を Mitsuba BSDF dict に変換する.
Args:
material: SceneConfig.materials の値(DiffuseMaterial / BlendBSDFConfig 等).
Returns:
Mitsuba BSDF dict.
- diffuse → {"type": "diffuse", "reflectance": {"type":"rgb","value":[r,g,b]}}
- dielectric → {"type": "dielectric", "int_ior": ..., "ext_ior": ...}
- thindielectric → {"type": "thindielectric", "int_ior": ..., "ext_ior": ...}
- roughconductor → {"type": "roughconductor", "alpha": ..., "distribution": ...,
"material": "none"}
- blendbsdf → {"type": "blendbsdf", "weight": ..., "bsdf_0": ..., "bsdf_1": ...}
Raises:
ValueError: 未対応の材料タイプが指定された場合.
"""
return _build_bsdf_dict(material)
def _is_opaque(material: object) -> bool:
"""不透明な反射専用 BSDF かどうかを判定する(内部ヘルパ).
誘電体(dielectric / thindielectric)は透過を伴い両面挙動が本質なので
twosided でラップしてはならない.拡散・粗面導体は片面 BSDF なので
ラップ対象になる.blendbsdf は全構成要素が不透明なときのみ不透明とみなす.
Args:
material: MaterialConfig のいずれか.
Returns:
twosided でラップしてよい不透明反射材なら True.
"""
if isinstance(material, (DiffuseMaterial, RoughConductorBSDFConfig)):
return True
if isinstance(material, BlendBSDFConfig):
return all(_is_opaque(c) for c in material.components)
# DielectricMaterial / ThinDielectricMaterial および未知型は非対象
return False
def build_scene_bsdf_dict(material: MaterialConfig) -> dict:
"""シーン登録用の BSDF dict を返す(不透明反射材は twosided でラップする).
`build_material_dict` が返す素の BSDF dict をシーンにそのまま登録すると,
Mitsuba の diffuse / roughconductor は**片面 BSDF** であるため,
法線が裏返ったメッシュ面は入射光を一切反射せず**エラーも警告もなく真っ黒**になる.
間接照明のみで評価板を照らす SmTIAS の構成では,内壁が 1 面でも黒化すると
輝度分布が静かに壊れる(TEST_12 のシェーディング法線バグと同じ「気づけない」故障モード).
そこで不透明反射材を `twosided` でラップし,面の向きに依存しない物理的に正しい
挙動にする.積分球の解析解による検証では,法線が正しい場合はラップの有無で
結果が完全に一致し(無影響),法線が裏返った場合のみ黒化を救済することを確認済み.
透過を伴う誘電体(dielectric / thindielectric)は両面挙動が本質なのでラップしない.
Args:
material: SceneConfig.materials の値.
Returns:
Mitsuba BSDF dict.不透明反射材は
{"type": "twosided", "bsdf": <素の BSDF dict>} の形になる.
"""
bsdf = build_material_dict(material)
if _is_opaque(material):
return {"type": "twosided", "bsdf": bsdf}
return bsdf