"""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:
"""テスト用 SensorConfig を組み立てるヘルパ."""
return SensorConfig(
type="perspective",
position=position,
target=target,
up=up,
fov_deg=fov_deg,
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"])