/** * 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;