添加风机
This commit is contained in:
18
public/models/README.md
Normal file
18
public/models/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
将你选中的海上风机资产导出为 GLB 文件,并放到下面这个路径:
|
||||||
|
|
||||||
|
`public/models/offshore-wind-turbine.glb`
|
||||||
|
|
||||||
|
当前代码会优先加载这个文件:
|
||||||
|
|
||||||
|
- 存在该文件:使用真实 GLB 风机资产
|
||||||
|
- 文件不存在或加载失败:自动回退到程序化风机占位模型
|
||||||
|
|
||||||
|
建议优先选择包含完整塔架、机舱、叶片的海上风机模型。
|
||||||
|
|
||||||
|
当前选定来源:
|
||||||
|
|
||||||
|
- Sketchfab: Generic Wind Turbine (V136 125.5h 145d)
|
||||||
|
- 链接: https://sketchfab.com/3d-models/generic-wind-turbine-v136-1255h-145d-90ad27be20c541d1a0e4818d4e501679
|
||||||
|
- 发布时间: 2020-06-11
|
||||||
|
- 许可: CC Attribution
|
||||||
|
- 规模: 约 2.1k triangles
|
||||||
BIN
public/models/offshore-wind-turbine.glb
Normal file
BIN
public/models/offshore-wind-turbine.glb
Normal file
Binary file not shown.
@@ -10,6 +10,7 @@ import Stats from 'three/addons/libs/stats.module.js';
|
|||||||
import { TerrainGenerator } from './TerrainGenerator.js';
|
import { TerrainGenerator } from './TerrainGenerator.js';
|
||||||
import { VegetationSystem } from './VegetationSystem.js';
|
import { VegetationSystem } from './VegetationSystem.js';
|
||||||
import { DEFAULT_SCENE_PARAMS } from './weatherPresets.js';
|
import { DEFAULT_SCENE_PARAMS } from './weatherPresets.js';
|
||||||
|
import { OffshoreWindTurbineAsset } from './objects/OffshoreWindTurbineAsset.js';
|
||||||
|
|
||||||
const RAIN_AUDIO_URL = '/audio/rain-calming.mp3';
|
const RAIN_AUDIO_URL = '/audio/rain-calming.mp3';
|
||||||
const THUNDER_AUDIO_URL = '/audio/thunder-distant.mp3';
|
const THUNDER_AUDIO_URL = '/audio/thunder-distant.mp3';
|
||||||
@@ -25,8 +26,10 @@ export class OceanScene {
|
|||||||
this.sky = null;
|
this.sky = null;
|
||||||
this.starField = null;
|
this.starField = null;
|
||||||
this.moonSprite = null;
|
this.moonSprite = null;
|
||||||
|
this.moonGlowSprite = null;
|
||||||
this.galaxyBand = null;
|
this.galaxyBand = null;
|
||||||
this.sun = new THREE.Vector3();
|
this.sun = new THREE.Vector3();
|
||||||
|
this.initialSun = new THREE.Vector3();
|
||||||
this.terrain = null;
|
this.terrain = null;
|
||||||
this.vegetation = null;
|
this.vegetation = null;
|
||||||
this.vegetationSystem = null;
|
this.vegetationSystem = null;
|
||||||
@@ -50,6 +53,7 @@ export class OceanScene {
|
|||||||
this.fogLayers = [];
|
this.fogLayers = [];
|
||||||
this.horizonFog = null;
|
this.horizonFog = null;
|
||||||
this.skyHazeBand = null;
|
this.skyHazeBand = null;
|
||||||
|
this.windTurbine = null;
|
||||||
this.rainAudioPool = [];
|
this.rainAudioPool = [];
|
||||||
this.rainAudioActiveIndex = 0;
|
this.rainAudioActiveIndex = 0;
|
||||||
this.rainAudioIsPlaying = false;
|
this.rainAudioIsPlaying = false;
|
||||||
@@ -85,6 +89,7 @@ export class OceanScene {
|
|||||||
this.initFog();
|
this.initFog();
|
||||||
await this.initWater();
|
await this.initWater();
|
||||||
await this.initTerrain();
|
await this.initTerrain();
|
||||||
|
await this.initWindTurbine();
|
||||||
await this.initVegetation();
|
await this.initVegetation();
|
||||||
this.initSunPosition();
|
this.initSunPosition();
|
||||||
this.initEventListeners();
|
this.initEventListeners();
|
||||||
@@ -645,6 +650,20 @@ export class OceanScene {
|
|||||||
this.moonSprite.scale.setScalar(490);
|
this.moonSprite.scale.setScalar(490);
|
||||||
this.scene.add(this.moonSprite);
|
this.scene.add(this.moonSprite);
|
||||||
|
|
||||||
|
const moonGlowTexture = this.createMoonGlowTexture();
|
||||||
|
const moonGlowMaterial = new THREE.SpriteMaterial({
|
||||||
|
map: moonGlowTexture,
|
||||||
|
color: 0xc6dbff,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0,
|
||||||
|
depthWrite: false,
|
||||||
|
depthTest: true,
|
||||||
|
blending: THREE.AdditiveBlending
|
||||||
|
});
|
||||||
|
this.moonGlowSprite = new THREE.Sprite(moonGlowMaterial);
|
||||||
|
this.moonGlowSprite.scale.setScalar(980);
|
||||||
|
this.scene.add(this.moonGlowSprite);
|
||||||
|
|
||||||
const galaxyTexture = this.createGalaxyTexture();
|
const galaxyTexture = this.createGalaxyTexture();
|
||||||
const galaxyMaterial = new THREE.SpriteMaterial({
|
const galaxyMaterial = new THREE.SpriteMaterial({
|
||||||
map: galaxyTexture,
|
map: galaxyTexture,
|
||||||
@@ -704,6 +723,28 @@ export class OceanScene {
|
|||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createMoonGlowTexture() {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 512;
|
||||||
|
canvas.height = 512;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const glow = ctx.createRadialGradient(256, 256, 0, 256, 256, 256);
|
||||||
|
glow.addColorStop(0, 'rgba(255,255,255,0.62)');
|
||||||
|
glow.addColorStop(0.12, 'rgba(236,242,255,0.46)');
|
||||||
|
glow.addColorStop(0.28, 'rgba(198,216,255,0.26)');
|
||||||
|
glow.addColorStop(0.52, 'rgba(148,184,255,0.11)');
|
||||||
|
glow.addColorStop(0.78, 'rgba(120,160,255,0.05)');
|
||||||
|
glow.addColorStop(1, 'rgba(120,160,255,0)');
|
||||||
|
ctx.fillStyle = glow;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
createGalaxyTexture() {
|
createGalaxyTexture() {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = 1024;
|
canvas.width = 1024;
|
||||||
@@ -1259,15 +1300,36 @@ export class OceanScene {
|
|||||||
const baseWaterOnBeforeRender = this.water.onBeforeRender.bind(this.water);
|
const baseWaterOnBeforeRender = this.water.onBeforeRender.bind(this.water);
|
||||||
this.water.onBeforeRender = (...args) => {
|
this.water.onBeforeRender = (...args) => {
|
||||||
const starsWereVisible = this.starField?.visible ?? false;
|
const starsWereVisible = this.starField?.visible ?? false;
|
||||||
|
const moonWasVisible = this.moonSprite?.visible ?? false;
|
||||||
|
const moonGlowWasVisible = this.moonGlowSprite?.visible ?? false;
|
||||||
|
const galaxyWasVisible = this.galaxyBand?.visible ?? false;
|
||||||
if (this.starField) {
|
if (this.starField) {
|
||||||
this.starField.visible = false;
|
this.starField.visible = false;
|
||||||
}
|
}
|
||||||
|
if (this.moonSprite) {
|
||||||
|
this.moonSprite.visible = false;
|
||||||
|
}
|
||||||
|
if (this.moonGlowSprite) {
|
||||||
|
this.moonGlowSprite.visible = false;
|
||||||
|
}
|
||||||
|
if (this.galaxyBand) {
|
||||||
|
this.galaxyBand.visible = false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
baseWaterOnBeforeRender(...args);
|
baseWaterOnBeforeRender(...args);
|
||||||
} finally {
|
} finally {
|
||||||
if (this.starField) {
|
if (this.starField) {
|
||||||
this.starField.visible = starsWereVisible;
|
this.starField.visible = starsWereVisible;
|
||||||
}
|
}
|
||||||
|
if (this.moonSprite) {
|
||||||
|
this.moonSprite.visible = moonWasVisible;
|
||||||
|
}
|
||||||
|
if (this.moonGlowSprite) {
|
||||||
|
this.moonGlowSprite.visible = moonGlowWasVisible;
|
||||||
|
}
|
||||||
|
if (this.galaxyBand) {
|
||||||
|
this.galaxyBand.visible = galaxyWasVisible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1299,6 +1361,18 @@ export class OceanScene {
|
|||||||
|
|
||||||
this.terrainGenerator = terrainGen;
|
this.terrainGenerator = terrainGen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initWindTurbine() {
|
||||||
|
this.windTurbine = new OffshoreWindTurbineAsset({
|
||||||
|
position: new THREE.Vector3(280, 0, -2350),
|
||||||
|
yaw: 0,
|
||||||
|
scale: 0.68,
|
||||||
|
rotorSpeed: 0.24
|
||||||
|
});
|
||||||
|
await this.windTurbine.load();
|
||||||
|
this.windTurbine.addToScene(this.scene);
|
||||||
|
this.windTurbine.faceDirection(this.sun);
|
||||||
|
}
|
||||||
|
|
||||||
async initVegetation() {
|
async initVegetation() {
|
||||||
const vegSystem = new VegetationSystem(this.terrainGenerator, {
|
const vegSystem = new VegetationSystem(this.terrainGenerator, {
|
||||||
@@ -1344,6 +1418,7 @@ export class OceanScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initSunPosition() {
|
initSunPosition() {
|
||||||
|
this.initialSun.copy(this.sun);
|
||||||
this.updateSun();
|
this.updateSun();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1352,6 +1427,9 @@ export class OceanScene {
|
|||||||
const theta = THREE.MathUtils.degToRad(this.params.azimuth);
|
const theta = THREE.MathUtils.degToRad(this.params.azimuth);
|
||||||
|
|
||||||
this.sun.setFromSphericalCoords(1, phi, theta);
|
this.sun.setFromSphericalCoords(1, phi, theta);
|
||||||
|
if (this.initialSun.lengthSq() === 0) {
|
||||||
|
this.initialSun.copy(this.sun);
|
||||||
|
}
|
||||||
|
|
||||||
this.sky.material.uniforms['sunPosition'].value.copy(this.sun);
|
this.sky.material.uniforms['sunPosition'].value.copy(this.sun);
|
||||||
this.water.material.uniforms['sunDirection'].value.copy(this.sun).normalize();
|
this.water.material.uniforms['sunDirection'].value.copy(this.sun).normalize();
|
||||||
@@ -1965,7 +2043,7 @@ export class OceanScene {
|
|||||||
this.starField.material.uniforms.intensity.value = opacity;
|
this.starField.material.uniforms.intensity.value = opacity;
|
||||||
|
|
||||||
if (this.moonSprite) {
|
if (this.moonSprite) {
|
||||||
const moonAngle = THREE.MathUtils.degToRad(this.params.azimuth - 42);
|
const moonAngle = Math.atan2(this.initialSun.z, this.initialSun.x);
|
||||||
const moonDistance = 3200;
|
const moonDistance = 3200;
|
||||||
this.moonSprite.position.set(
|
this.moonSprite.position.set(
|
||||||
Math.cos(moonAngle) * moonDistance,
|
Math.cos(moonAngle) * moonDistance,
|
||||||
@@ -1977,6 +2055,13 @@ export class OceanScene {
|
|||||||
this.moonSprite.material.opacity = THREE.MathUtils.lerp(0.0, 2.4, moonGlow);
|
this.moonSprite.material.opacity = THREE.MathUtils.lerp(0.0, 2.4, moonGlow);
|
||||||
const moonScale = THREE.MathUtils.lerp(410, 730, moonGlow);
|
const moonScale = THREE.MathUtils.lerp(410, 730, moonGlow);
|
||||||
this.moonSprite.scale.setScalar(moonScale);
|
this.moonSprite.scale.setScalar(moonScale);
|
||||||
|
|
||||||
|
if (this.moonGlowSprite) {
|
||||||
|
this.moonGlowSprite.position.copy(this.moonSprite.position);
|
||||||
|
this.moonGlowSprite.visible = this.moonSprite.visible;
|
||||||
|
this.moonGlowSprite.material.opacity = moonGlow * 1.18;
|
||||||
|
this.moonGlowSprite.scale.setScalar(THREE.MathUtils.lerp(980, 1680, moonGlow));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.galaxyBand) {
|
if (this.galaxyBand) {
|
||||||
@@ -2070,7 +2155,8 @@ export class OceanScene {
|
|||||||
requestAnimationFrame(() => this.animate());
|
requestAnimationFrame(() => this.animate());
|
||||||
this.stats?.begin();
|
this.stats?.begin();
|
||||||
|
|
||||||
const time = this.clock.getElapsedTime();
|
const delta = this.clock.getDelta();
|
||||||
|
const time = this.clock.elapsedTime;
|
||||||
this.updateLightning(time);
|
this.updateLightning(time);
|
||||||
this.updateThunder(time);
|
this.updateThunder(time);
|
||||||
this.updateRainAudioLoop();
|
this.updateRainAudioLoop();
|
||||||
@@ -2107,6 +2193,11 @@ export class OceanScene {
|
|||||||
this.vegetationSystem.update(time);
|
this.vegetationSystem.update(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.windTurbine) {
|
||||||
|
this.windTurbine.faceDirection(this.sun);
|
||||||
|
this.windTurbine.update(time, delta);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lightningFlash > 0.001) {
|
if (this.lightningFlash > 0.001) {
|
||||||
this.updateClouds();
|
this.updateClouds();
|
||||||
this.updateFog();
|
this.updateFog();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ async function main() {
|
|||||||
const container = document.getElementById('container');
|
const container = document.getElementById('container');
|
||||||
|
|
||||||
const oceanScene = new OceanScene(container);
|
const oceanScene = new OceanScene(container);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await oceanScene.init();
|
await oceanScene.init();
|
||||||
|
|
||||||
|
|||||||
133
src/objects/OffshoreWindTurbineAsset.js
Normal file
133
src/objects/OffshoreWindTurbineAsset.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
|
import { WindTurbine } from './WindTurbine.js';
|
||||||
|
|
||||||
|
const DEFAULT_MODEL_URL = '/models/offshore-wind-turbine.glb';
|
||||||
|
|
||||||
|
export class OffshoreWindTurbineAsset {
|
||||||
|
constructor({
|
||||||
|
modelUrl = DEFAULT_MODEL_URL,
|
||||||
|
position = new THREE.Vector3(360, 0, -260),
|
||||||
|
yaw = -Math.PI * 0.18,
|
||||||
|
scale = 1,
|
||||||
|
rotorSpeed = 0.34
|
||||||
|
} = {}) {
|
||||||
|
this.modelUrl = modelUrl;
|
||||||
|
this.position = position.clone();
|
||||||
|
this.yaw = yaw;
|
||||||
|
this.scale = scale;
|
||||||
|
this.rotorSpeed = rotorSpeed;
|
||||||
|
|
||||||
|
this.group = new THREE.Group();
|
||||||
|
this.group.position.copy(this.position);
|
||||||
|
this.group.rotation.y = this.yaw;
|
||||||
|
this.group.scale.setScalar(this.scale);
|
||||||
|
|
||||||
|
this.rotors = [];
|
||||||
|
this.mixer = null;
|
||||||
|
this.model = null;
|
||||||
|
this.usingFallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
const gltf = await this.loadGltf(this.modelUrl);
|
||||||
|
this.attachModel(gltf);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`风机资产加载失败,回退到程序化风机: ${this.modelUrl}`, error);
|
||||||
|
this.attachFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGltf(url) {
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
loader.load(url, resolve, undefined, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachModel(gltf) {
|
||||||
|
const model = gltf.scene;
|
||||||
|
this.fitModelToTargetHeight(model);
|
||||||
|
this.prepareModel(model);
|
||||||
|
this.alignModelToWaterline(model);
|
||||||
|
this.group.add(model);
|
||||||
|
this.model = model;
|
||||||
|
|
||||||
|
if (gltf.animations?.length) {
|
||||||
|
this.mixer = new THREE.AnimationMixer(model);
|
||||||
|
gltf.animations.forEach((clip) => {
|
||||||
|
this.mixer.clipAction(clip).play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachFallback() {
|
||||||
|
const fallback = new WindTurbine({
|
||||||
|
position: new THREE.Vector3(0, 0, 0),
|
||||||
|
yaw: 0,
|
||||||
|
rotorSpeed: this.rotorSpeed
|
||||||
|
});
|
||||||
|
this.group.add(fallback.group);
|
||||||
|
this.rotors = fallback.rotors;
|
||||||
|
this.usingFallback = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareModel(model) {
|
||||||
|
model.traverse((child) => {
|
||||||
|
if (!child.isMesh) return;
|
||||||
|
child.castShadow = true;
|
||||||
|
child.receiveShadow = true;
|
||||||
|
|
||||||
|
const materials = Array.isArray(child.material) ? child.material : [child.material];
|
||||||
|
materials.forEach((material) => {
|
||||||
|
if (!material) return;
|
||||||
|
if ('color' in material) {
|
||||||
|
material.color.set(0xe8edf3);
|
||||||
|
}
|
||||||
|
if ('metalness' in material && material.metalness < 0.05) {
|
||||||
|
material.metalness = 0.18;
|
||||||
|
}
|
||||||
|
if ('roughness' in material && material.roughness > 0.92) {
|
||||||
|
material.roughness = 0.82;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fitModelToTargetHeight(model, targetHeight = 340) {
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
if (size.y <= 0.0001) return;
|
||||||
|
|
||||||
|
const scaleFactor = targetHeight / size.y;
|
||||||
|
model.scale.multiplyScalar(scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
alignModelToWaterline(model) {
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
model.position.y -= box.min.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
addToScene(scene) {
|
||||||
|
scene.add(this.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
faceDirection(direction) {
|
||||||
|
const flatDirection = new THREE.Vector3(direction.x, 0, direction.z);
|
||||||
|
if (flatDirection.lengthSq() < 0.0001) return;
|
||||||
|
|
||||||
|
flatDirection.normalize();
|
||||||
|
const yaw = Math.atan2(flatDirection.x, flatDirection.z);
|
||||||
|
this.group.rotation.y = yaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(time, delta = 1 / 60) {
|
||||||
|
if (this.mixer) {
|
||||||
|
this.mixer.update(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
161
src/objects/WindTurbine.js
Normal file
161
src/objects/WindTurbine.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export class WindTurbine {
|
||||||
|
constructor({
|
||||||
|
position = new THREE.Vector3(360, 0, -260),
|
||||||
|
yaw = -Math.PI * 0.18,
|
||||||
|
rotorSpeed = 0.34
|
||||||
|
} = {}) {
|
||||||
|
this.group = new THREE.Group();
|
||||||
|
this.group.position.copy(position);
|
||||||
|
this.group.rotation.y = yaw;
|
||||||
|
|
||||||
|
this.rotors = [];
|
||||||
|
this.rotorSpeed = rotorSpeed;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const towerMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xe7edf2,
|
||||||
|
metalness: 0.28,
|
||||||
|
roughness: 0.55
|
||||||
|
});
|
||||||
|
const accentMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xc4d0da,
|
||||||
|
metalness: 0.35,
|
||||||
|
roughness: 0.48
|
||||||
|
});
|
||||||
|
const bladeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xf7fafc,
|
||||||
|
metalness: 0.16,
|
||||||
|
roughness: 0.62
|
||||||
|
});
|
||||||
|
const foundationMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x71808f,
|
||||||
|
metalness: 0.24,
|
||||||
|
roughness: 0.82
|
||||||
|
});
|
||||||
|
const platformMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.16,
|
||||||
|
roughness: 0.95,
|
||||||
|
metalness: 0
|
||||||
|
});
|
||||||
|
const ringMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x91a1ae,
|
||||||
|
metalness: 0.38,
|
||||||
|
roughness: 0.42
|
||||||
|
});
|
||||||
|
|
||||||
|
const foundation = new THREE.Mesh(
|
||||||
|
new THREE.CylinderGeometry(8.2, 10.6, 36, 24),
|
||||||
|
foundationMaterial
|
||||||
|
);
|
||||||
|
foundation.position.y = -17.8;
|
||||||
|
this.group.add(foundation);
|
||||||
|
|
||||||
|
const transition = new THREE.Mesh(
|
||||||
|
new THREE.CylinderGeometry(5.2, 6.8, 9, 24),
|
||||||
|
accentMaterial
|
||||||
|
);
|
||||||
|
transition.position.y = 4.2;
|
||||||
|
this.group.add(transition);
|
||||||
|
|
||||||
|
const tower = new THREE.Mesh(
|
||||||
|
new THREE.CylinderGeometry(2.9, 4.8, 92, 28),
|
||||||
|
towerMaterial
|
||||||
|
);
|
||||||
|
tower.position.y = 54;
|
||||||
|
this.group.add(tower);
|
||||||
|
|
||||||
|
const nacelle = new THREE.Mesh(
|
||||||
|
new THREE.CapsuleGeometry(3.2, 11, 8, 16),
|
||||||
|
towerMaterial
|
||||||
|
);
|
||||||
|
nacelle.rotation.z = Math.PI / 2;
|
||||||
|
nacelle.position.set(0, 102, 0);
|
||||||
|
this.group.add(nacelle);
|
||||||
|
|
||||||
|
const tailFin = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(1.1, 5.5, 2.8),
|
||||||
|
accentMaterial
|
||||||
|
);
|
||||||
|
tailFin.position.set(-8.5, 102.5, 0);
|
||||||
|
this.group.add(tailFin);
|
||||||
|
|
||||||
|
const rotor = this.createRotor(bladeMaterial, accentMaterial, ringMaterial);
|
||||||
|
rotor.position.set(6.8, 102, 0);
|
||||||
|
rotor.userData.rotationSpeed = this.rotorSpeed;
|
||||||
|
this.group.add(rotor);
|
||||||
|
this.rotors.push(rotor);
|
||||||
|
|
||||||
|
const platform = new THREE.Mesh(
|
||||||
|
new THREE.CylinderGeometry(7.6, 9.2, 0.5, 32),
|
||||||
|
platformMaterial
|
||||||
|
);
|
||||||
|
platform.position.y = 0.12;
|
||||||
|
this.group.add(platform);
|
||||||
|
|
||||||
|
this.group.traverse((child) => {
|
||||||
|
if (!child.isMesh) return;
|
||||||
|
child.castShadow = true;
|
||||||
|
child.receiveShadow = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createRotor(bladeMaterial, hubMaterial, ringMaterial) {
|
||||||
|
const rotor = new THREE.Group();
|
||||||
|
|
||||||
|
const hub = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(2.2, 20, 20),
|
||||||
|
hubMaterial
|
||||||
|
);
|
||||||
|
rotor.add(hub);
|
||||||
|
|
||||||
|
const bladeShape = new THREE.Shape();
|
||||||
|
bladeShape.moveTo(-0.3, 0);
|
||||||
|
bladeShape.quadraticCurveTo(1.2, 2.6, 0.7, 22);
|
||||||
|
bladeShape.quadraticCurveTo(0.2, 31.5, -0.55, 38.5);
|
||||||
|
bladeShape.quadraticCurveTo(-1.0, 31, -0.9, 21);
|
||||||
|
bladeShape.quadraticCurveTo(-0.82, 6.8, -0.3, 0);
|
||||||
|
|
||||||
|
const bladeGeometry = new THREE.ExtrudeGeometry(bladeShape, {
|
||||||
|
depth: 0.22,
|
||||||
|
bevelEnabled: false,
|
||||||
|
curveSegments: 20,
|
||||||
|
steps: 1
|
||||||
|
});
|
||||||
|
bladeGeometry.translate(0, -2.2, -0.11);
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const blade = new THREE.Mesh(bladeGeometry, bladeMaterial);
|
||||||
|
blade.rotation.x = Math.PI / 2;
|
||||||
|
blade.rotation.z = (i / 3) * Math.PI * 2;
|
||||||
|
blade.position.x = 0.55;
|
||||||
|
rotor.add(blade);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ring = new THREE.Mesh(
|
||||||
|
new THREE.TorusGeometry(2.65, 0.16, 10, 40),
|
||||||
|
ringMaterial
|
||||||
|
);
|
||||||
|
ring.rotation.y = Math.PI / 2;
|
||||||
|
rotor.add(ring);
|
||||||
|
|
||||||
|
return rotor;
|
||||||
|
}
|
||||||
|
|
||||||
|
addToScene(scene) {
|
||||||
|
scene.add(this.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(time, delta = 1 / 60) {
|
||||||
|
this.rotors.forEach((rotor, index) => {
|
||||||
|
const gust = 1.0 + Math.sin(time * 0.28 + index * 1.7) * 0.08;
|
||||||
|
rotor.rotation.x += rotor.userData.rotationSpeed * gust * delta;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user