|
<!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> |