Newer
Older
SmTIAS-LightSim / tests / scene / test_materials_blend_bsdf.py
"""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"