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にはカスタムのグリッド描画関数を用意しておくと便利。
座標系の違いは、二つの言語を同時に話すようなものだ。翻訳ミスは必ず起こる。重要なのは、翻訳層を一箇所に集約し、そこだけを注意深くテストすることだ。