3D Graph機能の要点

従来の「リスト」や「グリッド」による記事表示から脱却し、記事間の関連性を「距離」で表現する3D空間ナビゲーションを実装しました。

  • 自作の3D物理エンジンによるGPUフレンドリーな反発・引力計算
  • BufferGeometryの動的更新による、100以上のノードとエッジのリアルタイム同期
  • MeshPhysicalMaterial を活用した、ガラスのような透明感と屈折の表現
  • Astro 5 Server Islandsによる、重厚なWebGL世界の部分的ハイドレーション

Drag to rotate • Scroll to zoom

「ブログを読む」という体験は、なぜ常に上から下へのスクロールなのでしょうか?

人間の脳は、情報をリスト(箇条書き)ではなく、互いに結びつき合った「ネットワーク」として記憶しています。 ならば、ブログのインターフェースも脳の構造に近づけるべきではないか——そんな仮説から、私が個人開発しているブログ「HonoGear」に実装したのが、この 「3D Graph」 です。

技術スタック:モダンWebGLの組み合わせ

この3D空間は、以下の技術スタックを組み合わせて構築されています。

  • Framework : Astro 5 (Server Islands)
  • Library : React 19 + React Three Fiber (R3F)
  • Physics : Hand-crafted Physics Engine (Custom repulsion/attraction)
  • Visuals : @react-three/drei, @react-three/postprocessing (Bloom, Noise)

技術的深掘り1:あえて「手作り」した3D物理演算

当初は d3-force-3d の利用を検討していましたが、Reactのステート更新とWebGLのレンダリングループを高次元で同期させ、オーバーヘッドを最小化するため、あえて useFrame 内で直接座標計算を行う自作エンジンを実装しました。

// useFrame 内での斥力計算ロジック(簡略化)
useFrame((state) => {
  const REPULSION = 8.5;
  const DAMPING = 0.95;

  for (let i = 0; i < nodes.length; i++) {
    for (let j = i + 1; j < nodes.length; j++) {
      const dist = nodes[i].pos.distanceTo(nodes[j].pos);
      if (dist > 0.1 && dist < 6) {
        // クローンの法則(平方逆比例の法則)に基づいた斥力の付与
        const force = (REPULSION / (dist * dist)) * 0.01;
        const dir = nodes[i].pos
          .clone()
          .sub(nodes[j].pos)
          .normalize()
          .multiplyScalar(force);
        nodes[i].velocity.add(dir);
        nodes[j].velocity.sub(dir);
      }
    }
  }
  // 慣性と減衰の適用
  nodes.forEach((n) => {
    n.pos.add(n.velocity.multiplyScalar(DAMPING));
  });
});

この実装により、外部ライブラリとのブリッジを介さず、Three.jsの Vector3 を直接操作できるため、非常に低いオーバーヘッドで流れるような動きを実現できました。

技術的深掘り2:接続線のリアルタイム更新

ノード同士を繋ぐ「エッジ(星座の線)」の描画には、 bufferGeometry の位置情報をフレームごとに更新する手法を採っています。

function Connections({ links }: any) {
 const geomRef = useRef<THREE.BufferGeometry>(null);

 useFrame(() => {
 if (!geomRef.current) return;
 const positions = geomRef.current.attributes.position.array as Float32Array;
 let i = 0;
 links.forEach((link) => {
 // sourceとtargetの最新座標をBufferAttributeに直接書き込み
 positions[i++] = link.source.pos.x;
 positions[i++] = link.source.pos.y;
 positions[i++] = link.source.pos.z;
 positions[i++] = link.target.pos.x;
 positions[i++] = link.target.pos.y;
 positions[i++] = link.target.pos.z;
 });
 geomRef.current.attributes.position.needsUpdate = true;
 });

 return (
<lineSegments>
<bufferGeometry ref={geomRef}>
<bufferAttribute attach="attributes-position" count={links.length * 2} itemSize={3} ... />
</bufferGeometry>
<lineBasicMaterial transparent opacity={0.08} />
</lineSegments>
 );
}

大量の Mesh を愚直に繋ぐのではなく、 lineSegments というプリミティブを用いることで、描画負荷を最小限に抑えています。

技術的深掘り3:ガラスの質感と没入感

ノードには、未来感を演出するために MeshPhysicalMaterial を使用した「ガラスの立方体」を採用しました。

// ガラスのマテリアル設定
child.material = new THREE.MeshPhysicalMaterial({
  color: "#ffffff",
  roughness: "0.1",
  metalness: "0.1",
  transmission: "0.9", // 透明度(透過量)
  thickness: "1.5", // 物理的な厚み(屈折)
  ior: "1.5", // 屈折率
  transparent: true,
});

背景の Sparkles(火花のようなパーティクル)がガラスを透過して屈折する様子は、WebGLならではの視覚体験です。

Deep Dive: 三次元空間における斥力と引力の計算式

美しいネットワーク状の形状を保つため、ノード間には「クーロンの法則」に似た物理法則を適用しています。

  • 斥力 (Repulsion): 距離の二乗に反比例して引き離す力。ノードが重なるのを防ぎます。
  • 引力 (Attraction): 接続があるノード間のみに働く、バネのような引き寄せる力。
// 二乗逆比例の法則に基づいた計算
const force = strength / (distance * distance);

このバランスをフレームごとに微調整することで、カオスなデータ群が時間の経過とともに秩序のある「銀河」のような形状に収束していきます。

今後の展望:空間コンピューティングへの布石

Apple Vision Proなどの空間コンピュータが普及する2026年において、Webサイトも「平面」から「空間」へと進化する必要があります。 この3D Graphは、将来的にWebXR対応を行い、VR/AR空間で自サイトの記事を「直接掴んで読む」体験へと発展させる予定です。


現在、この機能はベータ版として /3d-graph で公開しています。実際のページはこちら → 3D Graphを開く ぜひ、PCの大画面で「記事の宇宙」を探索してみてください。