forked from Rockachopa/the-matrix
docs: add direction field to README custom-agent example
This commit is contained in:
28
js/agent-defs.js
Normal file
28
js/agent-defs.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* agent-defs.js — Single source of truth for all agent definitions.
|
||||
*
|
||||
* To add a new agent, append one entry to AGENT_DEFS below and pick an
|
||||
* unused (x, z) position. No other file needs to be edited.
|
||||
*
|
||||
* Fields:
|
||||
* id — unique string key used in WebSocket messages and state maps
|
||||
* label — display name shown in the 3D HUD and chat panel
|
||||
* color — hex integer (0xRRGGBB) used for Three.js materials and lights
|
||||
* role — human-readable role string shown under the label sprite
|
||||
* direction — cardinal facing direction (for future mesh orientation use)
|
||||
* x, z — world-space position on the horizontal plane (y is always 0)
|
||||
*/
|
||||
export const AGENT_DEFS = [
|
||||
{ id: 'alpha', label: 'ALPHA', color: 0x00ff88, role: 'orchestrator', direction: 'north', x: 0, z: -6 },
|
||||
{ id: 'beta', label: 'BETA', color: 0x00aaff, role: 'worker', direction: 'east', x: 6, z: 0 },
|
||||
{ id: 'gamma', label: 'GAMMA', color: 0xff6600, role: 'validator', direction: 'south', x: 0, z: 6 },
|
||||
{ id: 'delta', label: 'DELTA', color: 0xaa00ff, role: 'monitor', direction: 'west', x: -6, z: 0 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert an integer color (e.g. 0x00ff88) to a CSS hex string ('#00ff88').
|
||||
* Useful for DOM styling and canvas rendering.
|
||||
*/
|
||||
export function colorToCss(intColor) {
|
||||
return '#' + intColor.toString(16).padStart(6, '0');
|
||||
}
|
||||
157
js/agents.js
Normal file
157
js/agents.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import * as THREE from 'three';
|
||||
import { AGENT_DEFS, colorToCss } from './agent-defs.js';
|
||||
|
||||
const agents = new Map();
|
||||
let scene;
|
||||
let connectionLines = [];
|
||||
|
||||
class Agent {
|
||||
constructor(def) {
|
||||
this.id = def.id;
|
||||
this.label = def.label;
|
||||
this.color = def.color;
|
||||
this.role = def.role;
|
||||
this.position = new THREE.Vector3(def.x, 0, def.z);
|
||||
this.state = 'idle';
|
||||
this.pulsePhase = Math.random() * Math.PI * 2;
|
||||
|
||||
this.group = new THREE.Group();
|
||||
this.group.position.copy(this.position);
|
||||
|
||||
this._buildMeshes();
|
||||
this._buildLabel();
|
||||
}
|
||||
|
||||
_buildMeshes() {
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: this.color,
|
||||
emissive: this.color,
|
||||
emissiveIntensity: 0.4,
|
||||
roughness: 0.3,
|
||||
metalness: 0.8,
|
||||
});
|
||||
|
||||
const geo = new THREE.IcosahedronGeometry(0.7, 1);
|
||||
this.core = new THREE.Mesh(geo, mat);
|
||||
this.group.add(this.core);
|
||||
|
||||
const ringGeo = new THREE.TorusGeometry(1.1, 0.04, 8, 32);
|
||||
const ringMat = new THREE.MeshBasicMaterial({ color: this.color, transparent: true, opacity: 0.5 });
|
||||
this.ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
this.ring.rotation.x = Math.PI / 2;
|
||||
this.group.add(this.ring);
|
||||
|
||||
const glowGeo = new THREE.SphereGeometry(1.3, 16, 16);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: this.color,
|
||||
transparent: true,
|
||||
opacity: 0.05,
|
||||
side: THREE.BackSide,
|
||||
});
|
||||
this.glow = new THREE.Mesh(glowGeo, glowMat);
|
||||
this.group.add(this.glow);
|
||||
|
||||
const light = new THREE.PointLight(this.color, 1.5, 10);
|
||||
this.group.add(light);
|
||||
this.light = light;
|
||||
}
|
||||
|
||||
_buildLabel() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256; canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)';
|
||||
ctx.fillRect(0, 0, 256, 64);
|
||||
ctx.font = 'bold 22px Courier New';
|
||||
ctx.fillStyle = colorToCss(this.color);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(this.label, 128, 28);
|
||||
ctx.font = '14px Courier New';
|
||||
ctx.fillStyle = '#007722';
|
||||
ctx.fillText(this.role.toUpperCase(), 128, 50);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true });
|
||||
this.sprite = new THREE.Sprite(spriteMat);
|
||||
this.sprite.scale.set(2.4, 0.6, 1);
|
||||
this.sprite.position.y = 2;
|
||||
this.group.add(this.sprite);
|
||||
}
|
||||
|
||||
update(time) {
|
||||
const pulse = Math.sin(time * 0.002 + this.pulsePhase);
|
||||
const active = this.state === 'active';
|
||||
|
||||
const intensity = active ? 0.6 + pulse * 0.4 : 0.2 + pulse * 0.1;
|
||||
this.core.material.emissiveIntensity = intensity;
|
||||
this.light.intensity = active ? 2 + pulse : 0.8 + pulse * 0.3;
|
||||
|
||||
const scale = active ? 1 + pulse * 0.08 : 1 + pulse * 0.03;
|
||||
this.core.scale.setScalar(scale);
|
||||
this.ring.rotation.y += active ? 0.03 : 0.008;
|
||||
this.ring.material.opacity = 0.3 + pulse * 0.2;
|
||||
|
||||
this.group.position.y = this.position.y + Math.sin(time * 0.001 + this.pulsePhase) * 0.15;
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
export function initAgents(sceneRef) {
|
||||
scene = sceneRef;
|
||||
|
||||
AGENT_DEFS.forEach(def => {
|
||||
const agent = new Agent(def);
|
||||
agents.set(def.id, agent);
|
||||
scene.add(agent.group);
|
||||
});
|
||||
|
||||
buildConnectionLines();
|
||||
}
|
||||
|
||||
function buildConnectionLines() {
|
||||
connectionLines.forEach(l => scene.remove(l));
|
||||
connectionLines = [];
|
||||
|
||||
const agentList = [...agents.values()];
|
||||
const lineMat = new THREE.LineBasicMaterial({
|
||||
color: 0x003300,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
});
|
||||
|
||||
for (let i = 0; i < agentList.length; i++) {
|
||||
for (let j = i + 1; j < agentList.length; j++) {
|
||||
const a = agentList[i];
|
||||
const b = agentList[j];
|
||||
if (a.position.distanceTo(b.position) <= 8) {
|
||||
const points = [a.position.clone(), b.position.clone()];
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const line = new THREE.Line(geo, lineMat.clone());
|
||||
connectionLines.push(line);
|
||||
scene.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAgents(time) {
|
||||
agents.forEach(agent => agent.update(time));
|
||||
}
|
||||
|
||||
export function getAgentCount() {
|
||||
return agents.size;
|
||||
}
|
||||
|
||||
export function setAgentState(agentId, state) {
|
||||
const agent = agents.get(agentId);
|
||||
if (agent) agent.setState(state);
|
||||
}
|
||||
|
||||
export function getAgentDefs() {
|
||||
return [...agents.values()].map(a => ({
|
||||
id: a.id, label: a.label, role: a.role, color: a.color, state: a.state,
|
||||
}));
|
||||
}
|
||||
85
js/effects.js
vendored
Normal file
85
js/effects.js
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
let rainParticles;
|
||||
let rainPositions;
|
||||
let rainVelocities;
|
||||
const RAIN_COUNT = 2000;
|
||||
|
||||
export function initEffects(scene) {
|
||||
initMatrixRain(scene);
|
||||
initStarfield(scene);
|
||||
}
|
||||
|
||||
function initMatrixRain(scene) {
|
||||
const geo = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(RAIN_COUNT * 3);
|
||||
const velocities = new Float32Array(RAIN_COUNT);
|
||||
const colors = new Float32Array(RAIN_COUNT * 3);
|
||||
|
||||
for (let i = 0; i < RAIN_COUNT; i++) {
|
||||
positions[i * 3] = (Math.random() - 0.5) * 100;
|
||||
positions[i * 3 + 1] = Math.random() * 50 + 5;
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
|
||||
velocities[i] = 0.05 + Math.random() * 0.15;
|
||||
|
||||
const brightness = 0.3 + Math.random() * 0.7;
|
||||
colors[i * 3] = 0;
|
||||
colors[i * 3 + 1] = brightness;
|
||||
colors[i * 3 + 2] = 0;
|
||||
}
|
||||
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
rainPositions = positions;
|
||||
rainVelocities = velocities;
|
||||
|
||||
const mat = new THREE.PointsMaterial({
|
||||
size: 0.12,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
sizeAttenuation: true,
|
||||
});
|
||||
|
||||
rainParticles = new THREE.Points(geo, mat);
|
||||
scene.add(rainParticles);
|
||||
}
|
||||
|
||||
function initStarfield(scene) {
|
||||
const count = 500;
|
||||
const geo = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(count * 3);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
positions[i * 3] = (Math.random() - 0.5) * 300;
|
||||
positions[i * 3 + 1] = Math.random() * 80 + 10;
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * 300;
|
||||
}
|
||||
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
|
||||
const mat = new THREE.PointsMaterial({
|
||||
color: 0x003300,
|
||||
size: 0.08,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
});
|
||||
|
||||
const stars = new THREE.Points(geo, mat);
|
||||
scene.add(stars);
|
||||
}
|
||||
|
||||
export function updateEffects(_time) {
|
||||
if (!rainParticles) return;
|
||||
|
||||
for (let i = 0; i < RAIN_COUNT; i++) {
|
||||
rainPositions[i * 3 + 1] -= rainVelocities[i];
|
||||
if (rainPositions[i * 3 + 1] < -1) {
|
||||
rainPositions[i * 3 + 1] = 40 + Math.random() * 20;
|
||||
rainPositions[i * 3] = (Math.random() - 0.5) * 100;
|
||||
rainPositions[i * 3 + 2] = (Math.random() - 0.5) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
rainParticles.geometry.attributes.position.needsUpdate = true;
|
||||
}
|
||||
21
js/interaction.js
Normal file
21
js/interaction.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
|
||||
let controls;
|
||||
|
||||
export function initInteraction(camera, renderer) {
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.screenSpacePanning = false;
|
||||
controls.minDistance = 5;
|
||||
controls.maxDistance = 80;
|
||||
controls.maxPolarAngle = Math.PI / 2.1;
|
||||
controls.target.set(0, 0, 0);
|
||||
controls.update();
|
||||
|
||||
renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
|
||||
}
|
||||
|
||||
export function updateControls() {
|
||||
if (controls) controls.update();
|
||||
}
|
||||
49
js/main.js
Normal file
49
js/main.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { initWorld, onWindowResize } from './world.js';
|
||||
import { initAgents, updateAgents, getAgentCount } from './agents.js';
|
||||
import { initEffects, updateEffects } from './effects.js';
|
||||
import { initUI, updateUI } from './ui.js';
|
||||
import { initInteraction } from './interaction.js';
|
||||
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
|
||||
|
||||
let frameCount = 0;
|
||||
let lastFpsTime = performance.now();
|
||||
let currentFps = 0;
|
||||
|
||||
function main() {
|
||||
const { scene, camera, renderer } = initWorld();
|
||||
|
||||
initEffects(scene);
|
||||
initAgents(scene);
|
||||
initInteraction(camera, renderer);
|
||||
initUI();
|
||||
initWebSocket(scene);
|
||||
|
||||
window.addEventListener('resize', () => onWindowResize(camera, renderer));
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
const now = performance.now();
|
||||
frameCount++;
|
||||
if (now - lastFpsTime >= 1000) {
|
||||
currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime));
|
||||
frameCount = 0;
|
||||
lastFpsTime = now;
|
||||
}
|
||||
|
||||
updateEffects(now);
|
||||
updateAgents(now);
|
||||
updateUI({
|
||||
fps: currentFps,
|
||||
agentCount: getAgentCount(),
|
||||
jobCount: getJobCount(),
|
||||
connectionState: getConnectionState(),
|
||||
});
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
main();
|
||||
84
js/ui.js
Normal file
84
js/ui.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { getAgentDefs } from './agents.js';
|
||||
import { colorToCss } from './agent-defs.js';
|
||||
|
||||
const $agentCount = document.getElementById('agent-count');
|
||||
const $activeJobs = document.getElementById('active-jobs');
|
||||
const $fps = document.getElementById('fps');
|
||||
const $agentList = document.getElementById('agent-list');
|
||||
const $connStatus = document.getElementById('connection-status');
|
||||
const $chatPanel = document.getElementById('chat-panel');
|
||||
|
||||
const MAX_CHAT_ENTRIES = 12;
|
||||
const chatEntries = [];
|
||||
|
||||
export function initUI() {
|
||||
renderAgentList();
|
||||
}
|
||||
|
||||
function renderAgentList() {
|
||||
const defs = getAgentDefs();
|
||||
$agentList.innerHTML = defs.map(a => {
|
||||
const css = colorToCss(a.color);
|
||||
return `<div class="agent-row">
|
||||
<span class="label">[</span>
|
||||
<span style="color:${css}">${a.label}</span>
|
||||
<span class="label">]</span>
|
||||
<span id="agent-state-${a.id}" style="color:#003300"> IDLE</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
export function updateUI({ fps, agentCount, jobCount, connectionState }) {
|
||||
$fps.textContent = `FPS: ${fps}`;
|
||||
$agentCount.textContent = `AGENTS: ${agentCount}`;
|
||||
$activeJobs.textContent = `JOBS: ${jobCount}`;
|
||||
|
||||
if (connectionState === 'connected') {
|
||||
$connStatus.textContent = '● CONNECTED';
|
||||
$connStatus.className = 'connected';
|
||||
} else if (connectionState === 'connecting') {
|
||||
$connStatus.textContent = '◌ CONNECTING...';
|
||||
$connStatus.className = '';
|
||||
} else {
|
||||
$connStatus.textContent = '○ OFFLINE';
|
||||
$connStatus.className = '';
|
||||
}
|
||||
|
||||
const defs = getAgentDefs();
|
||||
defs.forEach(a => {
|
||||
const el = document.getElementById(`agent-state-${a.id}`);
|
||||
if (el) {
|
||||
el.textContent = ` ${a.state.toUpperCase()}`;
|
||||
el.style.color = a.state === 'active' ? '#00ff41' : '#003300';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a line to the chat panel.
|
||||
* @param {string} agentLabel — display name
|
||||
* @param {string} message — message text (HTML-escaped before insertion)
|
||||
* @param {string} cssColor — CSS color string, e.g. '#00ff88'
|
||||
*/
|
||||
export function appendChatMessage(agentLabel, message, cssColor) {
|
||||
const color = cssColor || '#00ff41';
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'chat-entry';
|
||||
entry.innerHTML = `<span class="agent-name" style="color:${color}">${agentLabel}</span>: ${escapeHtml(message)}`;
|
||||
chatEntries.push(entry);
|
||||
|
||||
if (chatEntries.length > MAX_CHAT_ENTRIES) {
|
||||
const removed = chatEntries.shift();
|
||||
$chatPanel.removeChild(removed);
|
||||
}
|
||||
|
||||
$chatPanel.appendChild(entry);
|
||||
$chatPanel.scrollTop = $chatPanel.scrollHeight;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
115
js/websocket.js
Normal file
115
js/websocket.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { AGENT_DEFS, colorToCss } from './agent-defs.js';
|
||||
import { setAgentState } from './agents.js';
|
||||
import { appendChatMessage } from './ui.js';
|
||||
|
||||
const WS_URL = import.meta.env.VITE_WS_URL || '';
|
||||
|
||||
const agentById = Object.fromEntries(AGENT_DEFS.map(d => [d.id, d]));
|
||||
|
||||
let ws = null;
|
||||
let connectionState = 'disconnected';
|
||||
let jobCount = 0;
|
||||
let reconnectTimer = null;
|
||||
const RECONNECT_DELAY_MS = 5000;
|
||||
|
||||
export function initWebSocket(_scene) {
|
||||
if (!WS_URL) {
|
||||
connectionState = 'disconnected';
|
||||
return;
|
||||
}
|
||||
connect();
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (ws) {
|
||||
ws.onclose = null;
|
||||
ws.close();
|
||||
}
|
||||
|
||||
connectionState = 'connecting';
|
||||
|
||||
try {
|
||||
ws = new WebSocket(WS_URL);
|
||||
} catch {
|
||||
connectionState = 'disconnected';
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
connectionState = 'connected';
|
||||
clearTimeout(reconnectTimer);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'agents',
|
||||
clientId: crypto.randomUUID(),
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
handleMessage(JSON.parse(event.data));
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
connectionState = 'disconnected';
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
connectionState = 'disconnected';
|
||||
scheduleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS);
|
||||
}
|
||||
|
||||
function handleMessage(msg) {
|
||||
switch (msg.type) {
|
||||
case 'agent_state': {
|
||||
if (msg.agentId && msg.state) {
|
||||
setAgentState(msg.agentId, msg.state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'job_started': {
|
||||
jobCount++;
|
||||
if (msg.agentId) setAgentState(msg.agentId, 'active');
|
||||
logEvent(`JOB ${(msg.jobId || '').slice(0, 8)} started`);
|
||||
break;
|
||||
}
|
||||
case 'job_completed': {
|
||||
if (jobCount > 0) jobCount--;
|
||||
if (msg.agentId) setAgentState(msg.agentId, 'idle');
|
||||
logEvent(`JOB ${(msg.jobId || '').slice(0, 8)} completed`);
|
||||
break;
|
||||
}
|
||||
case 'chat': {
|
||||
const def = agentById[msg.agentId];
|
||||
if (def && msg.text) {
|
||||
appendChatMessage(def.label, msg.text, colorToCss(def.color));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'agent_count':
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function logEvent(text) {
|
||||
appendChatMessage('SYS', text, colorToCss(0x003300));
|
||||
}
|
||||
|
||||
export function getConnectionState() {
|
||||
return connectionState;
|
||||
}
|
||||
|
||||
export function getJobCount() {
|
||||
return jobCount;
|
||||
}
|
||||
60
js/world.js
Normal file
60
js/world.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
let scene, camera, renderer;
|
||||
|
||||
export function initWorld() {
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x000000);
|
||||
scene.fog = new THREE.FogExp2(0x000000, 0.035);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 500);
|
||||
camera.position.set(0, 12, 28);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
document.body.prepend(renderer.domElement);
|
||||
|
||||
addLights(scene);
|
||||
addGrid(scene);
|
||||
|
||||
return { scene, camera, renderer };
|
||||
}
|
||||
|
||||
function addLights(scene) {
|
||||
const ambient = new THREE.AmbientLight(0x001a00, 0.6);
|
||||
scene.add(ambient);
|
||||
|
||||
const point = new THREE.PointLight(0x00ff41, 2, 80);
|
||||
point.position.set(0, 20, 0);
|
||||
scene.add(point);
|
||||
|
||||
const fill = new THREE.DirectionalLight(0x003300, 0.4);
|
||||
fill.position.set(-10, 10, 10);
|
||||
scene.add(fill);
|
||||
}
|
||||
|
||||
function addGrid(scene) {
|
||||
const grid = new THREE.GridHelper(100, 40, 0x003300, 0x001a00);
|
||||
grid.position.y = -0.01;
|
||||
scene.add(grid);
|
||||
|
||||
const planeGeo = new THREE.PlaneGeometry(100, 100);
|
||||
const planeMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x000a00,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
});
|
||||
const plane = new THREE.Mesh(planeGeo, planeMat);
|
||||
plane.rotation.x = -Math.PI / 2;
|
||||
plane.position.y = -0.02;
|
||||
scene.add(plane);
|
||||
}
|
||||
|
||||
export function onWindowResize(camera, renderer) {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
Reference in New Issue
Block a user