添加风机
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 { 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();
|
||||
|
||||
@@ -6,7 +6,6 @@ async function main() {
|
||||
const container = document.getElementById('container');
|
||||
|
||||
const oceanScene = new OceanScene(container);
|
||||
|
||||
try {
|
||||
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