パステル3Dルーム俯瞰
パステル色のミニチュアな部屋を、カメラがゆったり弧を描いて眺めます。家具がときどきシュッと弾む。ポートフォリオで人気の作り込んだ3D空間の最小構成を、プリミティブな形状だけで作るデモです。
外部ライブラリ: https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
<!-- MOON BREW: 店内の3Dミニチュア。カメラが弧を描いてカフェの一角を眺める -->
<div class="stage">
<canvas class="room" data-room aria-label="MOON BREW 店内の3Dミニチュア"></canvas>
</div>
* { 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%; }
// 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を増やしすぎないようにします。
応用例
家具をブランド什器に、配色をテーマに、カメラ経路やシバ(弾み)の対象を調整、クリックで家具にフォーカスなどに発展できます。
コード
<!-- パステル3Dルーム俯瞰:プリミティブだけのミニチュア部屋をカメラが弧を描いて眺める -->
<div class="stage">
<canvas class="room" data-room aria-label="パステルカラーの3Dミニチュアルーム"></canvas>
</div>
* { 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%; }
// 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エージェント用プロンプト
あなたは熟練のフロントエンドエンジニアです。私の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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。