시나리오 내용 반영

This commit is contained in:
KIM SU BO committed 2026-10-08 16:43:48 +09:00
1 parent d9287de729
commit 350bb92bf9
270 files changed
+198867

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
/**
* Synthesizer for Drone Rotor Hum and Tactical HUD Audio FX
* Uses Web Audio API without external audio file dependencies
*/
class DroneAudioSystem {
constructor() {
this.ctx = null;
this.enabled = false;
this.leaderGain = null;
this.followerGain = null;
this.leaderOsc1 = null;
this.leaderOsc2 = null;
this.followerOsc1 = null;
this.followerOsc2 = null;
}
init() {
if (this.ctx) return;
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
// Master compressor to prevent clipping
const compressor = this.ctx.createDynamicsCompressor();
compressor.threshold.setValueAtTime(-18, this.ctx.currentTime);
compressor.knee.setValueAtTime(30, this.ctx.currentTime);
compressor.ratio.setValueAtTime(12, this.ctx.currentTime);
compressor.attack.setValueAtTime(0.003, this.ctx.currentTime);
compressor.release.setValueAtTime(0.25, this.ctx.currentTime);
compressor.connect(this.ctx.destination);
this.masterGain = this.ctx.createGain();
this.masterGain.gain.setValueAtTime(0.2, this.ctx.currentTime);
this.masterGain.connect(compressor);
// Leader Drone sound synthesizer (Dual harmonic oscillator + lowpass filter)
this.leaderFilter = this.ctx.createBiquadFilter();
this.leaderFilter.type = "lowpass";
this.leaderFilter.frequency.setValueAtTime(650, this.ctx.currentTime);
this.leaderGain = this.ctx.createGain();
this.leaderGain.gain.setValueAtTime(0.0, this.ctx.currentTime);
this.leaderGain.connect(this.leaderFilter);
this.leaderFilter.connect(this.masterGain);
this.leaderOsc1 = this.ctx.createOscillator();
this.leaderOsc1.type = "sawtooth";
this.leaderOsc1.frequency.setValueAtTime(95, this.ctx.currentTime);
this.leaderOsc2 = this.ctx.createOscillator();
this.leaderOsc2.type = "triangle";
this.leaderOsc2.frequency.setValueAtTime(190, this.ctx.currentTime);
this.leaderOsc1.connect(this.leaderGain);
this.leaderOsc2.connect(this.leaderGain);
this.leaderOsc1.start();
this.leaderOsc2.start();
// Follower Drone sound synthesizer
this.followerFilter = this.ctx.createBiquadFilter();
this.followerFilter.type = "lowpass";
this.followerFilter.frequency.setValueAtTime(600, this.ctx.currentTime);
this.followerGain = this.ctx.createGain();
this.followerGain.gain.setValueAtTime(0.0, this.ctx.currentTime);
this.followerGain.connect(this.followerFilter);
this.followerFilter.connect(this.masterGain);
this.followerOsc1 = this.ctx.createOscillator();
this.followerOsc1.type = "sawtooth";
this.followerOsc1.frequency.setValueAtTime(98, this.ctx.currentTime);
this.followerOsc2 = this.ctx.createOscillator();
this.followerOsc2.type = "triangle";
this.followerOsc2.frequency.setValueAtTime(196, this.ctx.currentTime);
this.followerOsc1.connect(this.followerGain);
this.followerOsc2.connect(this.followerGain);
this.followerOsc1.start();
this.followerOsc2.start();
this.enabled = true;
} catch (e) {
console.warn("Web Audio not supported or blocked", e);
}
}
toggle() {
if (!this.ctx) {
this.init();
return true;
}
if (this.ctx.state === 'suspended') {
this.ctx.resume();
this.enabled = true;
return true;
}
this.enabled = !this.enabled;
if (!this.enabled && this.leaderGain && this.followerGain) {
this.leaderGain.gain.setValueAtTime(0.0, this.ctx.currentTime);
this.followerGain.gain.setValueAtTime(0.0, this.ctx.currentTime);
}
return this.enabled;
}
updateMotors(leaderRpm, followerRpm, speedLeader, speedFollower) {
if (!this.enabled || !this.ctx || this.ctx.state !== 'running') return;
const now = this.ctx.currentTime;
// Pitch proportional to RPM and velocity
const baseFreq1 = 85 + (speedLeader * 2.5) + ((leaderRpm - 4500) * 0.015);
const baseFreq2 = 88 + (speedFollower * 2.5) + ((followerRpm - 4500) * 0.015);
this.leaderOsc1.frequency.setTargetAtTime(Math.max(60, Math.min(220, baseFreq1)), now, 0.08);
this.leaderOsc2.frequency.setTargetAtTime(Math.max(120, Math.min(440, baseFreq1 * 2)), now, 0.08);
this.followerOsc1.frequency.setTargetAtTime(Math.max(60, Math.min(220, baseFreq2)), now, 0.08);
this.followerOsc2.frequency.setTargetAtTime(Math.max(120, Math.min(440, baseFreq2 * 2)), now, 0.08);
this.leaderGain.gain.setTargetAtTime(0.08, now, 0.1);
this.followerGain.gain.setTargetAtTime(0.08, now, 0.1);
}
playBeep(freq = 880, duration = 0.08) {
if (!this.enabled || !this.ctx) return;
try {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + duration);
} catch (e) {}
}
}
window.droneAudio = new DroneAudioSystem();
+319
View File
@@ -0,0 +1,319 @@
/**
* Procedural 3D Quadcopter Drone Builder in Three.js
* Generates high-fidelity carbon-fiber military/tactical UAV models with animated rotors,
* navigation LEDs, gimbal camera, landing skids, and thrust trail effects.
*/
class DroneModel {
constructor(scene, options = {}) {
this.scene = scene;
this.id = options.id || "drone";
this.name = options.name || "UAV";
this.primaryColor = options.primaryColor || 0x00f0ff;
this.accentColor = options.accentColor || 0x111625;
this.isLeader = !!options.isLeader;
this.group = new THREE.Group();
this.rotors = [];
this.rotorDiscs = [];
this.strobeLeds = [];
this.thrustParticles = [];
this._buildModel();
this.scene.add(this.group);
}
_buildModel() {
// High quality PBR materials
const carbonMat = new THREE.MeshStandardMaterial({
color: 0x1a1e24,
metalness: 0.85,
roughness: 0.3,
});
const metalMat = new THREE.MeshStandardMaterial({
color: 0x8892a0,
metalness: 0.95,
roughness: 0.15,
});
const canopyMat = new THREE.MeshStandardMaterial({
color: 0x0d131a,
metalness: 0.6,
roughness: 0.1,
clearcoat: 0.8,
clearcoatRoughness: 0.1
});
const accentMat = new THREE.MeshStandardMaterial({
color: this.primaryColor,
metalness: 0.7,
roughness: 0.3,
emissive: this.primaryColor,
emissiveIntensity: 0.35
});
const glowMat = new THREE.MeshBasicMaterial({
color: this.primaryColor
});
// 1. Central Body Chassis (Lower & Upper aerodynamic hull)
const bodyGeo = new THREE.CylinderGeometry(0.7, 0.9, 0.32, 8);
bodyGeo.rotateY(Math.PI / 8);
const bodyMesh = new THREE.Mesh(bodyGeo, carbonMat);
bodyMesh.castShadow = true;
bodyMesh.receiveShadow = true;
this.group.add(bodyMesh);
// Aerodynamic Top Canopy Shell
const canopyGeo = new THREE.SphereGeometry(0.65, 16, 12);
canopyGeo.scale(1.0, 0.45, 1.35);
const canopyMesh = new THREE.Mesh(canopyGeo, canopyMat);
canopyMesh.position.y = 0.18;
canopyMesh.castShadow = true;
this.group.add(canopyMesh);
// Center Accent Stripe
const stripeGeo = new THREE.BoxGeometry(0.12, 0.04, 1.4);
const stripeMesh = new THREE.Mesh(stripeGeo, accentMat);
stripeMesh.position.y = 0.34;
this.group.add(stripeMesh);
// 2. Gimbal Camera Sensor at Front Underbelly
const gimbalGroup = new THREE.Group();
gimbalGroup.position.set(0, -0.15, 0.7);
const gimbalBase = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 0.08, 12), metalMat);
gimbalGroup.add(gimbalBase);
const cameraSphere = new THREE.Mesh(new THREE.SphereGeometry(0.16, 16, 16), carbonMat);
cameraSphere.position.y = -0.1;
gimbalGroup.add(cameraSphere);
// Camera Optical Lens (Glowing ring)
const lensGeo = new THREE.CylinderGeometry(0.09, 0.09, 0.06, 16);
lensGeo.rotateX(Math.PI / 2);
const lensMesh = new THREE.Mesh(lensGeo, new THREE.MeshBasicMaterial({ color: 0x00ffff }));
lensMesh.position.set(0, -0.1, 0.14);
gimbalGroup.add(lensMesh);
this.group.add(gimbalGroup);
// 3. Carbon Fiber Rotor Arms (X-Configuration)
const armLength = 1.9;
const armRadius = 0.055;
const armPositions = [
{ angle: Math.PI * 0.25, dir: 1, ledColor: 0x00ff66 }, // Front-Right (Green nav)
{ angle: Math.PI * 0.75, dir: -1, ledColor: 0xff0044 }, // Rear-Right (Red nav)
{ angle: Math.PI * 1.25, dir: 1, ledColor: 0xff0044 }, // Rear-Left (Red nav)
{ angle: Math.PI * 1.75, dir: -1, ledColor: 0x00ff66 }, // Front-Left (Green nav)
];
this.rotors = [];
this.rotorDiscs = [];
armPositions.forEach((arm, i) => {
const armGroup = new THREE.Group();
armGroup.rotation.y = arm.angle;
// Arm Carbon Tube
const armGeo = new THREE.CylinderGeometry(armRadius, armRadius, armLength, 12);
armGeo.rotateZ(Math.PI / 2);
const armMesh = new THREE.Mesh(armGeo, carbonMat);
armMesh.position.x = armLength * 0.5;
armMesh.castShadow = true;
armGroup.add(armMesh);
// Diagonal support truss
const trussGeo = new THREE.CylinderGeometry(0.025, 0.025, armLength * 0.6, 8);
trussGeo.rotateZ(Math.PI / 2.8);
const trussMesh = new THREE.Mesh(trussGeo, carbonMat);
trussMesh.position.set(armLength * 0.35, -0.1, 0);
armGroup.add(trussMesh);
// Brushless Electric Motor Housing at Arm Tip
const motorGroup = new THREE.Group();
motorGroup.position.x = armLength;
const motorBase = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 0.22, 16), metalMat);
motorBase.position.y = 0.05;
motorBase.castShadow = true;
motorGroup.add(motorBase);
// Motor Accent Ring
const ringGeo = new THREE.TorusGeometry(0.185, 0.02, 8, 20);
ringGeo.rotateX(Math.PI / 2);
const ringMesh = new THREE.Mesh(ringGeo, accentMat);
ringMesh.position.y = 0.08;
motorGroup.add(ringMesh);
// Tip LED Navigation Light
const ledGeo = new THREE.SphereGeometry(0.06, 8, 8);
const ledMat = new THREE.MeshBasicMaterial({ color: arm.ledColor });
const ledMesh = new THREE.Mesh(ledGeo, ledMat);
ledMesh.position.set(0.12, -0.05, 0);
motorGroup.add(ledMesh);
this.strobeLeds.push(ledMesh);
// Rotating Rotor Assembly
const propGroup = new THREE.Group();
propGroup.position.y = 0.18;
// Rotor Hub Cap
const hub = new THREE.Mesh(new THREE.ConeGeometry(0.08, 0.1, 12), metalMat);
hub.position.y = 0.04;
propGroup.add(hub);
// 3-Blade Propeller
const bladeMat = new THREE.MeshStandardMaterial({
color: 0x111111,
roughness: 0.4,
metalness: 0.5
});
for (let b = 0; b < 3; b++) {
const bladeAngle = (b * Math.PI * 2) / 3;
const bladeGeo = new THREE.BoxGeometry(0.85, 0.015, 0.12);
const bladeMesh = new THREE.Mesh(bladeGeo, bladeMat);
bladeMesh.position.set(Math.cos(bladeAngle) * 0.42, 0, Math.sin(bladeAngle) * 0.42);
bladeMesh.rotation.y = -bladeAngle;
bladeMesh.rotation.x = 0.18; // Pitch twist
bladeMesh.castShadow = true;
propGroup.add(bladeMesh);
}
// Motion blur rotor disc (semi-transparent glowing disc when spinning fast)
const discGeo = new THREE.RingGeometry(0.1, 0.95, 24);
discGeo.rotateX(Math.PI / 2);
const discMat = new THREE.MeshBasicMaterial({
color: this.primaryColor,
transparent: true,
opacity: 0.22,
side: THREE.DoubleSide
});
const discMesh = new THREE.Mesh(discGeo, discMat);
discMesh.position.y = 0.02;
propGroup.add(discMesh);
motorGroup.add(propGroup);
armGroup.add(motorGroup);
this.group.add(armGroup);
this.rotors.push({ group: propGroup, dir: arm.dir });
this.rotorDiscs.push(discMesh);
});
// 4. Dual Carbon Landing Skids
for (const side of [-1, 1]) {
const skidGroup = new THREE.Group();
skidGroup.position.set(side * 0.55, -0.22, 0);
// Struts
const strut1 = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.4, 8), metalMat);
strut1.position.set(0, -0.15, 0.4);
strut1.rotation.z = side * 0.25;
skidGroup.add(strut1);
const strut2 = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.4, 8), metalMat);
strut2.position.set(0, -0.15, -0.4);
strut2.rotation.z = side * 0.25;
skidGroup.add(strut2);
// Longitudinal Runner
const runnerGeo = new THREE.CylinderGeometry(0.04, 0.04, 1.4, 8);
runnerGeo.rotateX(Math.PI / 2);
const runner = new THREE.Mesh(runnerGeo, carbonMat);
runner.position.set(side * 0.08, -0.35, 0);
skidGroup.add(runner);
this.group.add(skidGroup);
}
// 5. Underbelly Beacon Light
const beaconLight = new THREE.PointLight(this.primaryColor, 1.8, 12);
beaconLight.position.set(0, -0.25, 0);
this.group.add(beaconLight);
this.beaconLight = beaconLight;
// 6. Altitude Guide Drop Line & Ground Target Disc
this.altLineGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, -10, 0)
]);
this.altLineMat = new THREE.LineDashedMaterial({
color: this.primaryColor,
dashSize: 0.4,
gapSize: 0.2,
opacity: 0.5,
transparent: true
});
this.altLine = new THREE.Line(this.altLineGeo, this.altLineMat);
this.altLine.computeLineDistances();
this.scene.add(this.altLine);
// Ground shadow/projection ring
const groundRingGeo = new THREE.RingGeometry(0.3, 1.2, 24);
groundRingGeo.rotateX(-Math.PI / 2);
const groundRingMat = new THREE.MeshBasicMaterial({
color: this.primaryColor,
transparent: true,
opacity: 0.35,
side: THREE.DoubleSide
});
this.groundRing = new THREE.Mesh(groundRingGeo, groundRingMat);
this.scene.add(this.groundRing);
}
update(state, dt) {
if (!state) return;
// Update Position & Attitude smoothly
this.group.position.set(state.position.x, state.position.y, state.position.z);
// Pitch, Yaw, Roll rotations (Order: YXZ)
const pitchRad = THREE.MathUtils.degToRad(state.rotation.pitch);
const rollRad = THREE.MathUtils.degToRad(state.rotation.roll);
const yawRad = state.rotation.yaw !== undefined ? THREE.MathUtils.degToRad(state.rotation.yaw) : 0;
this.group.rotation.set(0, 0, 0);
this.group.rotation.order = "YXZ";
this.group.rotation.y = yawRad;
this.group.rotation.x = pitchRad;
this.group.rotation.z = rollRad;
// Spin Propellers at RPM rate
const speed = state.speed || 0;
const baseSpin = (dt * 55.0) + (speed * 0.05);
this.rotors.forEach((rotor, idx) => {
rotor.group.rotation.y += baseSpin * rotor.dir;
});
// Altitude Drop Line update
const posY = this.group.position.y;
const linePositions = new Float32Array([
this.group.position.x, this.group.position.y, this.group.position.z,
this.group.position.x, 0.02, this.group.position.z
]);
this.altLine.geometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3));
this.altLine.computeLineDistances();
// Ground Shadow Ring update
this.groundRing.position.set(this.group.position.x, 0.03, this.group.position.z);
const ringScale = Math.max(0.6, 1.0 + (posY * 0.08));
this.groundRing.scale.set(ringScale, ringScale, ringScale);
// Flashing Strobe LEDs
const strobe = Math.sin(Date.now() * 0.008) > 0.4 ? 1.0 : 0.2;
if (this.beaconLight) {
this.beaconLight.intensity = 1.0 + strobe * 1.5;
}
}
destroy() {
if (this.group && this.scene) this.scene.remove(this.group);
if (this.altLine && this.scene) this.scene.remove(this.altLine);
if (this.groundRing && this.scene) this.scene.remove(this.groundRing);
}
}
window.DroneModel = DroneModel;
+598
View File
@@ -0,0 +1,598 @@
/**
* 5-Drone Formation Flight Simulator - Main Application Controller
* Three.js 3D Environment, Mouse Raycasting Flight Control, WebSocket Telemetry Sync
*/
class DroneSimulatorApp {
constructor() {
this.container = document.getElementById("canvas-container");
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.clock = new THREE.Clock();
this.droneModels = [];
this.formationBeams = [];
this.targetReticle = null;
this.targetPillar = null;
this.radar = null;
// Mouse & Flight Target state
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.flightPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -8.0);
this.targetPos = new THREE.Vector3(0, 8.0, 0);
this.targetAltitude = 8.0;
this.isMouseDown = false;
// Camera mode: 'ORBIT', 'CHASE', 'FPV', 'FORMATION'
this.cameraMode = 'ORBIT';
// WebSocket Client
this.ws = null;
this.wsConnected = false;
this.lastTelemetry = null;
this.initThree();
this.initEnvironment();
this.initDrones();
this.initEventListeners();
this.initWebSocket();
this.initRadar();
this.animate = this.animate.bind(this);
requestAnimationFrame(this.animate);
}
initThree() {
// Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x060a10);
this.scene.fog = new THREE.FogExp2(0x060a10, 0.010);
// Camera
this.camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.2, 500);
this.camera.position.set(0, 26, 38);
// WebGL Renderer with Soft Shadows
this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.1;
this.container.appendChild(this.renderer.domElement);
// Orbit Controls for Tactical Free Cam
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.maxPolarAngle = Math.PI / 2 - 0.02;
this.controls.minDistance = 4;
this.controls.maxDistance = 160;
this.controls.target.set(0, 6, 0);
// Ambient & Directional Lighting
const ambientLight = new THREE.AmbientLight(0x223344, 1.2);
this.scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xddeeff, 2.0);
sunLight.position.set(40, 80, 30);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 10;
sunLight.shadow.camera.far = 200;
sunLight.shadow.camera.left = -70;
sunLight.shadow.camera.right = 70;
sunLight.shadow.camera.top = 70;
sunLight.shadow.camera.bottom = -70;
sunLight.shadow.bias = -0.0005;
this.scene.add(sunLight);
// Cyber Accent Rim Lights
const cyanRim = new THREE.DirectionalLight(0x00f0ff, 1.0);
cyanRim.position.set(-60, 30, -50);
this.scene.add(cyanRim);
const pinkRim = new THREE.DirectionalLight(0xff0066, 0.8);
pinkRim.position.set(60, 25, 60);
this.scene.add(pinkRim);
}
initEnvironment() {
// Ground Grid
const gridHelper = new THREE.GridHelper(200, 100, 0x00f0ff, 0x0f2233);
gridHelper.position.y = 0.01;
this.scene.add(gridHelper);
const subGrid = new THREE.GridHelper(200, 20, 0x00f0ff, 0x00f0ff);
subGrid.position.y = 0.02;
subGrid.material.opacity = 0.3;
subGrid.material.transparent = true;
this.scene.add(subGrid);
// Ground Plane Mesh
const groundGeo = new THREE.PlaneGeometry(400, 400);
groundGeo.rotateX(-Math.PI / 2);
const groundMat = new THREE.MeshStandardMaterial({
color: 0x060a10,
roughness: 0.85,
metalness: 0.2
});
const groundMesh = new THREE.Mesh(groundGeo, groundMat);
groundMesh.receiveShadow = true;
this.scene.add(groundMesh);
// Concentric Landing Rings
const padGroup = new THREE.Group();
const padRing1 = new THREE.Mesh(
new THREE.RingGeometry(14, 14.5, 64),
new THREE.MeshBasicMaterial({ color: 0x00f0ff, side: THREE.DoubleSide })
);
padRing1.rotateX(-Math.PI / 2);
padRing1.position.y = 0.03;
padGroup.add(padRing1);
const padRing2 = new THREE.Mesh(
new THREE.RingGeometry(28, 28.6, 64),
new THREE.MeshBasicMaterial({ color: 0x00f0ff, side: THREE.DoubleSide, opacity: 0.4, transparent: true })
);
padRing2.rotateX(-Math.PI / 2);
padRing2.position.y = 0.03;
padGroup.add(padRing2);
this.scene.add(padGroup);
// 3D Target Reticle Marker
this.targetReticle = new THREE.Group();
const reticleRing = new THREE.Mesh(
new THREE.RingGeometry(0.9, 1.15, 32),
new THREE.MeshBasicMaterial({ color: 0x00f0ff, side: THREE.DoubleSide, transparent: true, opacity: 0.85 })
);
reticleRing.rotateX(-Math.PI / 2);
this.targetReticle.add(reticleRing);
const crossGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-1.6, 0, 0), new THREE.Vector3(1.6, 0, 0),
new THREE.Vector3(0, 0, -1.6), new THREE.Vector3(0, 0, 1.6)
]);
const crossMat = new THREE.LineBasicMaterial({ color: 0x00f0ff, opacity: 0.9, transparent: true });
this.targetReticle.add(new THREE.LineSegments(crossGeo, crossMat));
// Altitude Drop Pillar
const pillarGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 8, 0)
]);
this.targetPillar = new THREE.Line(pillarGeo, new THREE.LineDashedMaterial({
color: 0x00f0ff,
dashSize: 0.5,
gapSize: 0.3,
transparent: true,
opacity: 0.6
}));
this.targetPillar.computeLineDistances();
this.scene.add(this.targetPillar);
this.scene.add(this.targetReticle);
}
initDrones() {
// 5 Drone Configuration: Leader + 4 Wingmen
const droneConfigs = [
{ id: "alpha_1", name: "VIPER-01 (Leader)", color: 0x00f0ff, isLeader: true },
{ id: "alpha_2", name: "GHOST-02 (Wingman 1)", color: 0xff0055, isLeader: false },
{ id: "alpha_3", name: "PHANTOM-03 (Wingman 2)", color: 0x00ff88, isLeader: false },
{ id: "alpha_4", name: "SPECTER-04 (Wingman 3)", color: 0xffb700, isLeader: false },
{ id: "alpha_5", name: "SHADOW-05 (Wingman 4)", color: 0xb026ff, isLeader: false },
];
this.droneModels = droneConfigs.map(cfg => {
return new DroneModel(this.scene, {
id: cfg.id,
name: cfg.name,
primaryColor: cfg.color,
isLeader: cfg.isLeader
});
});
// 4 Formation Laser Beams connecting Leader to each Wingman
this.formationBeams = [];
for (let i = 1; i < 5; i++) {
const beamGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 8, 0),
new THREE.Vector3(i * 3, 8, -i * 3)
]);
const beamMat = new THREE.LineBasicMaterial({
color: droneConfigs[i].color,
transparent: true,
opacity: 0.55,
linewidth: 1.5
});
const beam = new THREE.Line(beamGeo, beamMat);
this.scene.add(beam);
this.formationBeams.push(beam);
}
}
initRadar() {
this.radar = new TacticalRadar("tactical-radar");
}
initWebSocket() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws`;
const connect = () => {
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.wsConnected = true;
const statusDot = document.getElementById("connection-status-dot");
const statusText = document.getElementById("connection-status-text");
if (statusDot) statusDot.className = "status-dot";
if (statusText) statusText.innerText = "ONLINE (5-DRONES 60Hz)";
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.lastTelemetry = data;
this.updateUI(data);
} catch (e) {
console.error("Telemetry error", e);
}
};
this.ws.onclose = () => {
this.wsConnected = false;
const statusDot = document.getElementById("connection-status-dot");
const statusText = document.getElementById("connection-status-text");
if (statusDot) statusDot.className = "status-dot warning";
if (statusText) statusText.innerText = "OFFLINE (RETRYING...)";
setTimeout(connect, 2000);
};
};
connect();
}
sendWsCommand(payload) {
if (this.ws && this.wsConnected && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(payload));
}
}
initEventListeners() {
window.addEventListener("resize", () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
window.addEventListener("pointerdown", (e) => {
if (e.target.closest(".side-panel, .top-nav, .bottom-bar")) return;
this.isMouseDown = true;
this.updateMouseTarget(e);
});
window.addEventListener("pointermove", (e) => {
if (e.target.closest(".side-panel, .top-nav, .bottom-bar")) return;
if (this.isMouseDown || e.buttons === 1) {
this.updateMouseTarget(e);
}
});
window.addEventListener("pointerup", () => {
this.isMouseDown = false;
});
// Altitude Adjustment with Mouse Wheel
window.addEventListener("wheel", (e) => {
if (e.target.closest(".side-panel")) return;
if (e.ctrlKey || e.altKey) return;
const delta = e.deltaY > 0 ? -0.8 : 0.8;
this.targetAltitude = Math.max(1.5, Math.min(35.0, this.targetAltitude + delta));
this.flightPlane.constant = -this.targetAltitude;
const altSlider = document.getElementById("altitude-slider");
const altVal = document.getElementById("alt-value");
if (altSlider) altSlider.value = this.targetAltitude;
if (altVal) altVal.innerText = `${this.targetAltitude.toFixed(1)}m`;
this.sendWsCommand({
type: "SET_TARGET",
x: this.targetPos.x,
y: this.targetAltitude,
z: this.targetPos.z
});
}, { passive: true });
// Keyboard Controls
window.addEventListener("keydown", (e) => {
if (e.key === "w" || e.key === "W" || e.key === "ArrowUp") {
this.targetAltitude = Math.min(35.0, this.targetAltitude + 1.0);
} else if (e.key === "s" || e.key === "S" || e.key === "ArrowDown") {
this.targetAltitude = Math.max(1.5, this.targetAltitude - 1.0);
} else if (e.key === "1") {
this.setCameraMode("ORBIT");
} else if (e.key === "2") {
this.setCameraMode("CHASE");
} else if (e.key === "3") {
this.setCameraMode("FPV");
} else if (e.key === "4") {
this.setCameraMode("FORMATION");
} else if (e.key === "m" || e.key === "M") {
this.toggleAudio();
} else if (e.key === " ") {
this.targetPos.set(0, this.targetAltitude, 0);
}
this.flightPlane.constant = -this.targetAltitude;
this.sendWsCommand({
type: "SET_TARGET",
x: this.targetPos.x,
y: this.targetAltitude,
z: this.targetPos.z
});
});
// Formation Mode Buttons
document.querySelectorAll(".btn-formation").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".btn-formation").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
const mode = btn.dataset.mode;
this.sendWsCommand({ type: "SET_FORMATION", mode: mode });
if (window.droneAudio) window.droneAudio.playBeep(920, 0.06);
});
});
// Sliders
const distSlider = document.getElementById("formation-dist-slider");
if (distSlider) {
distSlider.addEventListener("input", (e) => {
const dist = parseFloat(e.target.value);
document.getElementById("dist-value").innerText = `${dist.toFixed(1)}m`;
this.sendWsCommand({ type: "SET_FORMATION", distance: dist });
});
}
const heightSlider = document.getElementById("formation-height-slider");
if (heightSlider) {
heightSlider.addEventListener("input", (e) => {
const hOff = parseFloat(e.target.value);
document.getElementById("height-value").innerText = `${hOff > 0 ? '+' : ''}${hOff.toFixed(1)}m`;
this.sendWsCommand({ type: "SET_FORMATION", height_offset: hOff });
});
}
const speedSlider = document.getElementById("speed-slider");
if (speedSlider) {
speedSlider.addEventListener("input", (e) => {
const spd = parseFloat(e.target.value);
document.getElementById("speed-value").innerText = `${spd.toFixed(1)}x`;
this.sendWsCommand({ type: "SET_SPEED", speed: spd });
});
}
const altSlider = document.getElementById("altitude-slider");
if (altSlider) {
altSlider.addEventListener("input", (e) => {
this.targetAltitude = parseFloat(e.target.value);
document.getElementById("alt-value").innerText = `${this.targetAltitude.toFixed(1)}m`;
this.flightPlane.constant = -this.targetAltitude;
this.sendWsCommand({
type: "SET_TARGET",
x: this.targetPos.x,
y: this.targetAltitude,
z: this.targetPos.z
});
});
}
const autoToggle = document.getElementById("auto-flight-toggle");
const autoSelect = document.getElementById("auto-pattern-select");
if (autoToggle) {
autoToggle.addEventListener("change", (e) => {
const enabled = e.target.checked;
const pattern = autoSelect ? autoSelect.value : "FIGURE_EIGHT";
this.sendWsCommand({ type: "SET_AUTONOMOUS", enabled: enabled, pattern: pattern });
if (window.droneAudio) window.droneAudio.playBeep(enabled ? 1200 : 600, 0.1);
});
}
if (autoSelect) {
autoSelect.addEventListener("change", (e) => {
const pattern = e.target.value;
const enabled = autoToggle ? autoToggle.checked : false;
this.sendWsCommand({ type: "SET_AUTONOMOUS", enabled: enabled, pattern: pattern });
});
}
document.querySelectorAll(".btn-cam").forEach(btn => {
btn.addEventListener("click", () => {
this.setCameraMode(btn.dataset.cam);
});
});
const resetBtn = document.getElementById("btn-reset");
if (resetBtn) {
resetBtn.addEventListener("click", () => {
this.targetPos.set(0, 8.0, 0);
this.targetAltitude = 8.0;
this.flightPlane.constant = -8.0;
this.sendWsCommand({ type: "RESET_POSITION" });
if (window.droneAudio) window.droneAudio.playBeep(440, 0.15);
});
}
const audioBtn = document.getElementById("btn-audio-toggle");
if (audioBtn) {
audioBtn.addEventListener("click", () => {
this.toggleAudio();
});
}
}
toggleAudio() {
if (!window.droneAudio) return;
const active = window.droneAudio.toggle();
const btn = document.getElementById("btn-audio-toggle");
if (btn) {
btn.innerText = active ? "🔊 SFX ON" : "🔇 SFX OFF";
btn.style.borderColor = active ? "var(--color-cyan)" : "rgba(255,255,255,0.2)";
}
}
setCameraMode(mode) {
this.cameraMode = mode;
document.querySelectorAll(".btn-cam").forEach(b => {
b.classList.toggle("active", b.dataset.cam === mode);
});
if (mode === 'ORBIT') {
this.controls.enabled = true;
} else {
this.controls.enabled = false;
}
if (window.droneAudio) window.droneAudio.playBeep(1050, 0.05);
}
updateMouseTarget(e) {
this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersectPoint = new THREE.Vector3();
if (this.raycaster.ray.intersectPlane(this.flightPlane, intersectPoint)) {
this.targetPos.x = intersectPoint.x;
this.targetPos.y = this.targetAltitude;
this.targetPos.z = intersectPoint.z;
this.sendWsCommand({
type: "SET_TARGET",
x: this.targetPos.x,
y: this.targetAltitude,
z: this.targetPos.z
});
}
}
updateUI(telemetry) {
if (!telemetry || !telemetry.drones) return;
const drones = telemetry.drones;
const formation = telemetry.formation;
// Render each drone in the telemetry list dynamically
drones.forEach((d, idx) => {
const posEl = document.getElementById(`tele-d${idx}-pos`);
const spdEl = document.getElementById(`tele-d${idx}-spd`);
const altEl = document.getElementById(`tele-d${idx}-alt`);
const batEl = document.getElementById(`tele-d${idx}-bat`);
const barEl = document.getElementById(`bar-d${idx}-bat`);
if (posEl) posEl.innerText = `${d.position.x}, ${d.position.y}, ${d.position.z}`;
if (spdEl) spdEl.innerText = `${d.speed} km/h`;
if (altEl) altEl.innerText = `${d.position.y.toFixed(1)}m`;
if (batEl) batEl.innerText = `${d.battery}%`;
if (barEl) barEl.style.width = `${d.battery}%`;
});
// Formation Card
const gaugeDist = document.getElementById("gauge-dist-val");
if (gaugeDist) gaugeDist.innerText = `${formation.distance.toFixed(1)}m`;
const statMsg = document.getElementById("formation-status-msg");
if (statMsg) statMsg.innerText = `5-DRONE SWARM: ${formation.mode}`;
// Audio update (leader + wingman 1)
if (window.droneAudio && drones.length >= 2) {
window.droneAudio.updateMotors(drones[0].rotors[0], drones[1].rotors[0], drones[0].speed, drones[1].speed);
}
}
updateCamera(dt) {
if (!this.lastTelemetry || !this.lastTelemetry.drones) return;
const leader = this.lastTelemetry.drones[0];
const leaderPos = leader.position;
const leaderYaw = THREE.MathUtils.degToRad(leader.rotation.yaw || 0);
const lVec = new THREE.Vector3(leaderPos.x, leaderPos.y, leaderPos.z);
if (this.cameraMode === 'CHASE') {
const offset = new THREE.Vector3(-Math.sin(leaderYaw) * 18, 8.0, -Math.cos(leaderYaw) * 18);
const targetCamPos = lVec.clone().add(offset);
this.camera.position.lerp(targetCamPos, dt * 4.0);
this.camera.lookAt(lVec.clone().add(new THREE.Vector3(0, 1.0, 0)));
} else if (this.cameraMode === 'FPV') {
this.camera.position.copy(lVec).add(new THREE.Vector3(0, -0.2, 0.8));
const forward = new THREE.Vector3(Math.sin(leaderYaw), 0, Math.cos(leaderYaw));
this.camera.lookAt(this.camera.position.clone().add(forward.multiplyScalar(20)));
} else if (this.cameraMode === 'FORMATION') {
// High angle side profile showing all 5 drones
const sideOffset = new THREE.Vector3(26, 12, 0);
this.camera.position.lerp(lVec.clone().add(sideOffset), dt * 3.0);
this.camera.lookAt(lVec);
} else {
this.controls.update();
}
}
animate() {
requestAnimationFrame(this.animate);
const dt = Math.min(0.05, this.clock.getDelta());
// Update 3D Target Reticle & Pillar
if (this.targetReticle) {
this.targetReticle.position.set(this.targetPos.x, 0.04, this.targetPos.z);
this.targetReticle.rotation.y += 0.02;
const pPos = new Float32Array([
this.targetPos.x, 0, this.targetPos.z,
this.targetPos.x, this.targetAltitude, this.targetPos.z
]);
this.targetPillar.geometry.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
this.targetPillar.computeLineDistances();
}
// Render 5 Drones from Telemetry Stream
if (this.lastTelemetry && this.lastTelemetry.drones) {
const drones = this.lastTelemetry.drones;
drones.forEach((d, idx) => {
if (this.droneModels[idx]) {
this.droneModels[idx].update(d, dt);
}
});
// Update 4 Formation Laser Beams from Leader to Wingmen
const lp = drones[0].position;
for (let i = 1; i < drones.length; i++) {
if (this.formationBeams[i - 1]) {
const fp = drones[i].position;
const beamPoints = new Float32Array([
lp.x, lp.y, lp.z,
fp.x, fp.y, fp.z
]);
this.formationBeams[i - 1].geometry.setAttribute('position', new THREE.BufferAttribute(beamPoints, 3));
}
}
// Update 2D Tactical Radar HUD
if (this.radar) {
this.radar.render(drones);
}
}
this.updateCamera(dt);
this.renderer.render(this.scene, this.camera);
}
}
window.addEventListener("DOMContentLoaded", () => {
window.simApp = new DroneSimulatorApp();
});
+196
View File
@@ -0,0 +1,196 @@
/**
* Tactical Radar HUD Canvas Component
* Real-time 2D top-down aerial map showing 5 drones, trail histories, and sweep beam
*/
class TacticalRadar {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
if (!this.canvas) return;
this.ctx = this.canvas.getContext('2d');
this.width = this.canvas.width = 220;
this.height = this.canvas.height = 220;
this.cx = this.width / 2;
this.cy = this.height / 2;
this.range = 75.0; // World meters represented on radar radius
this.sweepAngle = 0.0;
this.trails = {};
this.maxTrail = 28;
}
render(drones) {
if (!this.ctx || !drones || !drones.length) return;
const ctx = this.ctx;
const w = this.width;
const h = this.height;
const cx = this.cx;
const cy = this.cy;
const r = cx - 10;
// Clear with dark HUD background
ctx.fillStyle = "rgba(8, 14, 22, 0.45)";
ctx.fillRect(0, 0, w, h);
// Circular clip for radar scope
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
// Background subtle grid rings
ctx.strokeStyle = "rgba(0, 240, 255, 0.15)";
ctx.lineWidth = 1;
for (let i = 1; i <= 3; i++) {
ctx.beginPath();
ctx.arc(cx, cy, (r / 3) * i, 0, Math.PI * 2);
ctx.stroke();
}
// Crosshairs
ctx.beginPath();
ctx.moveTo(cx - r, cy);
ctx.lineTo(cx + r, cy);
ctx.moveTo(cx, cy - r);
ctx.lineTo(cx, cy + r);
ctx.stroke();
// 45-degree guide dashes
ctx.setLineDash([2, 4]);
ctx.strokeStyle = "rgba(0, 240, 255, 0.08)";
ctx.beginPath();
ctx.moveTo(cx - r * 0.7, cy - r * 0.7);
ctx.lineTo(cx + r * 0.7, cy + r * 0.7);
ctx.moveTo(cx - r * 0.7, cy + r * 0.7);
ctx.lineTo(cx + r * 0.7, cy - r * 0.7);
ctx.stroke();
ctx.setLineDash([]);
// Radar Sweep Line & Beam Gradient
this.sweepAngle += 0.035;
if (this.sweepAngle > Math.PI * 2) this.sweepAngle -= Math.PI * 2;
const sweepGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
sweepGrad.addColorStop(0, "rgba(0, 240, 255, 0.0)");
sweepGrad.addColorStop(1, "rgba(0, 240, 255, 0.18)");
ctx.fillStyle = sweepGrad;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, this.sweepAngle - 0.45, this.sweepAngle);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = "rgba(0, 240, 255, 0.8)";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(this.sweepAngle) * r, cy + Math.sin(this.sweepAngle) * r);
ctx.stroke();
// Convert world pos (X, Z) to radar canvas coords
const scale = r / this.range;
const toRadar = (pos) => ({
x: cx + (pos.x * scale),
y: cy + (pos.z * scale)
});
const leaderPos = toRadar(drones[0].position);
// Draw formation link lines from leader to all wingmen
ctx.strokeStyle = "rgba(0, 240, 255, 0.25)";
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
for (let i = 1; i < drones.length; i++) {
const fPos = toRadar(drones[i].position);
ctx.beginPath();
ctx.moveTo(leaderPos.x, leaderPos.y);
ctx.lineTo(fPos.x, fPos.y);
ctx.stroke();
}
ctx.setLineDash([]);
// Update & Render each drone
drones.forEach((d, idx) => {
const radPos = toRadar(d.position);
if (!this.trails[d.id]) this.trails[d.id] = [];
this.trails[d.id].push({ ...radPos });
if (this.trails[d.id].length > this.maxTrail) this.trails[d.id].shift();
// Draw Trail
this._drawTrail(this.trails[d.id], d.color);
// Draw Drone Blip
const yawRad = THREE.MathUtils.degToRad(d.rotation.yaw || 0);
const label = idx === 0 ? "L-01" : `W-0${idx + 1}`;
this._drawDroneBlip(radPos.x, radPos.y, yawRad, d.color, label, idx === 0);
});
ctx.restore();
// Outer Ring Border
ctx.strokeStyle = "rgba(0, 240, 255, 0.6)";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
// Compass Cardinal labels
ctx.fillStyle = "rgba(0, 240, 255, 0.75)";
ctx.font = "9px 'Share Tech Mono', monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("N", cx, cy - r + 8);
ctx.fillText("S", cx, cy + r - 8);
ctx.fillText("W", cx - r + 8, cy);
ctx.fillText("E", cx + r - 8, cy);
}
_drawTrail(trail, hexColor) {
if (trail.length < 2) return;
const ctx = this.ctx;
for (let i = 0; i < trail.length - 1; i++) {
const alpha = (i / trail.length) * 0.45;
ctx.strokeStyle = hexColor;
ctx.globalAlpha = alpha;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(trail[i].x, trail[i].y);
ctx.lineTo(trail[i + 1].x, trail[i + 1].y);
ctx.stroke();
}
ctx.globalAlpha = 1.0;
}
_drawDroneBlip(x, y, yaw, color, label, isLeader) {
const ctx = this.ctx;
ctx.save();
ctx.translate(x, y);
// Blip glow
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = isLeader ? 10 : 6;
ctx.beginPath();
ctx.arc(0, 0, isLeader ? 4.5 : 3.0, 0, Math.PI * 2);
ctx.fill();
// Heading arrow
ctx.rotate(yaw);
ctx.strokeStyle = color;
ctx.lineWidth = isLeader ? 2 : 1.5;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, isLeader ? 10 : 7);
ctx.stroke();
ctx.restore();
// Text label
ctx.fillStyle = "#ffffff";
ctx.font = "8px 'Share Tech Mono', monospace";
ctx.textAlign = "left";
ctx.fillText(label, x + 5, y - 3);
}
}
window.TacticalRadar = TacticalRadar;