Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save shricodev/5fc44c03fde651f5bea7008919377c1d to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/5fc44c03fde651f5bea7008919377c1d to your computer and use it in GitHub Desktop.
<!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>
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();
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 */
}
@shricodev
Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment