Use Cases (更新: 2026/6/2)

Claude CodeとThree.jsで3D Web実装を作る実践ガイド

Claude CodeとThree.jsで3D商品ビューアを作る手順、落とし穴、実務レビュー観点を解説。

Claude CodeとThree.jsで3D Web実装を作る実践ガイド

3DのWeb実装は、見た目の派手さよりも「どの端末で、どの情報を、どれくらい滑らかに見せるか」が成否を分けます。Three.jsはWebGLを扱いやすくするライブラリですが、canvas、カメラ、ライト、アニメーション、破棄処理までを自分で管理する必要があります。

Claude Codeを使うと、3Dの数学やAPI呼び出しを丸投げできるように見えます。しかし実務では、要件を曖昧にしたまま「かっこいい3Dにして」と依頼すると、真っ白なcanvas、重すぎるシーン、スマホで落ちる商品ビューアになりがちです。

この記事では、Vite + React + Three.jsで動く最小構成を作り、Claude Codeにレビューさせる観点までまとめます。対象は、3Dを初めて業務UIに入れるフロントエンド開発者です。

Claude Codeに任せる前に決めること

最初に決めるべきなのは、3Dモデルの格好よさではなく、ユーザーが3Dで確認したい情報です。たとえばECの商品ビューアなら、素材感、サイズ感、色の違い、背面や底面の確認が目的です。教育用のシーンなら、自由に回せることよりも、説明したい部品へ自然に視線が向くことが重要です。

Claude Codeへの依頼では、次のように条件を明文化します。

Vite + React + TypeScript + three で3D商品ビューアを作ってください。
必須条件:
- canvasは親要素いっぱいに表示する
- window resizeに追従する
- OrbitControlsで回転とズームを可能にする
- unmount時にgeometry、material、renderer、controlsをdisposeする
- スマホではpixelRatioを2以下に制限する
- コードはsrc/App.tsxにそのまま貼って動く形にする

このように書くと、Claude Codeは単に3Dオブジェクトを置くだけでなく、運用に必要な足場まで含めて実装しやすくなります。ここでいう足場とは、レンダラーの初期化、イベント登録、リサイズ、後始末を含む「エージェントが安全に作業するためのharness」です。

公式APIの細部はThree.js公式ドキュメントで確認できます。特にレンダラーまわりはWebGLRendererの設定が表示品質と負荷に直結します。

Vite/React + Three.jsの最小構成

まずはライブラリを増やしすぎず、Reactの中でThree.jsを直接扱います。React Three Fiberは便利ですが、最初の理解では「Three.jsの生のライフサイクル」を見たほうが、白画面やメモリリークの原因を追いやすくなります。

npm create vite@latest three-claude-demo -- --template react-ts
cd three-claude-demo
npm i three
npm run dev

この構成で作るファイルは src/App.tsxsrc/App.css だけです。外部モデルを読み込まないので、アセットパスの失敗で詰まることもありません。まずは立方体と床だけで、カメラ、ライト、操作、resize、disposeを確認します。

flowchart LR
  A["React component"] --> B["mount div"]
  B --> C["WebGLRenderer canvas"]
  C --> D["Scene"]
  D --> E["Camera and lights"]
  D --> F["Mesh and material"]
  C --> G["OrbitControls"]
  G --> H["resize and dispose"]

コピーして動く3Dビューア

次のコードを src/App.tsx に貼ると、回転する商品サンプルの3Dビューアが動きます。ポイントは、clientWidthclientHeight を使って親要素基準で描画サイズを決めていること、そしてクリーンアップでThree.jsのリソースを明示的に解放していることです。

import { useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "./App.css";

export default function App() {
  const mountRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const mount = mountRef.current;
    if (!mount) return;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf6f7fb);

    const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
    camera.position.set(3.5, 2.2, 4.5);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.shadowMap.enabled = true;
    mount.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.minDistance = 2.5;
    controls.maxDistance = 8;
    controls.target.set(0, 0.4, 0);

    const ambient = new THREE.HemisphereLight(0xffffff, 0x7c8594, 1.6);
    scene.add(ambient);

    const keyLight = new THREE.DirectionalLight(0xffffff, 2.4);
    keyLight.position.set(3, 5, 4);
    keyLight.castShadow = true;
    scene.add(keyLight);

    const productGeometry = new THREE.BoxGeometry(1.8, 1.2, 1.1, 4, 4, 4);
    const productMaterial = new THREE.MeshStandardMaterial({
      color: 0x2f6f73,
      roughness: 0.42,
      metalness: 0.08,
    });
    const product = new THREE.Mesh(productGeometry, productMaterial);
    product.castShadow = true;
    product.position.y = 0.75;
    scene.add(product);

    const floorGeometry = new THREE.CircleGeometry(2.2, 64);
    const floorMaterial = new THREE.MeshStandardMaterial({
      color: 0xd9dee8,
      roughness: 0.7,
    });
    const floor = new THREE.Mesh(floorGeometry, floorMaterial);
    floor.rotation.x = -Math.PI / 2;
    floor.receiveShadow = true;
    scene.add(floor);

    const resize = () => {
      const width = mount.clientWidth;
      const height = mount.clientHeight;
      if (width === 0 || height === 0) return;

      camera.aspect = width / height;
      camera.updateProjectionMatrix();
      renderer.setSize(width, height, false);
    };

    let frameId = 0;
    const clock = new THREE.Clock();

    const animate = () => {
      const elapsed = clock.getElapsedTime();
      product.rotation.y = elapsed * 0.45;
      product.rotation.x = Math.sin(elapsed * 0.8) * 0.08;
      controls.update();
      renderer.render(scene, camera);
      frameId = window.requestAnimationFrame(animate);
    };

    resize();
    animate();
    window.addEventListener("resize", resize);

    return () => {
      window.removeEventListener("resize", resize);
      window.cancelAnimationFrame(frameId);
      controls.dispose();

      scene.traverse((object) => {
        if (object instanceof THREE.Mesh) {
          object.geometry.dispose();
          const materials = Array.isArray(object.material)
            ? object.material
            : [object.material];
          materials.forEach((material) => material.dispose());
        }
      });

      renderer.dispose();
      renderer.domElement.remove();
    };
  }, []);

  return (
    <main className="viewerShell">
      <div className="copy">
        <p className="eyebrow">Three.js + Claude Code</p>
        <h1>3D product viewer</h1>
        <p>
          Drag to rotate, scroll to zoom, and resize the window to verify that
          the canvas follows its container.
        </p>
      </div>
      <div ref={mountRef} className="viewerStage" />
    </main>
  );
}

src/App.css は次のようにします。canvasの親要素に高さがないと、レンダラーを正しく初期化しても表示領域が0になり、結果として何も見えません。

body {
  margin: 0;
  font-family: Inter, system-ui, sans-serif;
  background: #eef2f7;
  color: #17202a;
}

.viewerShell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: minmax(260px, 0.8fr) minmax(320px, 1.2fr);
  gap: 32px;
  align-items: center;
  padding: 40px;
  box-sizing: border-box;
}

.copy {
  max-width: 520px;
}

.eyebrow {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #2f6f73;
}

.copy h1 {
  margin: 0 0 16px;
  font-size: clamp(36px, 5vw, 64px);
  line-height: 1;
}

.copy p {
  font-size: 17px;
  line-height: 1.7;
}

.viewerStage {
  height: min(62vh, 560px);
  min-height: 360px;
  border: 1px solid #ccd5df;
  background: #f6f7fb;
}

.viewerStage canvas {
  display: block;
}

@media (max-width: 760px) {
  .viewerShell {
    grid-template-columns: 1fr;
    padding: 24px;
  }

  .viewerStage {
    min-height: 300px;
  }
}

Claude Codeへのレビュー指示

実装後は、Claude Codeに「きれいにして」ではなく、チェック項目を指定してレビューさせます。3Dでは画面に表示されることだけを見ても不十分です。リソース解放、端末負荷、SSR、安全なアセット読み込みを見ます。

このThree.js実装をレビューしてください。
観点:
1. canvasが親要素のサイズ変更に追従しているか
2. geometry、material、renderer、controlsがunmount時にdisposeされているか
3. requestAnimationFrameが停止されるか
4. devicePixelRatioが高いスマホで過剰なGPU負荷にならないか
5. Next.jsやAstroのSSR環境でwindow参照が壊れないか
6. 色、ライト、カメラ距離が商品確認に向いているか
修正案は差分で示し、動作確認手順も書いてください。

このプロンプトを使うと、Claude Codeが単なるコード生成役からレビュー担当に変わります。特にdispose漏れは、ページ遷移を何度も繰り返したときに効いてくるため、公開前に必ず確認します。

実務ユースケース3選

ユースケース3Dにする意味Claude Codeに頼む作業
3D商品ビューア色、角度、奥行き、素材感を購入前に確認できるOrbitControls、色切り替え、ライト調整、モバイル負荷の確認
データ可視化平面グラフでは見えにくい分布や時間変化を直感的に見せる点群、棒グラフ、カメラ遷移、凡例UIの実装
ポートフォリオ/教育用シーン建築、機械、人体、教材などを回しながら説明できる注釈ラベル、部品のハイライト、順番に見せるアニメーション

3D商品ビューアでは、商品の形状だけでなく「買う前に不安を減らす情報」を優先します。たとえば家具なら設置面、家電なら背面端子、アパレルなら素材の光り方です。ここをClaude Codeに伝えないと、意味のある3Dではなく、ただ回転する箱になります。

データ可視化では、3Dにした瞬間に読みづらくなるケースもあります。棒が重なって奥の値が見えない、色の凡例が足りない、カメラ操作が分析の邪魔になる、といった問題です。Claude Codeには、2Dで十分な部分と3Dにする部分を分けて設計させます。

ポートフォリオや教育用シーンでは、自由操作だけでなく「見せる順番」が重要です。カメラを固定位置へ移動するボタン、注釈の表示、説明中だけ自動回転を止める処理を入れると、体験が急に実務向けになります。

よくある失敗と直し方

失敗例原因修正
canvasが真っ白親要素の高さが0、カメラが物体を向いていない、ライトがないCSSで高さを固定し、camera positionとcontrols targetを確認する
resize後に歪むcamera.aspectとrenderer.setSizeを更新していないresize関数でcamera.updateProjectionMatrixを呼ぶ
ページ遷移後に重くなるgeometry/material/renderer/controlsのdispose漏れunmount時にtraverseしてdisposeする
スマホで発熱するdevicePixelRatioが高すぎる、影やポリゴン数が重いpixelRatioを2以下にし、影とセグメント数を削る
SSRで落ちるrender時にwindowやdocumentへ直接アクセスしているuseEffect内で初期化し、必要ならクライアント専用コンポーネントに分離する

白画面の調査では、まずブラウザのコンソールを見ます。importパスの間違い、canvasサイズ0、WebGL context lost、モデルの404はすぐに分かります。次に、背景色だけを明るくし、立方体だけを置く最小状態へ戻します。複雑なモデル、HDR環境、ポストエフェクトを同時に疑うと時間を失います。

dispose漏れは目で見ても分かりません。Chrome DevToolsのPerformanceやMemoryでページ遷移を繰り返し、GPUメモリとJS heapが増え続けないか確認します。Three.jsはReactの仮想DOMが勝手に後始末してくれる世界ではないため、ここは人間とClaude Codeの両方でレビューします。

パフォーマンスと運用チェック

本番投入前に、最低でも次を確認します。

  • 主要スマホで30fps以上を維持できるか
  • 影、反射、テクスチャ解像度を落とした軽量モードがあるか
  • 3Dが読み込めないときに代替画像を出せるか
  • キーボード操作やスクリーンリーダー向けに説明テキストを用意しているか
  • モデルファイルの容量が初回表示を妨げていないか

Three.jsだけでなくCanvas全般の設計はClaude CodeでCanvas開発を進めるガイドも参考になります。アニメーションの調整はClaude Codeでアニメーション実装を効率化する方法と合わせて読むと、動きの作り方を整理しやすくなります。

Claude Code Labへの相談導線

Claude Code Labでは、3D商品ビューア、WebGLを使ったデータ可視化、教育用インタラクティブ教材の設計レビューやチーム向けトレーニングを相談できます。相談時は、完成イメージだけでなく、対象端末、既存フレームワーク、モデル形式、許容できる初回ロード時間を共有すると、実装方針をかなり絞れます。

特に社内でClaude Codeを使う場合は、プロンプトの書き方だけでなく、レビュー観点をテンプレート化することが重要です。生成された3Dコードを毎回雰囲気で判断するのではなく、resize、dispose、アクセシビリティ、パフォーマンス、公式ドキュメントとの整合をチェックリスト化します。

まとめ

Claude CodeとThree.jsの組み合わせは、3Dの初期実装を速くします。ただし、本当に価値が出るのは、Claude Codeに「何を描くか」だけでなく「どう壊れないようにするか」まで指示したときです。

最初はVite + React + Three.jsの小さなビューアで、カメラ、ライト、操作、resize、disposeを一通り確認します。そのうえで商品ビューア、データ可視化、教育用シーンへ広げると、見た目だけのデモから運用できる3D UIへ進めます。

実際に試した結果

この記事のコードは、ViteのReact TypeScriptテンプレートに貼り付け、デスクトップ幅とスマホ幅でresizeを確認しました。canvasの高さをCSSで明示しないと白画面に見えること、renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))を入れると高解像度端末でGPU負荷を抑えやすいこと、unmount時のdisposeをClaude Codeにレビューさせると漏れを見つけやすいことを確認しています。

#Claude Code #Three.js #3D #WebGL #フロントエンド
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。