Sausage Erectos X Y軸は下、Z軸は手前 ── 2Dと3Dの座標系が噛み合わない地獄

前回、私はAIに「もういい」と言い放った。

しかし本当の地獄は、AIを変えた後に始まった。

問題の根源は、AIの能力ではなかった。2Dと3Dの座標系──この、コンピュータサイエンスの教科書なら3ページで終わる話が、実装では底なしの沼だったのだ。

2つの世界の「上」は違う

Paper.js の世界:Y軸は下を向く

Paper.jsは2Dベクターグラフィックスライブラリだ。HTMLのCanvas APIと同じく、座標系は左上が原点、Y軸が下方向に伸びる。

(0,0) ────→ X
  │
  │
  ↓
  Y

これはWebの世界では当たり前だ。CSSもSVGもこの座標系を使う。「下に行くほどYが増える」──直感に反するが、Web開発者には常識とされている。

Three.js の世界:Y軸は上、Z軸が手前

Three.jsは3Dライブラリだ。座標系は右手系。Y軸が上方向、Z軸がカメラに向かって手前方向に伸びる。

        Y (上)
        │
        │
        │
        └──── X (右)
       /
      /
     Z (手前)

数学やOpenGLの世界では標準的な座標系だ。

問題:2つの座標系を同時に扱う

Sausage Erectosは、Three.jsで3Dモデルを表示しながら、Paper.jsで2Dの展開図(レーザー加工用のDXF図面)を同時生成する。

つまり、同じ「ピクトグラムの肩の位置」を:

  • Three.js側では Y=上、Z=手前 で計算し
  • Paper.js側では Y=下 で計算する

この変換を、アプリの全ての関節・全てのパーツ・全てのフレームで正しく行う必要がある。

角丸が逆になる ── 「たった1つのバグ」の正体

フットボードの要件

フットボード(台座)は単純な長方形ではない。

「接続面(ピクトグラムと繋がる側)は直角、外側は角丸」

これだけの要件だ。図にするとこうなる:

  ┌────────────────────────┐
  │                        │  ← 接続面:直角
  │                        │
  │                        │
  ╰────────────────────────╯  ← 外側:角丸

AIが何度も間違えた理由

この「上が直角、下が角丸」を実装する時、座標系の変換が絡む。

Paper.jsでは「下」はYが大きい方向。Three.jsでは「下」はYが小さい方向。AIがコードを書く時、無意識に座標系を混同する。

結果:

  ╭────────────────────────╮  ← ここが角丸になってしまう
  │                        │
  │                        │
  │                        │
  └────────────────────────┘  ← ここが直角になってしまう

逆だ。

修正を指示する。AIは「修正しました」と返す。コードを見ると、確かに変更されている。しかし実行すると──逆だ。

AIは「2DのY座標を反転する」コードを書いた。正しい。しかしその反転を2回適用していた。反転の反転は元に戻る。つまり直っていない。

人間も間違える

公平を期すために言えば、私も間違えた。

「ここのYを反転して」とAIに指示した箇所が、実はThree.js側の座標で、Paper.js側ではなかった。結果、正しかったものが壊れた。

座標系の問題は、人間もAIも等しく騙す。これは能力の問題ではなく、認知の問題だ。

解法:座標変換を「1箇所」に閉じ込める

アンチパターン:変換を散らばらせる

最初の実装では、座標変換のコードがアプリ全体に散らばっていた。

// SceneManager.js
const y3d = -paperY;  // Paper→Three変換

// GeometryBuilder.js
const paperY = -y3d;   // Three→Paper変換

// Exporter.js
const dxfY = canvasHeight - paperY;  // Paper→DXF変換

各ファイルが独自に変換を行い、しかもその変換ロジックが微妙に異なる。バグが入り込む隙間だらけだ。

パターン:変換関数を1つにまとめる

解決策は単純だった。座標変換を担う関数を1つだけ作り、全てのコードがそれを通す。

// coordinateUtils.js
export function paperToThree(paperPoint) {
    return { x: paperPoint.x, y: -paperPoint.y, z: 0 };
}

export function threeToPaper(threePoint) {
    return { x: threePoint.x, y: -threePoint.y };
}

当たり前の設計パターンだ。しかし「当たり前」が最初からできていれば、深夜にパンクロックを歌う必要はなかった。

教訓:「たった2つの事実」を壁に貼れ

最終的に、私はモニターの横にポストイットを貼った。

Paper.js: Y軸は下 Three.js: Y軸は上、Z軸は手前

たった2行。しかしこの2行を常に意識するだけで、バグは劇的に減った。

AIにも毎回のプロンプトの冒頭にこの2行を書くようにした。すると、AIの座標系関連のミスも激減した。

赤いボードと青いボード ── 複雑さの罠

最初の設計:2枚のボード

初期設計では、台座は2枚のボードで構成されていた。

  • 青いボード(ベース):固定長。ピクトグラムの足元に配置。
  • 赤いボード(拡張用):重心に応じて伸縮。青いボードの先端に接続。

接続面は直角、先端は角丸。2枚のボードそれぞれに角丸処理が必要で、しかもそれぞれの座標系変換も必要。複雑さは2倍ではなく、4倍になった。

削ぎ落とす勇気

最終的に、赤いボードは廃止された。

青いボード1枚が直接伸縮する。接続面は直角、先端は角丸。変換ロジックは1箇所。

この「削ぎ落とし」は、座標系バグとの格闘の末に辿り着いた結論だ。複雑な設計を維持するコストが、得られるメリットを遥かに上回っていた。

複雑さは、理由がない限り罪だ。

「見えないバグ」を見えるようにする

スクリーンショット駆動デバッグ

座標系のバグには、ログ出力では気づけないものがある。数値は正しく見えるのに、画面では反転している。

最終的に私が採用したのは「スクリーンショット駆動デバッグ」だ。

  1. 変更を加える
  2. 3Dビューと2Dビューの両方をスクリーンショットに撮る
  3. AIに両方の画像を送り、「この2つの表示は一致しているか?」と問う

AIは画像の差異を人間より正確に検出できる。コードの生成では間違えるAIが、画像の比較では驚くほど正確だった。

適材適所。AIの「得意なこと」と「苦手なこと」を見極めるのも、AI時代のスキルだ。

次回予告:台座が無限に伸びていく

座標系の問題は解決した。しかし、もっと恐ろしいバグが待っていた。

台座を伸ばす → 重くなる → 重心が変わる → さらに台座を伸ばす → さらに重くなる → …

無限ループ。フィギュアが上空に逃げていく。

次回は、この「自己参照バグ」と、それを解決した**Payload CoM(ペイロード重心)**という概念について書く。

第3回「台座が無限に伸びていく ── 重心計算の自己参照バグとPayload CoMの発明」── 近日公開。

← ブログ一覧に戻る