Compare commits

...

5 Commits

Author SHA1 Message Date
32ae719b00 添加两台风机 2026-04-01 16:07:37 +08:00
fdf920c5aa 替换风机模型并使其桨叶转动 2026-04-01 13:43:24 +08:00
64269a9088 调校场景 2026-03-28 15:29:48 +08:00
68f7e5bfb4 雾气上限改为2 2026-03-28 15:14:27 +08:00
502aafc41d 改用体积雾 2026-03-28 15:12:17 +08:00
10 changed files with 877 additions and 128 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

View File

@@ -0,0 +1,570 @@
{
"asset": {
"version": "2.0",
"generator": "babylon.js glTF exporter for 3dsmax 2023 v20240312.5"
},
"scene": 0,
"scenes": [
{
"nodes": [
0,
5
]
}
],
"nodes": [
{
"children": [
1,
2,
3
],
"mesh": 0,
"translation": [
0.00143703236,
103.750244,
4.00849152
],
"rotation": [
-8.14603354E-08,
0.0,
0.0,
1.0
],
"name": "FJ_HZ_F001"
},
{
"mesh": 1,
"translation": [
-0.000161597272,
1.64903259,
-5.50921869
],
"name": "FJ_HZ_B001"
},
{
"mesh": 2,
"translation": [
-0.000161597272,
1.64903259,
-5.50921869
],
"name": "FJ_HZ_R001"
},
{
"children": [
4
],
"mesh": 3,
"translation": [
0.0001473783,
1.788208,
1.96820593
],
"rotation": [
-0.0218098629,
0.03777547,
0.8652011,
0.499524176
],
"scale": [
1.48850107,
1.48850155,
1.48850131
],
"name": "FJ_LunGu001"
},
{
"mesh": 4,
"translation": [
0.0158500671,
-0.009151459,
-0.1445036
],
"rotation": [
-9.313228E-10,
0.0,
2.980233E-08,
1.0
],
"scale": [
0.9999998,
0.9999997,
0.999999762
],
"name": "FJ_ShanYe001"
},
{
"mesh": 5,
"translation": [
0.00143965613,
53.50479,
4.00849056
],
"rotation": [
-8.14603354E-08,
0.0,
0.0,
1.0
],
"name": "FJ_TaTong001"
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 1,
"NORMAL": 2,
"TEXCOORD_0": 3
},
"indices": 0,
"material": 0
}
],
"name": "FJ_HZ_F001"
},
{
"primitives": [
{
"attributes": {
"POSITION": 5,
"NORMAL": 6,
"TEXCOORD_0": 7
},
"indices": 4,
"material": 0
}
],
"name": "FJ_HZ_B001"
},
{
"primitives": [
{
"attributes": {
"POSITION": 9,
"NORMAL": 10,
"TEXCOORD_0": 11
},
"indices": 8,
"material": 0
}
],
"name": "FJ_HZ_R001"
},
{
"primitives": [
{
"attributes": {
"POSITION": 13,
"NORMAL": 14,
"TEXCOORD_0": 15
},
"indices": 12,
"material": 1
}
],
"name": "FJ_LunGu001"
},
{
"primitives": [
{
"attributes": {
"POSITION": 17,
"NORMAL": 18,
"COLOR_0": 19,
"TEXCOORD_0": 20
},
"indices": 16,
"material": 2
}
],
"name": "FJ_ShanYe001"
},
{
"primitives": [
{
"attributes": {
"POSITION": 22,
"NORMAL": 23,
"TEXCOORD_0": 24
},
"indices": 21,
"material": 2
}
],
"name": "FJ_TaTong001"
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5123,
"count": 5262,
"type": "SCALAR",
"name": "accessorIndices"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 1570,
"max": [
1.91735768,
3.84009123,
2.02482748
],
"min": [
-1.91583681,
0.144887447,
-3.62821436
],
"type": "VEC3",
"name": "accessorPositions"
},
{
"bufferView": 1,
"byteOffset": 18840,
"componentType": 5126,
"count": 1570,
"type": "VEC3",
"name": "accessorNormals"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 1570,
"type": "VEC2",
"name": "accessorUVs"
},
{
"bufferView": 0,
"byteOffset": 10524,
"componentType": 5123,
"count": 4368,
"type": "SCALAR",
"name": "accessorIndices"
},
{
"bufferView": 1,
"byteOffset": 37680,
"componentType": 5126,
"count": 1127,
"max": [
1.91751945,
1.93533087,
1.88101923
],
"min": [
-2.16255522,
-1.49184012,
-4.2311306
],
"type": "VEC3",
"name": "accessorPositions"
},
{
"bufferView": 1,
"byteOffset": 51204,
"componentType": 5126,
"count": 1127,
"type": "VEC3",
"name": "accessorNormals"
},
{
"bufferView": 2,
"byteOffset": 12560,
"componentType": 5126,
"count": 1127,
"type": "VEC2",
"name": "accessorUVs"
},
{
"bufferView": 0,
"byteOffset": 19260,
"componentType": 5123,
"count": 756,
"type": "SCALAR",
"name": "accessorIndices"
},
{
"bufferView": 1,
"byteOffset": 64728,
"componentType": 5126,
"count": 213,
"max": [
2.657733,
1.31850243,
1.19701219
],
"min": [
1.91751933,
-1.02121758,
-2.679065
],
"type": "VEC3",
"name": "accessorPositions"
},
{
"bufferView": 1,
"byteOffset": 67284,
"componentType": 5126,
"count": 213,
"type": "VEC3",
"name": "accessorNormals"
},
{
"bufferView": 2,
"byteOffset": 21576,
"componentType": 5126,
"count": 213,
"type": "VEC2",
"name": "accessorUVs"
},
{
"bufferView": 0,
"byteOffset": 20772,
"componentType": 5123,
"count": 12276,
"type": "SCALAR",
"name": "accessorIndices"
},
{
"bufferView": 1,
"byteOffset": 69840,
"componentType": 5126,
"count": 3032,
"max": [
1.61511183,
1.590561,
2.664262
],
"min": [
-1.61181664,
-1.20966589,
-0.0402874537
],
"type": "VEC3",
"name": "accessorPositions"
},
{
"bufferView": 1,
"byteOffset": 106224,
"componentType": 5126,
"count": 3032,
"type": "VEC3",
"name": "accessorNormals"
},
{
"bufferView": 2,
"byteOffset": 23280,
"componentType": 5126,
"count": 3032,
"type": "VEC2",
"name": "accessorUVs"
},
{
"bufferView": 0,
"byteOffset": 45324,
"componentType": 5123,
"count": 12366,
"type": "SCALAR",
"name": "accessorIndices"
},
{
"bufferView": 1,
"byteOffset": 142608,
"componentType": 5126,
"count": 2337,
"max": [
49.12784,
28.6720619,
4.548005
],
"min": [
-49.41851,
-56.8182373,
0.3628065
],
"type": "VEC3",
"name": "accessorPositions"
},
{
"bufferView": 1,
"byteOffset": 170652,
"componentType": 5126,
"count": 2337,
"type": "VEC3",
"name": "accessorNormals"
},
{
"bufferView": 3,
"componentType": 5126,
"count": 2337,
"type": "VEC4",
"name": "accessorColors"
},
{
"bufferView": 2,
"byteOffset": 47536,
"componentType": 5126,
"count": 2337,
"type": "VEC2",
"name": "accessorUVs"
},
{
"bufferView": 0,
"byteOffset": 70056,
"componentType": 5123,
"count": 5166,
"type": "SCALAR",
"name": "accessorIndices"
},
{
"bufferView": 1,
"byteOffset": 198696,
"componentType": 5126,
"count": 1389,
"max": [
2.067228,
50.37658,
2.06722975
],
"min": [
-2.06722379,
-53.5088959,
-2.067222
],
"type": "VEC3",
"name": "accessorPositions"
},
{
"bufferView": 1,
"byteOffset": 215364,
"componentType": 5126,
"count": 1389,
"type": "VEC3",
"name": "accessorNormals"
},
{
"bufferView": 2,
"byteOffset": 66232,
"componentType": 5126,
"count": 1389,
"type": "VEC2",
"name": "accessorUVs"
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 80388,
"name": "bufferViewScalar"
},
{
"buffer": 0,
"byteOffset": 80388,
"byteLength": 232032,
"byteStride": 12,
"name": "bufferViewFloatVec3"
},
{
"buffer": 0,
"byteOffset": 312420,
"byteLength": 77344,
"byteStride": 8,
"name": "bufferViewFloatVec2"
},
{
"buffer": 0,
"byteOffset": 389764,
"byteLength": 37392,
"byteStride": 16,
"name": "bufferViewFloatVec4"
}
],
"buffers": [
{
"uri": "BJY_FJ_Shell.bin",
"byteLength": 427156
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicFactor": 0.0,
"roughnessFactor": 0.399999976
},
"doubleSided": true,
"name": "FJ_HuZhao",
"extras": {
"babylonSeparateCullingPass": false
}
},
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 1
},
"metallicFactor": 0.0,
"roughnessFactor": 0.399999976
},
"doubleSided": true,
"name": "FJ_LunGU",
"extras": {
"babylonSeparateCullingPass": false
}
},
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 2
},
"metallicFactor": 0.0,
"roughnessFactor": 0.399999976
},
"doubleSided": true,
"name": "FJ_TaTong",
"extras": {
"babylonSeparateCullingPass": false
}
}
],
"textures": [
{
"sampler": 0,
"source": 0,
"name": "FJ_HuZhao.jpg"
},
{
"sampler": 0,
"source": 1,
"name": "FJ_LunGu.jpg"
},
{
"sampler": 0,
"source": 2,
"name": "FJ_TaTong.jpg"
}
],
"images": [
{
"uri": "FJ_HuZhao.jpg"
},
{
"uri": "FJ_LunGu.jpg"
},
{
"uri": "FJ_TaTong.jpg"
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9987
}
]
}

BIN
public/models/FJ_HuZhao.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
public/models/FJ_LunGu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
public/models/FJ_TaTong.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,18 +1,10 @@
将你选中的海上风机资产导出为 GLB 文件,并放到下面这个路径: 将你选中的海上风机资产导出为 glTF 资产,并放到下面这个路径:
`public/models/offshore-wind-turbine.glb` `public/models/BJY_FJ_Shell.gltf`
当前代码会优先加载这个文件: 当前代码会优先加载这个文件:
- 存在该文件:使用真实 GLB 风机资产 - 存在该文件:使用真实 glTF 风机资产
- 文件不存在或加载失败:自动回退到程序化风机占位模型 - 文件不存在或加载失败:自动回退到程序化风机占位模型
建议优先选择包含完整塔架、机舱、叶片的海上风机模型。 建议优先选择包含完整塔架、机舱、叶片的海上风机模型。
当前选定来源:
- 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

View File

@@ -5,6 +5,7 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import Stats from 'three/addons/libs/stats.module.js'; import Stats from 'three/addons/libs/stats.module.js';
import { TerrainGenerator } from './TerrainGenerator.js'; import { TerrainGenerator } from './TerrainGenerator.js';
@@ -43,8 +44,10 @@ export class OceanScene {
this.lightningCloudGlow = null; this.lightningCloudGlow = null;
this.composer = null; this.composer = null;
this.bloomPass = null; this.bloomPass = null;
this.fogPass = null;
this.rainPass = null; this.rainPass = null;
this.snowPass = null; this.snowPass = null;
this.depthTarget = null;
this.stats = null; this.stats = null;
this.cloudGroup = null; this.cloudGroup = null;
this.cloudMaterials = []; this.cloudMaterials = [];
@@ -53,7 +56,7 @@ export class OceanScene {
this.fogLayers = []; this.fogLayers = [];
this.horizonFog = null; this.horizonFog = null;
this.skyHazeBand = null; this.skyHazeBand = null;
this.windTurbine = null; this.windTurbines = [];
this.rainAudioPool = []; this.rainAudioPool = [];
this.rainAudioActiveIndex = 0; this.rainAudioActiveIndex = 0;
this.rainAudioIsPlaying = false; this.rainAudioIsPlaying = false;
@@ -87,7 +90,6 @@ export class OceanScene {
this.initStars(); this.initStars();
this.initNightSky(); this.initNightSky();
this.initClouds(); this.initClouds();
this.initFog();
await this.initWater(); await this.initWater();
await this.initTerrain(); await this.initTerrain();
await this.initWindTurbine(); await this.initWindTurbine();
@@ -124,7 +126,6 @@ export class OceanScene {
initScene() { initScene() {
this.scene = new THREE.Scene(); this.scene = new THREE.Scene();
this.scene.fog = new THREE.FogExp2(0x8cb8d4, 0.0006);
} }
initCamera() { initCamera() {
@@ -179,6 +180,13 @@ export class OceanScene {
} }
initPostProcessing() { initPostProcessing() {
this.depthTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);
this.depthTarget.depthTexture = new THREE.DepthTexture(window.innerWidth, window.innerHeight, THREE.UnsignedIntType);
this.depthTarget.depthTexture.format = THREE.DepthFormat;
this.depthTarget.texture.minFilter = THREE.NearestFilter;
this.depthTarget.texture.magFilter = THREE.NearestFilter;
this.depthTarget.texture.generateMipmaps = false;
this.composer = new EffectComposer(this.renderer); this.composer = new EffectComposer(this.renderer);
this.composer.addPass(new RenderPass(this.scene, this.camera)); this.composer.addPass(new RenderPass(this.scene, this.camera));
@@ -190,6 +198,13 @@ export class OceanScene {
); );
this.composer.addPass(this.bloomPass); this.composer.addPass(this.bloomPass);
this.fogPass = new ShaderPass(this.createVolumetricFogShader());
this.fogPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
this.fogPass.material.uniforms.tDepth.value = this.depthTarget.depthTexture;
this.fogPass.material.uniforms.cameraNear.value = this.camera.near;
this.fogPass.material.uniforms.cameraFar.value = this.camera.far;
this.composer.addPass(this.fogPass);
this.rainPass = new ShaderPass(this.createRainShader()); this.rainPass = new ShaderPass(this.createRainShader());
this.rainPass.enabled = this.params.rainEnabled; this.rainPass.enabled = this.params.rainEnabled;
this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
@@ -205,6 +220,8 @@ export class OceanScene {
this.snowPass.material.uniforms.intensity.value = this.params.snowIntensity; this.snowPass.material.uniforms.intensity.value = this.params.snowIntensity;
this.snowPass.material.uniforms.snowSpeed.value = this.params.snowSpeed; this.snowPass.material.uniforms.snowSpeed.value = this.params.snowSpeed;
this.composer.addPass(this.snowPass); this.composer.addPass(this.snowPass);
this.composer.addPass(new OutputPass());
} }
initAudio() { initAudio() {
@@ -455,6 +472,134 @@ export class OceanScene {
}; };
} }
createVolumetricFogShader() {
return {
uniforms: {
tDiffuse: { value: null },
tDepth: { value: null },
resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
cameraNear: { value: this.camera?.near ?? 1 },
cameraFar: { value: this.camera?.far ?? 20000 },
projectionMatrixInverse: { value: new THREE.Matrix4() },
viewMatrixInverse: { value: new THREE.Matrix4() },
cameraWorldPosition: { value: new THREE.Vector3() },
fogColor: { value: new THREE.Color(0x9ec5db) },
horizonColor: { value: new THREE.Color(0xcfe0ee) },
fogDensity: { value: 0.0 },
fogHeight: { value: this.params.fogHeight },
fogRange: { value: this.params.fogRange },
time: { value: 0 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform sampler2D tDepth;
uniform vec2 resolution;
uniform mat4 projectionMatrixInverse;
uniform mat4 viewMatrixInverse;
uniform vec3 cameraWorldPosition;
uniform vec3 fogColor;
uniform vec3 horizonColor;
uniform float fogDensity;
uniform float fogHeight;
uniform float fogRange;
uniform float time;
varying vec2 vUv;
float hash21(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec3 reconstructWorldPosition(vec2 uv, float depth) {
float z = depth * 2.0 - 1.0;
vec4 clipPosition = vec4(uv * 2.0 - 1.0, z, 1.0);
vec4 viewPosition = projectionMatrixInverse * clipPosition;
viewPosition /= max(viewPosition.w, 0.0001);
vec4 worldPosition = viewMatrixInverse * viewPosition;
return worldPosition.xyz;
}
float sampleMediumDensity(vec3 samplePosition, float traveledDistance) {
float seaLevel = 2.0;
float verticalOffset = max(samplePosition.y - seaLevel, 0.0);
float heightFalloff = mix(0.08, 0.02, fogHeight);
float heightMask = exp(-verticalOffset * heightFalloff);
float seaMask = smoothstep(210.0, -12.0, samplePosition.y);
float distanceMask = smoothstep(120.0, mix(900.0, 5400.0, fogRange), traveledDistance);
float upperFade = 1.0 - smoothstep(
mix(120.0, 260.0, fogHeight),
mix(360.0, 760.0, fogHeight),
samplePosition.y
);
float windNoise = hash21(samplePosition.xz * 0.0008 + vec2(time * 0.012, -time * 0.008));
float breakup = mix(0.82, 1.14, windNoise);
return fogDensity * heightMask * seaMask * distanceMask * upperFade * breakup;
}
void main() {
vec3 base = texture2D(tDiffuse, vUv).rgb;
float depth = texture2D(tDepth, vUv).x;
if (fogDensity <= 0.00001) {
gl_FragColor = vec4(base, 1.0);
return;
}
float sceneDepth = depth < 0.99999 ? depth : 0.99999;
vec3 endPosition = reconstructWorldPosition(vUv, sceneDepth);
vec3 ray = endPosition - cameraWorldPosition;
float rayLength = length(ray);
if (depth >= 0.99999) {
vec3 farPosition = reconstructWorldPosition(vUv, 0.99999);
ray = farPosition - cameraWorldPosition;
rayLength = min(length(ray), mix(2200.0, 6400.0, fogRange));
}
if (rayLength <= 0.0001) {
gl_FragColor = vec4(base, 1.0);
return;
}
vec3 rayDirection = ray / rayLength;
const int STEP_COUNT = 12;
float stepLength = rayLength / float(STEP_COUNT);
float transmittance = 1.0;
vec3 inscattering = vec3(0.0);
for (int i = 0; i < STEP_COUNT; i++) {
float jitter = hash21(gl_FragCoord.xy + float(i) * 13.37 + time * 24.0);
float traveled = (float(i) + 0.35 + jitter * 0.45) * stepLength;
vec3 samplePosition = cameraWorldPosition + rayDirection * traveled;
float localDensity = sampleMediumDensity(samplePosition, traveled);
vec3 localTint = mix(horizonColor, fogColor, smoothstep(-8.0, 140.0, samplePosition.y));
float extinction = 1.0 - exp(-localDensity * stepLength);
inscattering += transmittance * localTint * extinction;
transmittance *= exp(-localDensity * stepLength);
}
vec3 color = base * transmittance + inscattering;
gl_FragColor = vec4(color, 1.0);
}
`
};
}
createSnowShader() { createSnowShader() {
return { return {
uniforms: { uniforms: {
@@ -1364,15 +1509,24 @@ export class OceanScene {
} }
async initWindTurbine() { async initWindTurbine() {
this.windTurbine = new OffshoreWindTurbineAsset({ const turbineConfigs = [
position: new THREE.Vector3(280, 0, -2350), { position: new THREE.Vector3(280, 0, -2350), scale: 0.68, rotorSpeed: 0.24 },
yaw: 0, { position: new THREE.Vector3(-200, 0, -2900), scale: 0.54, rotorSpeed: 0.19 },
scale: 0.68, { position: new THREE.Vector3(700, 0, -1850), scale: 0.62, rotorSpeed: 0.28 },
rotorSpeed: 0.24 ];
});
await this.windTurbine.load(); await Promise.all(turbineConfigs.map(async (config) => {
this.windTurbine.addToScene(this.scene); const turbine = new OffshoreWindTurbineAsset({
this.windTurbine.faceDirection(this.sun); position: config.position,
yaw: 0,
scale: config.scale,
rotorSpeed: config.rotorSpeed
});
await turbine.load();
turbine.addToScene(this.scene);
turbine.faceDirection(this.sun);
this.windTurbines.push(turbine);
}));
} }
async initVegetation() { async initVegetation() {
@@ -1484,7 +1638,6 @@ export class OceanScene {
this.scene.environment = this.renderTarget.texture; this.scene.environment = this.renderTarget.texture;
this.scene.add(this.sky); this.scene.add(this.sky);
this.updateFog();
this.updateClouds(); this.updateClouds();
this.updateStars(); this.updateStars();
} }
@@ -1513,16 +1666,20 @@ export class OceanScene {
const sunMix = THREE.MathUtils.clamp((this.params.elevation + 10) / 100, 0, 1); const sunMix = THREE.MathUtils.clamp((this.params.elevation + 10) / 100, 0, 1);
const fogColor = new THREE.Color(this.getFogColor()); const fogColor = new THREE.Color(this.getFogColor());
const nightMix = THREE.MathUtils.clamp((-this.params.elevation + 1.0) / 12.0, 0, 1); const nightMix = THREE.MathUtils.clamp((-this.params.elevation + 1.0) / 12.0, 0, 1);
const clarityMix =
THREE.MathUtils.smoothstep(this.params.elevation, 52, 82) *
(1.0 - THREE.MathUtils.smoothstep(this.params.turbidity, 3.2, 8.0));
const warmHorizon = new THREE.Color(0xf0c7a3); const warmHorizon = new THREE.Color(0xf0c7a3);
const coolHorizon = new THREE.Color(0xcfe0ee); const coolHorizon = new THREE.Color(0xcfe0ee).lerp(new THREE.Color(0x8fd2ff), clarityMix);
const nightHorizon = new THREE.Color(0x27415f); const nightHorizon = new THREE.Color(0x27415f);
const horizonColor = warmHorizon.clone().lerp(coolHorizon, sunMix).lerp(nightHorizon, nightMix); const horizonColor = warmHorizon.clone().lerp(coolHorizon, sunMix).lerp(nightHorizon, nightMix);
const warmSkyBase = new THREE.Color(0xf6d7b8); const warmSkyBase = new THREE.Color(0xf6d7b8);
const coolSkyBase = new THREE.Color(0xbfd8eb); const coolSkyBase = new THREE.Color(0xbfd8eb).lerp(new THREE.Color(0x6fc4ff), clarityMix);
const nightSkyBase = new THREE.Color(0x08111f); const nightSkyBase = new THREE.Color(0x08111f);
const nightSkyBlend = new THREE.Color(0x182940); const nightSkyBlend = new THREE.Color(0x182940);
const skyBaseColor = warmSkyBase.clone().lerp(coolSkyBase, sunMix * 0.92).lerp(nightSkyBase, nightMix); const skyBaseColor = warmSkyBase.clone().lerp(coolSkyBase, sunMix * 0.92).lerp(nightSkyBase, nightMix);
const skyBlendColor = skyBaseColor.clone().lerp(fogColor, 0.42).lerp(nightSkyBlend, nightMix * 0.78); const fogBlend = THREE.MathUtils.lerp(0.42, 0.16, clarityMix);
const skyBlendColor = skyBaseColor.clone().lerp(fogColor, fogBlend).lerp(nightSkyBlend, nightMix * 0.78);
return { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor }; return { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor };
} }
@@ -1535,9 +1692,17 @@ export class OceanScene {
this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setSize(window.innerWidth, window.innerHeight);
if (this.depthTarget) {
this.depthTarget.setSize(window.innerWidth, window.innerHeight);
}
if (this.composer) { if (this.composer) {
this.composer.setSize(window.innerWidth, window.innerHeight); this.composer.setSize(window.innerWidth, window.innerHeight);
} }
if (this.fogPass) {
this.fogPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
this.fogPass.material.uniforms.cameraNear.value = this.camera.near;
this.fogPass.material.uniforms.cameraFar.value = this.camera.far;
}
if (this.rainPass) { if (this.rainPass) {
this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); this.rainPass.material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
} }
@@ -1618,8 +1783,13 @@ export class OceanScene {
} }
} }
setFogEnabled(value) {
this.params.fogEnabled = value;
this.updateFog();
}
setFogDensity(value) { setFogDensity(value) {
this.params.fogDensity = value; this.params.fogDensity = THREE.MathUtils.clamp(value, 0, 2);
this.updateFog(); this.updateFog();
} }
@@ -1823,6 +1993,7 @@ export class OceanScene {
this.setCloudCoverage(mergedParams.cloudCoverage); this.setCloudCoverage(mergedParams.cloudCoverage);
this.setCloudDensity(mergedParams.cloudDensity); this.setCloudDensity(mergedParams.cloudDensity);
this.setCloudElevation(mergedParams.cloudElevation); this.setCloudElevation(mergedParams.cloudElevation);
this.setFogEnabled(mergedParams.fogEnabled ?? true);
this.setFogDensity(mergedParams.fogDensity); this.setFogDensity(mergedParams.fogDensity);
this.setFogHeight(mergedParams.fogHeight); this.setFogHeight(mergedParams.fogHeight);
this.setFogRange(mergedParams.fogRange); this.setFogRange(mergedParams.fogRange);
@@ -2088,75 +2259,51 @@ export class OceanScene {
} }
updateFog() { updateFog() {
const { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor } = this.getAtmosphereColors(); if (!this.fogPass) return;
const fogDensity = THREE.MathUtils.lerp(0.00015, 0.0018, this.params.fogDensity);
const { fogColor, horizonColor } = this.getAtmosphereColors();
const lightningMix = THREE.MathUtils.clamp(this.lightningFlash * 0.42, 0, 1); const lightningMix = THREE.MathUtils.clamp(this.lightningFlash * 0.42, 0, 1);
fogColor.lerp(new THREE.Color(0xdbe8f5), lightningMix); fogColor.lerp(new THREE.Color(0xdbe8f5), lightningMix);
horizonColor.lerp(new THREE.Color(0xe5eef9), lightningMix); horizonColor.lerp(new THREE.Color(0xe5eef9), lightningMix);
skyBlendColor.lerp(new THREE.Color(0xdbe7f3), lightningMix);
if (this.scene.fog) { const uniforms = this.fogPass.material.uniforms;
this.scene.fog.color.copy(fogColor); uniforms.fogColor.value.copy(fogColor);
this.scene.fog.density = fogDensity * THREE.MathUtils.lerp(0.7, 1.4, this.params.fogRange); uniforms.horizonColor.value.copy(horizonColor);
} uniforms.fogDensity.value = this.params.fogEnabled
? THREE.MathUtils.lerp(0.00002, 0.00078, this.params.fogDensity / 2.0)
: 0.0;
uniforms.fogHeight.value = this.params.fogHeight;
uniforms.fogRange.value = this.params.fogRange;
uniforms.projectionMatrixInverse.value.copy(this.camera.projectionMatrixInverse);
uniforms.viewMatrixInverse.value.copy(this.camera.matrixWorld);
uniforms.cameraWorldPosition.value.copy(this.camera.position);
}
if (!this.fogGroup) return; updateDepthTarget() {
if (!this.depthTarget) return;
const fogLayerColor = horizonColor.clone().lerp(fogColor, 0.55); const hiddenObjects = [
const heightBase = THREE.MathUtils.lerp(-20, 95, this.params.fogHeight); this.sky,
const verticalSpread = THREE.MathUtils.lerp(20, 110, this.params.fogHeight); this.starField,
const rangeOpacity = THREE.MathUtils.lerp(0.55, 1.35, this.params.fogRange); this.moonSprite,
const nearSeaMist = THREE.MathUtils.lerp(0.12, 0.9, this.params.fogDensity) * THREE.MathUtils.lerp(0.8, 1.22, this.params.fogRange); this.moonGlowSprite,
this.galaxyBand
].filter(Boolean);
const previousVisibility = hiddenObjects.map((object) => object.visible);
this.fogLayers.forEach((layer, index) => { hiddenObjects.forEach((object) => {
if (layer.isLowLayer) { object.visible = false;
layer.mesh.position.y = THREE.MathUtils.lerp(1.5, 18.0, this.params.fogHeight) + index * 2.8;
layer.mesh.scale.set(
THREE.MathUtils.lerp(0.92, 1.28, this.params.fogRange),
1,
THREE.MathUtils.lerp(0.92, 1.24, this.params.fogRange)
);
layer.mesh.material.opacity = layer.baseOpacity * nearSeaMist;
layer.mesh.material.color.copy(horizonColor.clone().lerp(fogColor, 0.72));
} else {
layer.mesh.position.y = heightBase + (index - 2) * verticalSpread * 0.42;
layer.mesh.scale.setScalar(THREE.MathUtils.lerp(0.82, 1.2, this.params.fogRange));
layer.mesh.material.opacity = layer.baseOpacity * THREE.MathUtils.lerp(0.16, 1.18, this.params.fogDensity) * rangeOpacity;
layer.mesh.material.color.copy(fogLayerColor);
}
layer.mesh.visible = layer.mesh.material.opacity > 0.01;
}); });
if (this.horizonFog) { const previousRenderTarget = this.renderer.getRenderTarget();
this.horizonFog.material.color.copy(horizonColor.clone().lerp(fogColor, 0.18)); this.renderer.setRenderTarget(this.depthTarget);
this.horizonFog.material.opacity = this.renderer.clear();
THREE.MathUtils.lerp(0.12, 0.42, this.params.fogDensity) * this.renderer.render(this.scene, this.camera);
THREE.MathUtils.lerp(0.82, 1.34, this.params.fogRange); this.renderer.setRenderTarget(previousRenderTarget);
this.horizonFog.position.y = THREE.MathUtils.lerp(52, 168, this.params.fogHeight);
this.horizonFog.scale.set(
THREE.MathUtils.lerp(0.92, 1.34, this.params.fogRange),
THREE.MathUtils.lerp(0.72, 1.12, this.params.fogHeight),
THREE.MathUtils.lerp(0.92, 1.34, this.params.fogRange)
);
this.horizonFog.visible = this.horizonFog.material.opacity > 0.01;
}
if (this.skyHazeBand) { hiddenObjects.forEach((object, index) => {
this.skyHazeBand.material.color.copy(skyBlendColor); object.visible = previousVisibility[index];
this.skyHazeBand.material.opacity = });
THREE.MathUtils.lerp(0.04, 0.18, this.params.fogDensity) *
THREE.MathUtils.lerp(0.78, 1.08, this.params.fogRange);
const nightHaze = THREE.MathUtils.clamp((-this.params.elevation + 1.0) / 12.0, 0, 1);
this.skyHazeBand.material.color.lerp(new THREE.Color(0x203754), nightHaze * 0.72);
this.skyHazeBand.material.opacity += nightHaze * 0.12;
this.skyHazeBand.position.y = THREE.MathUtils.lerp(340, 560, this.params.fogHeight);
this.skyHazeBand.scale.set(
THREE.MathUtils.lerp(0.98, 1.12, this.params.fogRange),
THREE.MathUtils.lerp(0.92, 1.1, this.params.fogHeight),
THREE.MathUtils.lerp(0.98, 1.12, this.params.fogRange)
);
this.skyHazeBand.visible = this.skyHazeBand.material.opacity > 0.01;
}
} }
animate() { animate() {
@@ -2183,27 +2330,13 @@ export class OceanScene {
this.starField.material.uniforms.time.value = time; this.starField.material.uniforms.time.value = time;
} }
if (this.fogGroup) {
this.fogLayers.forEach((layer, index) => {
layer.texture.offset.x = time * layer.speedX;
layer.texture.offset.y = time * layer.speedY;
layer.mesh.rotation.z += Math.sin(time * 0.05 + index) * 0.00002;
});
if (this.horizonFog?.userData.texture) {
this.horizonFog.userData.texture.offset.x = time * 0.00035;
}
if (this.skyHazeBand?.userData.texture) {
this.skyHazeBand.userData.texture.offset.x = -time * 0.00018;
}
}
if (this.vegetationSystem) { if (this.vegetationSystem) {
this.vegetationSystem.update(time); this.vegetationSystem.update(time);
} }
if (this.windTurbine) { for (const turbine of this.windTurbines) {
this.windTurbine.faceDirection(this.sun); turbine.faceDirection(this.sun);
this.windTurbine.update(time, delta); turbine.update(time, delta);
} }
if (this.lightningFlash > 0.001) { if (this.lightningFlash > 0.001) {
@@ -2218,9 +2351,14 @@ export class OceanScene {
if (this.snowPass) { if (this.snowPass) {
this.snowPass.material.uniforms.time.value = time; this.snowPass.material.uniforms.time.value = time;
} }
if (this.fogPass) {
this.fogPass.material.uniforms.time.value = time;
}
this.controls.update(); this.controls.update();
this.updateFog();
if (this.composer) { if (this.composer) {
this.updateDepthTarget();
this.composer.render(); this.composer.render();
} else { } else {
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);

View File

@@ -54,9 +54,8 @@ function setupControls(oceanScene) {
'cloudCoverage', 'cloudCoverage',
'cloudDensity', 'cloudDensity',
'cloudElevation', 'cloudElevation',
'fogEnabled',
'fogDensity', 'fogDensity',
'fogHeight',
'fogRange',
'rainEnabled', 'rainEnabled',
'rainScreenIntensity', 'rainScreenIntensity',
'rainVeilIntensity', 'rainVeilIntensity',
@@ -85,9 +84,8 @@ function setupControls(oceanScene) {
cloudCoverage: '云层覆盖度', cloudCoverage: '云层覆盖度',
cloudDensity: '云层密度', cloudDensity: '云层密度',
cloudElevation: '云层高度', cloudElevation: '云层高度',
fogEnabled: '是否启用雾气',
fogDensity: '雾气浓度', fogDensity: '雾气浓度',
fogHeight: '雾气高度',
fogRange: '雾气范围',
rainEnabled: '是否启用雨效', rainEnabled: '是否启用雨效',
rainScreenIntensity: '屏幕雨滴强度', rainScreenIntensity: '屏幕雨滴强度',
rainVeilIntensity: '雨线强度', rainVeilIntensity: '雨线强度',
@@ -202,11 +200,6 @@ function setupControls(oceanScene) {
bindController(cloudFolder.add(params, 'cloudDensity', 0, 1, 0.01).name('密度'), (value) => oceanScene.setCloudDensity(value)); bindController(cloudFolder.add(params, 'cloudDensity', 0, 1, 0.01).name('密度'), (value) => oceanScene.setCloudDensity(value));
bindController(cloudFolder.add(params, 'cloudElevation', 0, 1, 0.01).name('高度'), (value) => oceanScene.setCloudElevation(value)); bindController(cloudFolder.add(params, 'cloudElevation', 0, 1, 0.01).name('高度'), (value) => oceanScene.setCloudElevation(value));
const fogFolder = gui.addFolder('雾气');
bindController(fogFolder.add(params, 'fogDensity', 0, 1, 0.01).name('浓度'), (value) => oceanScene.setFogDensity(value));
bindController(fogFolder.add(params, 'fogHeight', 0, 1, 0.01).name('高度'), (value) => oceanScene.setFogHeight(value));
bindController(fogFolder.add(params, 'fogRange', 0, 1, 0.01).name('范围'), (value) => oceanScene.setFogRange(value));
const rainFolder = gui.addFolder('雨效'); const rainFolder = gui.addFolder('雨效');
bindController(rainFolder.add(params, 'rainEnabled').name('启用雨效'), (value) => oceanScene.setRainEnabled(value)); bindController(rainFolder.add(params, 'rainEnabled').name('启用雨效'), (value) => oceanScene.setRainEnabled(value));
bindController(rainFolder.add(params, 'rainVeilIntensity', 0.5, 2.5, 0.01).name('雨线强度'), (value) => oceanScene.setRainVeilIntensity(value)); bindController(rainFolder.add(params, 'rainVeilIntensity', 0.5, 2.5, 0.01).name('雨线强度'), (value) => oceanScene.setRainVeilIntensity(value));
@@ -214,12 +207,16 @@ function setupControls(oceanScene) {
bindController(rainFolder.add(params, 'rainAudioVolume', 0, 1, 0.01).name('雨声音量'), (value) => oceanScene.setRainAudioVolume(value)); bindController(rainFolder.add(params, 'rainAudioVolume', 0, 1, 0.01).name('雨声音量'), (value) => oceanScene.setRainAudioVolume(value));
bindController(rainFolder.add(params, 'lightningEnabled').name('启用雷闪'), (value) => oceanScene.setLightningEnabled(value)); bindController(rainFolder.add(params, 'lightningEnabled').name('启用雷闪'), (value) => oceanScene.setLightningEnabled(value));
const fogFolder = gui.addFolder('雾气');
bindController(fogFolder.add(params, 'fogEnabled').name('启用雾气'), (value) => oceanScene.setFogEnabled(value));
bindController(fogFolder.add(params, 'fogDensity', 0, 2, 0.01).name('雾气浓度'), (value) => oceanScene.setFogDensity(value));
const snowFolder = gui.addFolder('雪效'); const snowFolder = gui.addFolder('雪效');
bindController(snowFolder.add(params, 'snowEnabled').name('启用降雪'), (value) => oceanScene.setSnowEnabled(value)); bindController(snowFolder.add(params, 'snowEnabled').name('启用降雪'), (value) => oceanScene.setSnowEnabled(value));
bindController(snowFolder.add(params, 'snowIntensity', 0, 1.5, 0.01).name('雪量'), (value) => oceanScene.setSnowIntensity(value)); bindController(snowFolder.add(params, 'snowIntensity', 0, 1.5, 0.01).name('雪量'), (value) => oceanScene.setSnowIntensity(value));
bindController(snowFolder.add(params, 'snowSpeed', 0.2, 2.2, 0.01).name('速度'), (value) => oceanScene.setSnowSpeed(value)); bindController(snowFolder.add(params, 'snowSpeed', 0.2, 2.2, 0.01).name('速度'), (value) => oceanScene.setSnowSpeed(value));
gui.close(); [skyFolder, bloomFolder, waterFolder, cloudFolder, rainFolder, fogFolder, snowFolder].forEach((folder) => folder.close());
updateStarControllerState(); updateStarControllerState();
} }

View File

@@ -2,7 +2,7 @@ import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { WindTurbine } from './WindTurbine.js'; import { WindTurbine } from './WindTurbine.js';
const DEFAULT_MODEL_URL = '/models/offshore-wind-turbine.glb'; const DEFAULT_MODEL_URL = '/models/BJY_FJ_Shell.gltf';
export class OffshoreWindTurbineAsset { export class OffshoreWindTurbineAsset {
constructor({ constructor({
@@ -26,7 +26,10 @@ export class OffshoreWindTurbineAsset {
this.rotors = []; this.rotors = [];
this.mixer = null; this.mixer = null;
this.model = null; this.model = null;
this.fallback = null;
this.usingFallback = false; this.usingFallback = false;
this.nacelleRoot = null;
this.nacelleBaseQuaternion = null;
} }
async load() { async load() {
@@ -52,6 +55,7 @@ export class OffshoreWindTurbineAsset {
const model = gltf.scene; const model = gltf.scene;
this.fitModelToTargetHeight(model); this.fitModelToTargetHeight(model);
this.prepareModel(model); this.prepareModel(model);
this.captureMovingParts(model);
this.alignModelToWaterline(model); this.alignModelToWaterline(model);
this.group.add(model); this.group.add(model);
this.model = model; this.model = model;
@@ -71,7 +75,7 @@ export class OffshoreWindTurbineAsset {
rotorSpeed: this.rotorSpeed rotorSpeed: this.rotorSpeed
}); });
this.group.add(fallback.group); this.group.add(fallback.group);
this.rotors = fallback.rotors; this.fallback = fallback;
this.usingFallback = true; this.usingFallback = true;
} }
@@ -98,6 +102,27 @@ export class OffshoreWindTurbineAsset {
} }
captureMovingParts(model) {
this.rotors = [];
this.nacelleRoot = null;
this.nacelleBaseQuaternion = null;
model.traverse((child) => {
const normalizedName = child.name.toLowerCase();
if (!this.nacelleRoot && normalizedName.includes('fj_hz_f')) {
this.nacelleRoot = child;
this.nacelleBaseQuaternion = child.quaternion.clone();
}
if (normalizedName.includes('lungu')) {
child.userData.baseQuaternion = child.quaternion.clone();
child.userData.spinAngle = 0;
this.rotors.push(child);
}
});
}
fitModelToTargetHeight(model, targetHeight = 340) { fitModelToTargetHeight(model, targetHeight = 340) {
const box = new THREE.Box3().setFromObject(model); const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3());
@@ -122,6 +147,16 @@ export class OffshoreWindTurbineAsset {
flatDirection.normalize(); flatDirection.normalize();
const yaw = Math.atan2(flatDirection.x, flatDirection.z); const yaw = Math.atan2(flatDirection.x, flatDirection.z);
if (this.nacelleRoot && this.nacelleBaseQuaternion) {
const nacelleYaw = yaw - this.group.rotation.y;
const yawOffset = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
nacelleYaw
);
this.nacelleRoot.quaternion.copy(this.nacelleBaseQuaternion).premultiply(yawOffset);
return;
}
this.group.rotation.y = yaw; this.group.rotation.y = yaw;
} }
@@ -129,5 +164,21 @@ export class OffshoreWindTurbineAsset {
if (this.mixer) { if (this.mixer) {
this.mixer.update(delta); this.mixer.update(delta);
} }
if (this.fallback) {
this.fallback.update(time, delta);
return;
}
this.rotors.forEach((rotor, index) => {
const gust = 1.0 + Math.sin(time * 0.28 + index * 1.7) * 0.08;
rotor.userData.spinAngle += this.rotorSpeed * gust * delta;
const spin = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 0, 1),
rotor.userData.spinAngle
);
rotor.quaternion.copy(rotor.userData.baseQuaternion).multiply(spin);
});
} }
} }

View File

@@ -11,6 +11,7 @@ export const DEFAULT_SCENE_PARAMS = {
cloudCoverage: 0.26, cloudCoverage: 0.26,
cloudDensity: 0.38, cloudDensity: 0.38,
cloudElevation: 0.66, cloudElevation: 0.66,
fogEnabled: true,
fogDensity: 0.16, fogDensity: 0.16,
fogHeight: 0.26, fogHeight: 0.26,
fogRange: 0.38, fogRange: 0.38,
@@ -68,18 +69,18 @@ export const WEATHER_PRESETS = {
...DEFAULT_SCENE_PARAMS, ...DEFAULT_SCENE_PARAMS,
elevation: 72, elevation: 72,
azimuth: 180, azimuth: 180,
exposure: 0.27, exposure: 0.45,
turbidity: 8.2, turbidity: 2.2,
rayleigh: 2.1, rayleigh: 0.18,
bloomStrength: 0.14, bloomStrength: 0.2,
bloomRadius: 0.06, bloomRadius: 0.03,
waterColor: '#2b78a4', waterColor: '#25b7d9',
cloudCoverage: 0.18, cloudCoverage: 0.06,
cloudDensity: 0.28, cloudDensity: 0.12,
cloudElevation: 0.7, cloudElevation: 0.82,
fogDensity: 0.1, fogDensity: 0.015,
fogHeight: 0.2, fogHeight: 0.12,
fogRange: 0.24, fogRange: 0.08,
starIntensity: 0.0, starIntensity: 0.0,
lightningEnabled: false lightningEnabled: false
} }