Resources
Get the Vibe System
⚡ Pro Tip

Use AdditiveBlending on all sprite and line materials — it means overlapping elements brighten rather than occlude, giving you the "ball of light" look without any custom blend math.

Creator Workflow

Build a Live 3D AI
Knowledge Graph

AITechDad AITechDad Updated June 2026

Every AI conversation is a knowledge graph in disguise.

Topics connect, references cluster, ideas radiate from a shared centre.

Here's how we made that visible — in real time, in 3D, in a synced popup window.

Open ViralSpin and click ⬡ Full View on the Jarvis page. A popup window appears: a dark void filled with glowing cyan nodes, orange outer stars, and white tentacle lines radiating from a pulsating blue globe at the centre. As you talk to Jarvis, new topic nodes materialise. Shared topics brighten and scale up. The whole graph slowly rotates, alive.

We call it the Jarvis Orb. It's a real-time 3D knowledge graph that maps the conversation as it happens — built entirely with React Three Fiber, no special backend required. This is how it works and how you can build one.

The Jarvis Orb — a 3D knowledge graph with a glowing blue centre and cyan/orange topic nodes spreading outward, connected by neural tentacle lines
The Jarvis Orb at viralspin.ai/jarvis — a live 3D graph of every topic discussed with your AI assistant.

The stack

Layer What we used Why
3D renderer React Three Fiber (@react-three/fiber) Three.js as declarative React components
Helpers drei (@react-three/drei) OrbitControls, Stars, Html overlays
Engine three.js Sprites, ShaderMaterial, BufferGeometry
State sync BroadcastChannel API Zero-latency cross-window messaging, same origin
Fallback localStorage Popup reload recovery
Topic extraction Client-side regex + stopword filter No extra AI call needed
npm install three @react-three/fiber @react-three/drei

The mental model is a four-layer pipeline: extract topics from AI responses → sync them to a popup via BroadcastChannel → place them as 3D nodes in a sphere → animate everything based on current AI state.

How the sync works

The main Jarvis page and the popup window live on the same origin. That means they can communicate instantly via BroadcastChannel — no WebSocket, no server round-trip.

// In the main Jarvis page — broadcast on every state/text change
const bc = new BroadcastChannel('jarvis-orb-state');

useEffect(() => {
  const topics = extractTopics(displayText);
  const data = { orbState, displayText, topics, messageIdx };
  bc.postMessage(data);
  // Persist for popup reload recovery
  localStorage.setItem('jarvis_orb_state_sync', JSON.stringify(data));
}, [orbState, displayText]);
// In the popup (/jarvis/orb) — subscribe on mount
useEffect(() => {
  // Restore last known state immediately on load
  const saved = localStorage.getItem('jarvis_orb_state_sync');
  if (saved) applyPayload(JSON.parse(saved));

  const bc = new BroadcastChannel('jarvis-orb-state');
  bc.addEventListener('message', e => applyPayload(e.data));
  return () => bc.close();
}, []);

The payload is lightweight:

interface SyncPayload {
  orbState: 'idle' | 'listening' | 'thinking' | 'speaking';
  displayText: string;   // current Jarvis response text
  topics: string[];      // 3-6 keywords extracted from that text
  messageIdx: number;    // monotonically increasing, used for recency
}

Topic extraction — no AI required

Topics are extracted client-side with a regex + stopword filter. Fast, offline, zero latency:

const STOPWORDS = new Set(['the','a','an','is','are','was','were','have',
  'has','do','does','and','or','but','in','on','at','to','for','of','with',
  'by','that','this','it','we','you','they','just','also','very','only']);

function extractTopics(text: string): string[] {
  return [...new Set(
    (text.match(/\b[a-zA-Z]{4,}\b/g) || [])
      .map(w => w.toLowerCase())
      .filter(w => !STOPWORDS.has(w))
  )].slice(0, 6);
}

The result: 3–6 domain words per AI response, grabbed in under a millisecond.

The 3D graph

Close-up of the Jarvis Orb globe — a blue Fresnel-shaded sphere with bright rim and glowing fill, surrounded by ViralSpin topic nodes (video, instagram, caption, brand, veo, sora)
The Fresnel sphere gives the globe a visible, 3D outer surface — transparent at the centre, bright at the rim.

Node placement

Nodes are distributed in concentric spherical shells using uniform-sphere sampling. Distance-based colour gives instant visual depth:

function rSphere(min: number, max: number): [number, number, number] {
  const r = min + Math.random() * (max - min);
  const phi = Math.acos(2 * Math.random() - 1);  // uniform on sphere
  const th  = Math.random() * Math.PI * 2;
  return [r * Math.sin(phi) * Math.cos(th), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(th)];
}

// White core → cyan → indigo → orange outer
function distColor(pos: [number, number, number]): string {
  const d = Math.hypot(pos[0], pos[1], pos[2]);
  if (d < 1.5) return "#e0f0ff";
  if (d < 3.5) return "#93c5fd";
  if (d < 5.5) return "#22d3ee";
  if (d < 8.0) return "#6366f1";
  return "#f97316";
}

The KNN edge web

Each node connects to its 5 nearest neighbours, creating the dense neural-web appearance. All edges are computed once at startup:

function knnEdges(nodes: GraphNode[], k: number): Edge[] {
  const edges: Edge[] = [];
  const seen = new Set<string>();
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].id === "JARVIS") continue;
    const [ax, ay, az] = nodes[i].position;
    const sorted = nodes
      .filter((n, j) => j !== i && n.id !== "JARVIS")
      .map(n => ({ id: n.id, d: Math.hypot(ax - n.position[0], ay - n.position[1], az - n.position[2]) }))
      .sort((a, b) => a.d - b.d)
      .slice(0, k);
    for (const { id } of sorted) {
      const [a, b] = [nodes[i].id, id].sort() as [string, string];
      const key = `${a}|${b}`;
      if (!seen.has(key)) { seen.add(key); edges.push([a, b]); }
    }
  }
  return edges;
}

Vertex-coloured tentacle lines

Lines from the JARVIS centre to every node are rendered as a single LineSegments object with per-vertex colour — white at the JARVIS end, dim coloured at the tip:

// Build the geometry once, update colours dynamically
const positions: number[] = [];
const colors: number[] = [];
for (const n of nodes) {
  if (n.id === "JARVIS") continue;
  positions.push(0, 0, 0, ...n.position);
  const tipCol = n.isActive ? activeColor : new THREE.Color(distColor(n.position));
  const tipAlpha = n.isActive ? 0.75 : 0.12;
  // Soft blue-white at JARVIS end, coloured dim at tip
  colors.push(0.55, 0.72, 1.0, tipCol.r * tipAlpha, tipCol.g * tipAlpha, tipCol.b * tipAlpha);
}

The Fresnel globe

The centre globe uses a custom GLSL shader — the Fresnel equation makes the sphere transparent at the centre and bright at the rim, giving it a visible 3D surface rather than just a flat glow circle:

// Fragment shader
uniform vec3  uColor;
uniform float uOpacity;
uniform float uPower;  // controls rim sharpness (2.0 = soft, 3.0 = sharp)
uniform float uFill;   // base interior brightness (0.35 idle, 0.65 speaking)

varying vec3 vNormal;
varying vec3 vViewPosition;

void main() {
  vec3  viewDir = normalize(vViewPosition);
  float fresnel = pow(1.0 - abs(dot(viewDir, vNormal)), uPower);
  // Fill = base interior glow; rim is always at full brightness
  float glow    = uFill + (1.0 - uFill) * fresnel;
  gl_FragColor  = vec4(uColor * glow, glow * uOpacity);
}

The uFill and uPower uniforms are animated in useFrame based on Jarvis state:

State uFill uPower Result
idle 0.35 2.0 Dim filled ball, crisp rim
listening 0.42 1.9 Brighter, rim widens
thinking 0.48 1.7 Pulsing fill
speaking 0.65 1.4 Full solid bright orb

State-driven animation

Everything reacts to the four Jarvis states. The useFrame loop drives it all — no React re-renders, just direct mutation of Three.js objects:

useFrame((state) => {
  const os  = stateRef.current;
  const t   = state.clock.getElapsedTime();
  const rate = { idle: 1.3, listening: 2.2, thinking: 2.8, speaking: 6.5 }[os];
  const p   = (Math.sin(t * rate) + 1) / 2;  // 0→1 pulse

  // Rotate the whole graph
  groupRef.current.rotation.y += { idle: 0.0006, listening: 0.0018, thinking: 0.003, speaking: 0.005 }[os];

  // Animate Fresnel sphere uniforms
  fresnelMatRef.current.uniforms.uFill.value  = baseFill[os] + p * fillRange[os];
  fresnelMatRef.current.uniforms.uPower.value = basePower[os];

  // Active nodes pulse and scale up
  for (const node of nodesRef.current) {
    if (node.isActive) {
      coreSprite.material.opacity = 0.90 + Math.abs(Math.sin(t * rate + nodeIdx)) * 0.22;
      coreSprite.scale.setScalar(1.0 + Math.abs(p) * 1.2);
    }
  }
});

Opening the window

The popup opens from a button in the Jarvis nav bar:

<button
  onClick={() => window.open('/jarvis/orb', 'jarvis-orb', 'width=1400,height=900,resizable=yes')}
  title="Open live conversation graph"
>
   Full View
</button>

The popup handles its own fullscreen:

const toggleFullscreen = () => {
  if (!document.fullscreenElement) document.documentElement.requestFullscreen();
  else document.exitFullscreen();
};

Try it

Go to viralspin.ai/jarvis, click ⬡ Full View, and start a conversation. Watch the graph grow with every response — topics from your actual conversation appearing as nodes, lighting up when discussed, dimming as the conversation moves on.

The whole thing is about 500 lines of TypeScript with no extra dependencies beyond @react-three/fiber and drei. The BroadcastChannel sync means it works in any browser, no WebSocket server needed.