# 面光源スケール軸バグの発見と修正 (Area-Light Scale-Axis Bug)

本ドキュメントは，**実機の単灯（下/左/右）実測データとシミュレーションを 1 灯ずつ突き合わせた結果，シミュ側の面光源（area emitter）変換に座標軸の取り違えバグが見つかり，これが「下灯がほかの 2 倍強い」という長年の不一致の主因だった**ことの発見・原因・修正・検証を記録する．

> ⚠️ 本件により [TEST_07 LED グループ別動作確認](TEST_07_LEDグループ別動作確認.md) の結論「下灯 2.1× は物理的に正しい／意図どおり」は**誤り**であったことが判明した（TEST_07 はバグ込みの出力を物理的に正当化してしまっていた）．TEST_07 冒頭にも訂正注記を追加済み．

## 発端 — 実機単灯データとの突き合わせ

実機（SmTIAS-Evaluation）で 3 灯を 1 灯ずつ単独点灯して撮影した白板 ROI 輝度マップ一式を入力として受領した（`lightsim/`：`luminance_maps/{bottom,left,right}_*.npy`，0〜255 linear / Rec.709 luma / LSC 補正済 / portrait）．

ユーザー観察：**「シミュでは下側がほかの約 2 倍になっているが，実機では 3 灯ほぼ均等」**．これを灯ごとに分解して原因を特定するのが目的．

灯ごとの白板 ROI 平均輝度（実機）と，既存シミュ単灯出力（`output/led_group_sanity/`，spp=1024）を比較：

| 灯 | 実機 mean | 実機 比 | シミュ(バグ) mean | シミュ(バグ) 比 |
| --- | --- | --- | --- | --- |
| bottom | 57.75 | **1.00（最暗）** | 98.82 | **2.10** |
| left | 61.46 | 1.06 | 47.08 | 1.00 |
| right | 63.55 | 1.10 | 47.10 | 1.00 |

→ **実機は 3 灯ほぼ均等で下灯が最暗**．シミュは**下灯だけ 2.10×**で，しかも左右が 47.08 vs 47.10 と完全対称．傾向が逆転している．

LED 定義（`configs/leds_extracted.yaml`）は全 18 灯が `direction:(0,0,-1)`・`radiance:2807.25` で完全同一．**LED の位置・向き・強度は Fusion 360 STL 由来で実機どおり**であり，radiance 差では説明できない．したがって原因は LED 以外（周囲幾何・変換処理）にあると絞り込んだ．

## 原因 — 面光源スケールの軸取り違え

### Mitsuba の rectangle 仕様（実機検証）

Mitsuba 3 の `rectangle` 形状を実際に生成して確認した：

```
中心法線 n = (0, 0, 1)        # 面は +Z を向く
面内範囲 = X∈[-1,1], Y∈[-1,1]  # XY 平面に寝た 1 辺 2 の正方形
Z 方向 = 厚みゼロ（潰れている）
```

→ **rectangle は「XY 平面・法線 +Z」**．サイズを与えるには **横 = X 軸，高さ = Y 軸**をスケールしなければならない（Z は潰れているのでスケールしても無意味）．

### バグ箇所

`src/sm_tias_lightsim/scene/lights.py` `_build_area_light_transform`：

```python
# 修正前（バグ）
scale = mi.ScalarTransform4f().scale([sw / 2.0, 1.0, sh / 2.0])
#                                       横X    縦Y     奥Z
#   横(X) = sw/2     … OK
#   縦(Y) = 1.0      … ★ スケールされず基準のまま = ±1 m（2000 mm）
#   奥(Z) = sh/2     … 高さを“潰れた厚み方向”に入れてしまい無効
```

コード上のコメントは「rectangle は **XZ 平面**上の正方形（**Y 軸正方向が前面**）」と書かれていたが，実体は XY 平面・+Z 前面．**前提を 1 軸勘違いしたまま実装されており，高さ `sh` を縦(Y) ではなく奥行き(Z) に入れ，縦(Y) は無補正のまま放置**されていた．

単位はメートル系（mm→m に position_scale=0.001 済）なので，縦が無補正 = **±1 m**．結果：

> 実物 **2.8 × 3.5 mm** の小さな LED が，シミュ内では **「幅 ~3 mm × 高さ 2000 mm」の細長い巨大ストリップ**になっていた．

法線（発光方向）は look_at で正しく `direction` を向くため，発光の向き自体は正しい（このバグでは形状サイズだけが壊れる）．

### なぜ「下灯だけ 2 倍」になったか

全 18 灯が同一の look_at（direction=(0,0,-1), up=(0,1,0)）を使うため local Y→world Y．つまり**全 LED が world Y 方向に 2 m 伸びた縦ストリップ**になる．各灯列の横位置（X）と評価板の関係：

| 灯 | LED 横位置 X | 評価板 X 範囲 [-49.5, +49.5] との関係 |
| --- | --- | --- |
| **bottom** | X∈[-10, +18] | **板中央の真裏**（中央に密集） |
| left | X = -66 | 板の外（左端より 16.5 mm 外側）＋側壁/カバー脇 |
| right | X = +66 | 板の外（右端より 16.5 mm 外側）＋側壁/カバー脇 |

2 m 縦長化により，**板中央の真裏にある下灯だけが板中央を効率よく照らし**，板の外側にある左右灯は伸びた光の多くが壁/カバーに当たって板に届きにくい．本来の小さな ~3.5 mm 面光源ならこの縦長効果は生じず偏らない．**バグによる巨大化が，たまたま中央裏にある下灯を過剰に効かせて 2.1× を生んでいた**．

TEST_07 はこの 2.1× を「直接光効率の幾何的優位＝意図した設計」と解釈してしまったが，実際は**変換バグによるアーティファクト**だった．

## 修正

スケールの軸を 1 か所入れ替えるのみ：

```python
# 修正後
scale = mi.ScalarTransform4f().scale([sw / 2.0, sh / 2.0, 1.0])
#                                       横X    縦Y     奥Z
#   横(X) = sw/2  縦(Y) = sh/2  奥(Z) = 1.0（潰れた厚みは無補正でよい）
```

高さ `sh` を，誤って入れていた奥行き(Z) から本来の縦(Y) へ移しただけ．これで LED が実物どおりの ~2.8 × 3.5 mm に戻る．

## 検証結果（spp=1024, max_depth=16 確定）

修正後に 3 単灯＋全灯合成を再レンダ（単灯 `output/sanity_fix_1024/`，全灯 `output/baseline_fix_1024/`，いずれも **spp=1024 / max_depth=16**）．暫定 spp=256 出力は `output/led_group_sanity_fix/`．

### ① 灯間バランス（mean 比，spp=1024）

| 灯 | バグ 比 | **修正後 比** | 実機 比 |
| --- | --- | --- | --- |
| bottom | 2.099 | **1.000（最暗）** | 1.000（最暗）|
| left | 1.000 | **1.028** | 1.064 |
| right | 1.000 | **1.026** | 1.100 |

→ **下灯 2 倍は消滅し，3 灯が ±3% で均等化**．さらに**「下灯が最暗」という実機の順序まで一致**した．灯間の振幅バランスは実機パターンに乗った．

### ② 各灯の形（peak/mean，spp=1024）

| 灯 | バグ peak/mean | 修正後 peak/mean | 実機 peak/mean |
| --- | --- | --- | --- |
| bottom | 1.60 | 1.78 | 1.22 |
| left | 1.78 | 1.69 | 1.27 |
| right | 1.73 | 1.75 | 1.20 |

- 絶対 mean は ~98/47 → ~4.5 へ低下：発光面積が 2.8mm×2000mm → 2.8mm×3.5mm へ約 570 倍縮小したため．絶対スケールは下流の露出ソルバ / mean anchor 正規化で吸収されるため灯間比・形が本質．
- **peak/mean はなお実機（1.2 台）に届かず（修正後 1.7 台）**＝各単灯フットプリントの**拡散不足**が残る（max_depth=8 の暫定では 2.9 と更に尖っていたが，depth=16 のバウンス増で 1.7 台まで滑らかになった）．

### ③ 線形性（単灯和 ≈ 全灯）

単灯 3 本の和と全灯合成の ROI mean を比較：**相対誤差 0.34%**．Mitsuba の理論線形性どおりで，レンダの健全性を確認．

### ④ 全灯合成 CoV（公式 sm-evaluate, ROI image_pixel 464×450）

| シーン | linear CoV | 対実機残差 | center/周辺比 | gradient |
| --- | --- | --- | --- | --- |
| 修正前（`baseline_v3_leds`，バグ） | 0.1815 | 2.61× | 1.318 | 24.13 |
| **修正後（`baseline_fix_1024`）** | **0.1641** | **2.36×** | 1.311 | 23.71 |
| 実機 | 0.0695 | — | — | — |

（参考：OETF/sRGB ドメインでは修正後 CoV=0.0790）

→ **合成 CoV は 0.1815→0.1641（残差 2.63×→2.36×）と小改善にとどまる**．center/周辺比・勾配はほぼ不変．灯間バランスは直ったが，合成の主な不均一（中央明・縦勾配）は**灯間バランスではなく各単灯フットプリントの拡散不足**に由来するため，本バグ修正だけでは合成 CoV は大きく動かない（ユーザーの事前直感どおり）．

## バグ実在の独立検証（報告用の一次証拠）

「全 LED が高さ 2m のストリップ化していた」という主張を，可視化マーカーの再現に頼らず**修正前の実コードと Mitsuba 自身の測定**で独立に裏付けた．3 重の証拠：

### 証拠1 — 修正前の実コード（git で確定）

修正コミット `84b0ee5` の diff より，修正前の実行は文字どおり以下だった：

```python
# src/sm_tias_lightsim/scene/lights.py  _build_area_light_transform（修正前）
scale = mi.ScalarTransform4f().scale([sw / 2.0, 1.0, sh / 2.0])
#                                       横X    縦Y     奥Z
#   縦 Y を 1.0 のまま（無補正）＝基準矩形 ±1（単位 m）のまま，高さ sh を潰れた Z に誤って入れていた
```

### 証拠2 — Mitsuba 自身が報告する矩形サイズ

修正前の式をそのまま使って実 LED（`led_bottom_0`, center=(-10,-71,-66.1)mm, size=2.8×3.5mm, dir=(0,0,-1)）の `rectangle` を生成し，**Mitsuba の `shape.bbox()` に世界サイズを報告させた**結果：

| | X 幅 | **Y 高さ** | Z | 面積 |
| --- | --- | --- | --- | --- |
| **修正前 `scale([sw/2, 1.0, sh/2])`** | 2.80 mm | **2000.00 mm** | 0 | 5600 mm² |
| 修正後 `scale([sw/2, sh/2, 1.0])` | 2.80 mm | 3.50 mm | 0 | 9.8 mm²（実物一致）|

修正前の bbox は **Y[-1.071m, +0.929m]**＝LED 中心 y=-71mm を中心に **±1000mm，高さちょうど 2000mm**．実物 9.8mm² に対し **約 570 倍の面積（5600mm²）の 2m ストリップ**であったことが，Mitsuba 自身の測定で確定．

### 証拠3 — この変換が実レンダで使われていた経路

可視化専用ではなく，本線のレンダリングで使われていたことを確認：

```
builder.py:139 build_light_dict() → lights.py:98 _build_area_light_transform()（バグの scale）
  → scene_dict → mi.render()
```

→ **結論: 「にわかに信じがたいが本当にそうなっていた」が正しい．** 全 18 LED が幅 2.8mm × 高さ 2000mm の縦ストリップとしてレンダされていた．検証スクリプトは一時的に実行（`mi.ScalarTransform4f` で修正前後の式を適用し `rectangle.bbox()` を比較）．

## 修正前後のカラーマップ比較 — 「バグが壊したのは形ではなく灯間バランス」

修正前（バグ）と修正後のレンダ結果（輝度マップ）を，単灯3＋合成で並べて比較した．**正規化（形）と共通スケール（灯間明るさ）の 2 視点**で見ると，バグの主効果が明確になる．

### 視点 A — mean 正規化（＝「形」の比較）

各マップを自身の mean で割って形だけを比べる（図 `output/_slide_panels/before_after_shape.png`）：

| 灯 | 修正前 peak/mean | 修正後 peak/mean |
| --- | --- | --- |
| bottom | 1.56 | 1.71 |
| left | 1.74 | 1.62 |
| right | 1.69 | 1.68 |
| **combined** | **1.54** | **1.54** |

→ **正規化した「形」は修正前後でほとんど変わらない**（合成は 1.54→1.54 で不変）．バグの主効果は形ではなかった．

### 視点 B — 共通スケール（＝「灯間の明るさ」の比較）

3 単灯を「left LED の mean = 1.0」基準の共通スケールで並べる（図 `output/_slide_panels/before_after_amplitude.png`）：

| 灯 | 修正前（left=1.0） | 修正後（left=1.0） |
| --- | --- | --- |
| bottom | **2.10** | **0.97** |
| left | 1.00 | 1.00 |
| right | 1.00 | 1.00 |

→ **バグが実際に壊していたのはこれ**．修正前は下灯だけ 2.10× 突出，修正後は 3 灯均等（下灯やや最暗で実機順序一致）．

### 結論

**バグ修正で直ったのは「下灯が 2 倍明るい」という灯間バランスであり，各灯の分布の“形”自体は元々あまり変わっていない**（だから合成 CoV も 0.1815→0.1641 の小改善にとどまった）．「カラーマップの形状」という観点では正規化すると修正前後で大差なく，違いは**明るさの偏り**に出ていた．形の残差は別問題（[TEST_09](TEST_09_照明分布の形不一致の原因切り分け.md) で B1 筐体幾何に絞り込み中）．

## 学びと残課題

1. **灯間の量的バランス問題は解決**：下灯 2.1× は変換バグのアーティファクトであり，修正で実機どおり（3 灯均等・下灯最暗）になった．
2. **論点が移動**：残る不一致は「灯間バランス」ではなく**各単灯フットプリントの拡散不足（peak/mean がシミュ 2.9 vs 実機 1.2）**．候補は LED 配光 ξ（指向の広がり）・ディフューザーのモデル化・面光源サイズの実測整合など．
3. **最終目標（3 灯合成 CoV）への効果は小さい（確定）**：spp=1024 で合成 CoV は 0.1815→0.1641（残差 2.63×→2.36×）の小改善にとどまった．残差の主因は灯間バランスではなく**各単灯フットプリントの拡散不足**であり，本修正だけでは合成 CoV は大きく動かない．次の本筋は配光 ξ／ディフューザー拡散のモデル化．
4. **過去の正当化に注意**：TEST_07 のように「シミュ出力を物理で後付け説明」すると，変換バグを設計意図と取り違える危険がある．実機グラウンドトゥルースとの灯ごと突き合わせが有効だった．

## 関連ドキュメント

- [TEST_07 LED グループ別動作確認](TEST_07_LEDグループ別動作確認.md) — バグ込み出力に基づく旧結論（本件で訂正）
- [TEST_01 キャリブレーション検証履歴](TEST_01_キャリブレーション検証履歴.md) — Step 8 系残差 2.63× の経緯
- [TEST_02 シミュレーション実行記録](TEST_02_シミュレーション実行記録.md) — `output/led_group_sanity{,_fix}/` の索引
- 修正箇所: `src/sm_tias_lightsim/scene/lights.py` `_build_area_light_transform`
- 実機入力: `lightsim/`（単灯実測．出典 SmTIAS-Evaluation TEST_04）
- LED 幾何の 3D 目視確認ツール: `scripts/show_led_geometry.py`（LED を実サイズ長方形＋発光方向矢印で表示．`--buggy` で修正前の 2m ストリップ形を再現，`--screenshot` で 4 アングル保存）．修正前後の比較図 `output/_slide_panels/led_geom{,_buggy}_{iso,front_XY,side_XZ,top_YZ}.png`
