"""scene/materials.py の blendbsdf / roughconductor 変換仕様ベーステスト.
仕様出典: 実装サマリー §D
- D1: DiffuseMaterial → Mitsuba dict (既存仕様・後方互換確認)
- D2: RoughConductorBSDFConfig → Mitsuba dict
- D3: BlendBSDFConfig (flat) → Mitsuba dict
- D4: ネスト BlendBSDFConfig → 再帰変換
- D5: build_material_dict 公開 API の後方互換
build_material_dict の戻り値 dict 構造のみを検証する.Mitsuba の load_dict は呼ばない.
"""
from __future__ import annotations
import pytest
from sm_tias_lightsim.scene.materials import build_material_dict
from sm_tias_lightsim.scene.schema import (
BlendBSDFConfig,
DiffuseMaterial,
RoughConductorBSDFConfig,
)
# ---------------------------------------------------------------------------
# D1: DiffuseMaterial → Mitsuba dict(既存仕様の後方互換確認)
# ---------------------------------------------------------------------------
class TestBuildMaterialDictDiffuse:
"""D1 系: DiffuseMaterial の dict 変換(既存仕様確認)."""
def test_diffuse_type_in_dict(self):
"""DiffuseMaterial の dict type が "diffuse" であること.
仕様 D1: DiffuseMaterial → {type: "diffuse", reflectance: {type: "rgb", value: [r,g,b]}}
"""
mat = DiffuseMaterial(type="diffuse", reflectance=0.5)
result = build_material_dict(mat)
assert result["type"] == "diffuse"
def test_diffuse_reflectance_type_is_rgb(self):
"""DiffuseMaterial の reflectance dict に type="rgb" が含まれること.
仕様 D1: reflectance sub-dict に type="rgb" キー
"""
mat = DiffuseMaterial(type="diffuse", reflectance=0.5)
result = build_material_dict(mat)
assert result["reflectance"]["type"] == "rgb"
def test_diffuse_scalar_reflectance_expands_to_rgb(self):
"""スカラ reflectance が RGB 3 要素リストに展開されること.
仕様 D1: value: [r, g, b]
"""
mat = DiffuseMaterial(type="diffuse", reflectance=0.85)
result = build_material_dict(mat)
assert result["reflectance"]["value"] == pytest.approx([0.85, 0.85, 0.85])
def test_diffuse_rgb_reflectance_preserved(self):
"""RGB reflectance タプルがそのまま 3 要素リストに変換されること.
仕様 D1: RGB 指定時も value: [r, g, b] 形式
"""
mat = DiffuseMaterial(type="diffuse", reflectance=(0.85, 0.85, 0.85))
result = build_material_dict(mat)
assert result["reflectance"]["value"] == pytest.approx([0.85, 0.85, 0.85])
# ---------------------------------------------------------------------------
# D2: RoughConductorBSDFConfig → Mitsuba dict
# ---------------------------------------------------------------------------
class TestBuildMaterialDictRoughConductor:
"""D2 系: RoughConductorBSDFConfig の dict 変換仕様."""
def test_roughconductor_type_in_dict(self):
"""RoughConductorBSDFConfig の dict type が "roughconductor" であること.
仕様 D2: {type: "roughconductor", ...}
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=0.3, distribution="ggx")
result = build_material_dict(mat)
assert result["type"] == "roughconductor"
def test_roughconductor_alpha_in_dict(self):
"""alpha 値が dict に格納されること.
仕様 D2: {alpha: 0.3, ...}
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=0.3, distribution="ggx")
result = build_material_dict(mat)
assert result["alpha"] == pytest.approx(0.3)
def test_roughconductor_distribution_in_dict(self):
"""distribution 値が dict に格納されること.
仕様 D2: {distribution: "ggx", ...}
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=0.3, distribution="ggx")
result = build_material_dict(mat)
assert result["distribution"] == "ggx"
def test_roughconductor_material_none_in_dict(self):
"""material キーが "none" で格納されること(完全反射体・eta/k なし).
仕様 D2: {material: "none"} は固定
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=0.3, distribution="ggx")
result = build_material_dict(mat)
assert result["material"] == "none"
def test_roughconductor_alpha_edge_zero(self):
"""alpha=0.0(鏡面)が dict に正しく格納されること.
仕様 D2: alpha 端値の変換
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=0.0)
result = build_material_dict(mat)
assert result["alpha"] == pytest.approx(0.0)
def test_roughconductor_alpha_edge_one(self):
"""alpha=1.0(マット)が dict に正しく格納されること.
仕様 D2: alpha 端値の変換
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=1.0)
result = build_material_dict(mat)
assert result["alpha"] == pytest.approx(1.0)
def test_roughconductor_beckmann_distribution_in_dict(self):
"""distribution="beckmann" が dict に格納されること.
仕様 D2: distribution は指定値がそのまま格納される
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=0.5, distribution="beckmann")
result = build_material_dict(mat)
assert result["distribution"] == "beckmann"
def test_roughconductor_no_reflectance_key(self):
"""roughconductor の dict に reflectance キーが含まれないこと.
仕様 D2: roughconductor は reflectance ではなく alpha/distribution
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=0.3)
result = build_material_dict(mat)
assert "reflectance" not in result
# ---------------------------------------------------------------------------
# D3: BlendBSDFConfig (flat) → Mitsuba dict
# ---------------------------------------------------------------------------
class TestBuildMaterialDictBlendFlat:
"""D3 系: BlendBSDFConfig(フラット 2 要素)の dict 変換仕様."""
def _make_cfg(self, weight: float = 0.15, alpha: float = 0.3) -> BlendBSDFConfig:
return BlendBSDFConfig(
type="blendbsdf",
weight=weight,
components=[
DiffuseMaterial(type="diffuse", reflectance=(0.85, 0.85, 0.85)),
RoughConductorBSDFConfig(type="roughconductor", alpha=alpha),
],
)
def test_blendbsdf_type_in_dict(self):
"""BlendBSDFConfig の dict type が "blendbsdf" であること.
仕様 D3: {type: "blendbsdf", ...}
"""
result = build_material_dict(self._make_cfg())
assert result["type"] == "blendbsdf"
def test_blendbsdf_weight_in_dict(self):
"""weight が dict に格納されること.
仕様 D3: {weight: 0.15, ...}
"""
result = build_material_dict(self._make_cfg(weight=0.15))
assert result["weight"] == pytest.approx(0.15)
def test_blendbsdf_has_bsdf_0_key(self):
"""dict に bsdf_0 キーが存在すること.
仕様 D3: {bsdf_0: <diffuse dict>, bsdf_1: <roughconductor dict>}
"""
result = build_material_dict(self._make_cfg())
assert "bsdf_0" in result
def test_blendbsdf_has_bsdf_1_key(self):
"""dict に bsdf_1 キーが存在すること.
仕様 D3: {bsdf_0: <diffuse dict>, bsdf_1: <roughconductor dict>}
"""
result = build_material_dict(self._make_cfg())
assert "bsdf_1" in result
def test_blendbsdf_bsdf_0_is_diffuse_dict(self):
"""bsdf_0 が diffuse の dict であること(component[0] = DiffuseMaterial).
仕様 D3: bsdf_0 = <diffuse dict>
"""
result = build_material_dict(self._make_cfg())
assert result["bsdf_0"]["type"] == "diffuse"
def test_blendbsdf_bsdf_1_is_roughconductor_dict(self):
"""bsdf_1 が roughconductor の dict であること(component[1] = RoughConductorBSDFConfig).
仕様 D3: bsdf_1 = <roughconductor dict>
"""
result = build_material_dict(self._make_cfg())
assert result["bsdf_1"]["type"] == "roughconductor"
def test_blendbsdf_bsdf_0_reflectance_value(self):
"""bsdf_0(diffuse)の reflectance.value が正しいこと.
仕様 D3: 子 bsdf_0 の内容は DiffuseMaterial 変換と同等
"""
result = build_material_dict(self._make_cfg())
assert result["bsdf_0"]["reflectance"]["value"] == pytest.approx([0.85, 0.85, 0.85])
def test_blendbsdf_bsdf_1_alpha_value(self):
"""bsdf_1(roughconductor)の alpha が正しいこと.
仕様 D3: 子 bsdf_1 の内容は RoughConductorBSDFConfig 変換と同等
"""
result = build_material_dict(self._make_cfg(alpha=0.3))
assert result["bsdf_1"]["alpha"] == pytest.approx(0.3)
def test_blendbsdf_bsdf_1_material_is_none(self):
"""bsdf_1(roughconductor)の material が "none" であること.
仕様 D3: 子 roughconductor の material="none" が再帰変換で保持される
"""
result = build_material_dict(self._make_cfg())
assert result["bsdf_1"]["material"] == "none"
def test_blendbsdf_no_components_key_in_dict(self):
"""dict に "components" キーが含まれないこと(Mitsuba は bsdf_0/bsdf_1 形式).
仕様 D3: Mitsuba 形式は bsdf_0/bsdf_1 で components キーは不要
"""
result = build_material_dict(self._make_cfg())
assert "components" not in result
def test_blendbsdf_weight_zero(self):
"""weight=0.0 が dict に正しく格納されること.
仕様 D3: weight 端値の変換
"""
result = build_material_dict(self._make_cfg(weight=0.0))
assert result["weight"] == pytest.approx(0.0)
def test_blendbsdf_weight_one(self):
"""weight=1.0 が dict に正しく格納されること.
仕様 D3: weight 端値の変換
"""
result = build_material_dict(self._make_cfg(weight=1.0))
assert result["weight"] == pytest.approx(1.0)
# ---------------------------------------------------------------------------
# D4: ネスト BlendBSDFConfig → 再帰変換
# ---------------------------------------------------------------------------
class TestBuildMaterialDictBlendNested:
"""D4 系: ネスト BlendBSDFConfig の再帰変換仕様."""
def _make_nested_cfg(self) -> BlendBSDFConfig:
"""BlendBSDFConfig の components[0] にさらに BlendBSDFConfig を持つ 2 段ネスト."""
inner = BlendBSDFConfig(
type="blendbsdf",
weight=0.3,
components=[
DiffuseMaterial(type="diffuse", reflectance=0.9),
RoughConductorBSDFConfig(type="roughconductor", alpha=0.1),
],
)
return BlendBSDFConfig(
type="blendbsdf",
weight=0.05,
components=[
inner,
DiffuseMaterial(type="diffuse", reflectance=0.5),
],
)
def test_nested_bsdf_0_is_blendbsdf_dict(self):
"""ネスト時の bsdf_0 が blendbsdf の dict であること.
仕様 D4: ネスト BlendBSDFConfig も再帰的に正しく変換される
"""
result = build_material_dict(self._make_nested_cfg())
assert result["bsdf_0"]["type"] == "blendbsdf"
def test_nested_bsdf_0_weight_preserved(self):
"""ネストした bsdf_0(blendbsdf)の weight が正しいこと.
仕様 D4: 内部 BlendBSDFConfig の weight が再帰変換で保持される
"""
result = build_material_dict(self._make_nested_cfg())
assert result["bsdf_0"]["weight"] == pytest.approx(0.3)
def test_nested_bsdf_0_inner_bsdf_0_type(self):
"""ネストした bsdf_0.bsdf_0 が diffuse であること.
仕様 D4: 2 段再帰が正しく動作する
"""
result = build_material_dict(self._make_nested_cfg())
assert result["bsdf_0"]["bsdf_0"]["type"] == "diffuse"
def test_nested_bsdf_0_inner_bsdf_1_type(self):
"""ネストした bsdf_0.bsdf_1 が roughconductor であること.
仕様 D4: 2 段再帰が正しく動作する
"""
result = build_material_dict(self._make_nested_cfg())
assert result["bsdf_0"]["bsdf_1"]["type"] == "roughconductor"
def test_nested_bsdf_0_inner_bsdf_1_alpha(self):
"""ネストした bsdf_0.bsdf_1(roughconductor)の alpha が正しいこと.
仕様 D4: 深いネストの alpha 値も保持される
"""
result = build_material_dict(self._make_nested_cfg())
assert result["bsdf_0"]["bsdf_1"]["alpha"] == pytest.approx(0.1)
def test_nested_outer_weight_preserved(self):
"""外側 BlendBSDFConfig の weight が正しいこと.
仕様 D4: 外側の weight は内側の再帰と独立
"""
result = build_material_dict(self._make_nested_cfg())
assert result["weight"] == pytest.approx(0.05)
def test_nested_bsdf_1_is_diffuse_dict(self):
"""外側の bsdf_1 が diffuse の dict であること.
仕様 D4: 外側の非ネスト component は通常通り変換される
"""
result = build_material_dict(self._make_nested_cfg())
assert result["bsdf_1"]["type"] == "diffuse"
# ---------------------------------------------------------------------------
# D5: build_material_dict 公開 API の後方互換
# ---------------------------------------------------------------------------
class TestBuildMaterialDictBackwardCompat:
"""D5 系: build_material_dict の公開 API 後方互換仕様."""
def test_existing_diffuse_material_still_works(self):
"""既存の DiffuseMaterial 入力が従来通り動作すること.
仕様 D5: 公開 API シグネチャは破壊しない
"""
mat = DiffuseMaterial(type="diffuse", reflectance=0.5)
result = build_material_dict(mat)
assert result["type"] == "diffuse"
assert result["reflectance"]["type"] == "rgb"
assert "value" in result["reflectance"]
def test_public_api_accepts_roughconductor(self):
"""公開 API が RoughConductorBSDFConfig を受け付けること.
仕様 D5: 公開 API シグネチャは新型式も受け付ける
"""
mat = RoughConductorBSDFConfig(type="roughconductor", alpha=0.3)
result = build_material_dict(mat)
assert result["type"] == "roughconductor"
def test_public_api_accepts_blend_bsdf(self):
"""公開 API が BlendBSDFConfig を受け付けること.
仕様 D5: 公開 API シグネチャは新型式も受け付ける
"""
mat = BlendBSDFConfig(
type="blendbsdf",
weight=0.15,
components=[
DiffuseMaterial(type="diffuse", reflectance=0.85),
RoughConductorBSDFConfig(type="roughconductor", alpha=0.3),
],
)
result = build_material_dict(mat)
assert result["type"] == "blendbsdf"