"""scene/sensors.py の仕様ベーステスト.
仕様出典:
- docs/04_SPEC/SPEC_02_シーン定義仕様.md(sensor セクション)
- docs/03_PLAN/PLAN_02_開発ステップ.md(Step 4: scene/sensors.py の役割)
build_sensor_dict の戻り値 dict 構造のみを検証する.Mitsuba の load_dict は呼ばない.
ただし mi.ScalarTransform4f を内部で使うため mitsuba のインポートは発生する.
"""
from __future__ import annotations
import pytest
from sm_tias_lightsim.scene.schema import FilmConfig, SensorConfig
from sm_tias_lightsim.scene.sensors import build_sensor_dict
def _make_sensor(
*,
position=(0.0, 0.0, 100.0),
target=(0.0, 0.0, 0.0),
up=(0.0, 1.0, 0.0),
fov_deg=60.0,
width=640,
height=480,
rfilter="gaussian",
) -> SensorConfig:
"""テスト用 perspective SensorConfig を組み立てるヘルパ."""
return SensorConfig(
type="perspective",
position=position,
target=target,
up=up,
fov_deg=fov_deg,
film=FilmConfig(width=width, height=height, rfilter=rfilter),
)
def _make_ortho_sensor(
*,
position=(0.0, 0.0, 100.0),
target=(0.0, 0.0, 0.0),
up=(0.0, 1.0, 0.0),
ortho_scale=(50.0, 52.0),
width=640,
height=480,
rfilter="gaussian",
) -> SensorConfig:
"""テスト用 orthographic SensorConfig を組み立てるヘルパ."""
return SensorConfig(
type="orthographic",
position=position,
target=target,
up=up,
fov_deg=None,
ortho_scale=ortho_scale,
film=FilmConfig(width=width, height=height, rfilter=rfilter),
)
# ---------------------------------------------------------------------------
# type
# ---------------------------------------------------------------------------
def test_sensor_type_is_perspective():
"""build_sensor_dict の戻り値 type が "perspective" であること.
仕様: sensor.type は perspective のみ対応 (SPEC_02 sensor セクション)
"""
sensor = _make_sensor()
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["type"] == "perspective"
# ---------------------------------------------------------------------------
# fov
# ---------------------------------------------------------------------------
def test_sensor_fov_matches_fov_deg():
"""fov_deg がそのまま fov キーに格納されること.
仕様: fov_deg → fov (SPEC_02 sensor セクション)
"""
sensor = _make_sensor(fov_deg=45.0)
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["fov"] == pytest.approx(45.0)
# ---------------------------------------------------------------------------
# film
# ---------------------------------------------------------------------------
def test_sensor_film_width_and_height():
"""film の width / height が正しく dict に格納されること.
仕様: film.width, film.height (SPEC_02 sensor セクション)
"""
sensor = _make_sensor(width=1920, height=1080)
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["film"]["width"] == 1920
assert result["film"]["height"] == 1080
def test_sensor_film_type_is_hdrfilm():
"""film の type が "hdrfilm" であること.
仕様: Mitsuba の HDR フィルム型 (SPEC_01 render/output 責務)
"""
sensor = _make_sensor()
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["film"]["type"] == "hdrfilm"
def test_sensor_film_rfilter_gaussian():
"""rfilter が "gaussian" のとき rfilter dict の type が "gaussian" になること.
仕様: film.rfilter (SPEC_02 sensor セクション)
"""
sensor = _make_sensor(rfilter="gaussian")
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["film"]["rfilter"]["type"] == "gaussian"
def test_sensor_film_rfilter_box():
"""rfilter が "box" のとき rfilter dict の type が "box" になること.
仕様: film.rfilter: gaussian | box | tent (SPEC_02 sensor セクション FilmConfig)
"""
sensor = _make_sensor(rfilter="box")
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["film"]["rfilter"]["type"] == "box"
# ---------------------------------------------------------------------------
# to_world
# ---------------------------------------------------------------------------
def test_sensor_to_world_key_exists():
"""build_sensor_dict の戻り値に to_world キーが存在すること.
仕様: perspective sensor は to_world で視点変換を持つ (SPEC_01 scene/sensors 責務)
"""
sensor = _make_sensor()
result = build_sensor_dict(sensor, position_scale=0.001)
assert "to_world" in result
def test_sensor_position_scale_affects_to_world():
"""position_scale が異なると to_world が変わること(文字列化で差分確認).
仕様: position_scale で position/target が変換される (SPEC_01 scene/sensors 責務)
"""
sensor = _make_sensor(position=(0.0, 0.0, 100.0), target=(0.0, 0.0, 0.0))
result_mm = build_sensor_dict(sensor, position_scale=0.001)
result_m = build_sensor_dict(sensor, position_scale=1.0)
assert str(result_mm["to_world"]) != str(result_m["to_world"])
# ---------------------------------------------------------------------------
# sampler
# ---------------------------------------------------------------------------
def test_sensor_sampler_type_is_independent():
"""sampler の type が "independent" であること.
仕様: spp は render 時に渡す (runner.py 設計)
"""
sensor = _make_sensor()
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["sampler"]["type"] == "independent"
# ---------------------------------------------------------------------------
# 境界値: 様々な position_scale
# ---------------------------------------------------------------------------
def test_sensor_position_scale_1_leaves_position_in_to_world():
"""position_scale=1.0 のとき position_scale=0.001 と to_world が異なること.
座標が 1000 倍差があれば to_world 行列も異なるはず.
"""
sensor_mm = _make_sensor(position=(0.0, 0.0, 500.0), target=(0.0, 0.0, 0.0))
result_mm = build_sensor_dict(sensor_mm, position_scale=0.001)
result_m = build_sensor_dict(sensor_mm, position_scale=1.0)
assert str(result_mm["to_world"]) != str(result_m["to_world"])
# ---------------------------------------------------------------------------
# orthographic センサ対応(Step 8 追加)
# ---------------------------------------------------------------------------
def test_ortho_sensor_type_is_orthographic():
"""orthographic SensorConfig の build_sensor_dict 戻り値 type が "orthographic" であること.
仕様: sensor.type = orthographic → Mitsuba sensor dict の type = "orthographic"
(sensors.py build_sensor_dict 分岐)
"""
sensor = _make_ortho_sensor()
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["type"] == "orthographic"
def test_ortho_sensor_has_no_fov_key():
"""orthographic センサの dict に "fov" キーが存在しないこと.
仕様: orthographic では fov は不要(Mitsuba orthographic は scale で撮影範囲を決める)
(sensors.py build_sensor_dict 分岐)
"""
sensor = _make_ortho_sensor()
result = build_sensor_dict(sensor, position_scale=0.001)
assert "fov" not in result
def test_ortho_sensor_has_to_world_key():
"""orthographic センサの dict に "to_world" キーが存在すること.
仕様: orthographic sensor は look_at × scale で to_world を生成する
(sensors.py build_sensor_dict 分岐)
"""
sensor = _make_ortho_sensor()
result = build_sensor_dict(sensor, position_scale=0.001)
assert "to_world" in result
def test_ortho_sensor_has_film_and_sampler():
"""orthographic センサの dict に "film" と "sampler" が含まれること.
仕様: perspective / orthographic 共通で film: hdrfilm, sampler: independent を持つ
(sensors.py build_sensor_dict)
"""
sensor = _make_ortho_sensor()
result = build_sensor_dict(sensor, position_scale=0.001)
assert result["film"]["type"] == "hdrfilm"
assert result["sampler"]["type"] == "independent"
def test_ortho_sensor_to_world_scale_applied():
"""ortho_scale=(50, 52) + position_scale=0.001 のとき,perspective センサと to_world が異なること.
仕様: orthographic の to_world は look_at × scale(sx*ps, sy*ps, 1.0) で構築される
perspective の to_world は look_at のみのため,両者は必ず異なる
(sensors.py build_sensor_dict 分岐)
"""
position = (0.0, 0.0, 100.0)
target = (0.0, 0.0, 0.0)
up = (0.0, 1.0, 0.0)
ortho_sensor = _make_ortho_sensor(position=position, target=target, up=up, ortho_scale=(50.0, 52.0))
persp_sensor = _make_sensor(position=position, target=target, up=up, fov_deg=60.0)
ortho_result = build_sensor_dict(ortho_sensor, position_scale=0.001)
persp_result = build_sensor_dict(persp_sensor, position_scale=0.001)
# scale が追加された分,to_world 文字列表現が異なるはず
assert str(ortho_result["to_world"]) != str(persp_result["to_world"])
def test_ortho_sensor_different_scales_produce_different_to_world():
"""ortho_scale が異なると to_world が変わること.
仕様: ortho_scale の値が to_world の scale 変換に反映される
(sensors.py build_sensor_dict: sx_m = ortho_scale[0] * position_scale)
"""
sensor_small = _make_ortho_sensor(ortho_scale=(25.0, 26.0))
sensor_large = _make_ortho_sensor(ortho_scale=(50.0, 52.0))
result_small = build_sensor_dict(sensor_small, position_scale=0.001)
result_large = build_sensor_dict(sensor_large, position_scale=0.001)
assert str(result_small["to_world"]) != str(result_large["to_world"])