Sausage Erectos X Three.js × Paper.js 実戦Tips ── 2Dと3Dを同時に扱う時に知っておくべき7つのこと

Three.js × Paper.js 座標系Tips集──実践で学んだ12のこと

Three.js(3D)とPaper.js(2D)を同一アプリケーションで使うSausage Erectosのカスタマイザーでは、座標系の違いが無数のバグを生んだ。この記事は、実際に遭遇したバグとその解決策をTips形式でまとめたものだ。

Tip 1: Y軸の方向が逆

Three.jsはY軸が上方向正、Paper.jsはY軸が下方向正。これは最も基本的だが、最も頻繁にバグを生む差異だ。

実際のバグ: 3Dモデルの関節位置を2Dのパネルに表示したとき、上下が逆転していた。ユーザーが「頭」をクリックしたつもりが「足」が選択される。

// Three.js → Paper.js への座標変換
function threeToPaper(threePoint, canvasHeight) {
  return new paper.Point(threePoint.x, canvasHeight - threePoint.y);
}

Tip 2: 原点の位置が違う

Three.jsの原点はシーンの中心、Paper.jsの原点はCanvasの左上。

実際のバグ: 3Dモデルの中心にあるべきラベルが、常に左上に表示されていた。

Tip 3: スケールの単位が異なる

Three.jsは任意単位(通常メートル)、Paper.jsはピクセル。カメラのズームレベルによって変換係数が変わることを忘れがち。

function worldToScreen(worldPos, camera, canvas) {
  const vec = worldPos.clone().project(camera);
  return {
    x: (vec.x + 1) / 2 * canvas.width,
    y: (-vec.y + 1) / 2 * canvas.height,
  };
}

Tip 4: 回転の表現方法

Three.jsはクォータニオンまたはオイラー角(ラジアン)、Paper.jsは回転角度(度数法)。

実際のバグ: Three.jsの回転値をそのままPaper.jsに渡したら、45°回転させたつもりが約2578°回転した(45ラジアン = 約2578度)。

const degrees = THREE.MathUtils.radToDeg(radians);

Tip 5: ジオメトリの共有でメモリ節約

同じ形状のオブジェクトが複数ある場合、ジオメトリを共有することでメモリを大幅に削減できる。

// 悪い例:各インスタンスに新しいジオメトリ
for (let i = 0; i < 100; i++) {
  const geo = new THREE.SphereGeometry(1, 32, 32); // 100個のジオメトリ
  const mesh = new THREE.Mesh(geo, material);
}

// 良い例:ジオメトリを共有
const sharedGeo = new THREE.SphereGeometry(1, 32, 32); // 1個だけ
for (let i = 0; i < 100; i++) {
  const mesh = new THREE.Mesh(sharedGeo, material);
}

Tip 6: マテリアルのキャッシュ

マテリアルも同様にキャッシュすべきだ。同じ色・質感のマテリアルを毎回newするのは無駄だ。

const materialCache = new Map();
function getMaterial(color) {
  if (!materialCache.has(color)) {
    materialCache.set(color, new THREE.MeshStandardMaterial({ color }));
  }
  return materialCache.get(color);
}

Tip 7: Paper.jsのヒットテスト活用

Paper.jsのhitTestは、2D上のクリック判定に非常に便利だ。しかし、デフォルトの許容範囲(tolerance)が小さすぎて、細い線がクリックしにくい問題があった。

const hitResult = paper.project.hitTest(event.point, {
  tolerance: 10,       // デフォルトは0。10pxに拡大
  stroke: true,
  fill: true,
  segments: true,
});

Tip 8: Three.jsのRaycasterとの連携

3D空間でのクリック判定にはRaycasterを使う。Paper.jsのhitTestと組み合わせて、「2Dパネルをクリックしたのか、3Dモデルをクリックしたのか」を判別する。

function onClick(event) {
  // まず2Dパネルのヒットテスト
  const paperHit = paper.project.hitTest(event.point);
  if (paperHit) {
    handle2DClick(paperHit);
    return; // 2Dが優先
  }
  // 2Dにヒットしなければ3Dのレイキャスト
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(scene.children, true);
  if (intersects.length > 0) {
    handle3DClick(intersects[0]);
  }
}

Tip 9: dispose()を忘れない

Three.jsのジオメトリ、マテリアル、テクスチャは明示的にdispose()しないとメモリリークする。

Tip 10: Paper.jsのviewとprojectの違い

paper.viewは表示領域、paper.projectはデータ。ズームやパンはviewを操作するが、アイテムの追加はprojectに対して行う。

Tip 11: requestAnimationFrameの二重登録

Three.jsとPaper.jsの両方がアニメーションループを持つと、requestAnimationFrameが二重に登録されてパフォーマンスが低下する。統一したループで管理すべし。

Tip 12: デバッグ用のヘルパーグリッド

座標系の問題をデバッグするとき、両方の空間にグリッドを表示すると問題が可視化される。Three.jsにはGridHelper、Paper.jsにはカスタムのグリッド描画関数を用意しておくと便利。

座標系の違いは、二つの言語を同時に話すようなものだ。翻訳ミスは必ず起こる。重要なのは、翻訳層を一箇所に集約し、そこだけを注意深くテストすることだ。

← ブログ一覧に戻る