From e93a8e5f329f3650bece6d6bfbf339ad9b8c4ec7 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 6 Oct 2026 16:11:29 +0200 Subject: [PATCH] Rework landscape and airplane: realistic flight look, detailed 3D airplane --- assets/index.html | 296 +++++++++++++++++++++++++++------------------- 1 file changed, 172 insertions(+), 124 deletions(-) diff --git a/assets/index.html b/assets/index.html index 0ccb187..85cd56a 100644 --- a/assets/index.html +++ b/assets/index.html @@ -37,22 +37,23 @@ height = canvas.height = window.innerHeight; } - function rotatePoint(point, angle) { - const cos = Math.cos(angle); - const sin = Math.sin(angle); - return { - x: point.x * cos - point.z * sin, - z: point.x * sin + point.z * cos - }; - } - window.addEventListener('resize', resize); resize(); const camera = { - fov: 600 + fov: 800 }; + function rotateY(point, angle) { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return { + x: point.x * cos - point.z * sin, + y: point.y, + z: point.x * sin + point.z * cos + }; + } + class Point3D { constructor(x, y, z) { this.x = x; @@ -61,22 +62,21 @@ } project() { - const rotated = rotatePoint(this, Math.PI); const scale = camera.fov / (camera.fov + this.z); - const xIso = rotated.x - rotated.z; - const yIso = (rotated.x + rotated.z) * 0.5; + const xIso = this.x - this.z; + const yIso = (this.x + this.z) * 0.5 - this.y; const x2d = xIso * scale + width / 2; const y2d = yIso * scale + height / 2; return { x: x2d, y: y2d, scale: scale }; } } - class Triangle { - constructor(p1, p2, p3) { + class TriangleMesh { + constructor(p1, p2, p3, color) { this.p1 = p1; this.p2 = p2; this.p3 = p3; - this.calculateNormal(); + this.color = color; } calculateNormal() { @@ -93,88 +93,71 @@ this.normal.y /= len; this.normal.z /= len; } - const light = { x: 0.3, y: 0.3, z: 0.8 }; + const light = { x: 0.3, y: 0.5, z: 0.8 }; const dot = this.normal.x * light.x + this.normal.y * light.y + this.normal.z * light.z; - this.intensity = Math.max(0.2, dot); + this.intensity = Math.max(0.15, dot); } - isBackfaceCulled() { - const v1 = { x: this.p2.x - this.p1.x, y: this.p2.y - this.p1.y, z: this.p2.z - this.p1.z }; - const v2 = { x: this.p3.x - this.p1.x, y: this.p3.y - this.p1.y, z: this.p3.z - this.p1.z }; - const crossZ = v1.x * v2.y - v1.y * v2.x; - return crossZ >= 0; + isBackface() { + return this.normal.y > 0.5; } - drawFilled() { + draw() { + if (this.isBackface()) return; + const proj1 = this.p1.project(); const proj2 = this.p2.project(); const proj3 = this.p3.project(); - if (this.isBackfaceCulled()) return; - - const colorVal = Math.floor(20 + this.intensity * 180); - const color = this.p1.isWater || this.p2.isWater || this.p3.isWater - ? `rgba(0, ${Math.floor(colorVal * 0.8)}, ${colorVal + 50}, 0.7)` - : `rgba(0, ${colorVal}, 0, 0.7)`; + const colorVal = Math.floor(this.intensity * 200); + const r = this.color.r || 0; + const g = this.color.g || (this.color.isWater ? 100 : colorVal); + const b = this.color.b || (this.color.isWater ? 200 : 0); + const fillColor = `rgba(${Math.floor(r * this.intensity)}, ${Math.floor(g * this.intensity)}, ${Math.floor(b * this.intensity)}, 0.85)`; ctx.beginPath(); ctx.moveTo(proj1.x, proj1.y); ctx.lineTo(proj2.x, proj2.y); ctx.lineTo(proj3.x, proj3.y); ctx.closePath(); - ctx.fillStyle = color; + ctx.fillStyle = fillColor; ctx.fill(); - } - drawWireframe() { - const proj1 = this.p1.project(); - const proj2 = this.p2.project(); - const proj3 = this.p3.project(); - - if (this.isBackfaceCulled()) return; - - const color = this.p1.isWater || this.p2.isWater || this.p3.isWater ? '#00ccff' : '#00ff00'; - - ctx.strokeStyle = color; + ctx.strokeStyle = this.color.isWater ? '#00ccff' : (this.color.r ? '#ff3333' : '#00ff66'); ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(proj1.x, proj1.y); - ctx.lineTo(proj2.x, proj2.y); - ctx.lineTo(proj3.x, proj3.y); - ctx.closePath(); ctx.stroke(); } } class Landscape { constructor() { - this.gridSize = 100; + this.gridSize = 120; this.gridRange = 30; this.offsetX = 0; this.offsetZ = 0; this.triangles = []; - this.waterLevel = 50; + this.waterLevel = 20; } generateHeight(x, z) { - const nx = x / 200; - const nz = z / 200; + const nx = x / 300; + const nz = z / 300; let y = 0; - y += Math.sin(nx) * 40; - y += Math.cos(nz) * 40; - y += Math.sin(nx * 2 + nz * 0.5) * 20; - y += Math.cos(nz * 2 - nx) * 20; - y += Math.sin(nx * 3 - nz * 2) * 10; + y += Math.sin(nx * 0.7) * 60; + y += Math.cos(nz * 0.7) * 60; + y += Math.sin(nx * 1.5 + nz * 0.3) * 30; + y += Math.cos(nz * 1.5 - nx * 0.4) * 25; + y += Math.sin(nx * 2.5 - nz * 1.8) * 10; return y; } - isWater(x, z, y) { + isWater(y) { return y < this.waterLevel; } update() { - this.offsetX -= 2; - this.offsetZ -= 1; + this.offsetX -= 1.5; + this.offsetZ -= 0.7; this.generateTriangles(); } @@ -190,103 +173,168 @@ const p3 = this.getPoint(x + 1, z + 1); const p4 = this.getPoint(x, z + 1); - const tri1 = new Triangle(p1, p2, p3); - const tri2 = new Triangle(p1, p3, p4); + const isWater1 = this.isWater(p1.y); + const color1 = isWater1 ? { isWater: true, r: 0, g: 0, b: 180 } : { r: 0, g: 180, b: 0 }; + const isWater2 = this.isWater(p2.y); + const color2 = isWater2 ? { isWater: true, r: 0, g: 0, b: 180 } : { r: 0, g: 180, b: 0 }; + const isWater3 = this.isWater(p3.y); + const color3 = isWater3 ? { isWater: true, r: 0, g: 0, b: 180 } : { r: 0, g: 180, b: 0 }; + const isWater4 = this.isWater(p4.y); + const color4 = isWater4 ? { isWater: true, r: 0, g: 0, b: 180 } : { r: 0, g: 180, b: 0 }; - this.triangles.push(tri1); - this.triangles.push(tri2); + this.triangles.push(new TriangleMesh(p1, p2, p3)); + this.triangles.push(new TriangleMesh(p1, p3, p4)); } } + + this.triangles.forEach(t => t.calculateNormal()); } getPoint(x, z) { const worldX = x * this.gridSize + this.offsetX; const worldZ = z * this.gridSize + this.offsetZ; const y = this.generateHeight(worldX, worldZ); - const point = new Point3D(worldX, y, worldZ); - point.isWater = this.isWater(worldX, worldZ, y); - return point; + return new Point3D(worldX, y, worldZ); } - draw() { - this.triangles.forEach(tri => tri.drawFilled()); - this.triangles.forEach(tri => tri.drawWireframe()); + render() { + this.triangles.sort((a, b) => { + const za = (a.p1.z + a.p2.z + a.p3.z) / 3; + const zb = (b.p1.z + b.p2.z + b.p3.z) / 3; + return zb - za; + }); + this.triangles.forEach(tri => tri.draw()); } } class Airplane { constructor() { - this.z = 120; - this.yaw = 0; - this.roll = 0; - this.pitch = 0; - this.wireframe = []; + this.vertices = []; + this.faces = []; this.init(); } init() { - const w = 20, h = 8, d = 30; - const vertices = [ - { x: 0, y: -h, z: 0 }, - { x: w, y: 0, z: d / 2 }, - { x: -w, y: 0, z: d / 2 }, - { x: 0, y: 0, z: -d / 2 }, - { x: w, y: 0, z: -d / 4 }, - { x: -w, y: 0, z: -d / 4 }, - { x: 0, y: h, z: 0 } - ]; + const verts = []; + const faces = []; - const edges = [ - [0, 1], [0, 2], [0, 6], - [1, 2], - [1, 4], [2, 5], - [3, 4], [3, 5], - [4, 5], - [6, 1], [6, 2], [6, 3] - ]; + const fuselageLength = 80; + const fuselageRadius = 8; + const segments = 12; - this.wireframe = { vertices, edges }; + for (let i = 0; i < segments; i++) { + const angle = (i / segments) * Math.PI * 2; + verts.push(new Point3D( + Math.cos(angle) * fuselageRadius, + Math.sin(angle) * fuselageRadius, + 0 + )); + } + + for (let i = 0; i < segments; i++) { + const angle1 = (i / segments) * Math.PI * 2; + const angle2 = ((i + 1) / segments) * Math.PI * 2; + verts.push(new Point3D( + Math.cos(angle1) * fuselageRadius, + Math.sin(angle1) * fuselageRadius, + fuselageLength + )); + verts.push(new Point3D( + Math.cos(angle2) * fuselageRadius, + Math.sin(angle2) * fuselageRadius, + fuselageLength + )); + } + + verts.push(new Point3D(0, 0, fuselageLength + 30)); + + const baseIdx = 0; + const middleStart = segments; + const middleBase = middleStart; + const tipIdx = verts.length - 1; + + for (let i = 0; i < segments; i++) { + const next = (i + 1) % segments; + const middleStart = segments + i * 2; + const middleNext = segments + ((i + 1) % segments) * 2; + + faces.push([baseIdx + i, baseIdx + next, tipIdx]); + faces.push([middleStart, middleNext, middleStart + 1]); + faces.push([middleStart + 1, middleNext, middleNext + 1]); + } + + const wingSpan = 100; + const wingChord = 25; + const wingY = -2; + + const wingBackLeft = verts.length; + verts.push(new Point3D(-wingSpan / 2, wingY, 10)); + verts.push(new Point3D(-wingSpan / 2 + 5, wingY, 35)); + verts.push(new Point3D(wingSpan / 2 - 5, wingY, 35)); + verts.push(new Point3D(wingSpan / 2, wingY, 10)); + + faces.push([wingBackLeft, wingBackLeft + 1, wingBackLeft + 2]); + faces.push([wingBackLeft, wingBackLeft + 2, wingBackLeft + 3]); + + const tailSpan = 40; + const tailChord = 15; + const tailY = 12; + + const tailBottom = verts.length; + verts.push(new Point3D(-tailSpan / 2, tailY, fuselageLength - 30)); + verts.push(new Point3D(-tailSpan / 2 + 3, tailY, fuselageLength - 5)); + verts.push(new Point3D(tailSpan / 2 - 3, tailY, fuselageLength - 5)); + verts.push(new Point3D(tailSpan / 2, tailY, fuselageLength - 30)); + + faces.push([tailBottom, tailBottom + 1, tailBottom + 2]); + faces.push([tailBottom, tailBottom + 2, tailBottom + 3]); + + const vertFinTop = verts.length; + verts.push(new Point3D(0, tailY + 25, fuselageLength - 25)); + verts.push(new Point3D(0, tailY + 25, fuselageLength - 5)); + + faces.push([vertFinTop, vertFinTop + 1, middleStart]); + + this.vertices = verts; + this.faces = faces; } update() { this.yaw = Math.sin(Date.now() / 1000) * 0.15; - this.roll = Math.sin(Date.now() / 600) * 0.3; - this.pitch = Math.cos(Date.now() / 800) * 0.2; + this.roll = Math.sin(Date.now() / 600) * 0.2; + this.pitch = Math.cos(Date.now() / 800) * 0.15; } - draw() { + render() { + const transformedVerts = this.vertices.map(v => { + const rot = rotateY(v, this.yaw); + const cosR = Math.cos(this.roll), sinR = Math.sin(this.roll); + const x1 = rot.x * cosR - rot.y * sinR; + const y1 = rot.x * sinR + rot.y * cosR; + const cosP = Math.cos(this.pitch), sinP = Math.sin(this.pitch); + const y2 = y1 * cosP - rot.z * sinP; + const z2 = y1 * sinP + rot.z * cosP; + return new Point3D(x1, y2, 150 + z2); + }); + ctx.strokeStyle = '#ff0000'; - ctx.lineWidth = 2.5; + ctx.lineWidth = 2; ctx.shadowBlur = 15; ctx.shadowColor = '#ff0000'; - const rotated = this.wireframe.vertices.map(v => { - let x = v.x, y = v.y, z = v.z; + this.faces.forEach((face) => { + const p1 = transformedVerts[face[0]]; + const p2 = transformedVerts[face[1]]; + const p3 = transformedVerts[face[2]]; - const cosY = Math.cos(this.yaw), sinY = Math.sin(this.yaw); - const x1 = x * cosY - z * sinY; - const z1 = x * sinY + z * cosY; - - const cosR = Math.cos(this.roll), sinR = Math.sin(this.roll); - const x2 = x1 * cosR - y * sinR; - const y1 = x1 * sinR + y * cosR; - - const cosP = Math.cos(this.pitch), sinP = Math.sin(this.pitch); - const y2 = y1 * cosP - z1 * sinP; - const z2 = y1 * sinP + z1 * cosP; - - return new Point3D(x2, y2, this.z + z2); + ctx.beginPath(); + ctx.moveTo(p1.project().x, p1.project().y); + ctx.lineTo(p2.project().x, p2.project().y); + ctx.lineTo(p3.project().x, p3.project().y); + ctx.closePath(); + ctx.stroke(); }); - ctx.beginPath(); - this.wireframe.edges.forEach(([a, b]) => { - const p1 = rotated[a].project(); - const p2 = rotated[b].project(); - ctx.moveTo(p1.x, p1.y); - ctx.lineTo(p2.x, p2.y); - }); - ctx.stroke(); - ctx.shadowBlur = 0; } } @@ -299,10 +347,10 @@ ctx.fillRect(0, 0, width, height); landscape.update(); - landscape.draw(); + landscape.render(); airplane.update(); - airplane.draw(); + airplane.render(); requestAnimationFrame(animate); } @@ -310,4 +358,4 @@ animate(); - + \ No newline at end of file