underrate commited on
Commit
e329e5a
·
verified ·
1 Parent(s): 5034610

Refactor code structure for improved readability and maintainability. Changed the plane to a SU-30 fighter jet

Browse files
Files changed (2) hide show
  1. src/Aircraft.js +476 -184
  2. su30-fighter-jet-test.jpg +0 -0
src/Aircraft.js CHANGED
@@ -38,202 +38,521 @@ export class Aircraft {
38
  }
39
 
40
  createAircraftMesh() {
41
- // Ultra-premium stealth sci-fi materials
42
- const hullMat = new THREE.MeshStandardMaterial({
43
- color: 0x11141a,
44
- roughness: 0.35,
45
- metalness: 0.85,
46
  flatShading: true
47
  });
48
- const panelMat = new THREE.MeshStandardMaterial({
49
- color: 0x0a0c10,
50
- roughness: 0.4,
51
- metalness: 0.9,
 
 
 
 
 
 
 
 
 
 
 
 
52
  flatShading: true
53
  });
54
- const glassMat = new THREE.MeshStandardMaterial({
55
- color: 0x000000,
56
- emissive: 0x0088ff,
57
- emissiveIntensity: 0.15,
58
- roughness: 0.1,
59
- metalness: 0.9,
60
  transparent: true,
61
- opacity: 0.85,
 
 
 
 
 
62
  flatShading: true
63
  });
64
- const accentMat = new THREE.MeshStandardMaterial({
65
- color: 0x00e5ff,
66
- emissive: 0x00e5ff,
67
- emissiveIntensity: 1.2,
68
- toneMapped: false
 
 
 
 
 
 
 
69
  });
70
 
71
  this.mesh = new THREE.Group();
72
 
73
- // 1. Fuselage
74
- const fwdFuselage = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.7, 4.5, 6), hullMat);
75
- fwdFuselage.rotation.x = Math.PI / 2;
76
- fwdFuselage.position.set(0, 0.2, 2.25);
77
- fwdFuselage.scale.set(1.1, 0.6, 1);
78
- this.mesh.add(fwdFuselage);
79
 
80
- const aftFuselage = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.4, 4.5, 6), hullMat);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  aftFuselage.rotation.x = Math.PI / 2;
82
- aftFuselage.position.set(0, 0.2, -2.25);
83
- aftFuselage.scale.set(1.1, 0.5, 1);
84
  this.mesh.add(aftFuselage);
85
 
86
- // Smooth belly
87
- const belly = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.2, 7.5), panelMat);
88
- belly.position.set(0, -0.15, -0.2);
89
- this.mesh.add(belly);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- // 2. Canopy
92
- const canopy = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.4, 3.2, 6), glassMat);
93
- canopy.rotation.x = Math.PI / 2 - 0.06; // tilted back slightly
94
- canopy.position.set(0, 0.65, 1.4);
95
- canopy.scale.set(1, 0.7, 1);
96
- this.mesh.add(canopy);
 
97
 
98
- // 3. Main Delta Wings
99
- const makeWing = (dir) => {
100
- const shape = new THREE.Shape();
101
- if (dir === 1) { // Right wing
102
- shape.moveTo(0, 2.0); // root front
103
- shape.lineTo(5.8, -1.0); // tip front
104
- shape.lineTo(5.8, -2.0); // tip back
105
- shape.lineTo(0, -3.5); // root back
106
- } else { // Left wing (reverse winding to avoid internal normals)
107
- shape.moveTo(0, 2.0);
108
- shape.lineTo(0, -3.5);
109
- shape.lineTo(-5.8, -2.0);
110
- shape.lineTo(-5.8, -1.0);
111
- }
112
- const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.1, bevelEnabled: true, bevelThickness: 0.02, bevelSize: 0.02, bevelSegments: 1 });
113
- const mesh = new THREE.Mesh(geo, panelMat);
114
- mesh.rotation.x = Math.PI / 2; // Flat on XZ plane
115
- mesh.position.set(0, 0.05, -0.5); // Shift back
116
- return mesh;
117
- };
118
- this.mesh.add(makeWing(1), makeWing(-1));
119
 
120
- // 4. Canards (Front wings)
121
- const makeCanard = (dir) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  const shape = new THREE.Shape();
 
123
  if (dir === 1) {
124
- shape.moveTo(0, 0.5);
125
- shape.lineTo(1.8, -0.2);
126
- shape.lineTo(1.8, -0.6);
127
- shape.lineTo(0, -0.8);
 
128
  } else {
129
- shape.moveTo(0, 0.5);
130
- shape.lineTo(0, -0.8);
131
- shape.lineTo(-1.8, -0.6);
132
- shape.lineTo(-1.8, -0.2);
 
133
  }
134
- const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.06, bevelEnabled: true, bevelThickness: 0.01, bevelSize: 0.01, bevelSegments: 1 });
135
- const mesh = new THREE.Mesh(geo, hullMat);
 
 
 
 
 
 
136
  mesh.rotation.x = Math.PI / 2;
137
- mesh.position.set(0, 0.15, 3.2); // Near front nose
138
  return mesh;
139
  };
140
- this.mesh.add(makeCanard(1), makeCanard(-1));
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- // 5. V-Tail Stabilizers
143
- const makeTail = (dir) => {
144
- const shape = new THREE.Shape();
145
- shape.moveTo(0.8, 0); // front base
146
- shape.lineTo(-1.2, 1.8); // front tip (swept back)
147
- shape.lineTo(-2.0, 1.8); // back tip
148
- shape.lineTo(-0.8, 0); // back base
 
 
149
 
150
- const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.08, bevelEnabled: true, bevelThickness: 0.02, bevelSize: 0.02, bevelSegments: 1 });
151
- const mesh = new THREE.Mesh(geo, panelMat);
 
 
 
 
 
 
 
 
 
152
 
153
- mesh.rotation.y = -Math.PI / 2; // Extrude along X, place on YZ plane
154
- mesh.rotation.x = dir * 0.35; // Cant outward (V shape)
155
- mesh.position.set(dir * 0.6, 0.4, -2.5); // Over the rear engines
156
- return mesh;
157
- };
158
- this.mesh.add(makeTail(1), makeTail(-1));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
- // 6. Engines & Glow
161
- const createEngine = (dir) => {
162
- // Nacelle container
163
- const engine = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.35, 1.5, 6), panelMat);
164
- engine.rotation.x = Math.PI / 2;
165
- engine.position.set(dir * 0.5, 0.05, -4.2);
166
- this.mesh.add(engine);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
- // Core inner glow
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  const glowMat = new THREE.MeshStandardMaterial({
170
- color: 0x00ffff, emissive: 0x00e5ff, emissiveIntensity: 1.5,
171
- transparent: true, opacity: 0.8, toneMapped: false
 
 
 
 
172
  });
173
- this.engineGlowMaterials.push(glowMat); // Captured for thrust animation
174
 
175
- const glow = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.2, 6), glowMat);
 
 
 
176
  glow.rotation.x = Math.PI / 2;
177
- glow.position.set(dir * 0.5, 0.05, -4.96);
178
  this.mesh.add(glow);
179
 
180
- // Exhaust Thruster cone
181
- const trailMat = new THREE.MeshBasicMaterial({
182
- color: 0x00e5ff, transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending, depthWrite: false
 
 
 
 
183
  });
184
- const trail = new THREE.Mesh(new THREE.ConeGeometry(0.2, 3.0, 6, 1, true), trailMat);
185
- // Map +Y to -Z so tip points fully backwards and wide base is at engine
186
- trail.rotation.x = Math.PI / 2;
187
- trail.position.set(dir * 0.5, 0.05, -6.4); // Centered down the trail length
188
- this.exhaustTrails.push(trail);
189
- this.mesh.add(trail);
190
- };
191
- createEngine(1);
192
- createEngine(-1);
193
-
194
- // 7. Neon Accents
195
- const spineGeo = new THREE.BoxGeometry(0.04, 0.02, 4.0);
196
- const spineAccent = new THREE.Mesh(spineGeo, accentMat);
197
- spineAccent.position.set(0, 0.48, -1.0);
198
- this.mesh.add(spineAccent);
199
-
200
- const wingAccentGeo = new THREE.BoxGeometry(4.0, 0.02, 0.05);
201
- const wL = new THREE.Mesh(wingAccentGeo, accentMat);
202
- wL.position.set(-2.5, 0.12, -2.0);
203
- wL.rotation.y = Math.PI / 8;
204
- const wR = new THREE.Mesh(wingAccentGeo, accentMat);
205
- wR.position.set(2.5, 0.12, -2.0);
206
- wR.rotation.y = -Math.PI / 8;
207
- this.mesh.add(wL, wR);
208
-
209
- // 8. Navigation Lights
 
 
 
 
210
  const redLightMat = new THREE.MeshStandardMaterial({
211
- color: 0xff3b3b, emissive: 0xff3b3b, emissiveIntensity: 1.5, roughness: 0.2
 
 
 
212
  });
213
  const greenLightMat = new THREE.MeshStandardMaterial({
214
- color: 0x3bff68, emissive: 0x3bff68, emissiveIntensity: 1.5, roughness: 0.2
 
 
 
215
  });
216
- const strobeLightMat = new THREE.MeshStandardMaterial({
217
- color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.5, roughness: 0.2
 
 
 
218
  });
219
 
220
- const leftWingLight = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), redLightMat);
221
- leftWingLight.position.set(-5.9, 0.1, -1.6);
 
222
  this.mesh.add(leftWingLight);
223
 
224
- const rightWingLight = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), greenLightMat);
225
- rightWingLight.position.set(5.9, 0.1, -1.6);
226
  this.mesh.add(rightWingLight);
227
 
228
- const tailStrobe = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), strobeLightMat);
229
- tailStrobe.position.set(0, -0.2, -4.0);
230
- this.mesh.add(tailStrobe);
 
 
 
 
 
 
 
 
231
 
232
  this.navLights.push({ material: redLightMat, mode: 'steady', base: 1.2 });
233
  this.navLights.push({ material: greenLightMat, mode: 'steady', base: 1.2 });
234
- this.navLights.push({ material: strobeLightMat, mode: 'strobe', base: 0.2 });
235
 
236
- // Enable shadows for solid pieces
237
  this.mesh.traverse((child) => {
238
  if (child.isMesh && (!child.material || !child.material.transparent)) {
239
  child.castShadow = true;
@@ -247,28 +566,6 @@ export class Aircraft {
247
  setupControls() {
248
  document.addEventListener('keydown', (event) => this.handleKeyDown(event));
249
  document.addEventListener('keyup', (event) => this.handleKeyUp(event));
250
-
251
- // Set up mouse flight controls
252
- document.addEventListener('mousemove', (event) => this.handleMouseMove(event));
253
-
254
- // Request pointer lock on click to capture mouse movement infinitely
255
- document.addEventListener('click', () => {
256
- if (document.pointerLockElement !== document.body) {
257
- document.body.requestPointerLock();
258
- }
259
- });
260
- }
261
-
262
- handleMouseMove(event) {
263
- // Only apply mouse controls if pointer is locked
264
- if (document.pointerLockElement === document.body) {
265
- // Accumulate mouse delta into target inputs (very low sensitivity)
266
- this._pitchInput += -event.movementY * 0.002;
267
- this._rollInput += event.movementX * 0.002;
268
- // Clamp so you can't overload the buffer
269
- this._pitchInput = THREE.MathUtils.clamp(this._pitchInput, -1, 1);
270
- this._rollInput = THREE.MathUtils.clamp(this._rollInput, -1, 1);
271
- }
272
  }
273
 
274
  handleKeyDown(event) {
@@ -285,8 +582,12 @@ export class Aircraft {
285
  return;
286
  }
287
  switch (event.key.toLowerCase()) {
288
- case 'a': this.yaw = 1; break; // Yaw left (turn left)
289
- case 'd': this.yaw = -1; break; // Yaw right (turn right)
 
 
 
 
290
  }
291
  }
292
 
@@ -304,7 +605,9 @@ export class Aircraft {
304
  return;
305
  }
306
  switch (event.key.toLowerCase()) {
307
- case 'a': case 'd': this.yaw = 0; break;
 
 
308
  }
309
  }
310
 
@@ -317,20 +620,9 @@ export class Aircraft {
317
  this.cruiseThrust = THREE.MathUtils.clamp(this.cruiseThrust - 0.4 * deltaTime, 0, 1);
318
  }
319
 
320
- // Initialize smoothed input buffers
321
- if (this._pitchInput === undefined) { this._pitchInput = 0; this._rollInput = 0; }
322
-
323
  const speed = Math.max(this.velocity.length(), 0.01);
324
  const speedRatio = THREE.MathUtils.clamp(speed / 80, 0.35, 1.6);
325
 
326
- // ── Smooth input: lerp actual control values toward buffered inputs ──
327
- this.pitch = THREE.MathUtils.lerp(this.pitch, this._pitchInput, deltaTime * 6);
328
- this.roll = THREE.MathUtils.lerp(this.roll, this._rollInput, deltaTime * 6);
329
-
330
- // Decay the input buffer back toward zero (spring-return joystick feel)
331
- this._pitchInput = THREE.MathUtils.lerp(this._pitchInput, 0, deltaTime * 3);
332
- this._rollInput = THREE.MathUtils.lerp(this._rollInput, 0, deltaTime * 3);
333
-
334
  // ── Apply rotation rates using YXZ Euler order ──
335
  this.rotation.order = 'YXZ';
336
  this.rotation.x += this.pitch * this.pitchRate * deltaTime;
@@ -344,11 +636,11 @@ export class Aircraft {
344
  this.rotation.x = THREE.MathUtils.clamp(this.rotation.x, -1.05, 1.05);
345
  this.rotation.z = THREE.MathUtils.clamp(this.rotation.z, -1.2, 1.2);
346
 
347
- // Auto-leveling: gently stabilise when input is near zero
348
- if (Math.abs(this.pitch) < 0.03 && Math.abs(this._pitchInput) < 0.03) {
349
  this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, 0, deltaTime * 0.4);
350
  }
351
- if (Math.abs(this.roll) < 0.03 && Math.abs(this._rollInput) < 0.03) {
352
  this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, 0, deltaTime * 1.0);
353
  }
354
 
 
38
  }
39
 
40
  createAircraftMesh() {
41
+ // Su-30 Fighter Jet Materials - Russian Air Force blue-gray camouflage
42
+ const mainBodyMat = new THREE.MeshStandardMaterial({
43
+ color: 0x4a5868, // Blue-gray base (Russian camouflage)
44
+ roughness: 0.5,
45
+ metalness: 0.45,
46
  flatShading: true
47
  });
48
+ const darkPanelMat = new THREE.MeshStandardMaterial({
49
+ color: 0x2a3540, // Darker blue-gray panels
50
+ roughness: 0.55,
51
+ metalness: 0.5,
52
+ flatShading: true
53
+ });
54
+ const lightPanelMat = new THREE.MeshStandardMaterial({
55
+ color: 0x6a7585, // Lighter blue-gray panels
56
+ roughness: 0.45,
57
+ metalness: 0.4,
58
+ flatShading: true
59
+ });
60
+ const noseConeMat = new THREE.MeshStandardMaterial({
61
+ color: 0x1a1a1a, // Dark radar nose cone
62
+ roughness: 0.3,
63
+ metalness: 0.2,
64
  flatShading: true
65
  });
66
+ const canopyGlassMat = new THREE.MeshStandardMaterial({
67
+ color: 0x0a1520,
68
+ emissive: 0x1a3040,
69
+ emissiveIntensity: 0.12,
70
+ roughness: 0.05,
71
+ metalness: 0.95,
72
  transparent: true,
73
+ opacity: 0.75
74
+ });
75
+ const canopyFrameMat = new THREE.MeshStandardMaterial({
76
+ color: 0x1a1a1a,
77
+ roughness: 0.4,
78
+ metalness: 0.6,
79
  flatShading: true
80
  });
81
+ const exhaustNozzleMat = new THREE.MeshStandardMaterial({
82
+ color: 0x2a2825,
83
+ roughness: 0.7,
84
+ metalness: 0.3,
85
+ flatShading: true
86
+ });
87
+ // Camouflage secondary color for upper surfaces
88
+ const camoDarkMat = new THREE.MeshStandardMaterial({
89
+ color: 0x354555, // Darker camo patches
90
+ roughness: 0.5,
91
+ metalness: 0.45,
92
+ flatShading: true
93
  });
94
 
95
  this.mesh = new THREE.Group();
96
 
97
+ // Helper function to create symmetrical parts
98
+ const addPair = (factory) => {
99
+ this.mesh.add(factory(1));
100
+ this.mesh.add(factory(-1));
101
+ };
 
102
 
103
+ // ==================== FUSELAGE ====================
104
+ // Nose cone (radome) - pointed conical shape
105
+ const noseCone = new THREE.Mesh(new THREE.ConeGeometry(0.45, 2.2, 8), noseConeMat);
106
+ noseCone.rotation.x = Math.PI / 2;
107
+ noseCone.position.set(0, 0.05, 5.1);
108
+ this.mesh.add(noseCone);
109
+
110
+ // Forward fuselage - contains tandem cockpit
111
+ const forwardFuselage = new THREE.Mesh(
112
+ new THREE.CylinderGeometry(0.55, 0.72, 3.8, 8),
113
+ mainBodyMat
114
+ );
115
+ forwardFuselage.rotation.x = Math.PI / 2;
116
+ forwardFuselage.position.set(0, 0.08, 3.0);
117
+ forwardFuselage.scale.set(1.0, 0.75, 1.0);
118
+ this.mesh.add(forwardFuselage);
119
+
120
+ // Center fuselage - wider, contains intakes
121
+ const centerFuselage = new THREE.Mesh(
122
+ new THREE.CylinderGeometry(0.85, 0.78, 3.0, 8),
123
+ mainBodyMat
124
+ );
125
+ centerFuselage.rotation.x = Math.PI / 2;
126
+ centerFuselage.position.set(0, 0.0, 0.3);
127
+ centerFuselage.scale.set(1.0, 0.7, 1.0);
128
+ this.mesh.add(centerFuselage);
129
+
130
+ // Aft fuselage - narrowing toward engines
131
+ const aftFuselage = new THREE.Mesh(
132
+ new THREE.CylinderGeometry(0.78, 0.55, 3.0, 8),
133
+ mainBodyMat
134
+ );
135
  aftFuselage.rotation.x = Math.PI / 2;
136
+ aftFuselage.position.set(0, 0.0, -2.0);
137
+ aftFuselage.scale.set(1.0, 0.65, 1.0);
138
  this.mesh.add(aftFuselage);
139
 
140
+ // Engine nacelle area (between engines)
141
+ const tailBoom = new THREE.Mesh(
142
+ new THREE.BoxGeometry(0.6, 0.45, 2.5),
143
+ darkPanelMat
144
+ );
145
+ tailBoom.position.set(0, 0.1, -3.8);
146
+ this.mesh.add(tailBoom);
147
+
148
+ // Belly spine
149
+ const bellySpine = new THREE.Mesh(
150
+ new THREE.BoxGeometry(0.5, 0.25, 6.0),
151
+ darkPanelMat
152
+ );
153
+ bellySpine.position.set(0, -0.38, 0.3);
154
+ this.mesh.add(bellySpine);
155
+
156
+ // ==================== CANOPY (Tandem Two-Seat) ====================
157
+ // Front canopy (pilot)
158
+ const frontCanopy = new THREE.Mesh(
159
+ new THREE.SphereGeometry(0.48, 12, 10),
160
+ canopyGlassMat
161
+ );
162
+ frontCanopy.scale.set(1.0, 0.55, 1.2);
163
+ frontCanopy.position.set(0, 0.42, 3.4);
164
+ this.mesh.add(frontCanopy);
165
+
166
+ // Rear canopy (WSO - Weapons Systems Officer)
167
+ const rearCanopy = new THREE.Mesh(
168
+ new THREE.SphereGeometry(0.48, 12, 10),
169
+ canopyGlassMat
170
+ );
171
+ rearCanopy.scale.set(1.0, 0.5, 1.1);
172
+ rearCanopy.position.set(0, 0.42, 2.3);
173
+ this.mesh.add(rearCanopy);
174
+
175
+ // Canopy frames
176
+ const frontFrame = new THREE.Mesh(
177
+ new THREE.BoxGeometry(0.06, 0.06, 0.9),
178
+ canopyFrameMat
179
+ );
180
+ frontFrame.position.set(0, 0.52, 3.4);
181
+ this.mesh.add(frontFrame);
182
+
183
+ const rearFrame = new THREE.Mesh(
184
+ new THREE.BoxGeometry(0.06, 0.06, 0.85),
185
+ canopyFrameMat
186
+ );
187
+ rearFrame.position.set(0, 0.50, 2.3);
188
+ this.mesh.add(rearFrame);
189
 
190
+ // Divider frame between front and rear canopies
191
+ const dividerFrame = new THREE.Mesh(
192
+ new THREE.BoxGeometry(0.7, 0.04, 0.06),
193
+ canopyFrameMat
194
+ );
195
+ dividerFrame.position.set(0, 0.46, 2.85);
196
+ this.mesh.add(dividerFrame);
197
 
198
+ // Dorsal spine hump behind cockpit (houses avionics for two-seat config)
199
+ const dorsalHump = new THREE.Mesh(
200
+ new THREE.BoxGeometry(0.55, 0.18, 1.2),
201
+ mainBodyMat
202
+ );
203
+ dorsalHump.position.set(0, 0.32, 1.5);
204
+ this.mesh.add(dorsalHump);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
+ // Dorsal airbrake (behind the hump)
207
+ const dorsalAirbrake = new THREE.Mesh(
208
+ new THREE.BoxGeometry(0.45, 0.4, 0.08),
209
+ darkPanelMat
210
+ );
211
+ dorsalAirbrake.position.set(0, 0.45, 0.8);
212
+ dorsalAirbrake.rotation.x = -0.15;
213
+ this.mesh.add(dorsalAirbrake);
214
+
215
+ // Canopy rear bulkhead
216
+ const canopyRearFrame = new THREE.Mesh(
217
+ new THREE.BoxGeometry(0.65, 0.05, 0.06),
218
+ canopyFrameMat
219
+ );
220
+ canopyRearFrame.position.set(0, 0.44, 1.8);
221
+ this.mesh.add(canopyRearFrame);
222
+
223
+ // ==================== IRST (Infra-Red Search and Track) ====================
224
+ // Distinctive ball in front of the windshield - Su-30 feature
225
+ const irstBall = new THREE.Mesh(
226
+ new THREE.SphereGeometry(0.12, 12, 12),
227
+ new THREE.MeshStandardMaterial({
228
+ color: 0x1a1a1a,
229
+ roughness: 0.2,
230
+ metalness: 0.1
231
+ })
232
+ );
233
+ irstBall.position.set(0, 0.35, 4.2);
234
+ this.mesh.add(irstBall);
235
+
236
+ // IRST housing
237
+ const irstHousing = new THREE.Mesh(
238
+ new THREE.CylinderGeometry(0.08, 0.12, 0.15, 8),
239
+ mainBodyMat
240
+ );
241
+ irstHousing.rotation.x = Math.PI / 2;
242
+ irstHousing.position.set(0, 0.28, 4.15);
243
+ this.mesh.add(irstHousing);
244
+
245
+ // ==================== LERX (Leading Edge Root Extensions) ====================
246
+ addPair((dir) => {
247
+ const lerx = new THREE.Mesh(
248
+ new THREE.BoxGeometry(2.2, 0.08, 1.8),
249
+ mainBodyMat
250
+ );
251
+ lerx.position.set(dir * 1.1, 0.0, 1.8);
252
+ lerx.rotation.y = dir * 0.35;
253
+ return lerx;
254
+ });
255
+
256
+ // ==================== MAIN DELTA WINGS ====================
257
+ const createWing = (dir) => {
258
  const shape = new THREE.Shape();
259
+ // Delta wing planform - typical Su-27/J-16 shape
260
  if (dir === 1) {
261
+ shape.moveTo(0.6, 2.2); // root leading edge
262
+ shape.lineTo(6.5, -0.5); // wing tip leading edge
263
+ shape.lineTo(6.5, -1.3); // wing tip trailing edge
264
+ shape.lineTo(1.2, -3.2); // wing root trailing edge (flaperon)
265
+ shape.lineTo(0.6, 2.2); // back to start
266
  } else {
267
+ shape.moveTo(-0.6, 2.2);
268
+ shape.lineTo(-1.2, -3.2);
269
+ shape.lineTo(-6.5, -1.3);
270
+ shape.lineTo(-6.5, -0.5);
271
+ shape.lineTo(-0.6, 2.2);
272
  }
273
+ const geo = new THREE.ExtrudeGeometry(shape, {
274
+ depth: 0.12,
275
+ bevelEnabled: true,
276
+ bevelThickness: 0.02,
277
+ bevelSize: 0.02,
278
+ bevelSegments: 1
279
+ });
280
+ const mesh = new THREE.Mesh(geo, mainBodyMat);
281
  mesh.rotation.x = Math.PI / 2;
282
+ mesh.position.set(0, 0.02, 0.3);
283
  return mesh;
284
  };
285
+ this.mesh.add(createWing(1));
286
+ this.mesh.add(createWing(-1));
287
+
288
+ // Upper wing camouflage patches
289
+ addPair((dir) => {
290
+ const camoPatch = new THREE.Mesh(
291
+ new THREE.BoxGeometry(2.5, 0.02, 1.5),
292
+ camoDarkMat
293
+ );
294
+ camoPatch.position.set(dir * 3.0, 0.14, -0.8);
295
+ camoPatch.rotation.y = dir * 0.15;
296
+ return camoPatch;
297
+ });
298
 
299
+ addPair((dir) => {
300
+ const camoPatch2 = new THREE.Mesh(
301
+ new THREE.BoxGeometry(1.8, 0.02, 1.2),
302
+ camoDarkMat
303
+ );
304
+ camoPatch2.position.set(dir * 4.5, 0.14, -0.3);
305
+ camoPatch2.rotation.y = dir * 0.1;
306
+ return camoPatch2;
307
+ });
308
 
309
+ // Wing fuel tanks / pods
310
+ addPair((dir) => {
311
+ const tank = new THREE.Mesh(
312
+ new THREE.CapsuleGeometry(0.18, 2.0, 6, 8),
313
+ lightPanelMat
314
+ );
315
+ tank.rotation.x = Math.PI / 2;
316
+ tank.rotation.z = dir * 0.1;
317
+ tank.position.set(dir * 4.5, -0.15, -0.8);
318
+ return tank;
319
+ });
320
 
321
+ // ==================== CANARDS (Front control surfaces) ====================
322
+ addPair((dir) => {
323
+ const canardShape = new THREE.Shape();
324
+ if (dir === 1) {
325
+ canardShape.moveTo(0, 0.6);
326
+ canardShape.lineTo(1.6, 0.1);
327
+ canardShape.lineTo(1.5, -0.3);
328
+ canardShape.lineTo(0, -0.5);
329
+ } else {
330
+ canardShape.moveTo(0, 0.6);
331
+ canardShape.lineTo(0, -0.5);
332
+ canardShape.lineTo(-1.5, -0.3);
333
+ canardShape.lineTo(-1.6, 0.1);
334
+ }
335
+ const canardGeo = new THREE.ExtrudeGeometry(canardShape, {
336
+ depth: 0.06,
337
+ bevelEnabled: true,
338
+ bevelThickness: 0.01,
339
+ bevelSize: 0.01,
340
+ bevelSegments: 1
341
+ });
342
+ const canard = new THREE.Mesh(canardGeo, mainBodyMat);
343
+ canard.rotation.x = Math.PI / 2;
344
+ canard.position.set(0, 0.15, 4.2);
345
+ return canard;
346
+ });
347
 
348
+ // ==================== AIR INTAKES ====================
349
+ addPair((dir) => {
350
+ // Main intake structure
351
+ const intake = new THREE.Mesh(
352
+ new THREE.BoxGeometry(0.65, 0.55, 2.8),
353
+ darkPanelMat
354
+ );
355
+ intake.position.set(dir * 1.05, -0.15, 1.2);
356
+ this.mesh.add(intake);
357
+
358
+ // Intake lip
359
+ const intakeLip = new THREE.Mesh(
360
+ new THREE.BoxGeometry(0.72, 0.62, 0.15),
361
+ lightPanelMat
362
+ );
363
+ intakeLip.position.set(dir * 1.05, -0.15, 2.5);
364
+ this.mesh.add(intakeLip);
365
+
366
+ // Intake splitter plate
367
+ const splitter = new THREE.Mesh(
368
+ new THREE.BoxGeometry(0.08, 0.48, 2.0),
369
+ mainBodyMat
370
+ );
371
+ splitter.position.set(dir * 0.72, -0.12, 1.8);
372
+ return splitter;
373
+ });
374
+
375
+ // ==================== TWIN VERTICAL STABILIZERS ====================
376
+ addPair((dir) => {
377
+ // Vertical stabilizer (tail fin)
378
+ const tailShape = new THREE.Shape();
379
+ tailShape.moveTo(0, 0); // leading edge bottom
380
+ tailShape.lineTo(-0.8, 1.9); // leading edge top (swept)
381
+ tailShape.lineTo(-1.6, 1.85); // trailing edge top
382
+ tailShape.lineTo(-1.2, 0); // trailing edge bottom
383
+
384
+ const tailGeo = new THREE.ExtrudeGeometry(tailShape, {
385
+ depth: 0.08,
386
+ bevelEnabled: true,
387
+ bevelThickness: 0.015,
388
+ bevelSize: 0.015,
389
+ bevelSegments: 1
390
+ });
391
+ const tail = new THREE.Mesh(tailGeo, mainBodyMat);
392
+ tail.rotation.y = Math.PI / 2;
393
+ tail.rotation.x = dir * 0.12; // Slight outward cant
394
+ tail.position.set(dir * 0.95, 0.2, -4.2);
395
+ this.mesh.add(tail);
396
+
397
+ // Rudder
398
+ const rudder = new THREE.Mesh(
399
+ new THREE.BoxGeometry(0.05, 1.4, 0.4),
400
+ darkPanelMat
401
+ );
402
+ rudder.position.set(dir * 1.45, 1.0, -4.8);
403
+ rudder.rotation.x = dir * 0.12;
404
+ return rudder;
405
+ });
406
+
407
+ // ==================== HORIZONTAL STABILIZERS ====================
408
+ addPair((dir) => {
409
+ const stabShape = new THREE.Shape();
410
+ if (dir === 1) {
411
+ stabShape.moveTo(0, 0.5);
412
+ stabShape.lineTo(2.2, 0.15);
413
+ stabShape.lineTo(2.0, -0.25);
414
+ stabShape.lineTo(0, -0.6);
415
+ } else {
416
+ stabShape.moveTo(0, 0.5);
417
+ stabShape.lineTo(0, -0.6);
418
+ stabShape.lineTo(-2.0, -0.25);
419
+ stabShape.lineTo(-2.2, 0.15);
420
+ }
421
+ const stabGeo = new THREE.ExtrudeGeometry(stabShape, {
422
+ depth: 0.06,
423
+ bevelEnabled: true,
424
+ bevelThickness: 0.01,
425
+ bevelSize: 0.01,
426
+ bevelSegments: 1
427
+ });
428
+ const stabilizer = new THREE.Mesh(stabGeo, mainBodyMat);
429
+ stabilizer.rotation.x = Math.PI / 2;
430
+ stabilizer.position.set(0, 0.15, -4.5);
431
+ return stabilizer;
432
+ });
433
 
434
+ // ==================== TWIN ENGINES ====================
435
+ addPair((dir) => {
436
+ // Engine nacelle
437
+ const nacelle = new THREE.Mesh(
438
+ new THREE.CylinderGeometry(0.38, 0.42, 2.2, 10),
439
+ mainBodyMat
440
+ );
441
+ nacelle.rotation.x = Math.PI / 2;
442
+ nacelle.position.set(dir * 0.65, -0.08, -4.0);
443
+ this.mesh.add(nacelle);
444
+
445
+ // Exhaust nozzle
446
+ const nozzle = new THREE.Mesh(
447
+ new THREE.CylinderGeometry(0.32, 0.28, 0.5, 12),
448
+ exhaustNozzleMat
449
+ );
450
+ nozzle.rotation.x = Math.PI / 2;
451
+ nozzle.position.set(dir * 0.65, -0.08, -5.15);
452
+ this.mesh.add(nozzle);
453
+
454
+ // Engine glow
455
  const glowMat = new THREE.MeshStandardMaterial({
456
+ color: 0xff6633,
457
+ emissive: 0xff4411,
458
+ emissiveIntensity: 1.0,
459
+ transparent: true,
460
+ opacity: 0.85,
461
+ toneMapped: false
462
  });
463
+ this.engineGlowMaterials.push(glowMat);
464
 
465
+ const glow = new THREE.Mesh(
466
+ new THREE.CylinderGeometry(0.24, 0.26, 0.15, 12),
467
+ glowMat
468
+ );
469
  glow.rotation.x = Math.PI / 2;
470
+ glow.position.set(dir * 0.65, -0.08, -5.38);
471
  this.mesh.add(glow);
472
 
473
+ // Afterburner flame
474
+ const flameMat = new THREE.MeshBasicMaterial({
475
+ color: 0xff8844,
476
+ transparent: true,
477
+ opacity: 0.45,
478
+ blending: THREE.AdditiveBlending,
479
+ depthWrite: false
480
  });
481
+ const flame = new THREE.Mesh(
482
+ new THREE.ConeGeometry(0.22, 2.5, 10, 1, true),
483
+ flameMat
484
+ );
485
+ flame.rotation.x = Math.PI / 2;
486
+ flame.position.set(dir * 0.65, -0.08, -6.6);
487
+ this.exhaustTrails.push(flame);
488
+ this.mesh.add(flame);
489
+ return flame; // Return to satisfy addPair expectation
490
+ });
491
+
492
+ // ==================== DETAILS & ANTENNAS ====================
493
+ // IFR (In-flight refueling) probe
494
+ const ifrProbe = new THREE.Mesh(
495
+ new THREE.CylinderGeometry(0.02, 0.025, 0.8, 6),
496
+ darkPanelMat
497
+ );
498
+ ifrProbe.rotation.x = Math.PI / 2;
499
+ ifrProbe.position.set(0, 0.35, 4.8);
500
+ this.mesh.add(ifrProbe);
501
+
502
+ // Dorsal antenna
503
+ const dorsalAntenna = new THREE.Mesh(
504
+ new THREE.BoxGeometry(0.08, 0.15, 0.8),
505
+ darkPanelMat
506
+ );
507
+ dorsalAntenna.position.set(0, 0.52, -0.5);
508
+ this.mesh.add(dorsalAntenna);
509
+
510
+ // ==================== NAVIGATION LIGHTS ====================
511
  const redLightMat = new THREE.MeshStandardMaterial({
512
+ color: 0xff2222,
513
+ emissive: 0xff0000,
514
+ emissiveIntensity: 1.5,
515
+ roughness: 0.2
516
  });
517
  const greenLightMat = new THREE.MeshStandardMaterial({
518
+ color: 0x22ff44,
519
+ emissive: 0x00ff22,
520
+ emissiveIntensity: 1.5,
521
+ roughness: 0.2
522
  });
523
+ const whiteLightMat = new THREE.MeshStandardMaterial({
524
+ color: 0xffffff,
525
+ emissive: 0xffffff,
526
+ emissiveIntensity: 0.8,
527
+ roughness: 0.2
528
  });
529
 
530
+ // Port (red) and Starboard (green) lights on wing tips
531
+ const leftWingLight = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 8), redLightMat);
532
+ leftWingLight.position.set(-6.4, 0.08, -0.9);
533
  this.mesh.add(leftWingLight);
534
 
535
+ const rightWingLight = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 8), greenLightMat);
536
+ rightWingLight.position.set(6.4, 0.08, -0.9);
537
  this.mesh.add(rightWingLight);
538
 
539
+ // Tail lights (white)
540
+ addPair((dir) => {
541
+ const tailLight = new THREE.Mesh(new THREE.SphereGeometry(0.05, 8, 8), whiteLightMat);
542
+ tailLight.position.set(dir * 1.0, 1.9, -4.8);
543
+ return tailLight;
544
+ });
545
+
546
+ // Belly strobe
547
+ const bellyStrobe = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 8), whiteLightMat);
548
+ bellyStrobe.position.set(0, -0.42, -1.0);
549
+ this.mesh.add(bellyStrobe);
550
 
551
  this.navLights.push({ material: redLightMat, mode: 'steady', base: 1.2 });
552
  this.navLights.push({ material: greenLightMat, mode: 'steady', base: 1.2 });
553
+ this.navLights.push({ material: whiteLightMat, mode: 'strobe', base: 0.3 });
554
 
555
+ // ==================== SHADOWS ====================
556
  this.mesh.traverse((child) => {
557
  if (child.isMesh && (!child.material || !child.material.transparent)) {
558
  child.castShadow = true;
 
566
  setupControls() {
567
  document.addEventListener('keydown', (event) => this.handleKeyDown(event));
568
  document.addEventListener('keyup', (event) => this.handleKeyUp(event));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  }
570
 
571
  handleKeyDown(event) {
 
582
  return;
583
  }
584
  switch (event.key.toLowerCase()) {
585
+ case 'w': this.pitch = -1; break; // Pitch up (nose rises)
586
+ case 's': this.pitch = 1; break; // Pitch down (nose drops)
587
+ case 'a': this.roll = -1; break; // Roll left (left wing down)
588
+ case 'd': this.roll = 1; break; // Roll right (right wing down)
589
+ case 'q': this.yaw = 1; break; // Yaw left (turn left)
590
+ case 'e': this.yaw = -1; break; // Yaw right (turn right)
591
  }
592
  }
593
 
 
605
  return;
606
  }
607
  switch (event.key.toLowerCase()) {
608
+ case 'w': case 's': this.pitch = 0; break;
609
+ case 'a': case 'd': this.roll = 0; break;
610
+ case 'q': case 'e': this.yaw = 0; break;
611
  }
612
  }
613
 
 
620
  this.cruiseThrust = THREE.MathUtils.clamp(this.cruiseThrust - 0.4 * deltaTime, 0, 1);
621
  }
622
 
 
 
 
623
  const speed = Math.max(this.velocity.length(), 0.01);
624
  const speedRatio = THREE.MathUtils.clamp(speed / 80, 0.35, 1.6);
625
 
 
 
 
 
 
 
 
 
626
  // ── Apply rotation rates using YXZ Euler order ──
627
  this.rotation.order = 'YXZ';
628
  this.rotation.x += this.pitch * this.pitchRate * deltaTime;
 
636
  this.rotation.x = THREE.MathUtils.clamp(this.rotation.x, -1.05, 1.05);
637
  this.rotation.z = THREE.MathUtils.clamp(this.rotation.z, -1.2, 1.2);
638
 
639
+ // Auto-leveling: gently stabilise when no input
640
+ if (this.pitch === 0) {
641
  this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, 0, deltaTime * 0.4);
642
  }
643
+ if (this.roll === 0) {
644
  this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, 0, deltaTime * 1.0);
645
  }
646
 
su30-fighter-jet-test.jpg ADDED