3D visualization for AI agent swarms built with Three.js. Matrix green/noir cyberpunk aesthetic. - 4 agents: Timmy (orchestrator), Forge (builder), Seer (planner), Echo (comms) - Central core pillar, animated green grid, digital rain - Agent info panels, chat, task list, memory views - WebSocket protocol for real-time state updates - iPad-ready: touch controls, add-to-homescreen - Post-processing: bloom, scanlines, vignette - No build step — pure ES modules via esm.sh CDN Created with Perplexity Computer
207 lines
5.8 KiB
JavaScript
207 lines
5.8 KiB
JavaScript
// ===== World: Ground plane, grid, core pillar, environment =====
|
|
import * as THREE from 'three';
|
|
|
|
// Grid shader - animated green grid on the ground
|
|
const gridVertexShader = `
|
|
varying vec2 vUv;
|
|
varying vec3 vWorldPos;
|
|
void main() {
|
|
vUv = uv;
|
|
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
|
vWorldPos = worldPos.xyz;
|
|
gl_Position = projectionMatrix * viewMatrix * worldPos;
|
|
}
|
|
`;
|
|
|
|
const gridFragmentShader = `
|
|
uniform float uTime;
|
|
uniform float uRadius;
|
|
varying vec2 vUv;
|
|
varying vec3 vWorldPos;
|
|
|
|
void main() {
|
|
vec2 p = vWorldPos.xz;
|
|
float dist = length(p);
|
|
|
|
// Circular falloff
|
|
float edge = smoothstep(uRadius, uRadius - 8.0, dist);
|
|
|
|
// Grid lines
|
|
float gridSize = 2.0;
|
|
vec2 grid = abs(fract(p / gridSize - 0.5) - 0.5) / fwidth(p / gridSize);
|
|
float line = min(grid.x, grid.y);
|
|
float gridAlpha = 1.0 - min(line, 1.0);
|
|
|
|
// Pulse wave from center
|
|
float pulse = sin(dist * 0.3 - uTime * 1.5) * 0.5 + 0.5;
|
|
pulse = smoothstep(0.3, 0.7, pulse);
|
|
|
|
// Sub-grid (finer lines)
|
|
float subGridSize = 0.5;
|
|
vec2 subGrid = abs(fract(p / subGridSize - 0.5) - 0.5) / fwidth(p / subGridSize);
|
|
float subLine = min(subGrid.x, subGrid.y);
|
|
float subGridAlpha = 1.0 - min(subLine, 1.0);
|
|
|
|
// Combine
|
|
float alpha = (gridAlpha * 0.5 + subGridAlpha * 0.08) * edge;
|
|
alpha += gridAlpha * edge * pulse * 0.2;
|
|
|
|
// Radial glow near center
|
|
float centerGlow = smoothstep(15.0, 0.0, dist) * 0.04;
|
|
alpha += centerGlow * edge;
|
|
|
|
// Green color with slight variation
|
|
vec3 color = mix(vec3(0.0, 0.4, 0.02), vec3(0.0, 1.0, 0.25), pulse * 0.5 + gridAlpha * 0.5);
|
|
|
|
gl_FragColor = vec4(color, alpha);
|
|
}
|
|
`;
|
|
|
|
export function createWorld(scene) {
|
|
const worldGroup = new THREE.Group();
|
|
|
|
// ---- Ground Plane with Grid Shader ----
|
|
const groundGeom = new THREE.PlaneGeometry(120, 120, 1, 1);
|
|
groundGeom.rotateX(-Math.PI / 2);
|
|
|
|
const gridMaterial = new THREE.ShaderMaterial({
|
|
vertexShader: gridVertexShader,
|
|
fragmentShader: gridFragmentShader,
|
|
uniforms: {
|
|
uTime: { value: 0 },
|
|
uRadius: { value: 50 },
|
|
},
|
|
transparent: true,
|
|
depthWrite: false,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
const groundMesh = new THREE.Mesh(groundGeom, gridMaterial);
|
|
groundMesh.position.y = -0.01;
|
|
groundMesh.renderOrder = -1;
|
|
worldGroup.add(groundMesh);
|
|
|
|
// ---- Dark solid floor underneath ----
|
|
const floorGeom = new THREE.CircleGeometry(52, 64);
|
|
floorGeom.rotateX(-Math.PI / 2);
|
|
const floorMat = new THREE.MeshBasicMaterial({ color: 0x030503, transparent: true, opacity: 0.9 });
|
|
const floorMesh = new THREE.Mesh(floorGeom, floorMat);
|
|
floorMesh.position.y = -0.05;
|
|
worldGroup.add(floorMesh);
|
|
|
|
// ---- THE CORE — Central Pillar ----
|
|
const coreGroup = new THREE.Group();
|
|
coreGroup.name = 'core';
|
|
|
|
// Main pillar - octagonal prism
|
|
const pillarGeom = new THREE.CylinderGeometry(1.2, 1.5, 14, 8, 1);
|
|
const pillarMat = new THREE.MeshBasicMaterial({
|
|
color: 0x00ff41,
|
|
wireframe: true,
|
|
transparent: true,
|
|
opacity: 0.3,
|
|
});
|
|
const pillar = new THREE.Mesh(pillarGeom, pillarMat);
|
|
pillar.position.y = 7;
|
|
coreGroup.add(pillar);
|
|
|
|
// Inner glowing core
|
|
const innerGeom = new THREE.CylinderGeometry(0.6, 0.8, 12, 8, 1);
|
|
const innerMat = new THREE.MeshBasicMaterial({
|
|
color: 0x00ff41,
|
|
transparent: true,
|
|
opacity: 0.6,
|
|
});
|
|
const inner = new THREE.Mesh(innerGeom, innerMat);
|
|
inner.position.y = 7;
|
|
coreGroup.add(inner);
|
|
|
|
// Floating rings around core
|
|
for (let i = 0; i < 3; i++) {
|
|
const ringGeom = new THREE.TorusGeometry(2.2 + i * 0.6, 0.04, 8, 32);
|
|
const ringMat = new THREE.MeshBasicMaterial({
|
|
color: 0x00ff41,
|
|
transparent: true,
|
|
opacity: 0.4 - i * 0.1,
|
|
});
|
|
const ring = new THREE.Mesh(ringGeom, ringMat);
|
|
ring.position.y = 4 + i * 4;
|
|
ring.rotation.x = Math.PI / 2 + (i * 0.2);
|
|
ring.userData.ringIndex = i;
|
|
coreGroup.add(ring);
|
|
}
|
|
|
|
// Core base glow
|
|
const baseGlowGeom = new THREE.CylinderGeometry(2.5, 3, 0.3, 8, 1);
|
|
const baseGlowMat = new THREE.MeshBasicMaterial({
|
|
color: 0x00ff41,
|
|
transparent: true,
|
|
opacity: 0.15,
|
|
});
|
|
const baseGlow = new THREE.Mesh(baseGlowGeom, baseGlowMat);
|
|
baseGlow.position.y = 0.15;
|
|
coreGroup.add(baseGlow);
|
|
|
|
// Point light at core
|
|
const coreLight = new THREE.PointLight(0x00ff41, 2, 30);
|
|
coreLight.position.y = 7;
|
|
coreGroup.add(coreLight);
|
|
|
|
worldGroup.add(coreGroup);
|
|
|
|
// ---- Ambient lighting ----
|
|
const ambient = new THREE.AmbientLight(0x112211, 0.3);
|
|
worldGroup.add(ambient);
|
|
|
|
// Dim directional for slight form
|
|
const dirLight = new THREE.DirectionalLight(0x00ff41, 0.15);
|
|
dirLight.position.set(10, 20, 10);
|
|
worldGroup.add(dirLight);
|
|
|
|
// ---- Green fog ----
|
|
scene.fog = new THREE.FogExp2(0x020502, 0.012);
|
|
|
|
scene.add(worldGroup);
|
|
|
|
return {
|
|
worldGroup,
|
|
gridMaterial,
|
|
coreGroup,
|
|
coreLight,
|
|
|
|
update(time, delta) {
|
|
// Update grid pulse
|
|
gridMaterial.uniforms.uTime.value = time;
|
|
|
|
// Rotate core slowly
|
|
coreGroup.rotation.y = time * 0.15;
|
|
|
|
// Pulse core light
|
|
coreLight.intensity = 1.5 + Math.sin(time * 2) * 0.5;
|
|
|
|
// Animate rings
|
|
coreGroup.children.forEach(child => {
|
|
if (child.userData.ringIndex !== undefined) {
|
|
const i = child.userData.ringIndex;
|
|
child.rotation.z = time * (0.2 + i * 0.1);
|
|
child.position.y = 4 + i * 4 + Math.sin(time * 0.8 + i) * 0.3;
|
|
}
|
|
});
|
|
|
|
// Pulse inner core opacity
|
|
inner.material.opacity = 0.4 + Math.sin(time * 3) * 0.2;
|
|
},
|
|
|
|
dispose() {
|
|
worldGroup.traverse(obj => {
|
|
if (obj.geometry) obj.geometry.dispose();
|
|
if (obj.material) {
|
|
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
|
|
else obj.material.dispose();
|
|
}
|
|
});
|
|
scene.remove(worldGroup);
|
|
}
|
|
};
|
|
}
|