世界40社超のAI企業を、太陽系の天体に見立てた3Dインタラクティブマップを作った。NVIDIAが太陽(時価総額$4.31兆)、Google・Microsoftが惑星、OpenAI・Anthropicが内惑星として公転する。
完成したもの: AI Gravity Map 2026
「Three.jsをWordPressで使う」という情報が意外と少なかったので、ハマったところを中心にまとめる。
この記事でできること
- Three.jsの基本的な3Dシーンのセットアップ手順を理解できる
- WordPressで外部ライブラリを安全に読み込む方法がわかる
-
&&演算子がWordPressで壊れる問題と回避策を知れる - 3Dオブジェクトへのクリックイベント(Raycaster)の実装方法がわかる
- 実際に動作するツールはAI Gravity Map 2026で確認できる
環境・前提
- ブラウザ: Chrome 120以上(WebGL対応が必要)
- Three.js: r164(CDNで読み込む)
- WordPress: 6.x(SWELLテーマを使用)
- JavaScript: ES5で記述(ES6+は後述の理由で避けた)
- 外部ライブラリ: OrbitControls(Three.jsのaddons)
完成形
企業データ(時価総額・設立年・カテゴリ)をJSON形式で定義し、Three.jsで球体として3D空間に配置する。球の大きさは時価総額のログスケールで決まる。クリックで企業の詳細が表示され、カテゴリフィルターで絞り込みができる。
完成したもの: AI Gravity Map 2026
手順
Step 1: Three.jsをWordPressで読み込む
NG例(SWELLでは動かない):
<script src="https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.min.js"></script>
WordPressのSWELLテーマは、カスタムHTMLブロック内の<script src="...">タグを無視する。コンソールにエラーも出ない。ただ黙って読み込まない。
OK例(動的ロード):
function loadThree(callback) {
if (typeof THREE !== 'undefined') {
callback();
return;
}
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.min.js';
s.onload = function() {
// OrbitControlsも連鎖してロード
var oc = document.createElement('script');
oc.src = 'https://cdn.jsdelivr.net/npm/three@0.164.0/examples/js/controls/OrbitControls.js';
oc.onload = callback;
document.head.appendChild(oc);
};
document.head.appendChild(s);
}
loadThree(function() {
initScene(); // ここからThree.jsのコードを書く
});
typeof THREE !== 'undefined'のチェックが大事。同じページに複数のブロックがある場合、最初のブロックがロードしたThree.jsを全ブロックで共有できる。
つまずきポイント: script.onloadの中でさらに別のスクリプト(OrbitControls)をロードする。2つを同時にappendすると読み込み順が保証されないので、連鎖(チェーン)させる。
Step 2: && 演算子を使わない
NG例(WordPressが壊す):
if (isMobile && isLoaded) {
initMobile();
}
WordPressのコンテンツフィルターが && を && にHTMLエンティティ変換する。JavaScriptの構文エラーになり、スクリプト全体が止まる。
OK例(ネストif):
if (isMobile) {
if (isLoaded) {
initMobile();
}
}
または三項演算子:
var result = (isMobile ? mobileInit() : desktopInit());
つまずきポイント: エラーメッセージが「構文エラー」としか出ないので、どこが壊れているか特定しにくい。Three.jsのコードは量が多いので、最初から&&を使わない書き方に慣れておくと楽。
Step 3: 天体データの定義
企業データを配列で定義する。valUSD(時価総額・十億ドル)が球のサイズを決める。
var BODIES = [
{
id: 'nvidia',
type: 'star', // 恒星(最大)
name: 'NVIDIA',
valUSD: 4310, // 43.1億ドル = $4.31兆
category: 'chip',
founded: 1993,
desc: 'AI半導体の絶対王者。GPU市場を独占しAI革命を牽引する。'
},
{
id: 'openai',
type: 'planet', // 惑星
name: 'OpenAI',
valUSD: 852, // $8520億
parentId: null, // 独立した天体
// ...
},
{
id: 'palantir',
type: 'moon', // 衛星
name: 'Palantir',
valUSD: 355,
parentId: 'microsoft', // Microsoftの衛星として公転
// ...
}
];
ポイント: typeで天体の種別(star/planet/moon/dwarf/asteroid)を決める。moon(衛星)はparentIdで親天体を指定し、親の周りを公転させる。
Step 4: 基本シーンのセットアップ
function initScene() {
var container = document.getElementById('galaxy-canvas');
// レンダラー
var isMobile = window.innerWidth <= 767;
var renderer = new THREE.WebGLRenderer({
antialias: !isMobile, // スマホはfalse(パフォーマンス優先)
alpha: true
});
renderer.setSize(container.offsetWidth, container.offsetHeight);
container.appendChild(renderer.domElement);
// シーン・カメラ
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, container.offsetWidth / container.offsetHeight, 1, 5000);
camera.position.set(0, 80, 600);
// OrbitControls(ドラッグ回転・ズーム)
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 慣性
controls.autoRotate = true; // 自動回転
controls.autoRotateSpeed = 0.3;
// 照明
scene.add(new THREE.AmbientLight(0x222244, 0.8));
scene.add(new THREE.PointLight(0x9369A8, 0.5, 800)); // 銀河中心グロー
}
つまずきポイント: renderer.setSize()にコンテナのサイズを渡す必要がある。window.innerWidthを渡すと全画面になるが、ページの一部に配置したい場合はコンテナのサイズを使う。
Step 5: 球体の生成とクリックイベント
var starMeshes = [];
BODIES.forEach(function(body) {
// ログスケールでサイズを計算
var normalized = normalizeLog(body.valUSD, 0.1, 5000);
var radius = 0.3 + normalized * 4.2;
// スマホはポリゴン数を下げる
var segs = isMobile ? [8, 6] : [32, 16];
var geo = new THREE.SphereGeometry(radius, segs[0], segs[1]);
// カテゴリ別カラー
var color = getCategoryColor(body.catKey);
var mat = new THREE.MeshStandardMaterial({
color: color,
emissive: color,
emissiveIntensity: 0.4
});
var mesh = new THREE.Mesh(geo, mat);
mesh.userData.bodyId = body.id; // クリック時に使う
scene.add(mesh);
starMeshes.push(mesh);
});
// Raycaster(クリック判定)
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
renderer.domElement.addEventListener('click', function(e) {
var rect = renderer.domElement.getBoundingClientRect();
// canvasのオフセットを補正(ページ内に配置している場合に必要)
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var hits = raycaster.intersectObjects(starMeshes);
if (hits.length > 0) {
var bodyId = hits[0].object.userData.bodyId;
showDetail(bodyId); // 詳細パネルを表示する関数
}
});
つまずきポイント: Raycasterの座標計算にgetBoundingClientRect()が必要。Three.jsの公式サンプルは全画面前提が多いので、ページ内配置の場合は必ずrect.leftとrect.topで補正する。
Step 6: アニメーションループ
var animId;
function animate() {
animId = requestAnimationFrame(animate);
controls.update(); // damping有効時は毎フレーム必要
updateOrbits(); // 衛星の公転位置を更新
renderer.render(scene, camera);
}
// タブ非表示でアニメーション停止(バッテリー節約)
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
cancelAnimationFrame(animId);
} else {
animate();
}
});
animate();
つまずきポイント: controls.enableDamping = trueにした場合、controls.update()を毎フレーム呼ばないと慣性が効かない。忘れがちなポイント。
つまずきポイントまとめ
-
外部スクリプトが読み込まれない: SWELLでは
<script src="...">が無視される。document.createElement('script')で動的ロードに切り替える -
&&演算子でスクリプト全体が止まる: WordPress(wpautop)が&&を壊す。三項演算子かネストifに書き換える -
クリック座標がずれる: Three.jsのcanvasをページ内に配置している場合、
getBoundingClientRect()でオフセット補正が必要 -
スマホが重い:
SphereGeometryの分割数をスマホでは(8,6)に下げる。PCの(32,16)から約16分の1のポリゴン数になる -
OrbitControlsが動かない:
controls.update()をアニメーションループ内で毎フレーム呼ぶ必要がある
参考
- Three.js公式ドキュメント: https://threejs.org/docs/
- 完成したツール: AI Gravity Map 2026
