| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Ultimate Three.js Flight Simulator</title> |
| <style> |
| body { margin: 0; background-color: #000; overflow: hidden; } |
| canvas { display: block; } |
| #instruments { position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: #0F0; padding: 10px; font-family: monospace; font-size: 14px; } |
| </style> |
| </head> |
| <body> |
|
|
| <div id="instruments"> |
| <b>Cockpit Instruments:</b><br> |
| Alt: <span id="altimeter">0 ft</span><br> |
| Airspeed: <span id="airspeed">0 kts</span><br> |
| Heading: <span id="heading">0°</span><br> |
| RPM: <span id="rpm">0</span><br> |
| Fuel: <span id="fuel">100%</span><br> |
| Flaps: <span id="flaps">0%</span> |
| </div> |
|
|
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
| <script> |
| |
| let scene = new THREE.Scene(); |
| |
| |
| let sun = new THREE.DirectionalLight(0xFFFFFF, 1); |
| sun.position.set(100, 100, 100); |
| scene.add(sun); |
| let skyGeom = new THREE.SphereGeometry(500, 32, 32); |
| let skyMat = new THREE.ShaderMaterial({ |
| vertexShader: ` |
| varying vec3 vWorldPosition; |
| void main() { |
| vec4 worldPosition = modelMatrix * vec4(position, 1.0); |
| vWorldPosition = worldPosition.xyz; |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| } |
| `, |
| fragmentShader: ` |
| uniform vec3 sunPosition; |
| varying vec3 vWorldPosition; |
| void main() { |
| vec3 viewDirection = normalize(vWorldPosition); |
| vec3 sunDir = normalize(sunPosition); |
| float sunDot = max(dot(viewDirection, sunDir), 0.0); |
| vec3 dayColor = vec3(0.2, 0.5, 1.0); // Blue sky |
| vec3 sunsetColor = vec3(1.0, 0.5, 0.2); // Orange |
| vec3 nightColor = vec3(0.05, 0.05, 0.1); // Dark blue |
| float sunHeight = sunPosition.y / 100.0; |
| vec3 skyColor = mix(nightColor, mix(sunsetColor, dayColor, sunHeight), smoothstep(-0.1, 0.1, sunHeight)); |
| gl_FragColor = vec4(skyColor + sunDot * 0.5, 1.0); |
| } |
| `, |
| side: THREE.BackSide, |
| uniforms: { sunPosition: { value: sun.position } } |
| }); |
| let sky = new THREE.Mesh(skyGeom, skyMat); |
| scene.add(sky); |
| |
| |
| let terrainSize = 256; |
| let terrainGeom = new THREE.PlaneGeometry(200, 200, terrainSize, terrainSize); |
| for (let i = 0; i < terrainGeom.attributes.position.count; i++) { |
| let x = terrainGeom.attributes.position.getX(i); |
| let z = terrainGeom.attributes.position.getZ(i); |
| let noise = (Math.sin(x * 0.1) + Math.sin(z * 0.1)) * 5 + (Math.random() * 2 - 1) * 2; |
| terrainGeom.attributes.position.setY(i, noise); |
| } |
| let terrainMat = new THREE.MeshLambertMaterial({ color: 0x228B22 }); |
| let terrain = new THREE.Mesh(terrainGeom, terrainMat); |
| terrain.rotation.x = -Math.PI / 2; |
| terrain.receiveShadow = true; |
| scene.add(terrain); |
| |
| |
| let planeGeom = new THREE.Group(); |
| let fuselageGeom = new THREE.BoxGeometry(2, 0.4, 6); |
| let fuselageMat = new THREE.MeshLambertMaterial({ color: 0xFFFFFF }); |
| let fuselage = new THREE.Mesh(fuselageGeom, fuselageMat); |
| fuselage.castShadow = true; |
| let wingGeom = new THREE.BufferGeometry(); |
| |
| let wingMat = new THREE.MeshLambertMaterial({ color: 0xFFFFFF }); |
| let wingLeft = new THREE.Mesh(wingGeom, wingMat); |
| wingLeft.position.x = -0.5; |
| let wingRight = wingLeft.clone(); |
| wingRight.position.x = 0.5; |
| planeGeom.add(fuselage); |
| planeGeom.add(wingLeft); |
| planeGeom.add(wingRight); |
| |
| |
| |
| let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
| planeGeom.add(camera); |
| camera.position.y = 1.5; |
| camera.position.z = 2; |
| |
| |
| let ambientLight = new THREE.AmbientLight(0x333333); |
| scene.add(ambientLight); |
| |
| |
| let renderer = new THREE.WebGLRenderer({ antialias: true }); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| renderer.shadowMap.enabled = true; |
| document.body.appendChild(renderer.domElement); |
| |
| |
| let mass = 1500; |
| let wingArea = 10; |
| let wingLiftCoeff = 5.0; |
| let dragCoeff = 0.5; |
| let thrustMax = 5000; |
| let thrust = 0; |
| let rpm = 0; |
| let fuel = 100; |
| let flapsDeploy = 0; |
| let velocity = new THREE.Vector3(); |
| let wind = new THREE.Vector3((Math.random() * 2 - 1) * 5, 0, (Math.random() * 2 - 1) * 5); |
| |
| |
| let keys = { w: false, s: false, a: false, d: false, q: false, e: false, f: false }; |
| document.addEventListener('keydown', (e) => { |
| switch (e.key) { |
| case 'w': keys.w = true; break; |
| case 's': keys.s = true; break; |
| case 'a': keys.a = true; break; |
| case 'd': keys.d = true; break; |
| case 'q': keys.q = true; break; |
| case 'e': keys.e = true; break; |
| case 'f': if (flapsDeploy < 100) flapsDeploy += 10; break; |
| } |
| }); |
| document.addEventListener('keyup', (e) => { |
| switch (e.key) { |
| case 'w': keys.w = false; break; |
| case 's': keys.s = false; break; |
| case 'a': keys.a = false; break; |
| case 'd': keys.d = false; break; |
| case 'q': keys.q = false; break; |
| case 'e': keys.e = false; break; |
| } |
| }); |
| |
| |
| function updateInstruments() { |
| document.getElementById('altimeter').innerText = Math.round(planeGeom.position.y * 3.28084) + ' ft'; |
| document.getElementById('airspeed').innerText = Math.round(velocity.length() * 1.94384) + ' kts'; |
| document.getElementById('heading').innerText = Math.round(THREE.Math.radToDeg(planeGeom.rotation.y)) % 360 + '°'; |
| document.getElementById('rpm').innerText = Math.round(rpm); |
| document.getElementById('fuel').innerText = Math.round(fuel) + '%'; |
| document.getElementById('flaps').innerText = flapsDeploy + '%'; |
| } |
| |
| let pitch = 0, yaw = 0, roll = 0; |
| function animate() { |
| requestAnimationFrame(animate); |
| |
| |
| if (keys.w && fuel > 0) { |
| thrust = Math.min(thrustMax, thrust + 100); |
| rpm = Math.min(2500, rpm + 50); |
| fuel -= 0.05; |
| } else { |
| thrust = Math.max(0, thrust - 100); |
| rpm = Math.max(0, rpm - 50); |
| } |
| |
| |
| let airspeedVec = velocity.clone().sub(wind); |
| let airspeed = airspeedVec.length(); |
| let angleOfAttack = Math.acos(airspeedVec.dot(new THREE.Vector3(0, 1, 0).applyQuaternion(planeGeom.quaternion))); |
| let lift = 0.5 * 1.225 * wingArea * wingLiftCoeff * airspeed * airspeed * Math.sin(angleOfAttack) * (1 + flapsDeploy / 100); |
| let drag = 0.5 * 1.225 * dragCoeff * wingArea * airspeed * airspeed; |
| if (angleOfAttack > Math.PI / 4) lift *= 0.5; |
| |
| |
| let liftVec = new THREE.Vector3(0, lift, 0).applyQuaternion(planeGeom.quaternion); |
| let dragVec = airspeedVec.clone().multiplyScalar(-drag / airspeed); |
| let thrustVec = new THREE.Vector3(0, 0, thrust).applyQuaternion(planeGeom.quaternion); |
| let totalForce = new THREE.Vector3().add(liftVec).add(dragVec).add(thrustVec).add(new THREE.Vector3(0, -mass * 9.81, 0)); |
| |
| |
| velocity.add(totalForce.divideScalar(mass).multiplyScalar(1/60)); |
| planeGeom.position.add(velocity.clone().multiplyScalar(1/60)); |
| |
| |
| let raycaster = new THREE.Raycaster(planeGeom.position, new THREE.Vector3(0, -1, 0)); |
| let intersects = raycaster.intersectObject(terrain); |
| if (intersects.length > 0 && intersects[0].distance < 5) { |
| planeGeom.position.y = intersects[0].point.y + 2; |
| velocity.y = Math.max(0, velocity.y * 0.8); |
| } |
| |
| |
| if (keys.a) yaw -= 0.01; |
| if (keys.d) yaw += 0.01; |
| if (keys.q) roll -= 0.02; |
| if (keys.e) roll += 0.02; |
| if (keys.s) pitch += 0.005; |
| if (keys.w) pitch -= 0.005; |
| |
| |
| planeGeom.rotation.order = 'ZXY'; |
| planeGeom.rotation.z = roll; |
| planeGeom.rotation.x = pitch; |
| planeGeom.rotation.y = yaw; |
| |
| |
| let time = Date.now() * 0.00005; |
| sun.position.x = Math.sin(time) * 100; |
| sun.position.z = Math.cos(time) * 100; |
| sun.position.y = Math.sin(time * 0.5) * 50 + 50; |
| skyMat.uniforms.sunPosition.value.copy(sun.position); |
| ambientLight.intensity = 0.2 + sun.position.y / 200; |
| |
| updateInstruments(); |
| renderer.render(scene, camera); |
| } |
| animate(); |
| </script> |
| </body> |
| </html> |