添加风机

This commit is contained in:
2026-03-27 18:11:27 +08:00
parent 4a9410802d
commit c0ca5b51c4
6 changed files with 405 additions and 3 deletions

18
public/models/README.md Normal file
View 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

Binary file not shown.

View File

@@ -10,6 +10,7 @@ import Stats from 'three/addons/libs/stats.module.js';
import { TerrainGenerator } from './TerrainGenerator.js';
import { VegetationSystem } from './VegetationSystem.js';
import { DEFAULT_SCENE_PARAMS } from './weatherPresets.js';
import { OffshoreWindTurbineAsset } from './objects/OffshoreWindTurbineAsset.js';
const RAIN_AUDIO_URL = '/audio/rain-calming.mp3';
const THUNDER_AUDIO_URL = '/audio/thunder-distant.mp3';
@@ -25,8 +26,10 @@ export class OceanScene {
this.sky = null;
this.starField = null;
this.moonSprite = null;
this.moonGlowSprite = null;
this.galaxyBand = null;
this.sun = new THREE.Vector3();
this.initialSun = new THREE.Vector3();
this.terrain = null;
this.vegetation = null;
this.vegetationSystem = null;
@@ -50,6 +53,7 @@ export class OceanScene {
this.fogLayers = [];
this.horizonFog = null;
this.skyHazeBand = null;
this.windTurbine = null;
this.rainAudioPool = [];
this.rainAudioActiveIndex = 0;
this.rainAudioIsPlaying = false;
@@ -85,6 +89,7 @@ export class OceanScene {
this.initFog();
await this.initWater();
await this.initTerrain();
await this.initWindTurbine();
await this.initVegetation();
this.initSunPosition();
this.initEventListeners();
@@ -645,6 +650,20 @@ export class OceanScene {
this.moonSprite.scale.setScalar(490);
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 galaxyMaterial = new THREE.SpriteMaterial({
map: galaxyTexture,
@@ -704,6 +723,28 @@ export class OceanScene {
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() {
const canvas = document.createElement('canvas');
canvas.width = 1024;
@@ -1259,15 +1300,36 @@ export class OceanScene {
const baseWaterOnBeforeRender = this.water.onBeforeRender.bind(this.water);
this.water.onBeforeRender = (...args) => {
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) {
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 {
baseWaterOnBeforeRender(...args);
} finally {
if (this.starField) {
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;
}
}
};
@@ -1300,6 +1362,18 @@ export class OceanScene {
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() {
const vegSystem = new VegetationSystem(this.terrainGenerator, {
grassCount: 0, // 随机草簇数量,设为 0 时只使用 grassAreas
@@ -1344,6 +1418,7 @@ export class OceanScene {
}
initSunPosition() {
this.initialSun.copy(this.sun);
this.updateSun();
}
@@ -1352,6 +1427,9 @@ export class OceanScene {
const theta = THREE.MathUtils.degToRad(this.params.azimuth);
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.water.material.uniforms['sunDirection'].value.copy(this.sun).normalize();
@@ -1965,7 +2043,7 @@ export class OceanScene {
this.starField.material.uniforms.intensity.value = opacity;
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;
this.moonSprite.position.set(
Math.cos(moonAngle) * moonDistance,
@@ -1977,6 +2055,13 @@ export class OceanScene {
this.moonSprite.material.opacity = THREE.MathUtils.lerp(0.0, 2.4, moonGlow);
const moonScale = THREE.MathUtils.lerp(410, 730, moonGlow);
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) {
@@ -2070,7 +2155,8 @@ export class OceanScene {
requestAnimationFrame(() => this.animate());
this.stats?.begin();
const time = this.clock.getElapsedTime();
const delta = this.clock.getDelta();
const time = this.clock.elapsedTime;
this.updateLightning(time);
this.updateThunder(time);
this.updateRainAudioLoop();
@@ -2107,6 +2193,11 @@ export class OceanScene {
this.vegetationSystem.update(time);
}
if (this.windTurbine) {
this.windTurbine.faceDirection(this.sun);
this.windTurbine.update(time, delta);
}
if (this.lightningFlash > 0.001) {
this.updateClouds();
this.updateFog();

View File

@@ -6,7 +6,6 @@ async function main() {
const container = document.getElementById('container');
const oceanScene = new OceanScene(container);
try {
await oceanScene.init();

View 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
View 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;
});
}
}