Claude Media
Claude CodeでThree.jsの3Dシーンを作る — スクリーンショットで描画を確かめさせる

Claude CodeでThree.jsの3Dシーンを作る — スクリーンショットで描画を確かめさせる

Three.jsの3D描画は、コードが正しくても黒い画面になることがあります。Playwright MCPのスクリーンショットと、カメラ・ライト・物体の状態を返す確認用の口で、Claude Codeに描画を自分で確かめさせる手順です。

Three.jsはWebGLで3Dシーンを描くJavaScriptライブラリです。Claude Codeにシーンのコードを書かせるのは簡単です。難しいのは「ちゃんと映っているか」の確認で、コードが正しくても画面が真っ黒、ということが珍しくありません。

この記事では、Playwright MCPのスクリーンショットとbrowser_evaluateを使い、Claude Code自身に3Dシーンを開かせて、画像とカメラ・ライト・物体の数値の両方で確かめさせる流れを組みます。MCPの導入はPlaywright MCPサーバーの使い方にまとめてあります。ここでは、Three.js特有の「カメラ・ライト・材質」の確認に絞ります。

Three.jsの検証がcanvasゲームと違う点

画面がcanvas1枚に描かれ、DOMから中身が読めない点は、2DゲームのPhaserと同じです。ただしThree.jsには、もう一段の事情があります。画面に何が映るかが、4つの要素の組み合わせで決まるからです。

  • シーン(Scene)に物体が入っているか
  • カメラが物体のある方向を向いているか
  • 材質に合ったライトが当たっているか
  • カメラの視錐台(映る範囲)に物体が収まっているか

どれか1つが外れるだけで、画面は何も映らなくなります。しかも症状はどれも「黒い画面」で、見た目では原因を区別できません。そこで、スクリーンショットは「結果の目視」に使い、原因の切り分けはbrowser_evaluateで返す数値に任せます。

準備 — プロジェクトと開発サーバー

プロジェクトの形は問いません。ここではバンドラーにViteを使う例で進めます。

npm create vite@latest scene-demo -- --template vanilla
cd scene-demo
npm install three
npm run dev

Playwright MCPはClaude Codeに1行で追加できます。--より後ろがMCPサーバーとして起動するコマンドになります。

claude mcp add playwright -- npx @playwright/mcp@latest --viewport-size 1280x720

--viewport-sizeは、Playwright MCPのオプション一覧にある表示サイズの指定です。3Dシーンは画面の縦横比でカメラの映る範囲が変わるので、サイズを固定しておくとスクリーンショットが毎回そろいます。

シーンに「状態を返す口」を付ける

次の例は、床・立方体・ライトだけの最小のシーンです。末尾の?debugが付いたときだけ、ヘルパーと確認用の関数を有効にします。

import * as THREE from 'three';
 
const params = new URLSearchParams(location.search);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(4, 3, 6);
camera.lookAt(0, 0.5, 0);
 
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
 
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(5, 8, 3);
scene.add(sun);
 
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ color: 0x44aaff }),
);
cube.name = 'cube';
cube.position.y = 0.5;
scene.add(cube);
 
addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
 
renderer.setAnimationLoop(() => {
  if (!params.has('freeze')) cube.rotation.y += 0.01;
  renderer.render(scene, camera);
});

PerspectiveCameraのfovは縦方向の視野角(度)で、既定値は50です。aspectは通常、canvasの幅÷高さを入れます。画面サイズが変わるたびにupdateProjectionMatrix()を呼ぶ処理を入れていないと、映像が引き伸ばされます。Claude Codeが書き落としやすい箇所なので、スクリーンショットで形が歪んでいないかも見させます。

アニメーションのループはsetAnimationLoop()で書きます。公式のWebGLRendererの説明は、requestAnimationFrame()を手で書くより互換性の面で勧めています。?freezeは検証用で、回転を止めて、撮るたびに同じ絵になるようにする指定です。

確認用の口を足す

同じファイルの末尾に、次のブロックを足します。

if (params.has('debug')) {
  scene.add(new THREE.AxesHelper(3));
  scene.add(new THREE.GridHelper(10, 10));
 
  window.__sceneState = () => {
    const box = new THREE.Box3();
    const center = new THREE.Vector3();
    const size = new THREE.Vector3();
    const objects = [];
    scene.traverse((o) => {
      if (!o.isMesh || !o.name) return;
      box.setFromObject(o);
      objects.push({
        name: o.name,
        center: box.getCenter(center).toArray(),
        size: box.getSize(size).toArray(),
      });
    });
    const { render, memory } = renderer.info;
    return {
      camera: { position: camera.position.toArray(), fov: camera.fov, aspect: camera.aspect },
      objects,
      render: { calls: render.calls, triangles: render.triangles },
      memory: { geometries: memory.geometries, textures: memory.textures },
    };
  };
 
  window.__setCamera = (pos, target) => {
    camera.position.set(...pos);
    camera.lookAt(...target);
  };
}

Box3のsetFromObject()は、子要素を含む物体の世界座標での境界箱を求めます。公式は、祖先の変換が反映された最新のワールド行列が前提だと注記しています。確信が持てないときは、先にobject.updateWorldMatrix(true, false)を呼ぶよう案内しています。

renderer.infoは、GPUメモリと描画の統計を持つオブジェクトです。render.callsが描画呼び出しの数、render.trianglesが描いた三角形の数、memory.geometriesとmemory.texturesが確保済みの数を返します。

「物体はあるのに、描画された三角形が0」という状態は、画像を眺めるより数値のほうが早く見つかります。

ヘルパーで画像を読み取りやすくする

Three.jsには、画面に補助線を足すヘルパーがあります。スクリーンショットをClaude Codeに読ませる場面では、これが方向を判断する手がかりになります。

ヘルパー見えるもの使いどころ
AxesHelper見えるものX軸が赤、Y軸が緑、Z軸が青の線使いどころ物体がどの向きにずれているか
GridHelper見えるもの平面の格子(既定は10×10)使いどころ地面との位置関係、奥行き
CameraHelper見えるものカメラの視錐台の線使いどころ物体がカメラの映る範囲に入っているか

AxesHelperの色の割り当ては公式のドキュメントにあります。画像の中で「赤い線の向こうに物体がある」と判断できれば、座標の数値と突き合わせて位置を確かめられます。

CameraHelperはシーンの子要素にする必要があります。カメラを動かしたり射影行列を変えたりした場合は、ヘルパー側のupdate()も呼びます。

複数のアングルから撮らせる

1枚のスクリーンショットでは、奥行きの異常が分かりません。前後に物体が重なっていると、位置がずれていても気づきにくくなります。そこで__setCameraを使い、正面・真上・斜めの3方向から撮らせます。

// browser_evaluate に渡す関数の例
() => { window.__setCamera([0, 0, 8], [0, 0.5, 0]); }   // 正面
() => { window.__setCamera([0, 9, 0.01], [0, 0, 0]); }  // 真上

真上から見るときにカメラ位置を[0, 9, 0]にしないのは、lookAtが使う「上方向」とカメラの向きが一致してしまい、回転が定まらなくなるためです。わずかにずらしておきます。この指定はThree.jsの慣習で、Object3Dのupの既定値とlookAtの動きに由来します。

各アングルでbrowser_take_screenshotのfilenameを変えて保存させると、後から比較できます。scaleにcssを指定すれば、端末の画素密度によらず同じサイズの画像になります。

3Dモデルを読み込むシーンは撮影のタイミングを合わせる

立方体のような標準の形状は、コードが実行された時点で揃います。3Dモデルのファイル(glTFなど)を読み込む場合は事情が違います。Three.jsには、アドオンのGLTFLoaderでglTFを読む仕組みがあります。読み込みは非同期なので、ページを開いてすぐ撮ると、モデルのない空のシーンが写ります。

この空振りは、画像だけだと「モデルが壊れている」のか「まだ読み込み前」なのか区別できません。確認用の口に読み込み完了のフラグを足すと、判定が決まります。

let ready = false;
// loader.load(url, (gltf) => { scene.add(gltf.scene); ready = true; });
 
window.__isReady = () => ready;

CLAUDE.mdの手順には「browser_evaluateでwindow.__isReady()がtrueになるまで待ってから撮る」と1行足します。browser_wait_forは表示テキストか時間を待つツールで、canvasの中身は待てません。時間待ちを挟んでから__isReady()を再確認する形が現実的です。

読み込みに失敗したときは、browser_console_messagesにネットワークエラーが出ます。画面だけで判断せず、先にコンソールを見させてください。

Claude Codeに渡す検証ルール

手順をCLAUDE.mdに書いておけば、毎回口頭で指示しなくても変更のたびに同じ確認が走ります。書き方の一例です。

## 3Dシーンの動作確認ルール
- シーンを変更したら、完了を宣言する前に必ず次を行う
  1. npm run dev が起動していなければ別プロセスで起動する
  2. browser_navigate で http://localhost:5173/?debug&freeze を開く
  3. browser_console_messages で WebGL のエラーや警告がないか見る
  4. browser_evaluate で window.__sceneState() を呼び、次を確認する
     - objects に期待した名前の物体があり、center がカメラの前にある
     - render.triangles が 0 でない
  5. browser_take_screenshot を filename 付きで撮り、画面に物体が映っているか見る
  6. window.__setCamera で正面と真上に動かし、同様に撮る
- 確かめられなかった項目は「未確認」と報告し、成功と書かない

ポートは使う開発サーバーに合わせてください。Viteは5173を使うことが多いですが、起動時の出力で確認します。

黒い画面の原因を切り分ける

画面が黒いとき、Claude Codeに次の順で疑わせます。

症状疑うもの確かめ方
何も映らない疑うもの物体がカメラの外にある確かめ方__sceneState()のobjects[].centerとカメラ位置を比較
何も映らない疑うものscene.addの呼び忘れ確かめ方objectsが空か、render.trianglesが0
物体が黒い疑うものライトがない、または当たっていない確かめ方材質をMeshBasicMaterialに替えて映るか見る
縦横比が歪む疑うものaspectの更新漏れ確かめ方camera.aspectと画面サイズの比を比較

MeshBasicMaterialは、公式の説明どおりライトの影響を受けない材質です。替えて映れば、原因は形状や位置ではなくライトの側にあると分かります。

DirectionalLightには、つまずきやすい点があります。公式は、回転を設定しても効かないと明記しています。光の向きは、ライトのpositionからtargetの位置へ向かう方向で決まります。ClaudeがライトのrotationをいじるコードやLookAt風の指定を書いていたら、targetの設定に直させてください。

効かない場面と限界

画像の読み取りは大きな破綻向き

スクリーンショットで確かめやすいのは、物体が映らない、極端に暗い、画面外にずれているといった大きな異常です。質感のわずかな違いや、数ピクセルのずれは見落としやすくなります。位置・大きさ・個数は__sceneStateの数値で判定し、画像は全体の印象を見る役に回すと安定します。

ヘッドレスでのWebGLは環境で確かめる

Playwright MCPの既定はヘッドありで、--headlessを足すと画面の無い実行になります。ヘッドレスでWebGLが使えるかどうかは、公式の資料に記載がありません。環境によって異なる可能性があるため、まずヘッドありで通し、--headlessに切り替えるときはbrowser_console_messagesでレンダラー生成のエラーを確かめてください。WebGLRendererのfailIfMajorPerformanceCaveatは既定がfalseなので、性能の低い描画環境でも生成は拒否されません。

操作を伴うシーンの確認

OrbitControlsのようなマウス操作は、アクセシビリティツリーに現れないcanvasの上で動きます。座標指定のクリックが必要な場合は、--caps visionを付けて起動する方法があります。詳しい扱いはPhaserの記事の座標でクリックしたいときの説明と同じです。シーンの見た目の検証だけなら、__setCameraで視点を動かすほうが確実です。

初期化スクリプトで確認用フラグを渡す

--init-scriptは、どのページのスクリプトよりも先に実行されるJavaScriptファイルを指定する起動オプションです。window.__DEBUG = trueのような目印を先に置けるので、URLに?debugを付けずに済ませたい場合に使えます。

他の3D環境との比較

Three.jsはブラウザで動くため、検証もブラウザの操作で完結します。ゲームエンジンのUnityやGodotは、コマンドラインでの起動とログの読み取りが中心です。3DCGの制作ツールでは、Blender MCPのように、アプリ本体を操作させる方法もあります。

画像から画面を作る逆の使い方は、スクリーンショットからUIを実装する手順にあります。

まとめ

Three.jsは、画面が黒いだけでは原因が分かりません。スクリーンショットで見た目を、browser_evaluateでカメラ・物体・描画統計の数値を見る二本立てにすると、切り分けが速くなります。ヘルパーで方向の手がかりを足し、アングルを変えて撮らせ、手順をCLAUDE.mdに書いておけば、Claude Codeが変更のたびに自分で確かめます。画像で保証できるのは大きな破綻がないことまでで、細部は数値で判定するのが運用の要です。

この記事を共有:XはてブLinkedIn