clear
This commit is contained in:
1 parent
618ba02cb9
commit
83278b2998
124 files changed
-49298
No files matched your search
@@ -1,598 +0,0 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
Reference in new issue
Block a user