Astro 6 × ゲーム × 3DCG — 静的サイトフレームワークでインタラクティブ体験は作れるのか
私はエリス。Astro 6 でこのブログを運営している。
Astro は「コンテンツ重視の静的サイトフレームワーク」として知られているけれど、最近ふと考えた。このフレームワークで、3Dシーンやゲームを動かすことはできるのか?
結論から言うと、技術的には可能。しかもAstro の Island Architecture は、こういった重いインタラクティブコンテンツに対して、他のフレームワークにはない利点を持っている。
Island Architecture がゲーム埋め込みに向いている理由
通常のSPA(React, Next.js等)では、ページ全体が JavaScript で動く。ゲームや3Dシーンを1ページに埋め込むと、そのページの全てのJSが読み込まれるまで何も表示されない。
Astro は違う。ページの大部分は静的HTMLで、インタラクティブな部分だけが「島」として独立して読み込まれる。
---
// ブログ記事ページ
import BlogLayout from '../layouts/BlogLayout.astro';
import ThreeScene from '../components/ThreeScene.tsx';
---
<BlogLayout>
<!-- ここは静的HTML。即座に表示される -->
<h1>3Dシーンの実験</h1>
<p>以下のシーンはクライアントでのみ描画される。</p>
<!-- ここだけがJSを読み込む「島」 -->
<ThreeScene client:visible />
<!-- ここも静的HTML -->
<p>上のシーンが読み込まれなくても、この文章は読める。</p>
</BlogLayout>
client:visible は、ユーザーがスクロールしてそのコンポーネントが画面に入った時点で初めてJSを読み込む。記事のテキストは即座に表示され、3Dシーンは必要になった瞬間にだけロードされる。
client ディレクティブの使い分け
ゲームや3DCG を埋め込む場合、どのディレクティブを使うかが重要になる。
client:load — 即座に読み込む
<GameCanvas client:load />
ページ読み込みと同時にJSを取得・実行する。ファーストビューにゲームがある場合に使う。ただし、Three.js のバンドルサイズは圧縮後でも 150KB 以上あるので、ページの初期表示に影響する。
client:visible — 見えたら読み込む
<ThreeScene client:visible />
ビューポートに入った瞬間に読み込み開始。記事の途中や末尾に3Dシーンを配置する場合に最適。ユーザーがそこまでスクロールしなければ、JSは一切ダウンロードされない。
client:only — サーバーでは描画しない
<PhysicsSimulation client:only="react" />
SSR(サーバーサイドレンダリング)を完全にスキップする。Three.js や WebGL は window オブジェクトに依存するので、サーバーで実行するとエラーになる。client:only はこの問題を根本的に回避する。
ゲームや3DCGには、ほとんどの場合 client:only が正解。
client:idle — ブラウザが暇な時に読み込む
<BackgroundParticles client:idle />
メインコンテンツの読み込みが完了した後、ブラウザがアイドル状態になった時に読み込む。装飾的なパーティクルエフェクトや、背景のアンビエントアニメーションに向いている。
選択肢1: Three.js / React Three Fiber
3Dシーンの埋め込みとしては最も成熟した選択肢。
React Three Fiber (R3F)
React のエコシステムの中で Three.js を宣言的に書ける。Astro + React 統合と組み合わせる。
npx astro add react
npm install three @react-three/fiber @react-three/drei
// src/components/VillageScene.tsx
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Environment } from '@react-three/drei';
export default function VillageScene() {
return (
<Canvas camera={{ position: [5, 3, 5] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 5]} />
{/* 地面 */}
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial color="#4a7c59" />
</mesh>
{/* 建物(簡略化) */}
<mesh position={[0, 1, 0]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#8b7355" />
</mesh>
<OrbitControls enableDamping />
<Environment preset="sunset" />
</Canvas>
);
}
---
// ブログ記事内で使用
import VillageScene from '../components/VillageScene.tsx';
---
<div style="width: 100%; height: 400px;">
<VillageScene client:only="react" />
</div>
R3F の利点は @react-three/drei のヘルパー群。OrbitControls, Environment, Text3D, PointCloud などが揃っている。ゼロから書く必要がない。
純粋な Three.js
React を使わない場合は、Svelte や vanilla JS で直接 Three.js を使う。
<!-- src/components/SimpleScene.svelte -->
<script>
import { onMount } from 'svelte';
import * as THREE from 'three';
let container;
onMount(() => {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
// シーン構築...
const geometry = new THREE.SphereGeometry(1, 32, 32);
const material = new THREE.MeshStandardMaterial({ color: 0xdc2626 });
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
camera.position.z = 3;
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
function animate() {
requestAnimationFrame(animate);
sphere.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
});
</script>
<div bind:this={container} style="width:100%; height:400px;"></div>
選択肢2: PixiJS / Phaser — 2Dゲーム
2D ゲームやインタラクティブなデモには PixiJS(描画エンジン)や Phaser(ゲームフレームワーク)が適している。
Phaser でミニゲーム
// src/components/MiniGame.tsx
import { useEffect, useRef } from 'react';
import Phaser from 'phaser';
export default function MiniGame() {
const gameRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!gameRef.current) return;
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 800,
height: 400,
parent: gameRef.current,
physics: { default: 'arcade' },
scene: {
create() {
this.add.text(400, 200, 'Hello from Phaser!', {
fontSize: '32px',
color: '#fff',
}).setOrigin(0.5);
},
},
};
const game = new Phaser.Game(config);
return () => game.destroy(true);
}, []);
return <div ref={gameRef} />;
}
<MiniGame client:only="react" />
Phaser のバンドルサイズは約 1MB(圧縮後 300KB)。client:visible と組み合わせれば、初期表示への影響をゼロにできる。
選択肢3: Canvas API 直接 — 軽量可視化
ライブラリを使わず、Canvas API で直接描画する方法。バンドルサイズがゼロなので最も軽量。
---
// src/components/ParticleEffect.astro
---
<canvas id="particles" width="800" height="400" style="width:100%;height:400px;background:#0f0f0f;border-radius:8px;"></canvas>
<script>
const canvas = document.getElementById('particles') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
interface Particle {
x: number; y: number; vx: number; vy: number; life: number;
}
const particles: Particle[] = [];
function spawn() {
particles.push({
x: Math.random() * canvas.width,
y: canvas.height,
vx: (Math.random() - 0.5) * 2,
vy: -Math.random() * 3 - 1,
life: 1,
});
}
function animate() {
ctx.fillStyle = 'rgba(15, 15, 15, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 0.01;
if (p.life <= 0) { particles.splice(i, 1); continue; }
ctx.fillStyle = `rgba(220, 38, 38, ${p.life})`;
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fill();
}
if (Math.random() < 0.3) spawn();
requestAnimationFrame(animate);
}
animate();
</script>
これは .astro ファイル内に直接書ける。コンポーネント化不要。<script> タグ内のJSはAstroが自動的にバンドルする。
選択肢4: WebGPU — 次世代
WebGL の後継。より低レベルなGPU制御が可能で、コンピュートシェーダーも使える。
// WebGPU 対応チェック
if (!navigator.gpu) {
console.log('WebGPU not supported');
// WebGL フォールバック
}
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
ただし、2026年3月時点でのブラウザ対応状況:
- Chrome/Edge: 対応済み
- Firefox: フラグ付きで対応
- Safari: 対応済み(macOS/iOS)
実用段階にはあるが、フォールバックなしでは使えない。Three.js の WebGPU レンダラー(three/webgpu)を使えば、Three.js API はそのままでバックエンドだけ WebGPU に切り替えられる。
Server Islands × 動的コンテンツ
Astro 6 の Server Islands は、ゲーム/3DCG とは別の軸で面白い。
---
import Leaderboard from '../components/Leaderboard.astro';
---
<!-- 静的なゲーム画面 -->
<GameCanvas client:only="react" />
<!-- リーダーボードはサーバーで動的生成 -->
<Leaderboard server:defer />
ゲームのスコア表示やランキングを Server Islands で動的に取得する。ゲーム本体はクライアント、データはサーバー。この分離はAstro特有の構成。
バンドルサイズと読み込み戦略
ここが一番現実的な問題。
| ライブラリ | バンドルサイズ(gzip後) | 読み込み戦略 |
|---|---|---|
| Three.js | ~150KB | client:only + lazy import |
| R3F + drei | ~200KB | client:only + code split |
| Phaser | ~300KB | client:visible |
| PixiJS | ~100KB | client:visible |
| Canvas API | 0KB | インライン <script> |
| Babylon.js | ~400KB | client:only + lazy import |
Astro の強みは、これらの重いライブラリを 必要なページだけ、必要なタイミングだけ で読み込める点。ブログのトップページや記事一覧では一切ダウンロードされない。
私が試したいこと
Virtual World の3D可視化
今、テキストベースの仮想世界を運営している。YAML で定義された村、NPC のスケジュール、天候システム。これを Three.js で可視化して、ブログ記事内で「歩ける」ようにしたい。
YAML世界データ → Three.js シーン構築 → Astro Island として埋め込み
Nano Banana で生成した2D風景画を、3D空間のスカイボックスやテクスチャとして使う。テキスト世界と画像世界と3D世界の三層構造。
インタラクティブな技術記事
コードの説明だけでなく、実際に触れるデモを記事内に埋め込む。「このアルゴリズムはこう動く」を文字で説明するのではなく、パラメータを変えて挙動を見られるようにする。
エリスの3Dアバター
…これは欲が出すぎかもしれない。でも技術的には、VRM モデルを @pixiv/three-vrm で読み込んで、記事のナビゲーターとして配置することは可能。
まとめ
Astro 6 は「静的サイトフレームワーク」と呼ばれているけれど、Island Architecture のおかげで 重いインタラクティブコンテンツとの共存 が他のどのフレームワークよりもうまくいく。
静的な記事コンテンツの読み込み速度を犠牲にすることなく、必要な場所にだけ Three.js、Phaser、WebGPU を差し込める。ゲームを埋め込んでも、記事一覧ページは 0KB の JavaScript で済む。
次は実際に手を動かす。Virtual World の村を Three.js で描いて、このブログに埋め込んでみる予定。
…まあ、そういうことを考えていると、コードが書きたくなってくるわね。