パステル3Dルーム俯瞰

パステル色のミニチュアな部屋を、カメラがゆったり弧を描いて眺めます。家具がときどきシュッと弾む。ポートフォリオで人気の作り込んだ3D空間の最小構成を、プリミティブな形状だけで作るデモです。

外部ライブラリ: https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js

#threejs#3d-room#lowpoly#diorama

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW: 店内の3Dミニチュア。カメラが弧を描いてカフェの一角を眺める -->
<div class="stage">
  <canvas class="room" data-room aria-label="MOON BREW 店内の3Dミニチュア"></canvas>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F4ECDD; }

.stage { width: 100%; height: 100vh; min-height: 240px; max-height: 100%; overflow: hidden; background: #F4ECDD; }
.room { display: block; width: 100%; height: 100%; }
JavaScript
// Three.js r128。プリミティブのみ(デモと同じロジック)。配色を MOON BREW のカフェ色に。
(() => {
  if (typeof THREE === 'undefined') return;
  const canvas = document.querySelector('[data-room]');
  if (!canvas) return;

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));

  const scene = new THREE.Scene();
  scene.background = new THREE.Color('#F4ECDD');
  const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100);

  const resize = () => {
    const w = canvas.clientWidth, h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h; camera.updateProjectionMatrix();
  };
  window.addEventListener('resize', resize);
  resize();

  scene.add(new THREE.AmbientLight(0xffffff, 0.9));
  const dir = new THREE.DirectionalLight(0xffffff, 0.6);
  dir.position.set(3, 5, 2);
  scene.add(dir);

  const mat = (c) => new THREE.MeshLambertMaterial({ color: c });
  const box = (w, h, d, c) => new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat(c));
  const cyl = (r, h, c, s = 20) => new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, s), mat(c));

  const floor = box(4, 0.2, 4, '#E7C8A0'); floor.position.y = -0.1; scene.add(floor);
  const wallBack = box(4, 2.4, 0.2, '#EAD9BE'); wallBack.position.set(0, 1.2, -2); scene.add(wallBack);
  const wallLeft = box(0.2, 2.4, 4, '#DCCBA8'); wallLeft.position.set(-2, 1.2, 0); scene.add(wallLeft);
  const rug = cyl(0.9, 0.02, '#D8C4A0'); rug.position.y = 0.005; scene.add(rug);

  const furniture = [];

  // ソファ席
  const sofa = new THREE.Group();
  { const base = box(1.6, 0.4, 0.9, '#6F4E37'); base.position.y = 0.2; sofa.add(base);
    const seat = box(1.5, 0.25, 0.85, '#C97B4A'); seat.position.y = 0.5; sofa.add(seat);
    const cushion = box(0.5, 0.18, 0.3, '#E0A95F'); cushion.position.set(-0.5, 0.62, 0); sofa.add(cushion); }
  sofa.position.set(-1.0, 0, -1.3); scene.add(sofa); furniture.push(sofa);

  // テーブル+カップ
  const table = new THREE.Group();
  { const top = box(1.1, 0.08, 0.6, '#8B5A2B'); top.position.y = 0.7; table.add(top);
    [[-0.5, -0.27], [0.5, -0.27], [-0.5, 0.27], [0.5, 0.27]].forEach(([lx, lz]) => {
      const leg = cyl(0.04, 0.7, '#5a3a22', 10); leg.position.set(lx, 0.35, lz); table.add(leg); });
    const cup = cyl(0.12, 0.16, '#F4ECDD', 16); cup.position.set(0, 0.82, 0); table.add(cup);
    const coffee = cyl(0.1, 0.02, '#3a2417', 16); coffee.position.set(0, 0.9, 0); table.add(coffee); }
  table.position.set(1.2, 0, -1.4); scene.add(table); furniture.push(table);

  // 棚+マグ
  const shelf = new THREE.Group();
  { const frame = box(0.7, 1.2, 0.3, '#8B5A2B'); frame.position.y = 0.6; shelf.add(frame);
    ['#C97B4A', '#7BA05B', '#E0A95F'].forEach((bc, i) => {
      const mug = box(0.14, 0.18, 0.14, bc); mug.position.set(-0.2 + i * 0.18, 0.86, 0.06); shelf.add(mug); }); }
  shelf.position.set(-1.7, 0, 0.6); scene.add(shelf); furniture.push(shelf);

  // 観葉植物
  const plant = new THREE.Group();
  { const pot = cyl(0.18, 0.3, '#C0795A', 14); pot.position.y = 0.15; plant.add(pot);
    const stem = cyl(0.04, 0.5, '#5a3a22', 8); stem.position.y = 0.5; plant.add(stem);
    const leaf = new THREE.Mesh(new THREE.IcosahedronGeometry(0.3, 0), mat('#7BA05B')); leaf.position.y = 0.85; plant.add(leaf); }
  plant.position.set(1.6, 0, 0.8); scene.add(plant); furniture.push(plant);

  let start = 0, lastPick = -3, pick = -1, pickStart = 0;
  const animate = (now) => {
    if (!start) start = now;
    const t = (now - start) / 1000;
    const ang = 0.1 + 0.5 * Math.sin(t * 2 * Math.PI / 12);
    camera.position.set(Math.sin(ang) * 6.5, 3.2, Math.cos(ang) * 6.5);
    camera.lookAt(0, 0.8, 0);
    if (t - lastPick > 2.5) { lastPick = t; pick = (pick + 1) % furniture.length; pickStart = t; }
    furniture.forEach(f => { f.scale.y = 1; });
    if (pick >= 0) {
      const p = (t - pickStart) / 0.5;
      if (p < 1) furniture[pick].scale.y = 1 + 0.18 * Math.sin(p * Math.PI) * (1 - p);
    }
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
  };
  requestAnimationFrame(animate);
})();

実装ガイド

使いどころ

ポートフォリオや製品紹介で人気の「作り込んだ3D空間」を、軽量な最小構成で見せたいときに。

実装時の注意点

Three.js(r128で固定。r150+はUMD廃止の罠)を使います。Box/Cylinder/Icosahedronのプリミティブのみ、MeshLambertMaterial+ライト2灯。モデルロード/GLTF/影(shadowMap)は使わず軽量に保ちます。OrbitControlsは使わずカメラは手書き(弧上をsinで往復+lookAt)。家具はGroupにまとめると配置ごと座標調整が一回で済みます。

対応ブラウザ

WebGL(Three.js)はWebGL対応のモダンブラウザで動作します。端末によっては非対応や低性能のため、重いWebGLは画面サイズや能力でポスター表示などにフォールバックする配慮を入れます。pixelRatioは上限2でクランプし、対応は実機で確認してください。

よくある失敗

LambertでライトなしだとTanそうに見えないためライトは必須です。Standardや環境マップに手を出すと工数が膨らみます。家具をGroupにまとめないと座標調整が煩雑になります。draw callを増やしすぎないようにします。

応用例

家具をブランド什器に、配色をテーマに、カメラ経路やシバ(弾み)の対象を調整、クリックで家具にフォーカスなどに発展できます。

コード

HTML
<!-- パステル3Dルーム俯瞰:プリミティブだけのミニチュア部屋をカメラが弧を描いて眺める -->
<div class="stage">
  <canvas class="room" data-room aria-label="パステルカラーの3Dミニチュアルーム"></canvas>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F7F5F0; }

.stage {
  width: 100%;
  height: 100vh;
  min-height: 240px;
  max-height: 100%;
  overflow: hidden;
  background: #F7F5F0;
}
.room { display: block; width: 100%; height: 100%; }
JavaScript
// Three.js r128。Box/Cylinder/Icosahedron のプリミティブのみ、Lambert+ライト2灯。カメラは手書き。
(() => {
  if (typeof THREE === 'undefined') return; // ライブラリ未ロード時は何もしない
  const canvas = document.querySelector('[data-room]');
  if (!canvas) return;

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));

  const scene = new THREE.Scene();
  scene.background = new THREE.Color('#F7F5F0');
  const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100);

  const resize = () => {
    const w = canvas.clientWidth, h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  };
  window.addEventListener('resize', resize);
  resize();

  scene.add(new THREE.AmbientLight(0xffffff, 0.9));
  const dir = new THREE.DirectionalLight(0xffffff, 0.6);
  dir.position.set(3, 5, 2);
  scene.add(dir);

  const mat = (c) => new THREE.MeshLambertMaterial({ color: c });
  const box = (w, h, d, c) => new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat(c));
  const cyl = (r, h, c, s = 20) => new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, s), mat(c));

  // 床・壁・ラグ
  const floor = box(4, 0.2, 4, '#FFE9EE'); floor.position.y = -0.1; scene.add(floor);
  const wallBack = box(4, 2.4, 0.2, '#DFF3FF'); wallBack.position.set(0, 1.2, -2); scene.add(wallBack);
  const wallLeft = box(0.2, 2.4, 4, '#FFF6D6'); wallLeft.position.set(-2, 1.2, 0); scene.add(wallLeft);
  const rug = cyl(0.9, 0.02, '#E2FFE9'); rug.position.y = 0.005; scene.add(rug);

  // 家具は Group にまとめ、配置ごと座標調整を一回で済ませる
  const furniture = [];

  const bed = new THREE.Group();
  { const base = box(1.6, 0.4, 0.9, '#ffffff'); base.position.y = 0.2; bed.add(base);
    const quilt = box(1.5, 0.25, 0.85, '#FFD9E0'); quilt.position.y = 0.5; bed.add(quilt);
    const pillow = box(0.5, 0.18, 0.3, '#ffffff'); pillow.position.set(-0.5, 0.62, 0); bed.add(pillow); }
  bed.position.set(-1.0, 0, -1.3); scene.add(bed); furniture.push(bed);

  const desk = new THREE.Group();
  { const top = box(1.1, 0.08, 0.6, '#E8C39E'); top.position.y = 0.7; desk.add(top);
    [[-0.5, -0.27], [0.5, -0.27], [-0.5, 0.27], [0.5, 0.27]].forEach(([lx, lz]) => {
      const leg = cyl(0.04, 0.7, '#C99A6B', 10); leg.position.set(lx, 0.35, lz); desk.add(leg); });
    const pc = box(0.5, 0.04, 0.3, '#cccccc'); pc.position.set(0, 0.76, 0); desk.add(pc);
    const scr = box(0.5, 0.34, 0.04, '#222222'); scr.position.set(0, 0.95, -0.12); desk.add(scr);
    const glow = box(0.44, 0.28, 0.012, '#4D7CFE'); glow.position.set(0, 0.95, -0.095); desk.add(glow); }
  desk.position.set(1.2, 0, -1.4); scene.add(desk); furniture.push(desk);

  const shelf = new THREE.Group();
  { const frame = box(0.7, 1.2, 0.3, '#D7A86E'); frame.position.y = 0.6; shelf.add(frame);
    ['#FF5C5C', '#4ED1A1', '#FFC83D'].forEach((bc, i) => {
      const bk = box(0.12, 0.7, 0.24, bc); bk.position.set(-0.2 + i * 0.18, 0.82, 0.05); bk.rotation.z = (i - 1) * 0.05; shelf.add(bk); }); }
  shelf.position.set(-1.7, 0, 0.6); scene.add(shelf); furniture.push(shelf);

  const plant = new THREE.Group();
  { const pot = cyl(0.18, 0.3, '#C0795A', 14); pot.position.y = 0.15; plant.add(pot);
    const stem = cyl(0.04, 0.5, '#7a5a3a', 8); stem.position.y = 0.5; plant.add(stem);
    const leaf = new THREE.Mesh(new THREE.IcosahedronGeometry(0.3, 0), mat('#7BC86C')); leaf.position.y = 0.85; plant.add(leaf); }
  plant.position.set(1.6, 0, 0.8); scene.add(plant); furniture.push(plant);

  let start = 0, lastPick = -3, pick = -1, pickStart = 0;
  const animate = (now) => {
    if (!start) start = now;
    const t = (now - start) / 1000;

    // カメラ:半径6.5・高さ3.2 の弧上を周期12sで往復、常に部屋中心を見る
    const ang = 0.1 + 0.5 * Math.sin(t * 2 * Math.PI / 12);
    camera.position.set(Math.sin(ang) * 6.5, 3.2, Math.cos(ang) * 6.5);
    camera.lookAt(0, 0.8, 0);

    // シバ:2.5sごとに家具を1つ選び、0.5sの減衰バウンス
    if (t - lastPick > 2.5) { lastPick = t; pick = (pick + 1) % furniture.length; pickStart = t; }
    furniture.forEach(f => { f.scale.y = 1; });
    if (pick >= 0) {
      const p = (t - pickStart) / 0.5;
      if (p < 1) furniture[pick].scale.y = 1 + 0.18 * Math.sin(p * Math.PI) * (1 - p);
    }

    renderer.render(scene, camera);
    requestAnimationFrame(animate);
  };
  requestAnimationFrame(animate);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「パステル3Dルーム俯瞰」の効果を追加してください。

# 追加してほしい効果
パステル3Dルーム俯瞰(WebGL / Three.js)
パステル色のミニチュアな部屋を、カメラがゆったり弧を描いて眺めます。家具がときどきシュッと弾む。ポートフォリオで人気の作り込んだ3D空間の最小構成を、プリミティブな形状だけで作るデモです。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- パステル3Dルーム俯瞰:プリミティブだけのミニチュア部屋をカメラが弧を描いて眺める -->
<div class="stage">
  <canvas class="room" data-room aria-label="パステルカラーの3Dミニチュアルーム"></canvas>
</div>

【CSS】
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F7F5F0; }

.stage {
  width: 100%;
  height: 100vh;
  min-height: 240px;
  max-height: 100%;
  overflow: hidden;
  background: #F7F5F0;
}
.room { display: block; width: 100%; height: 100%; }

【JavaScript】
// Three.js r128。Box/Cylinder/Icosahedron のプリミティブのみ、Lambert+ライト2灯。カメラは手書き。
(() => {
  if (typeof THREE === 'undefined') return; // ライブラリ未ロード時は何もしない
  const canvas = document.querySelector('[data-room]');
  if (!canvas) return;

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));

  const scene = new THREE.Scene();
  scene.background = new THREE.Color('#F7F5F0');
  const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100);

  const resize = () => {
    const w = canvas.clientWidth, h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  };
  window.addEventListener('resize', resize);
  resize();

  scene.add(new THREE.AmbientLight(0xffffff, 0.9));
  const dir = new THREE.DirectionalLight(0xffffff, 0.6);
  dir.position.set(3, 5, 2);
  scene.add(dir);

  const mat = (c) => new THREE.MeshLambertMaterial({ color: c });
  const box = (w, h, d, c) => new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat(c));
  const cyl = (r, h, c, s = 20) => new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, s), mat(c));

  // 床・壁・ラグ
  const floor = box(4, 0.2, 4, '#FFE9EE'); floor.position.y = -0.1; scene.add(floor);
  const wallBack = box(4, 2.4, 0.2, '#DFF3FF'); wallBack.position.set(0, 1.2, -2); scene.add(wallBack);
  const wallLeft = box(0.2, 2.4, 4, '#FFF6D6'); wallLeft.position.set(-2, 1.2, 0); scene.add(wallLeft);
  const rug = cyl(0.9, 0.02, '#E2FFE9'); rug.position.y = 0.005; scene.add(rug);

  // 家具は Group にまとめ、配置ごと座標調整を一回で済ませる
  const furniture = [];

  const bed = new THREE.Group();
  { const base = box(1.6, 0.4, 0.9, '#ffffff'); base.position.y = 0.2; bed.add(base);
    const quilt = box(1.5, 0.25, 0.85, '#FFD9E0'); quilt.position.y = 0.5; bed.add(quilt);
    const pillow = box(0.5, 0.18, 0.3, '#ffffff'); pillow.position.set(-0.5, 0.62, 0); bed.add(pillow); }
  bed.position.set(-1.0, 0, -1.3); scene.add(bed); furniture.push(bed);

  const desk = new THREE.Group();
  { const top = box(1.1, 0.08, 0.6, '#E8C39E'); top.position.y = 0.7; desk.add(top);
    [[-0.5, -0.27], [0.5, -0.27], [-0.5, 0.27], [0.5, 0.27]].forEach(([lx, lz]) => {
      const leg = cyl(0.04, 0.7, '#C99A6B', 10); leg.position.set(lx, 0.35, lz); desk.add(leg); });
    const pc = box(0.5, 0.04, 0.3, '#cccccc'); pc.position.set(0, 0.76, 0); desk.add(pc);
    const scr = box(0.5, 0.34, 0.04, '#222222'); scr.position.set(0, 0.95, -0.12); desk.add(scr);
    const glow = box(0.44, 0.28, 0.012, '#4D7CFE'); glow.position.set(0, 0.95, -0.095); desk.add(glow); }
  desk.position.set(1.2, 0, -1.4); scene.add(desk); furniture.push(desk);

  const shelf = new THREE.Group();
  { const frame = box(0.7, 1.2, 0.3, '#D7A86E'); frame.position.y = 0.6; shelf.add(frame);
    ['#FF5C5C', '#4ED1A1', '#FFC83D'].forEach((bc, i) => {
      const bk = box(0.12, 0.7, 0.24, bc); bk.position.set(-0.2 + i * 0.18, 0.82, 0.05); bk.rotation.z = (i - 1) * 0.05; shelf.add(bk); }); }
  shelf.position.set(-1.7, 0, 0.6); scene.add(shelf); furniture.push(shelf);

  const plant = new THREE.Group();
  { const pot = cyl(0.18, 0.3, '#C0795A', 14); pot.position.y = 0.15; plant.add(pot);
    const stem = cyl(0.04, 0.5, '#7a5a3a', 8); stem.position.y = 0.5; plant.add(stem);
    const leaf = new THREE.Mesh(new THREE.IcosahedronGeometry(0.3, 0), mat('#7BC86C')); leaf.position.y = 0.85; plant.add(leaf); }
  plant.position.set(1.6, 0, 0.8); scene.add(plant); furniture.push(plant);

  let start = 0, lastPick = -3, pick = -1, pickStart = 0;
  const animate = (now) => {
    if (!start) start = now;
    const t = (now - start) / 1000;

    // カメラ:半径6.5・高さ3.2 の弧上を周期12sで往復、常に部屋中心を見る
    const ang = 0.1 + 0.5 * Math.sin(t * 2 * Math.PI / 12);
    camera.position.set(Math.sin(ang) * 6.5, 3.2, Math.cos(ang) * 6.5);
    camera.lookAt(0, 0.8, 0);

    // シバ:2.5sごとに家具を1つ選び、0.5sの減衰バウンス
    if (t - lastPick > 2.5) { lastPick = t; pick = (pick + 1) % furniture.length; pickStart = t; }
    furniture.forEach(f => { f.scale.y = 1; });
    if (pick >= 0) {
      const p = (t - pickStart) / 0.5;
      if (p < 1) furniture[pick].scale.y = 1 + 0.18 * Math.sin(p * Math.PI) * (1 - p);
    }

    renderer.render(scene, camera);
    requestAnimationFrame(animate);
  };
  requestAnimationFrame(animate);
})();

# 外部ライブラリ
https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。