...
``` Let me write the full thing now. For terrain rendering with rivers: I'll modify the heightmap. River path: a curve through the terrain. Let me make a river that flows from north to south, meandering. ``` riverCenterX(z) = Math.sin(z * 0.0005) * 800; ``` For each terrain vertex, compute distance to river center, if within river width, lower the terrain to a base river elevation. Actually, let me make the river carve into the terrain: ``` riverDist = Math.abs(x - riverCenterX(z)); if (riverDist < riverWidth) { height = Math.min(height, riverBedHeight) - (1 - riverDist/riverWidth) * 5; } ``` And color the river blue (water material or vertex color based on height). I'll use vertex colors for terrain based on height: - Low (water): blue - Beach: sand - Grass: green - Rock: gray - Snow: white Let me write the code now. ```js import * as THREE from 'three'; // Scene const scene = new THREE.Scene(); scene.background = new THREE.Color(0x87ceeb); scene.fog = new THREE.Fog(0xa0c8e0, 1000, 6000); const camera = new THREE.PerspectiveCamera(70, innerWidth/innerHeight, 0.5, 8000); const renderer = new THREE.WebGLRenderer({canvas: document.getElementById('c'), antialias: true}); renderer.setSize(innerWidth, innerHeight); renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // Lights const sun = new THREE.DirectionalLight(0xfff4e0, 1.2); sun.position.set(500, 800, 300); scene.add(sun); scene.add(new THREE.HemisphereLight(0x87ceeb, 0x4a5a3a, 0.6)); // Terrain function hash(x, y) { ... } function noise(x, y) { ... } function fbm(x, y) { ... } function terrainHeight(x, z) { const base = fbm(x * 0.0008, z * 0.0008) * 500; const detail = fbm(x * 0.005, z * 0.005) * 60; let h = base + detail - 80; // Flatten near runway const distFromOrigin = Math.sqrt(x*x + z*z); if (distFromOrigin < 400) { h = h * Math.max(0, (distFromOrigin - 200) / 200); } // River const riverCenter = Math.sin(z * 0.0006) * 600 + Math.cos(z * 0.0002) * 300; const riverDist = Math.abs(x - riverCenter); if (riverDist < 80) { const t = riverDist / 80; const riverDepth = (1 - t) * 30; h = Math.min(h, -10) - riverDepth; } return h; } ``` Wait, the runway should be along a specific direction. Let me put the runway along the Z axis near origin. So flatten a strip from z=-200 to z=200, x from -15 to 15. Let me redo the flatten: ``` if (Math.abs(x) < 30 && Math.abs(z) < 250) { h = 0; // flat runway area } else { // blend } ``` Actually I'll smooth it. Let me use a smooth blend. For the terrain mesh, I'll create a PlaneGeometry rotated to XZ plane, with 256x256 segments covering 6000x6000 area. Then set vertex y based on terrainHeight(x, z). Actually for better performance and detail near the plane, I could do LOD, but let's keep it simple with a big mesh. Let me set terrain size to 6000x6000 with 200x200 segments = 40000 quads. That should be fine. Wait, the plane will fly far. Let me make terrain follow the plane — recenter terrain under plane. Hmm, that's complex. Let me just make terrain large enough (8000x8000) and have the plane fly within it. If plane goes beyond, wrap around or just let it fly into fog. Actually, let me make the terrain 10000x10000 and place mountains around the edges so the player naturally stays in the middle. OK let me write the full code now. I'll keep it focused. Plane physics: ```js const plane = { position: new THREE.Vector3(0, 0.5, 200), // on runway, looking -Z (north) quaternion: new THREE.Quaternion(), velocity: new THREE.Vector3(), throttle: 0, pitchRate: 0, rollRate: 0, yawRate: 0, onGround: true, }; const controls = { pitch: 0, roll: 0, yaw: 0, throttleUp: false, throttleDown: false }; // Each frame: const dt = Math.min(clock.getDelta(), 0.05); // Throttle if (controls.throttleUp) plane.throttle = Math.min(1, plane.throttle + dt * 0.5); if (controls.throttleDown) plane.throttle = Math.max(0, plane.throttle - dt * 0.5); // Angular control (smooth) const targetPitchRate = controls.pitch * 1.0; // rad/s const targetRollRate = controls.roll * 1.5; const targetYawRate = controls.yaw * 0.5; plane.pitchRate += (targetPitchRate - plane.pitchRate) * dt * 3; plane.rollRate += (targetRollRate - plane.rollRate) * dt * 4; plane.yawRate += (targetYawRate - plane.yawRate) * dt * 3; // Apply rotation in local frame const dq = new THREE.Quaternion(); const eulerRates = new THREE.Vector3(plane.pitchRate, plane.yawRate, plane.rollRate); // Order matters. Let's apply each axis separately: const pitchQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0), plane.pitchRate * dt); const yawQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), plane.yawRate * dt); const rollQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1), plane.rollRate * dt); plane.quaternion.multiply(yawQ).multiply(pitchQ).multiply(rollQ); // Forces const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(plane.quaternion); const up = new THREE.Vector3(0, 1, 0).applyQuaternion(plane.quaternion); const right = new THREE.Vector3(1, 0, 0).applyQuaternion(plane.quaternion); const speed = plane.velocity.length(); const thrustForce = forward.clone().multiplyScalar(plane.throttle * 200); const gravity = new THREE.Vector3(0, -9.8 * 5, 0); // scaled mass // Lift: along plane's up, magnitude based on speed and AoA const velDir = speed > 0.01 ? plane.velocity.clone().normalize() : forward.clone(); const aoa = Math.asin(Math.max(-1, Math.min(1, velDir.dot(forward) * -1))); // Actually AoA = angle between velocity and forward, in plane's pitch axis // Let's compute AoA as angle of velocity projected on plane's forward-up plane const velLocal = plane.velocity.clone().applyQuaternion(plane.quaternion.clone().invert()); const aoa = Math.atan2(-velLocal.y, -velLocal.z); // if z is forward (we use -z), then... // Actually velLocal.z is forward component (negative = forward), velLocal.y is vertical // AoA = atan2(-velLocal.y, -velLocal.z) maybe const liftMag = speed * speed * 0.02 * (1 + aoa * 2); const liftForce = up.clone().multiplyScalar(liftMag); // Drag const dragForce = plane.velocity.clone().multiplyScalar(-0.015 * speed); const accel = new THREE.Vector3(); accel.add(thrustForce).add(gravity).add(liftForce).add(dragForce); plane.velocity.add(accel.multiplyScalar(dt)); plane.position.add(plane.velocity.clone().multiplyScalar(dt)); // Ground collision const groundY = terrainHeight(plane.position.x, plane.position.z) + 0.5; if (plane.position.y < groundY) { plane.position.y = groundY; // If landing gear, dampen vertical velocity if (plane.velocity.y < 0) plane.velocity.y = 0; // friction plane.velocity.x *= 0.98; plane.velocity.z *= 0.98; plane.onGround = true; } else { plane.onGround = false; } ``` Hmm, the lift calculation needs to be more grounded. Let me think: - Lift force acts perpendicular to velocity, in the plane's "up" direction (roughly) - Lift magnitude = 0.5 * rho * v^2 * S * CL where CL depends on AoA Simplified: liftMag = k * speed^2, direction = plane's up (but only the component perpendicular to velocity). For simplicity: liftForce = up * (speed^2 * 0.015). When plane is level and moving forward, up is world up, so lift counters gravity. At speed v, lift = 0.015 * v^2. Gravity force = 9.8 * 5 = 49 (with mass 5). For lift = gravity, v^2 = 49/0.015 = 3267, v = 57 m/s. That's about 110 knots, reasonable for takeoff. Thrust = 200 max. Drag = 0.015 * v * v (since dragForce = -v * 0.015 * v = -0.015 v^2 magnitude). At equilibrium, thrust = drag: 200 = 0.015 v^2, v^2 = 13333, v = 115 m/s. OK so max speed ~115 m/s. Hmm wait, drag = -velocity * (0.015 * speed) = -v_dir * 0.015 * v^2. So drag magnitude = 0.015 v^2. Thrust max = 200. Equilibrium: 200 = 0.015 v^2 → v = 115 m/s. OK. But at takeoff, we need lift > gravity at lower speed. Hmm. Let me adjust: lift coefficient higher. liftMag = 0.03 * v^2. Then lift = gravity at v^2 = 49/0.03 = 1633, v = 40 m/s = 78 knots. Better. But then at v=115, lift = 0.03 * 13225 = 397, way more than gravity. So plane will accelerate upward unless pitched. That's actually realistic — you pitch down to maintain altitude. OK. Let me make lift depend on AoA properly: ``` liftMag = 0.02 * speed * speed * Math.max(0, Math.sin(aoa + 0.1)); ``` where aoa is the angle of attack. When level flight, AoA is small but positive due to wing incidence. Actually let me just compute AoA properly: ``` const localVel = plane.velocity.clone().applyQuaternion(plane.quaternion.clone().invert()); // localVel.z is forward component (negative when moving forward since forward is -z) // localVel.y is vertical // AoA = angle between -Z (forward) and velocity projected in YZ plane const forwardSpeed = -localVel.z; const verticalSpeed = localVel.y; const aoa = Math.atan2(verticalSpeed, forwardSpeed); // positive aoa = velocity coming from below = nose up relative to motion ``` Hmm wait, if plane is moving forward (vel has -z in local), and pitched up slightly, then local vel has +y component? Let me think again. If plane is pitched up (nose up), and moving forward along world horizontal, then in local frame, the world horizontal velocity has a component along -z (forward) and -y (downward in local frame, since plane is pitched up). So localVel.y < 0 means AoA > 0 (nose up relative to motion). So: ``` const aoa = Math.atan2(-localVel.y, forwardSpeed); ``` OK. Then: ``` const liftCoef = Math.max(0, Math.sin(aoa + 0.15) * 2); // stall at high AoA const liftMag = 0.5 * 1.2 * speed * speed * 0.03 * liftCoef; ``` This is getting complex. Let me simplify with a model that "feels right": ``` const speed = plane.velocity.length(); const localVel = plane.velocity.clone().applyQuaternion(plane.quaternion.clone().invert()); const forwardSpeed = Math.max(0, -localVel.z); const aoa = Math.atan2(-localVel.y, Math.max(0.1, forwardSpeed)); // Lift along plane's up, magnitude scales with speed^2 and AoA const liftMag = forwardSpeed * forwardSpeed * 0.025 * Math.max(0, Math.sin(aoa + 0.1)); const liftForce = up.clone().multiplyScalar(liftMag); // Drag const dragMag = speed * speed * 0.012; const dragForce = plane.velocity.clone().normalize().multiplyScalar(-dragMag); // Thrust const thrustForce = forward.clone().multiplyScalar(plane.throttle * 180); // Gravity (mass = 1, g = 9.8) const gravity = new THREE.Vector3(0, -9.8, 0); ``` For level flight at altitude: lift = gravity → 0.025 * v^2 * sin(0.1) ≈ 0.025 * v^2 * 0.1 = 0.0025 v^2 = 9.8 → v^2 = 3920, v = 62.5 m/s. Hmm that's a high cruise speed. Let me make lift stronger: 0.04 instead of 0.025, and AoA offset 0.2: 0.04 * v^2 * sin(0.2) ≈ 0.04 * v^2 * 0.2 = 0.008 v^2 = 9.8 → v^2 = 1225, v = 35 m/s = 68 knots. Better for level flight at low throttle. Takeoff: need lift > weight. At v=30, lift = 0.04 * 900 * 0.2 = 7.2 < 9.8. Hmm. Need higher speed or higher AoA. Let me increase: 0.05, offset 0.25: 0.05 * v^2 * 0.25 = 0.0125 v^2 = 9.8 → v = 28 m/s. Takeoff at ~30 m/s = 60 knots. Good. Max speed: thrust = drag: 180 = 0.012 * v^2 → v^2 = 15000, v = 122 m/s = 240 knots. Good. OK let me go with these numbers and tune later. Now for the controls, I want them to feel responsive. Let me use: - Arrow Up: pitch down (nose down) - Arrow Down: pitch up (nose up) - Arrow Left: roll left - Arrow Right: roll right - A: yaw left - D: yaw right - W: throttle up - S: throttle down Wait, traditionally: - Pulling back (Down arrow) = nose up - Pushing forward (Up arrow) = nose down That's the stick metaphor. I'll go with that. Roll: left arrow = roll left (left wing dips) Yaw: A = yaw left Let me code it up. For the HUD, I'll use SVG for the artificial horizon and HTML for the rest. Artificial horizon SVG: - 200x200 circle - Sky rect (blue) and ground rect (brown), clipped to circle - Horizon line - Pitch ladder - Roll arc with tick marks - Fixed aircraft reference (yellow W shape) The horizon group rotates by -roll and translates by pitch * pixelsPerDegree. Let me write it: ```html ``` Update with JS: ```js const horizonGroup = document.getElementById('horizonGroup'); horizonGroup.setAttribute('transform', `rotate(${-roll * 180/Math.PI}) translate(0, ${pitch * 180/Math.PI * 2})`); ``` Wait, the rotation should be around the center, and translate should happen in the rotated frame. Hmm. Let me think: - When plane rolls right (right wing down), the horizon should appear to rotate left (counterclockwise) from pilot's view. - When plane pitches up, the horizon should move down. So: `transform = rotate(-roll_deg) translate(0, pitch_deg * pixelsPerDeg)`. But the order matters. We want to rotate the horizon around the center, then translate within the rotated frame. Actually, the translate should be in the unrotated frame because pitch is independent of roll. Hmm. Actually, in real aircraft, when you roll, the horizon tilts. When you pitch, the horizon moves up/down. These are independent in the sense that the horizon line tilts with roll and moves with pitch. If we do `rotate(-roll) translate(0, pitchOffset)`, the translate happens in the rotated frame, so the horizon moves diagonally when both roll and pitch are non-zero. That's actually correct — when banked and pitched up, the horizon is below you and tilted. Actually wait, no. The horizon is a fixed line in the world. When you bank, it appears to rotate. When you pitch up, it appears to move down. If you do both, the horizon line is rotated AND its position is shifted perpendicular to the horizon line itself (in the rotated frame). That's what `rotate then translate` does. Yes, that's correct. OK let me write the full code. Pitch ladder: lines at every 10 degrees of pitch, with labels. In the rotated frame, the line at pitch angle p is at y = -p * pixelsPerDeg (since pitch up moves horizon down, so positive pitch = line below). Wait: when plane pitches up by 10°, the horizon should appear to move DOWN by 10° in the display. So if pitch_deg = 10, horizon line should be at y = +10 * pixPerDeg (positive y is down in SVG). So `translate(0, pitch_deg * pixPerDeg)`. And the ladder lines: line at pitch angle p appears at y = p * pixPerDeg (with p being the angle from current pitch). Hmm, actually the ladder is fixed to the world, so it moves with the horizon. The ladder lines at world pitch angles 0, ±10, ±20, etc. When plane pitches up to 10°, the world pitch=0 line appears at y = +10 * pixPerDeg, the world pitch=10 line (above horizon) appears at y = 0, etc. So in the horizonGroup, I draw ladder lines at y = p * pixPerDeg for various p, with labels. Then the whole group is translated by pitch_deg * pixPerDeg. So the line at p=0 ends up at y = pitch_deg * pixPerDeg, which is below center when pitch is positive (nose up). Correct. OK let me write the code. I'll use pixPerDeg = 3. Let me also add a heading tape at top, airspeed tape at left, altitude tape at right. Heading tape: a horizontal strip showing current heading in the center, with tick marks for surrounding headings. Width 300px, showing ±30°. Airspeed: vertical tape on left, showing current speed in knots, with ticks. Altitude: vertical tape on right. Actually, let me simplify and just show numbers with bars. I'll do: - Top center: heading tape (horizontal scrolling) - Left: airspeed (number + small tape) - Right: altitude (number + small tape) - Center: artificial horizon - Bottom left: throttle bar - Bottom right: vertical speed Let me write it. For the cockpit frame, I'll add CSS borders and a dashboard at the bottom with some rendered instruments. Let me also add a 3D dashboard in the scene — a simple dark panel below the camera with some glowing instrument circles. Actually that might be complex. Let me just do CSS overlay for the cockpit frame. OK time to write. Let me get to it. ```html Flight Simulator ``` Let me write the complete file. I'll keep it focused. Actually let me think about the cockpit framing more carefully. I want it to feel like sitting in a cockpit. So: - Dark frame around the edges (like the canopy) - A pillar on each side (A-pillars of canopy) - A dashboard at the bottom with some gauges (visual only) - The HUD overlay on top The 3D scene shows the outside world through the "window". Let me also add a subtle nose/cone visible at the bottom of the screen — actually that's part of the cockpit view. I could add a 3D nose cone in front of the camera. Let me add a 3D plane interior: dashboard panel with some glowing circles, and a nose cone extending forward. These are children of the plane group, so they move with the camera. ```js // Cockpit interior (children of plane) const dashGeom = new THREE.BoxGeometry(4, 0.6, 1.5); const dashMat = new THREE.MeshStandardMaterial({color: 0x222222, roughness: 0.8}); const dash = new THREE.Mesh(dashGeom, dashMat); dash.position.set(0, -0.8, 1.2); plane.add(dash); // Nose cone (visible at bottom of view) const noseGeom = new THREE.ConeGeometry(0.5, 4, 16); const noseMat = new THREE.MeshStandardMaterial({color: 0x8899aa, metalness: 0.6, roughness: 0.4}); const nose = new THREE.Mesh(noseGeom, noseMat); nose.rotation.x = -Math.PI/2; nose.position.set(0, -0.3, -3); // forward and slightly down plane.add(nose); ``` Hmm, the nose should be visible at the bottom of the screen. If camera is at (0, 0.5, 0) looking -Z, and nose is at (0, -0.3, -3), it would appear in the lower-center of the view. Good. Wait, the camera is at the plane's position with the plane's quaternion. If the plane group has children, the camera (which is positioned at plane.position with plane.quaternion) won't see them unless they're in front. Let me check: camera at plane origin, looking down -Z (plane's local -Z). The nose at z=-3 is in front. Yes, visible. But wait, the camera is in world space, not a child of the plane. So when I render, the camera is at plane.position with plane.quaternion. Children of the plane group are positioned in plane's local frame. They'll render correctly relative to the camera because they're transformed by the same plane transform. OK let me write the code. For the runway, I'll add a long box or plane: ```js const runwayGeom = new THREE.PlaneGeometry(30, 400); const runwayMat = new THREE.MeshStandardMaterial({color: 0x333333, roughness: 0.9}); const runway = new THREE.Mesh(runwayGeom, runwayMat); runway.rotation.x = -Math.PI/2; runway.position.set(0, 0.1, 0); // slightly above terrain scene.add(runway); // Add stripes ``` Add center line stripes and threshold markings. Let me also add some trees / buildings scattered around for visual reference. Trees: simple cones with brown cylinder trunk. Scatter a few thousand using InstancedMesh. OK let me write the final code. I'll keep it reasonable in size. ```js // Trees const treeGeom = new THREE.ConeGeometry(3, 12, 6); const treeMat = new THREE.MeshStandardMaterial({color: 0x2a5a2a}); const trunkGeom = new THREE.CylinderGeometry(0.5, 0.7, 4, 6); const trunkMat = new THREE.MeshStandardMaterial({color: 0x3a2a1a}); const treeGroup = new THREE.Group(); for (let i = 0; i < 500; i++) { const x = (Math.random() - 0.5) * 5000; const z = (Math.random() - 0.5) * 5000; // Skip near runway if (Math.abs(x) < 50 && Math.abs(z) < 250) continue; // Skip in river const riverC = Math.sin(z * 0.0006) * 600 + Math.cos(z * 0.0002) * 300; if (Math.abs(x - riverC) < 100) continue; const y = terrainHeight(x, z); if (y < 5 || y > 300) continue; // only on reasonable terrain const tree = new THREE.Group(); const leaves = new THREE.Mesh(treeGeom, treeMat); leaves.position.y = 8; const trunk = new THREE.Mesh(trunkGeom, trunkMat); trunk.position.y = 2; tree.add(leaves, trunk); tree.position.set(x, y, z); tree.scale.setScalar(0.5 + Math.random() * 0.8); treeGroup.add(tree); } scene.add(treeGroup); ``` 500 group instances might be slow. Let me use InstancedMesh for trees. Actually 500 groups with 2 meshes each = 1000 draw calls. That's borderline. Let me use InstancedMesh: ```js const treeCount = 1000; const leavesMesh = new THREE.InstancedMesh(treeGeom, treeMat, treeCount); const trunkMesh = new THREE.InstancedMesh(trunkGeom, trunkMat, treeCount); const dummy = new THREE.Object3D(); let idx = 0; for (let i = 0; i < treeCount * 3; i++) { if (idx >= treeCount) break; const x = (Math.random() - 0.5) * 5000; const z = (Math.random() - 0.5) * 5000; if (Math.abs(x) < 60 && Math.abs(z) < 250) continue; const riverC = Math.sin(z * 0.0006) * 600 + Math.cos(z * 0.0002) * 300; if (Math.abs(x - riverC) < 100) continue; const y = terrainHeight(x, z); if (y < 5 || y > 350) continue; const s = 0.5 + Math.random() * 0.8; dummy.position.set(x, y + 2 * s, z); dummy.scale.setScalar(s); dummy.updateMatrix(); trunkMesh.setMatrixAt(idx, dummy.matrix); dummy.position.y = y + 8 * s; dummy.updateMatrix(); leavesMesh.setMatrixAt(idx, dummy.matrix); idx++; } leavesMesh.count = idx; trunkMesh.count = idx; scene.add(leavesMesh, trunkMesh); ``` OK. Let me also add water for the river. I'll create a separate plane for water at a low height. Actually, let me color the terrain with vertex colors based on height, and make the river bed lower. Then add a water plane at y = -5 covering the whole terrain, but only the river parts will show it because terrain is higher elsewhere. Wait, that means I need a water plane that's semi-transparent. Let me do that: ```js const waterGeom = new THREE.PlaneGeometry(8000, 8000); const waterMat = new THREE.MeshStandardMaterial({color: 0x2a5a8a, transparent: true, opacity: 0.7, roughness: 0.2, metalness: 0.5}); const water = new THREE.Mesh(waterGeom, waterMat); water.rotation.x = -Math.PI/2; water.position.y = -5; scene.add(water); ``` Then the river bed (carved into terrain) will be below water level, and the water plane shows through. OK let me write the whole thing now. I'll be concise. For terrain vertex colors: ```js const colors = []; for each vertex: const h = height; let color; if (h < 0) color = blue (water/sand); else if (h < 20) color = sand; else if (h < 200) color = green; else if (h < 350) color = gray (rock); else color = white (snow); ``` OK let me finalize the code. One more thing: I want to make sure the plane starts on the runway, looking down the runway. Let me set: - Runway along Z axis, from z=-200 to z=200 - Plane starts at (0, 0.5, 180), looking -Z (so it'll take off toward -Z) - Forward is -Z, so plane needs to accelerate in -Z direction Camera positioned at plane origin + (0, 0.5, 0) looking -Z. Hmm wait, the cockpit should be slightly above the plane's center. Let me put camera at (0, 0.6, 0.5) in plane local frame. The plane's origin is at the CG, slightly above the ground (0.5). Camera at (0, 1.1, 0.5) world initially. Let me code the camera update: ```js const camOffset = new THREE.Vector3(0, 0.6, 0.3); camera.position.copy(plane.position).add(camOffset.clone().applyQuaternion(plane.quaternion)); camera.quaternion.copy(plane.quaternion); ``` OK let me write the final HTML now. I'll keep it complete but not too long. Let me also handle the "landing back on the runway" — I'll detect if the plane is on the runway and slow it down with friction. If the plane lands off-runway, it crashes (or just bumpy). Actually, let me keep it simple: any ground contact = landing with friction. If vertical speed is too high, show "CRASH" message and reset. Let me add a status message system: - "READY" at start - "TAKE OFF" when leaving ground - "AIRBORNE" when climbing - "LANDING" when descending close to ground - "LANDED" when on ground - "CRASH" if hard landing, then reset I'll show these as a small text in the HUD. Let me write the code now. I'll write it all out. Actually let me think about the controls more. I want it to be playable with keyboard. Let me also add mouse-look option? No, keep it simple — keyboard only. Pitch: Arrow Up = nose down, Arrow Down = nose up Roll: Arrow Left = roll left, Arrow Right = roll right Yaw: A = yaw left, D = yaw right Throttle: W = up, S = down R = reset B = brakes (when on ground) Let me write the final code. ```js const keys = {}; addEventListener('keydown', e => { keys[e.code] = true; if (e.code === 'KeyR') resetPlane(); }); addEventListener('keyup', e => keys[e.code] = false); function getControls() { return { pitch: (keys.ArrowUp ? 1 : 0) - (keys.ArrowDown ? 1 : 0), // +1 = nose down roll: (keys.ArrowRight ? 1 : 0) - (keys.ArrowLeft ? 1 : 0), // +1 = roll right yaw: (keys.KeyD ? 1 : 0) - (keys.KeyA ? 1 : 0), throttleUp: !!keys.KeyW, throttleDown: !!keys.KeyS, brake: !!keys.KeyB, }; } ``` Hmm wait, S is both throttle down and... I had yaw on A/D. Let me use: - W/S: throttle up/down - A/D: yaw - Arrows: pitch/roll OK that's fine. Now let me write everything. Let me also make sure the plane's rotation is intuitive: - Pitch up (nose up): rotate around +X axis (right wing axis), positive rotation = nose up? Let me check: rotating around +X by positive angle, the +Y axis rotates toward +Z, and -Z (forward) rotates toward +Y. So forward goes up = nose up. Yes, positive X rotation = nose up. So if ArrowDown = nose up, then pitchInput = -1 (from -ArrowDown + ArrowUp = -1 + 0 = -1) means we want negative X rotation? Wait: - pitchInput = (ArrowUp ? 1 : 0) - (ArrowDown ? 1 : 0) - ArrowDown pressed: pitchInput = -1 - We want nose up, which is positive X rotation - So rotation = -pitchInput? No wait, let me re-think. If pitchInput = -1 (ArrowDown), we want nose up = positive X rotation. So pitchRate = -pitchInput * maxRate = +maxRate. Hmm. Or, let me redefine: pitchInput = (ArrowDown - ArrowUp) so positive = nose up. Then pitchRate = pitchInput * maxRate, axis = +X. Let me just be careful with signs in the code. OK let me write the whole thing. I'll start writing the final HTML now. Let me be concise but complete. Final structure: - HTML with HUD overlay - Three.js scene with terrain, runway, water, trees, plane - Flight physics - HUD updates - Cockpit framing Let me write it. ```html Skyhawk Flight Simulator
AIRSPEED
0
KNOTS
ALTITUDE
0
FEET
000°
THR
0%
V/S
0
FT/MIN
W/S Throttle   / Pitch   / Roll   A/D Rudder   B Brake   R Reset
``` Let me review and fix issues: 1. The pitch ladder: I need to verify the math. When pitchDeg = +10 (nose up), the horizon (p=0 line, drawn at y=0) should be at y = +10*PPD in the final transform. The transform is `translate(0, pitchDeg * PPD)`, so the line at y=0 ends at y=+10*PPD. Good. 2. Heading tape: I'm setting transform twice, the second one overrides. Let me clean up. 3. The plane starts at z=200 looking -Z (north). The runway is from z=-240 to z=240. So plane starts at the south end of the runway, will accelerate toward -Z (north). Good. 4. The terrain flatten: I'm flattening around the origin. The runway is at the origin. Good. 5. Lift coefficient: at fwdSpeed=30, lift = 900 * 0.045 * sin(0.18) ≈ 900*0.045*0.18 = 7.3. Gravity = 9.8. So need higher speed. At 40: 1600*0.045*0.18 = 13 > 9.8. Takeoff at ~35 m/s = 68 knots. OK. But wait, on the ground, the plane is level. AoA = 0 (velocity is horizontal, plane is level). So lift = speed^2 * 0.045 * sin(0.18) = speed^2 * 0.008. To exceed gravity (9.8): speed^2 > 1225, speed > 35. OK. 6. The auto-level stability might fight user input. Let me reduce it or only apply when no roll input AND in air. Actually I already have that condition. But it might still be too aggressive. Let me reduce. 7. The plane needs to actually take off. With throttle 1, thrust = 200. Drag at v=30: 30*30*0.012 + 30*1.5 = 10.8 + 45 = 55.8. Net forward = 200 - 55.8 = 144. Acceleration = 144 m/s². That's too much. Wait, drag at v=30 with my formula: dragMag = speed*speed*0.012 + fwdSpeed*1.5 = 900*0.012 + 30*1.5 = 10.8 + 45 = 55.8. Hmm. Actually the second term (fwdSpeed * 1.5) is a constant drag that's too high at low speed. Let me remove it and just use quadratic drag. Let me reconsider drag. At v=0, drag should be 0. At v=30, drag = 900*0.012 = 10.8. Thrust=200. Net = 189. Acc = 189 m/s². Way too high. Hmm, the issue is the mass. I'm using mass=1 implicitly. Let me add a mass: - mass = 5 - gravity force = 5 * 9.8 = 49 - thrust = 200 → accel = 40 m/s² at v=0 - At v=30: drag = 10.8, net = 189/5 = 37.8 m/s². Still high but more reasonable. - Equilibrium: thrust = drag → 200 = 0.012 v^2 → v^2 = 16667 → v = 129 m/s. OK. - Lift = gravity: 0.045 * v^2 * 0.18 = 49 → v^2 = 6049 → v = 78 m/s for level flight. Hmm, that's high. Let me adjust. I want takeoff around 35-45 m/s, cruise around 50-70 m/s, max around 100-130 m/s. Let me redo: - mass m = 5 - gravity = 9.8 * 5 = 49 N - lift coef: L = 0.5 * ρ * v^2 * S * CL. Let's say lift = 0.04 * v^2 * sin(aoa + 0.2). - At aoa=0: lift = 0.04 * v^2 * 0.2 = 0.008 v^2. For lift=49: v^2 = 6125, v=78. Too high. - Let me increase: 0.08 * v^2 * sin(aoa + 0.2). At aoa=0: 0.016 v^2 = 49 → v = 55. Still high. - 0.12 * v^2 * sin(aoa + 0.25). At aoa=0: 0.03 v^2 = 49 → v = 40. Takeoff at 40 m/s = 78 knots. Good. - At v=60, aoa=0: lift = 0.03 * 3600 = 108 > 49. So plane climbs. Pitch down to reduce lift. OK. - Max speed: thrust = drag. thrust = 200. drag = 0.012 v^2. 200 = 0.012 v^2 → v = 129 m/s. Good. Wait, but the lift formula has the 0.12 factor and sin(aoa+0.25). At cruise, aoa is small. If we're level at v=60, lift = 0.12 * 3600 * sin(0.25) = 0.12 * 3600 * 0.247 = 107. That's more than gravity (49). So plane accelerates upward. To maintain altitude, pitch down so aoa becomes negative: sin(-0.18+0.25) = sin(0.07) = 0.07. lift = 0.12 * 3600 * 0.07 = 30. Less than gravity. Hmm. Actually, the plane should naturally find equilibrium where lift = gravity. At v=60, aoa such that 0.12 * 3600 * sin(aoa+0.25) = 49 → sin(aoa+0.25) = 0.1136 → aoa+0.25 = 0.114 → aoa = -0.136 rad = -7.8°. So plane needs to pitch down 7.8° from velocity vector. That's a lot. Let me just make lift simpler: lift = k * v^2, where k is set so takeoff is at the right speed. Then AoA affects lift via a multiplier. Simpler model: ``` const liftMag = fwdSpeed * fwdSpeed * 0.012 * (1 + aoa * 1.5); // Stall if aoa too high if (aoa > 0.3) liftMag *= 0.3 / aoa; ``` At aoa=0, v=30: lift = 900 * 0.012 = 10.8. Hmm, low. Need v=64 for lift=49. At aoa=0.1, v=30: lift = 900 * 0.012 * 1.15 = 12.4. Still low. Let me use 0.025: At aoa=0, v=30: lift = 900 * 0.025 = 22.5. v=44 for lift=49. OK takeoff at 44 m/s = 85 knots. Acceptable. At aoa=0.1, v=44: lift = 0.025 * 1936 * 1.15 = 55.6. Climbs. At aoa=0, v=60: lift = 0.025 * 3600 = 90. Climbs. At aoa=-0.1, v=60: lift = 0.025 * 3600 * 0.85 = 76.5. Still climbs. At aoa=-0.3, v=60: lift = 0.025 * 3600 * 0.55 = 49.5. Level. So pitch down 17° to maintain altitude at v=60. Still a lot. Hmm, the issue is that lift grows with v^2 but gravity is constant. So at high speed, you need to pitch down a lot. Real aircraft use flaps and trim, but for a simple sim, let me reduce the lift coefficient and accept higher takeoff speed. Actually, real aircraft do pitch down at high speeds. Let me just accept it. The player will learn to manage pitch. Let me also reduce the lift coefficient so the plane doesn't climb too aggressively: - lift = 0.018 * v^2 * (1 + aoa * 1.5) - At aoa=0, v=52: lift = 0.018 * 2704 = 48.7 ≈ 49. So level flight at 52 m/s = 100 knots. Takeoff at ~55 m/s. - At v=80, aoa=0: lift = 0.018 * 6400 = 115. To level: 1 + aoa*1.5 = 49/115 = 0.426 → aoa = -0.38 rad = -22°. Too much. The problem is the v^2 growth. Let me cap the lift or use a different model. Actually, real aircraft pitch down to descend and use throttle to control altitude. Let me just make the plane feel right at moderate speeds and let the player manage it. Let me use: - lift = 0.02 * v^2 * cos(aoa) * (something) Hmm, this is getting complicated. Let me just use a simpler approach: lift always acts in plane's up direction, magnitude = throttle-dependent + speed-dependent with a cap. Actually let me try this: the plane has a "trim" that automatically pitches to maintain lift = gravity at current speed. But that's not realistic either. Let me just go with my model and tune: - mass = 5 - gravity = 49 N - thrust max = 250 N - lift = 0.02 * v^2 * (0.3 + aoa * 2) (so lift at aoa=0 is 0.006 v^2, at aoa=0.2 is 0.02 v^2) - drag = 0.015 * v^2 + 0.5 * v At v=0, drag=0, thrust=250, accel = 50 m/s². Hmm too much. Let me increase mass to 10. - mass = 10, gravity = 98 N - thrust = 250, accel at v=0 = 25 m/s². OK. - lift = 0.04 * v^2 * (0.3 + aoa * 2). At aoa=0, v=50: 0.04 * 2500 * 0.3 = 30. Less than gravity. Need higher v or aoa. - At aoa=0.1, v=50: 0.04 * 2500 * 0.5 = 50. Still less. - At aoa=0.2, v=50: 0.04 * 2500 * 0.7 = 70. Still less than 98. - At v=70, aoa=0.1: 0.04 * 4900 * 0.5 = 98. Level flight at 70 m/s = 136 knots. Takeoff at ~80 m/s = 155 knots. That's too fast for a small plane. OK let me go back to mass=5: - gravity = 49 - lift = 0.04 * v^2 * (0.3 + aoa * 2). At aoa=0.1, v=50: 0.04 * 2500 * 0.5 = 50 ≈ 49. Level at 50 m/s = 97 knots. Good. - At v=30, aoa=0.1: 0.04 * 900 * 0.5 = 18. Less than 49. Need higher aoa or speed. - At v=30, aoa=0.3: 0.04 * 900 * 0.9 = 32.4. Still less. - At v=40, aoa=0.2: 0.04 * 1600 * 0.7 = 44.8. Close to 49. - At v=45, aoa=0.2: 0.04 * 2025 * 0.7 = 56.7. Takeoff! So takeoff at ~40-45 m/s with some pitch up. Good. Thrust = 250, mass = 5, accel at v=0 = 50 m/s². That's 5G, way too much. Let me reduce thrust to 100. - accel at v=0 = 20 m/s². Still 2G. Hmm. - Max speed: thrust = drag. 100 = 0.015 v^2 + 0.5 v. At v=70: 0.015*4900 + 35 = 73.5 + 35 = 108.5 > 100. At v=60: 54 + 30 = 84 < 100. So max ~65 m/s = 126 knots. OK. Actually, accel of 20 m/s² is fine for a game. Real fighters do 10 m/s². Let me keep thrust=100, mass=5. Wait, with thrust=100, accel on ground at v=0 is 20 m/s². To reach 45 m/s takes 2.25 seconds. That's quick but OK for a game. Hmm, actually with drag, it's slower. At v=20: drag = 0.015*400 + 10 = 16. Net = 84. Accel = 16.8. At v=40: drag = 24 + 20 = 44. Net = 56. Accel = 11.2. So takes ~3 seconds to reach takeoff speed. OK. Let me finalize the physics: - mass = 5 - gravity = 9.8 * 5 = 49 (in force units, applied as accel = force/mass = 9.8) - Actually let me just use accel directly: gravity accel = 9.8 m/s² - thrust accel = throttle * 40 (so max 40 m/s²) - lift accel = 0.04 * v^2 * (0.3 + aoa * 2) / mass = 0.008 * v^2 * (0.3 + aoa * 2) - At v=50, aoa=0.1: 0.008 * 2500 * 0.5 = 10. Need 9.8 for level. Close! - At v=45, aoa=0.15: 0.008 * 2025 * 0.6 = 9.7. Level at 45 m/s. - drag accel = (0.015 * v^2 + 0.5 * v) / mass = 0.003 * v^2 + 0.1 * v - At v=50: 7.5 + 5 = 12.5. Thrust = 40. Net = 27.5. Hmm still accelerating. - At v=80: 19.2 + 8 = 27.2. Net = 12.8. - At v=100: 30 + 10 = 40. Net = 0. Max speed = 100 m/s = 194 knots. OK. Let me code this as accelerations directly (no mass): ``` const thrustAccel = forward.clone().multiplyScalar(state.throttle * 40); const liftAccel = up.clone().multiplyScalar(0.008 * fwdSpeed * fwdSpeed * Math.max(0, 0.3 + aoa * 2)); const dragAccel = state.velocity.clone().normalize().multiplyScalar(-(0.003 * speed * speed + 0.1 * speed)); const gravityAccel = new THREE.Vector3(0, -9.8, 0); ``` Wait, but lift should be perpendicular to velocity, not just along plane's up. For simplicity, let me use plane's up. When the plane is