function randomFlyTarget() { return new THREE.Vector3( (Math.random() - 0.5) * (ROOM_SIZE - 1), FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN), (Math.random() - 0.5) * (ROOM_SIZE - 1)); } /** * Creates a single fly mesh (small cone/tetrahedron). * @returns {THREE.Group} */ function createFlyMesh() { const flyGroup = new THREE.Group(); const flyMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, // Dark fly color shininess: 50, }); // Small Cone/Tetrahedron for a simple shape const bodyGeometry = new THREE.ConeGeometry(0.01, 0.02, 3); const body = new THREE.Mesh(bodyGeometry, flyMaterial); body.rotation.x = degToRad(90); // Point nose in Z direction body.castShadow = true; body.receiveShadow = true; flyGroup.add(body); // Initial state and parameters for the fly flyGroup.userData = { state: 'flying', // 'flying' or 'landed' landTimer: 0, t: 0, // Curve progression t parameter (0 to 1) speed: FLY_FLIGHT_SPEED_FACTOR + Math.random() * 0.01, curve: null, landCheckTimer: 0, oscillationTime: Math.random() * 100, // For smooth y-axis buzzing }; // Initial random position flyGroup.position = randomFlyTarget(); return flyGroup; } /** * Creates a new Quadratic Bezier curve for a fly's flight path. * @param {THREE.Group} fly - The fly mesh group. * @param {THREE.Vector3} endPoint - The target position for the end of the curve. */ function createFlyCurve(fly, endPoint) { const startPoint = fly.position.clone(); // Calculate the midpoint const midPoint = new THREE.Vector3().lerpVectors(startPoint, endPoint, 0.5); // Calculate a random offset for the control point to create curvature const offsetMagnitude = startPoint.distanceTo(endPoint) * 0.5; const offsetAngle = Math.random() * Math.PI * 2; // Displace the control point randomly to create a swooping path. // Control point y is usually higher than start/end for a nice arc. const controlPoint = new THREE.Vector3( midPoint.x + Math.cos(offsetAngle) * offsetMagnitude * 0.5, midPoint.y + Math.random() * 0.5 + 0.5, midPoint.z + Math.sin(offsetAngle) * offsetMagnitude * 0.5 ); fly.userData.curve = new THREE.QuadraticBezierCurve3( startPoint, controlPoint, endPoint ); fly.userData.t = 0; // Reset progression fly.userData.landCheckTimer = 50 + Math.random() * 50; // New landing decision window } /** * Creates and places the 'flies' meshes. */ function setupFlies() { for (let i = 0; i < FLIES_COUNT; i++) { const fly = createFlyMesh(); scene.add(fly); flies.push(fly); } } /** * Updates the position and state of the flies using Bezier curves. */ function updateFlies() { flies.forEach(fly => { const data = fly.userData; if (data.state === 'flying' || data.state === 'landing') { if (!data.curve) { // Initialize the first curve const newTargetPos = randomFlyTarget(); createFlyCurve(fly, newTargetPos); data.t = 0; } // Advance curve progression data.t += data.speed; // Check for landing readiness during the flight path data.landCheckTimer--; if (data.t >= 1) { // Path finished if (data.state === 'landing') { data.state = 'landed'; data.landTimer = FLY_WAIT_BASE + Math.random() * 1000; // Land for a random duration data.t = 0; return; // Stop updates for this fly } // 1. Check for landing decision if (data.landCheckTimer <= 0 && Math.random() > FLY_LAND_CHANCE) { // Raycast down from the current position to find a landing spot raycaster.set(fly.position, new THREE.Vector3(0, -1, 0)); const intersects = raycaster.intersectObjects(landingSurfaces, false); if (intersects.length > 0) { const intersect = intersects[0]; data.state = 'landing'; // Land slightly above the surface let newTargetPos = new THREE.Vector3(intersect.point.x, intersect.point.y + 0.05, intersect.point.z); // const newTargetPos = randomFlyTarget(); createFlyCurve(fly, newTargetPos); data.t = 0; } } if (data.state !== 'landing') { // 2. If not landing, generate a new random flight path const newTargetPos = randomFlyTarget(); createFlyCurve(fly, newTargetPos); data.t = 0; // Reset T for the new curve } } // Set position along the curve fly.position.copy(data.curve.getPoint(Math.min(data.t, 1))); // Set rotation tangent to the curve const tangent = data.curve.getTangent(Math.min(data.t, 1)).normalize(); fly.rotation.y = Math.atan2(tangent.x, tangent.z); // Add slight Y oscillation for buzzing feel (on top of curve) data.oscillationTime += 0.1; fly.position.y += Math.sin(data.oscillationTime * 4) * 0.01; } else if (data.state === 'landed') { // --- Landed State --- data.landTimer--; if (data.landTimer <= 0) { // Take off: Generate new flight curve from current landed position data.state = 'flying'; const newTargetPos = randomFlyTarget(); createFlyCurve(fly, newTargetPos); data.t = 0; } } }); }