Created
March 27, 2025 14:01
-
-
Save shricodev/5fc44c03fde651f5bea7008919377c1d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Simple Flight Simulator</title> | |
| <link rel="stylesheet" href="style.css" /> | |
| </head> | |
| <body> | |
| <div id="info"> | |
| Simple Flight Simulator<br /> | |
| [W/S] or [↑/↓]: Throttle | [A/D] or [←/→]: Roll | [Q/E]: Yaw (Rudder) | | |
| [R/F]: Pitch (Elevator) <br /> | |
| Reach takeoff speed (~50) to lift off. | |
| </div> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js" | |
| } | |
| } | |
| </script> | |
| <script type="module" src="script.js"></script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as THREE from "three"; | |
| // --- Basic Setup --- | |
| const scene = new THREE.Scene(); | |
| scene.fog = new THREE.Fog(0x87ceeb, 500, 2000); // Add fog for depth perception | |
| const camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 3000, | |
| ); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setClearColor(0x87ceeb); // Sky blue background | |
| renderer.shadowMap.enabled = true; // Enable shadows | |
| document.body.appendChild(renderer.domElement); | |
| // --- Lighting --- | |
| const ambientLight = new THREE.AmbientLight(0xaaaaaa); // Soft ambient light | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5); | |
| directionalLight.position.set(100, 150, 100); | |
| directionalLight.castShadow = true; | |
| // Configure shadow properties | |
| directionalLight.shadow.mapSize.width = 1024; | |
| directionalLight.shadow.mapSize.height = 1024; | |
| directionalLight.shadow.camera.near = 50; | |
| directionalLight.shadow.camera.far = 500; | |
| directionalLight.shadow.camera.left = -200; | |
| directionalLight.shadow.camera.right = 200; | |
| directionalLight.shadow.camera.top = 200; | |
| directionalLight.shadow.camera.bottom = -200; | |
| scene.add(directionalLight); | |
| // --- Ground / Runway --- | |
| const groundSize = 4000; | |
| const groundGeometry = new THREE.PlaneGeometry(groundSize, groundSize); | |
| const groundMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x55aa55, | |
| side: THREE.DoubleSide, | |
| }); // Green grass | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; // Rotate flat | |
| ground.receiveShadow = true; | |
| scene.add(ground); | |
| // Runway | |
| const runwayWidth = 50; | |
| const runwayLength = 1000; | |
| const runwayGeometry = new THREE.PlaneGeometry(runwayWidth, runwayLength); | |
| const runwayMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x404040, | |
| side: THREE.DoubleSide, | |
| }); | |
| const runway = new THREE.Mesh(runwayGeometry, runwayMaterial); | |
| runway.rotation.x = -Math.PI / 2; | |
| runway.position.y = 0.01; // Slightly above ground to prevent z-fighting | |
| runway.position.z = -(runwayLength / 2) + 200; // Position it starting near origin | |
| runway.receiveShadow = true; | |
| scene.add(runway); | |
| // --- Simple Plane Model --- | |
| const plane = new THREE.Group(); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc }); // Light grey body | |
| const wingMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // Red wings | |
| // Fuselage | |
| const fuselageGeometry = new THREE.BoxGeometry(1, 1, 5); // x, y, z dimensions | |
| const fuselage = new THREE.Mesh(fuselageGeometry, bodyMaterial); | |
| fuselage.castShadow = true; | |
| plane.add(fuselage); | |
| // Wings | |
| const wingGeometry = new THREE.BoxGeometry(8, 0.2, 1.5); // Span, thickness, chord | |
| const wing = new THREE.Mesh(wingGeometry, wingMaterial); | |
| wing.position.z = -0.5; // Position along fuselage | |
| wing.castShadow = true; | |
| plane.add(wing); | |
| // Tail Fin (Vertical Stabilizer) | |
| const tailFinGeometry = new THREE.BoxGeometry(0.2, 1.5, 1); | |
| const tailFin = new THREE.Mesh(tailFinGeometry, wingMaterial); | |
| tailFin.position.z = 2; // Back of the fuselage | |
| tailFin.position.y = 0.75; | |
| tailFin.castShadow = true; | |
| plane.add(tailFin); | |
| // Horizontal Stabilizer | |
| const hStabGeometry = new THREE.BoxGeometry(3, 0.15, 0.8); | |
| const hStab = new THREE.Mesh(hStabGeometry, wingMaterial); | |
| hStab.position.z = 2.2; | |
| hStab.position.y = 0.5; // Attach near base of tail fin | |
| hStab.castShadow = true; | |
| plane.add(hStab); | |
| plane.position.set(0, 1.0, 0); // Start slightly above ground on the runway | |
| plane.rotation.y = Math.PI; // Point down the runway (positive Z initially) | |
| scene.add(plane); | |
| // --- Cityscape Generation --- | |
| const cityArea = 1500; // Square area size around origin | |
| const buildingMaxHeight = 150; | |
| const buildingPadding = 10; // Minimum space between buildings | |
| const numBuildings = 200; | |
| const buildings = new THREE.Group(); | |
| // Simple window texture function | |
| function createWindowTexture() { | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = 32; | |
| canvas.height = 64; | |
| const context = canvas.getContext("2d"); | |
| context.fillStyle = "#BBB"; // Building color | |
| context.fillRect(0, 0, canvas.width, canvas.height); | |
| context.fillStyle = "#444"; // Window color | |
| for (let y = 4; y < canvas.height - 4; y += 8) { | |
| for (let x = 4; x < canvas.width - 4; x += 8) { | |
| context.fillRect(x, y, 4, 4); // Simple square windows | |
| } | |
| } | |
| return new THREE.CanvasTexture(canvas); | |
| } | |
| const buildingTexture = createWindowTexture(); | |
| buildingTexture.wrapS = THREE.RepeatWrapping; | |
| buildingTexture.wrapT = THREE.RepeatWrapping; | |
| for (let i = 0; i < numBuildings; i++) { | |
| const width = Math.random() * 30 + 15; // Random width | |
| const depth = Math.random() * 30 + 15; // Random depth | |
| const height = Math.random() * buildingMaxHeight + 20; // Random height | |
| const buildingGeometry = new THREE.BoxGeometry(width, height, depth); | |
| // Adjust texture repeat based on building size | |
| const buildingMaterial = new THREE.MeshStandardMaterial({ | |
| map: buildingTexture, | |
| }); | |
| // Clone material for unique texture offsets if needed, but usually not necessary for this effect | |
| buildingMaterial.map.repeat.set( | |
| Math.ceil(width / 10), | |
| Math.ceil(height / 10), | |
| ); | |
| buildingMaterial.map.needsUpdate = true; // Important when changing repeat | |
| const building = new THREE.Mesh(buildingGeometry, buildingMaterial); | |
| // Random position, avoiding runway area | |
| let posX, posZ; | |
| const placeTryLimit = 10; // Prevent infinite loops | |
| let tries = 0; | |
| do { | |
| posX = (Math.random() - 0.5) * cityArea; | |
| posZ = (Math.random() - 0.5) * cityArea; | |
| tries++; | |
| } while ( | |
| tries < placeTryLimit && | |
| Math.abs(posX) < runwayWidth / 2 + buildingPadding + width / 2 && | |
| posZ > -runwayLength - buildingPadding - depth / 2 && | |
| posZ < 200 + buildingPadding + depth / 2 | |
| ); | |
| if (tries < placeTryLimit) { | |
| // Only place if a suitable spot was found | |
| building.position.set(posX, height / 2, posZ); // Position base on the ground | |
| building.castShadow = true; | |
| building.receiveShadow = true; | |
| buildings.add(building); | |
| } | |
| } | |
| scene.add(buildings); | |
| // --- Physics and Control Variables --- | |
| let speed = 0; | |
| const maxSpeed = 200; | |
| const minSpeed = 0; | |
| const acceleration = 0.5; | |
| const deceleration = 0.3; | |
| const takeoffSpeed = 50; | |
| const climbRate = 0.5; // Rate of vertical speed increase after takeoff | |
| const gravity = 0.98; // Simplified gravity effect | |
| let verticalSpeed = 0; | |
| const rollSpeed = 1.5; | |
| const pitchSpeed = 1.0; | |
| const yawSpeed = 1.0; | |
| const keys = {}; // Keep track of pressed keys | |
| // --- Event Listeners --- | |
| document.addEventListener("keydown", (event) => { | |
| keys[event.code] = true; | |
| }); | |
| document.addEventListener("keyup", (event) => { | |
| keys[event.code] = false; | |
| }); | |
| // Handle window resize | |
| window.addEventListener( | |
| "resize", | |
| () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }, | |
| false, | |
| ); | |
| // --- Animation Loop --- | |
| const clock = new THREE.Clock(); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const deltaTime = clock.getDelta(); // Time since last frame in seconds | |
| // --- Handle Input and Update Physics --- | |
| let appliedThrust = 0; | |
| let appliedRoll = 0; | |
| let appliedPitch = 0; | |
| let appliedYaw = 0; | |
| // Throttle (W/S or Up/Down Arrows) | |
| if (keys["KeyW"] || keys["ArrowUp"]) appliedThrust = acceleration; | |
| if (keys["KeyS"] || keys["ArrowDown"]) appliedThrust = -deceleration * 2; // Stronger braking | |
| // Roll (A/D or Left/Right Arrows) | |
| if (keys["KeyA"] || keys["ArrowLeft"]) appliedRoll = rollSpeed; | |
| if (keys["KeyD"] || keys["ArrowRight"]) appliedRoll = -rollSpeed; | |
| // Pitch (R/F) - Nose Up/Down | |
| if (keys["KeyF"]) appliedPitch = pitchSpeed; // Nose Up | |
| if (keys["KeyR"]) appliedPitch = -pitchSpeed; // Nose Down | |
| // Yaw (Q/E) - Rudder Left/Right | |
| if (keys["KeyQ"]) appliedYaw = yawSpeed; | |
| if (keys["KeyE"]) appliedYaw = -yawSpeed; | |
| // Update Speed | |
| speed += appliedThrust * deltaTime; | |
| // Apply drag/friction (simple linear drag) | |
| if (appliedThrust === 0) { | |
| speed -= deceleration * deltaTime; | |
| } | |
| speed = Math.max(minSpeed, Math.min(speed, maxSpeed)); // Clamp speed | |
| // --- Apply Rotations --- | |
| // Rotations are applied relative to the plane's local axes | |
| // Yaw (Turn left/right) - Rotate around local Y axis | |
| plane.rotateY(appliedYaw * deltaTime); | |
| // Pitch (Nose up/down) - Rotate around local X axis | |
| plane.rotateX(appliedPitch * deltaTime); | |
| // Roll (Bank left/right) - Rotate around local Z axis | |
| plane.rotateZ(appliedRoll * deltaTime); | |
| // Auto-leveling tendency (optional, makes it easier to fly) | |
| // Gently reduce roll and pitch if no input is given | |
| // if (appliedRoll === 0) plane.rotation.z *= 0.98; | |
| // if (appliedPitch === 0) plane.rotation.x *= 0.98; // Be careful with this one, might fight gravity/lift | |
| // --- Handle Takeoff and Flight --- | |
| const altitude = plane.position.y; | |
| const isOnGround = altitude <= 1.0; // Consider slightly above 0 as ground contact | |
| if (isOnGround) { | |
| verticalSpeed = 0; // No vertical movement on ground | |
| plane.position.y = 1.0; // Keep it firmly on ground level | |
| // Takeoff condition | |
| if (speed > takeoffSpeed) { | |
| verticalSpeed = climbRate * (speed / takeoffSpeed); // Initial jump based on speed excess | |
| console.log("Takeoff!"); | |
| } | |
| } else { | |
| // --- In the Air --- | |
| // Basic Lift Simulation: Proportional to speed squared (simplified) and angle of attack (approximated by pitch) | |
| // We need the plane's upward direction vector relative to the world | |
| const localUp = new THREE.Vector3(0, 1, 0); | |
| const worldUp = localUp.applyQuaternion(plane.quaternion); | |
| // Simplified lift: More lift if speed is high enough to counteract gravity | |
| // This is VERY basic - just enough to stay airborne easily | |
| let lift = 0; | |
| if (speed > takeoffSpeed * 0.8) { | |
| // Need some speed to generate lift | |
| // More lift if pitched slightly up, less if pitched down | |
| const pitchEffect = Math.max(0, worldUp.y); // Use the world Y component of the plane's up vector | |
| lift = speed * speed * 0.001 * pitchEffect; // Adjust the 0.001 factor to balance flight | |
| } | |
| // Apply Gravity | |
| verticalSpeed -= gravity * deltaTime; | |
| // Apply Lift | |
| verticalSpeed += lift * deltaTime; | |
| // Update altitude | |
| plane.position.y += verticalSpeed * deltaTime; | |
| // Ground collision | |
| if (plane.position.y < 1.0) { | |
| plane.position.y = 1.0; | |
| verticalSpeed = 0; | |
| speed *= 0.8; // Lose some speed on landing | |
| console.log("Landed/Crashed"); | |
| // Optional: Reset orientation on hard landing/crash | |
| // plane.rotation.set(0, plane.rotation.y, 0); // Level wings and pitch | |
| } | |
| } | |
| // --- Update Position based on Speed and Direction --- | |
| const forwardVector = new THREE.Vector3(0, 0, -1); // Plane's local forward axis is -Z | |
| forwardVector.applyQuaternion(plane.quaternion); // Rotate vector by plane's orientation | |
| plane.position.add(forwardVector.multiplyScalar(speed * deltaTime)); | |
| // --- Update Camera --- | |
| // Simple third-person follow camera | |
| const cameraOffset = new THREE.Vector3(0, 5, 15); // Behind and slightly above | |
| const cameraTarget = new THREE.Vector3(); | |
| // Apply plane's rotation to the offset vector | |
| cameraOffset.applyQuaternion(plane.quaternion); | |
| // Add the rotated offset to the plane's position | |
| cameraTarget.copy(plane.position).add(cameraOffset); | |
| // Smoothly interpolate camera position (lerp) | |
| camera.position.lerp(cameraTarget, 0.1); // Adjust 0.1 for faster/slower camera follow | |
| // Make camera look at the plane | |
| camera.lookAt(plane.position); | |
| // --- Render --- | |
| renderer.render(scene, camera); | |
| } | |
| // Start the animation loop | |
| animate(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| body { | |
| margin: 0; | |
| overflow: hidden; /* Hide scrollbars */ | |
| font-family: Arial, sans-serif; | |
| background-color: #87ceeb; /* Sky blue background */ | |
| } | |
| #info { | |
| position: absolute; | |
| top: 10px; | |
| width: 100%; | |
| text-align: center; | |
| z-index: 100; | |
| display: block; | |
| color: white; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| padding: 5px 0; | |
| font-size: 14px; | |
| } | |
| canvas { | |
| display: block; /* Remove potential extra space below canvas */ | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output: https://youtu.be/LhW7h9i-Cys