|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Enhanced Three.js Flight Simulator</title> |
|
<style> |
|
body { margin: 0; background-color: #000; overflow: hidden; } |
|
canvas { display: block; } |
|
#controls { position: absolute; top: 10px; left: 10px; background: rgba(255,255,255,0.7); padding: 10px; font-family: monospace; font-size: 14px; } |
|
#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="controls"> |
|
<b>Flight Simulator Controls:</b><br> |
|
W / β : Pitch Up (Climb)<br> |
|
S / β : Pitch Down (Dive)<br> |
|
A / β : Yaw Left (Turn Left, but use roll for real turns)<br> |
|
D / β : Yaw Right (Turn Right, but use roll for real turns)<br> |
|
Q / E : Roll Left/Right (Bank for turns)<br> |
|
ββ Speed up (gradually)<br> |
|
ββ Slow down (gradually)<br> |
|
Mouse Look (drag to change view direction) |
|
</div> |
|
|
|
<div id="instruments"> |
|
<b>Flight 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> |
|
</div> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
|
<script> |
|
|
|
let scene = new THREE.Scene(); |
|
|
|
|
|
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: ` |
|
varying vec3 vWorldPosition; |
|
void main() { |
|
float heightFactor = (vWorldPosition.y + 250.0) / 500.0; |
|
heightFactor = clamp(heightFactor, 0.0, 1.0); |
|
vec3 topColor = vec3(0.1, 0.4, 0.8); // Light blue |
|
vec3 bottomColor = vec3(0.8, 0.6, 0.2); // Light brown |
|
gl_FragColor = vec4(mix(bottomColor, topColor, heightFactor), 1.0); |
|
} |
|
`, |
|
side: THREE.BackSide |
|
}); |
|
let sky = new THREE.Mesh(skyGeom, skyMat); |
|
scene.add(sky); |
|
|
|
|
|
let groundGeom = new THREE.PlaneGeometry(200, 200, 64, 64); |
|
for (let i = 0; i < groundGeom.attributes.position.count; i++) { |
|
let x = groundGeom.attributes.position.getX(i); |
|
let z = groundGeom.attributes.position.getZ(i); |
|
let y = Math.sin(x * 0.1) * Math.cos(z * 0.1) * 5.0; |
|
groundGeom.attributes.position.setY(i, y); |
|
} |
|
let groundMat = new THREE.MeshLambertMaterial({ color: 0x228B22 }); |
|
let ground = new THREE.Mesh(groundGeom, groundMat); |
|
ground.rotation.x = -Math.PI / 2; |
|
ground.receiveShadow = true; |
|
scene.add(ground); |
|
|
|
|
|
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(); |
|
const wingVertices = new Float32Array([ |
|
-2, 0, 1, 2, 0, 1, 2, 0, -2, |
|
-2, 0, 1, 2, 0, -2, -2, 0, -2, |
|
]); |
|
let posAttr = new THREE.BufferAttribute(wingVertices, 3); |
|
wingGeom.setAttribute('position', posAttr); |
|
let wingMat = new THREE.MeshLambertMaterial({ color: 0xFFFFFF }); |
|
let wingLeft = new THREE.Mesh(wingGeom, wingMat); |
|
wingLeft.position.x = -0.5; |
|
wingLeft.castShadow = true; |
|
let wingRight = wingLeft.clone(); |
|
wingRight.position.x = 0.5; |
|
let tailGeom = new THREE.BufferGeometry(); |
|
const tailVertices = new Float32Array([ |
|
0, 0.5, -3, -0.5, 0, -3, 0.5, 0, -3 |
|
]); |
|
let tailAttr = new THREE.BufferAttribute(tailVertices, 3); |
|
tailGeom.setAttribute('position', tailAttr); |
|
let tailMat = new THREE.MeshLambertMaterial({ color: 0xFFFFFF }); |
|
let tail = new THREE.Mesh(tailGeom, tailMat); |
|
tail.castShadow = true; |
|
planeGeom.add(fuselage); |
|
planeGeom.add(wingLeft); |
|
planeGeom.add(wingRight); |
|
planeGeom.add(tail); |
|
scene.add(planeGeom); |
|
|
|
|
|
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 dirLight = new THREE.DirectionalLight(0xFFFFFF, 0.8); |
|
dirLight.position.set(10, 20, 10); |
|
dirLight.castShadow = true; |
|
dirLight.shadow.mapSize.width = 2048; |
|
dirLight.shadow.mapSize.height = 2048; |
|
dirLight.shadow.camera.near = 1; |
|
dirLight.shadow.camera.far = 100; |
|
scene.add(dirLight); |
|
|
|
|
|
let renderer = new THREE.WebGLRenderer({ antialias: true }); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
renderer.shadowMap.enabled = true; |
|
document.body.appendChild(renderer.domElement); |
|
|
|
|
|
let pitch = 0; |
|
let yaw = 0; |
|
let roll = 0; |
|
let velocity = new THREE.Vector3(0, 0, 0); |
|
let airspeed = 0; |
|
let altitude = 0; |
|
let heading = 0; |
|
let liftForce = 0; |
|
let dragForce = 0; |
|
let gravity = new THREE.Vector3(0, -9.81, 0); |
|
let mass = 1000; |
|
let wingLiftCoefficient = 10.0; |
|
let dragCoefficient = 0.5; |
|
let maxStallAngle = Math.PI / 6; |
|
|
|
|
|
let keys = { |
|
w: false, s: false, a: false, d: false, q: false, e: false |
|
}; |
|
document.addEventListener('keydown', (e) => { |
|
switch (e.key) { |
|
case 'w': case 'ArrowUp': keys.w = true; break; |
|
case 's': case 'ArrowDown': keys.s = true; break; |
|
case 'a': case 'ArrowLeft': keys.a = true; break; |
|
case 'd': case 'ArrowRight': keys.d = true; break; |
|
case 'q': keys.q = true; break; |
|
case 'e': keys.e = true; break; |
|
} |
|
}); |
|
document.addEventListener('keyup', (e) => { |
|
switch (e.key) { |
|
case 'w': case 'ArrowUp': keys.w = false; break; |
|
case 's': case 'ArrowDown': keys.s = false; break; |
|
case 'a': case 'ArrowLeft': keys.a = false; break; |
|
case 'd': case 'ArrowRight': keys.d = false; break; |
|
case 'q': keys.q = false; break; |
|
case 'e': keys.e = false; break; |
|
} |
|
}); |
|
|
|
|
|
let mouseDown = false; |
|
let lastMouseX, lastMouseY; |
|
let sensitivity = 0.005; |
|
document.addEventListener('mousedown', (e) => { |
|
mouseDown = true; |
|
lastMouseX = e.clientX; |
|
lastMouseY = e.clientY; |
|
}); |
|
document.addEventListener('mouseup', () => mouseDown = false); |
|
document.addEventListener('mousemove', (e) => { |
|
if (mouseDown) { |
|
let dx = e.clientX - lastMouseX; |
|
let dy = e.clientY - lastMouseY; |
|
yaw -= dx * sensitivity; |
|
pitch -= dy * sensitivity; |
|
pitch = Math.max(-Math.PI/2, Math.min(Math.PI/2, pitch)); |
|
lastMouseX = e.clientX; |
|
lastMouseY = e.clientY; |
|
} |
|
}); |
|
|
|
|
|
function updateInstruments() { |
|
document.getElementById('altimeter').innerText = Math.round(altitude * 3.28084) + ' ft'; |
|
document.getElementById('airspeed').innerText = Math.round(airspeed) + ' kts'; |
|
document.getElementById('heading').innerText = Math.round(THREE.Math.radToDeg(yaw)) % 360 + 'Β°'; |
|
} |
|
|
|
|
|
function animate() { |
|
requestAnimationFrame(animate); |
|
|
|
|
|
let velocityDir = new THREE.Vector3(0, 0, -1); |
|
velocityDir.applyQuaternion(planeGeom.quaternion).normalize(); |
|
airspeed = velocity.length() * 1.94384; |
|
let angleOfAttack = Math.acos(velocityDir.dot(new THREE.Vector3(0, 1, 0).applyQuaternion(planeGeom.quaternion))); |
|
liftForce = wingLiftCoefficient * airspeed * airspeed * Math.sin(angleOfAttack); |
|
if (angleOfAttack > maxStallAngle) liftForce *= (1 - (angleOfAttack - maxStallAngle) / (Math.PI / 2 - maxStallAngle)); |
|
dragForce = dragCoefficient * airspeed * airspeed; |
|
|
|
|
|
let lift = new THREE.Vector3(0, liftForce, 0).applyQuaternion(planeGeom.quaternion); |
|
let drag = velocityDir.clone().multiplyScalar(-dragForce); |
|
let totalForce = new THREE.Vector3().add(gravity).add(lift).add(drag).divideScalar(mass); |
|
|
|
|
|
velocity.add(totalForce.multiplyScalar(1/60)); |
|
planeGeom.position.add(velocity.clone().multiplyScalar(1/60)); |
|
|
|
|
|
altitude = planeGeom.position.y - groundGeom.attributes.position.getY(0); |
|
if (altitude < 0) { |
|
planeGeom.position.y += -altitude; |
|
velocity.y = Math.max(0, velocity.y * 0.8); |
|
} |
|
|
|
|
|
if (keys.w) pitch -= 0.005; |
|
if (keys.s) pitch += 0.005; |
|
if (keys.a) yaw -= 0.01; |
|
if (keys.d) yaw += 0.01; |
|
if (keys.q) roll -= 0.02; |
|
if (keys.e) roll += 0.02; |
|
|
|
|
|
pitch = Math.max(-Math.PI/3, Math.min(Math.PI/4, pitch)); |
|
roll = Math.max(-Math.PI/4, Math.min(Math.PI/4, roll)); |
|
|
|
|
|
planeGeom.rotation.order = 'ZXY'; |
|
planeGeom.rotation.z = roll; |
|
planeGeom.rotation.x = pitch; |
|
planeGeom.rotation.y = yaw; |
|
|
|
|
|
heading = THREE.Math.radToDeg(yaw) % 360; |
|
|
|
updateInstruments(); |
|
renderer.render(scene, camera); |
|
} |
|
animate(); |
|
</script> |
|
</body> |
|
</html> |