76bad46d09
Loading screen, main menu, 3D flight sim with CRT post-processing, procedural terrain, airport with buildings, low-poly aircraft, flight physics, HUD instruments, and sound. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
169 lines
5.0 KiB
JavaScript
169 lines
5.0 KiB
JavaScript
import * as THREE from 'three';
|
|
|
|
const EGA = {
|
|
GREEN: new THREE.Color(0.20, 0.67, 0.13),
|
|
DARK_GREEN: new THREE.Color(0.15, 0.40, 0.10),
|
|
BROWN: new THREE.Color(0.45, 0.30, 0.15),
|
|
TAN: new THREE.Color(0.55, 0.45, 0.30),
|
|
WHITE: new THREE.Color(1, 1, 1),
|
|
BLUE: new THREE.Color(0, 0, 0.67),
|
|
};
|
|
|
|
export class Terrain {
|
|
constructor(size = 2000, segments = 100) {
|
|
this.size = size;
|
|
this.segments = segments;
|
|
this.heightData = null;
|
|
this.cellSize = size / segments;
|
|
}
|
|
|
|
hash(x, z) {
|
|
const n = Math.sin(x * 127.1 + z * 311.7) * 43758.5453;
|
|
return n - Math.floor(n);
|
|
}
|
|
|
|
smoothNoise(x, z) {
|
|
const ix = Math.floor(x);
|
|
const iz = Math.floor(z);
|
|
const fx = x - ix;
|
|
const fz = z - iz;
|
|
const sfx = fx * fx * (3 - 2 * fx);
|
|
const sfz = fz * fz * (3 - 2 * fz);
|
|
|
|
const n00 = this.hash(ix, iz);
|
|
const n10 = this.hash(ix + 1, iz);
|
|
const n01 = this.hash(ix, iz + 1);
|
|
const n11 = this.hash(ix + 1, iz + 1);
|
|
|
|
const nx0 = n00 + (n10 - n00) * sfx;
|
|
const nx1 = n01 + (n11 - n01) * sfx;
|
|
return nx0 + (nx1 - nx0) * sfz;
|
|
}
|
|
|
|
getHeightRaw(x, z) {
|
|
let h = 0;
|
|
h += this.smoothNoise(x * 0.002, z * 0.002) * 50;
|
|
h += this.smoothNoise(x * 0.006, z * 0.006) * 20;
|
|
h += this.smoothNoise(x * 0.015, z * 0.015) * 8;
|
|
return Math.max(0, h - 15);
|
|
}
|
|
|
|
buildHeightData() {
|
|
const { segments: seg, size } = this;
|
|
this.heightData = new Float32Array((seg + 1) * (seg + 1));
|
|
const half = size / 2;
|
|
for (let iz = 0; iz <= seg; iz++) {
|
|
for (let ix = 0; ix <= seg; ix++) {
|
|
const x = -half + ix * this.cellSize;
|
|
const z = -half + iz * this.cellSize;
|
|
this.heightData[iz * (seg + 1) + ix] = this.getHeightRaw(x, z);
|
|
}
|
|
}
|
|
}
|
|
|
|
getHeightAt(x, z) {
|
|
if (!this.heightData) return this.getHeightRaw(x, z);
|
|
const { size, segments: seg } = this;
|
|
const half = size / 2;
|
|
const gx = ((x + half) / size) * seg;
|
|
const gz = ((z + half) / size) * seg;
|
|
const ix = Math.floor(gx);
|
|
const iz = Math.floor(gz);
|
|
if (ix < 0 || ix >= seg || iz < 0 || iz >= seg) return 0;
|
|
const fx = gx - ix;
|
|
const fz = gz - iz;
|
|
const row = seg + 1;
|
|
const h00 = this.heightData[iz * row + Math.min(ix, seg)];
|
|
const h10 = this.heightData[iz * row + Math.min(ix + 1, seg)];
|
|
const h01 = this.heightData[(iz + 1) * row + Math.min(ix, seg)];
|
|
const h11 = this.heightData[(iz + 1) * row + Math.min(ix + 1, seg)];
|
|
const nx0 = h00 + (h10 - h00) * fx;
|
|
const nx1 = h01 + (h11 - h01) * fx;
|
|
return nx0 + (nx1 - nx0) * fz;
|
|
}
|
|
|
|
flattenAirport(radius) {
|
|
if (!this.heightData) return;
|
|
const { segments: seg } = this;
|
|
const row = seg + 1;
|
|
const center = seg / 2;
|
|
for (let iz = 0; iz <= seg; iz++) {
|
|
for (let ix = 0; ix <= seg; ix++) {
|
|
const dx = ix - center;
|
|
const dz = iz - center;
|
|
const dist = Math.sqrt(dx * dx + dz * dz) * this.cellSize;
|
|
if (dist < radius) {
|
|
const fade = 1 - Math.min(1, dist / radius);
|
|
this.heightData[iz * row + ix] *= (1 - fade * 0.95);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
createMesh() {
|
|
this.buildHeightData();
|
|
this.flattenAirport(200);
|
|
|
|
const { segments: seg, size } = this;
|
|
const geometry = new THREE.PlaneGeometry(size, size, seg, seg);
|
|
geometry.rotateX(-Math.PI / 2);
|
|
|
|
const positions = geometry.attributes.position.array;
|
|
const colors = new Float32Array(positions.length);
|
|
|
|
for (let i = 0; i < positions.length; i += 3) {
|
|
const x = positions[i];
|
|
const z = positions[i + 2];
|
|
const h = this.getHeightAt(x, z);
|
|
positions[i + 1] = h;
|
|
|
|
if (h < 3) {
|
|
colors[i] = EGA.GREEN.r;
|
|
colors[i + 1] = EGA.GREEN.g;
|
|
colors[i + 2] = EGA.GREEN.b;
|
|
} else if (h < 12) {
|
|
colors[i] = EGA.DARK_GREEN.r;
|
|
colors[i + 1] = EGA.DARK_GREEN.g;
|
|
colors[i + 2] = EGA.DARK_GREEN.b;
|
|
} else if (h < 30) {
|
|
colors[i] = EGA.BROWN.r;
|
|
colors[i + 1] = EGA.BROWN.g;
|
|
colors[i + 2] = EGA.BROWN.b;
|
|
} else if (h < 45) {
|
|
colors[i] = EGA.TAN.r;
|
|
colors[i + 1] = EGA.TAN.g;
|
|
colors[i + 2] = EGA.TAN.b;
|
|
} else {
|
|
const mix = Math.min(1, (h - 45) / 20);
|
|
colors[i] = EGA.TAN.r + (EGA.WHITE.r - EGA.TAN.r) * mix;
|
|
colors[i + 1] = EGA.TAN.g + (EGA.WHITE.g - EGA.TAN.g) * mix;
|
|
colors[i + 2] = EGA.TAN.b + (EGA.WHITE.b - EGA.TAN.b) * mix;
|
|
}
|
|
}
|
|
|
|
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
geometry.computeVertexNormals();
|
|
|
|
const material = new THREE.MeshLambertMaterial({
|
|
vertexColors: true,
|
|
flatShading: true,
|
|
});
|
|
|
|
return new THREE.Mesh(geometry, material);
|
|
}
|
|
|
|
createWater() {
|
|
const geometry = new THREE.PlaneGeometry(this.size * 2, this.size * 2);
|
|
geometry.rotateX(-Math.PI / 2);
|
|
const material = new THREE.MeshLambertMaterial({
|
|
color: EGA.BLUE,
|
|
transparent: true,
|
|
opacity: 0.7,
|
|
flatShading: true,
|
|
});
|
|
const water = new THREE.Mesh(geometry, material);
|
|
water.position.y = 0.5;
|
|
return water;
|
|
}
|
|
}
|