本ドキュメントは,TEST_12 と同じ 「エラーも警告も出ないまま結果だけが静かに歪む」故障モードが他に残っていないかを一括で 洗い出し,見つかった問題の修正と再発防止テストの常設化を行った記録である(2026-08-30).
TEST_12 のバグ(PLY 自動スムーズ法線 × 評価板の巨大三角形)は,CoV の 6 割超を占める 系統誤差を生みながら Step 8-13 まで気づかれなかった.損失は計算コストではなく, 間違った前提の上に積み上げた考察と設計判断そのものだった.同種の故障は TEST_08(面光源スケール軸バグ)でも起きており, そのときは TEST_07 が誤った出力を「物理的に正しい」と 一度は正当化してしまっている.
このパターンは 2 度繰り返されている.3 度目を防ぐため,レンダリング規約・幾何・単位系・ エネルギー収支を横断的に検査し,検出力を実証したテストだけを通常実行に常設した.
| # | 監査項目 | 結果 | 対応 |
|---|---|---|---|
| 1 | シェーディング法線 == 幾何法線 | 健全(実測逸脱 0.044 度) | 再発防止テストを追加(負のコントロール付き) |
| 2 | 片面 BSDF による無警告の黒化 | 潜在的脆弱性あり | twosided ラップで恒久修正 |
| 3 | メッシュ法線の向き | 健全(裏面ヒット 0 件 / 全 15 STL の巻き順一貫) | 監査関数と回帰テストを追加 |
| 4 | 絶対放射量(単位系・反射率・光源パワー・センサ) | 健全(解析解と 0.2% 一致) | 積分球テストを常設 |
| 5 | 多重反射の打ち切り(max_depth) | 問題あり(基準設計シーンで 9.7% のエネルギー欠損) | 警告を実装.設定変更は要判断 |
| 6 | 面光源のサイズ→軸マッピング(TEST_08 の再発) | 健全だがテストが存在しなかった | 実効的な回帰テストを追加 |
| 7 | レンダ経路のテスト(slow 層) | 長期間全滅していた | LLVM 初期化順序のバグを修正 |
| 8 | 3D 可視化のテスト(slow 層) | 全滅(Smart App Control が VTK の DLL をブロック) | vtk==9.4.2 に固定して解消.import 監視テストを常設 |
読み込み済み Mitsuba シーンの空洞内部からレイを飛ばし,si.sh_frame.n(シェーディング法線)と
si.n(幾何法線)を実測比較する監査関数 scene/audit.py: audit_scene_normals を追加した.
基準設計シーン(configs/scene_baseline.yaml,実 STL 8 個)での実測:
face_normals: True が Mitsuba レベルで実効)既存の再発防止テストは scene dict に face_normals キーが存在することしか見ていなかった.
本監査は実際にレンダラへ読み込ませて挙動を測るので,キーの意味が変わった場合も検出できる.
負のコントロール: face_normals を外すと逸脱が 30 度以上に跳ね上がることをテストで確認している.
検出力を実証できない検査は再発防止にならないため,この対照実験は必ず維持する.
Mitsuba の diffuse / roughconductor は片面 BSDF であり,法線が裏返った面は
入射光を一切反射せずエラーも警告もなく真っ黒になる.SmTIAS は LED 直接光が評価板に
届かない間接照明のみの構成(SPEC_03)なので,
内壁が 1 面でも黒化すれば輝度分布が静かに壊れる.
現行の STL は全て法線が正しいため実害は出ていなかったが,STL を差し替えた瞬間に 発現しうる「気づけない」構造だった.積分球の解析解で定量化した結果:
| 法線 | BSDF | 観測放射輝度 | 解析解比 |
|---|---|---|---|
| 内向き(正常) | diffuse | 0.407280 | 0.9996 |
| 内向き(正常) | twosided | 0.407280 | 0.9996(完全に同一=無影響) |
| 外向き(裏返り) | diffuse | 0.000000 | 0.0000 |
| 外向き(裏返り) | twosided | 0.407475 | 1.0001 |
修正: scene/materials.py に build_scene_bsdf_dict を追加し,不透明反射材
(diffuse / roughconductor / 不透明のみで構成される blendbsdf)を twosided で包む.
透過を伴う誘電体は両面挙動が本質なのでラップしない.
実シーン(scene_baseline,spp=256 / max_depth=16)での修正前後比較は
mean +0.015% / CoV -0.009%(MC ノイズの範囲内)で,過去の結果に影響しないことを確認した.
内面反射率 rho の閉じた球殻(積分球)は厳密な閉形式解を持つ.全放射束 Phi の光源を
内部に置くと壁面の全照度は E = Phi / (A (1 - rho))(A = 4 pi R^2)となり,
直接光が届かない領域の壁面放射輝度は L = rho^2 Phi / (pi A (1 - rho)) になる.
これを 1 本のテストで突き合わせると,単位系(mm→m)・反射率・発光面の放射束・ センサ校正・多重反射の収束をまとめて検証できる.いずれかが壊れていれば一致しない.
実測(rho=0.8・R=100mm・発光面 4x4mm・max_depth 収束後):
| max_depth | 観測 L | 解析解比 |
|---|---|---|
| 8 | 0.300061 | 0.7365 |
| 16 | 0.388804 | 0.9543 |
| 32 | 0.406155 | 0.9969 |
| 64 | 0.406630 | 0.9980 |
| 128 | 0.406630 | 0.9980 |
収束値は解析解の 0.998(残差 0.2% は icosphere が内接多面体であることによる面積欠損). パイプラインの絶対放射量は正しいと結論できる.
上表が示すとおり max_depth の打ち切りは無視できない.基準設計シーンは
LED 直接光が評価板に届かない間接照明のみの構成であり,輝度は白筐体内壁
(rho=0.85)の多重反射の累積だけで決まるため,影響はより大きい.
configs/scene_baseline.yaml(spp=256)での実測:
| max_depth | mean | CoV | 最深値比(mean) |
|---|---|---|---|
| 8 | 12.634 | 0.26035 | 0.6176 |
| 16(現行設定) | 18.476 | 0.25270 | 0.9032 |
| 32 | 20.334 | 0.25181 | 0.9940 |
| 64 | 20.457 | 0.25181 | 1.0000 |
現行の max_depth=16 は収束値の 90.3% しかエネルギーを拾えていない(約 10% の過小評価).
ただし CoV への影響は +0.35% と軽微である.これまでの検討の主題は形状指標 (CoV・勾配・C/P・NCC)であり,絶対輝度は実測へのアンカー(mean anchor / OETF 露出ソルバ)で 吸収されるため,過去の形状に関する結論は影響を受けない.
一方で以下には影響する.
対応: scene/audit.py: check_truncation_bias を実装し,build_scene 時に
警告ログを出す.基準設計シーン(rho=0.85)では以下が出力される.
render.max_depth=16 は最大反射率 0.850 の多重反射を拾いきれていません (相対エネルギー欠損の見積もり 8.7%).絶対輝度が系統的に過小評価されます. max_depth >= 30 を推奨します.
欠損の見積もり式 rho^(max_depth - 1) は実測とよく一致する(積分球 rho=0.8: 予測 3.5% / 実測 4.6%,
基準設計 rho=0.85: 予測 8.7% / 実測 9.7%).
⚠
configs/*.yamlのmax_depthは未変更.値を上げると絶対輝度の基準が動き LED radiance の再校正が必要になるため,設定変更は人間の判断に委ねる. 計算コストは max_depth=16 で 80 秒 / 64 で 96 秒(spp=256・512x512)と +20% 程度.
TEST_08 のバグ(発光面の高さを縦 Y ではなく奥行き Z に入れていた)に対する
実効的な回帰テストが存在しなかった.既存テストは to_world キーの存在と,
position_scale を変えたときに文字列表現が変わることしか検証しておらず,
軸を入れ替えても検出できない.
単位図形の頂点を実際に to_world で変換し,世界座標での寸法を検証するテストを追加した
(現状は健全: size=(2.8, 3.5) が X=2.8mm / Y=3.5mm / Z=0 に正しく対応).
slow マークのテストを実行したところ 47 件中 47 件が失敗していた.
うち 8 件はレンダリング経路で,原因は LLVM 初期化の順序バグ.
render/runner.py は mi.set_variant の前に ensure_drjit_libllvm_path() を呼んで
DRJIT_LIBLLVM_PATH を自動探索する設計だったが,scene/lights.py と scene/sensors.py が
モジュール読み込み時に mitsuba を import するため,runner を import した時点で
既に Dr.Jit の LLVM 初期化が走っており,環境変数の設定が手遅れになっていた.
開発機では環境変数が User scope に永続設定されているため症状が隠れていたが, 環境変数が無い環境ではレンダリングが一切できない(ENV_02 が 案内する自動探索が機能していなかった).
修正: scene/lights.py / scene/sensors.py の mitsuba import 直前と,
パッケージの __init__.py で ensure_drjit_libllvm_path() を呼ぶ.これで 8 件が復活した.
教訓: 物理的健全性の検査を
slowに隔離すると,通常実行から外れて壊れても気づかない. 本監査で追加した積分球テストは 1 本 0.2 秒(32x32 / spp=128)で合計 1.5 秒程度なので,slowを付けず通常実行に含めている.
残る slow 失敗 39 件は全て tests/visualize/ と tests/cli/test_visualize_cli.py で,
sm-visualize も実行できない状態だった.
依存関係の破損ではなく,OS のアプリケーション制御ポリシーが VTK の DLL を弾いていた.
DLL load failed while importing vtkRenderingVolume: アプリケーション制御ポリシーによってこのファイルがブロックされました。
開発機の Smart App Control が**オン(強制モード)**になっていた.
| 確認項目 | 値 |
|---|---|
HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy\VerifiedAndReputablePolicyState | 1(オン・強制) |
Win32_DeviceGuard.UsermodeCodeIntegrityPolicyEnforcementStatus | 2(強制中) |
Smart App Control は署名の有無ではなくファイル単位のクラウドレピュテーションで判定する.
そのため vtk 9.6.1 では 209 個の DLL のうち 14 個だけがブロックされ,残りは通るという
状態になっていた.ブロックされたのは以下(vtkRenderingAnnotation はスカラーバー・座標軸,
vtkInteractionWidgets はウィジェットを担い,どちらも PyVista の描画に必須).
vtkRenderingVolume vtkRenderingVolumeOpenGL2 vtkRenderingVolumeAMR vtkRenderingAnnotation vtkInteractionWidgets vtkInteractionImage vtkImagingColor vtkRenderingVR vtkRenderingOpenXR vtkRenderingOpenXRRemoting vtkTestingRendering vtkViewsInfovis vtkWebCore vtkWebGLExporter
依存の破損でないことの根拠:
.pyd はすべて正しく存在していたvtkRenderingCore も弾かれた vtkRenderingVolume もどちらも未署名
(Get-AuthenticodeSignature).署名の有無は判定基準ではないuv.lock の vtk 9.6.1 は Step 7(2026-05-17)から変わっておらず,
Dependabot による更新も入っていない最初に観測した症状は
ImportError: Failed to load vtkCommonMath: No module named vtkmodules.vtkCommonCore
で,上記のアプリケーション制御エラーとは別のメッセージだった.両者の関係は完全には
確定していない.事実として確認できたのは以下である.
vtkCommonCore はブロックされた 14 個に含まれず,単独では import できたimport pyvista が一度失敗し,のちに成功した(判定に揺れがある)vtkmodules は独自のローダ(vtkmodules/__init__.py の exec_module)で依存モジュールを
読み込むため,DLL がブロックされた事実を握りつぶして「モジュールが無い」という
誤解を招くメッセージに化けさせた二次症状と推定しているが,機構までは確認していない.
最初の切り分けで「VTK のインストール破損」と誤診したのはこのメッセージが原因であり,
ネイティブ拡張の ImportError は表示されたメッセージを額面どおり受け取らないという
教訓として記録しておく.
より長く配布されレピュテーションが確立しているバージョンへ下げる方針を採った
(pyvista 0.48.0 の要求は vtk>=9.2.2, <9.7.0, !=9.4.1).
vtk==9.4.2 では 147 モジュール全てが読み込め,ブロック 0 件.sm-visualize の
3D 表示(筐体・LED マーカー 18 個・センサマーカー・座標軸)も復旧し,
slow の可視化テスト 54 件が全て通るようになった.
pyproject.toml に固定理由をコメントで残している(/deps-update や Dependabot が
9.6 系へ戻して静かに再破損するのを防ぐため).
Smart App Control の無効化は一度オフにすると Windows を再インストールしない限り 再びオンにできないため採用しなかった.可視化ツール 1 本のために恒久的に マルウェア保護を手放すのは釣り合わない.
破損は 2026-08-17 から 8-30 まで約 2 週間気づかれなかった.可視化テストが全て
slow マークで通常実行から外れていたことが原因であり,検査 7(LLVM 初期化バグで
レンダテストが長期間全滅していた件)とまったく同じ構図である.
そこで tests/visualize/test_vtk_import_guard.py を slow を付けずに追加した.
pyvista とブロック実績のあるモジュールの import,および固定バージョンの確認のみを行い,
レンダリングはしないので 1 秒未満で完了する.
軸対称シーンの対称性テストは検出力を実証できなかったため採用しなかった.
立方体空洞+軸上点光源+軸上 orthographic カメラで,face_normals の有無による
象限比を比較したところ 1.0020 vs 1.0028 とほぼ差が出なかった(一方で mean は
394.4 → 300.2 と 24% 変化した).TEST_11 で異方性が顕在化したのは評価板メッシュの
非対称な三角形分割が効いていたためで,合成シーンでは再現できない.
検出できない検査をテストとして常設すると「検査済み」という誤った安心を与えるため, 対称性チェックは手動の診断手段として残し,自動テストには含めない.
すべて通常実行(uv run pytest)に含まれる.合計約 2 秒.
| テストファイル | 内容 | 負のコントロール |
|---|---|---|
tests/scene/test_audit.py | 打ち切りバイアスの見積もり・警告生成 | — |
tests/scene/test_audit_normals.py | シェーディング法線 == 幾何法線 / 裏面ヒット 0 | あり(face_normals を外す・メッシュを裏返す) |
tests/scene/test_materials_twosided.py | 不透明反射材の twosided ラップと誘電体の除外 | あり |
tests/scene/test_lights_axis_mapping.py | 面光源サイズの世界座標軸マッピング(TEST_08) | — |
tests/render/test_physics_validation.py | 積分球の解析解一致・線形性・法線裏返り耐性・打ち切り | あり |
tests/scene/test_audit_normals.py には基準設計シーン(実 STL 8 個)に対する
slow マークのテストも含む.
twosided 化は無影響(MC ノイズ以内)で,
max_depth の打ち切りが CoV に与える影響も +0.35% にとどまる.max_depth を揃えるだけでなく,反射回数分布の差による系統差が残ることに留意する.max_depth を 32〜64 に引き上げるかの判断(引き上げる場合は LED radiance の再校正が必要)