import * as THREE from 'three'; import { Water } from 'three/addons/objects/Water.js'; import { Sky } from 'three/addons/objects/Sky.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { TerrainGenerator } from './TerrainGenerator.js'; import { VegetationSystem } from './VegetationSystem.js'; export class OceanScene { constructor(container) { this.container = container; this.scene = null; this.camera = null; this.renderer = null; this.controls = null; this.water = null; this.sky = null; this.sun = new THREE.Vector3(); this.terrain = null; this.vegetation = null; this.vegetationSystem = null; this.pmremGenerator = null; this.renderTarget = null; this.sunLight = null; this.vegetationFillLight = null; this.composer = null; this.bloomPass = null; this.cloudGroup = null; this.cloudMaterials = []; this.cloudLayers = []; this.params = { elevation: 2, azimuth: 180, exposure: 0.1, turbidity: 10, rayleigh: 2, bloomStrength: 0.1, bloomRadius: 0, bloomThreshold: 0, cloudCoverage: 0.4, cloudDensity: 0.5, cloudElevation: 0.5, mieCoefficient: 0.005, mieDirectionalG: 0.8 }; this.clock = new THREE.Clock(); this.frameCount = 0; this.lastTime = performance.now(); } async init() { this.initRenderer(); this.initScene(); this.initCamera(); this.initControls(); this.initLighting(); this.initPostProcessing(); await this.initSky(); this.initClouds(); await this.initWater(); await this.initTerrain(); await this.initVegetation(); this.initSunPosition(); this.initEventListeners(); } initRenderer() { this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' }); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.toneMapping = THREE.ACESFilmicToneMapping; this.renderer.toneMappingExposure = this.params.exposure; this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; this.container.appendChild(this.renderer.domElement); } initScene() { this.scene = new THREE.Scene(); this.scene.fog = new THREE.FogExp2(0x8cb8d4, 0.0008); } initCamera() { this.camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 20000 ); this.camera.position.set(100, 50, 200); } initControls() { this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping = true; this.controls.dampingFactor = 0.05; this.controls.maxPolarAngle = Math.PI * 0.48; this.controls.minDistance = 30; this.controls.maxDistance = 1000; this.controls.target.set(0, 10, 0); this.controls.update(); } initLighting() { const ambientLight = new THREE.AmbientLight(0x555555); this.scene.add(ambientLight); this.sunLight = new THREE.DirectionalLight(0xffffff, 1.5); this.sunLight.castShadow = true; this.sunLight.shadow.mapSize.width = 2048; this.sunLight.shadow.mapSize.height = 2048; this.sunLight.shadow.camera.near = 0.5; this.sunLight.shadow.camera.far = 500; this.sunLight.shadow.camera.left = -100; this.sunLight.shadow.camera.right = 100; this.sunLight.shadow.camera.top = 100; this.sunLight.shadow.camera.bottom = -100; this.scene.add(this.sunLight); this.vegetationFillLight = new THREE.DirectionalLight(0xffb06a, 0.95); this.vegetationFillLight.castShadow = false; this.scene.add(this.vegetationFillLight); } initPostProcessing() { this.composer = new EffectComposer(this.renderer); this.composer.addPass(new RenderPass(this.scene, this.camera)); this.bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), this.params.bloomStrength, this.params.bloomRadius, this.params.bloomThreshold ); this.composer.addPass(this.bloomPass); } async initSky() { this.sky = new Sky(); this.sky.scale.setScalar(10000); this.scene.add(this.sky); const skyUniforms = this.sky.material.uniforms; skyUniforms['turbidity'].value = this.params.turbidity; skyUniforms['rayleigh'].value = this.params.rayleigh; skyUniforms['mieCoefficient'].value = this.params.mieCoefficient; skyUniforms['mieDirectionalG'].value = this.params.mieDirectionalG; this.pmremGenerator = new THREE.PMREMGenerator(this.renderer); } initClouds() { const cloudTexture = this.createCloudTexture(); this.cloudGroup = new THREE.Group(); this.cloudGroup.position.y = -120; this.addCloudDomeLayer(cloudTexture, { radius: 4200, opacity: 0.42, repeatX: 5.5, repeatY: 2.4, speedX: 0.00045, speedY: 0.00012 }); this.addCloudDomeLayer(cloudTexture, { radius: 3900, opacity: 0.28, repeatX: 7.5, repeatY: 3.2, speedX: -0.00032, speedY: 0.00018 }); this.scene.add(this.cloudGroup); this.setCloudElevation(this.params.cloudElevation); this.setCloudCoverage(this.params.cloudCoverage); this.updateClouds(); } addCloudDomeLayer(texture, config) { const layerTexture = texture.clone(); layerTexture.wrapS = THREE.RepeatWrapping; layerTexture.wrapT = THREE.RepeatWrapping; layerTexture.repeat.set(config.repeatX, config.repeatY); layerTexture.needsUpdate = true; const material = new THREE.MeshBasicMaterial({ map: layerTexture, alphaMap: layerTexture, color: 0xffffff, transparent: true, opacity: config.opacity, side: THREE.BackSide, depthWrite: false, alphaTest: 0.08, fog: false }); const geometry = new THREE.SphereGeometry( config.radius, 32, 20, 0, Math.PI * 2, 0, Math.PI * 0.48 ); const mesh = new THREE.Mesh(geometry, material); mesh.rotation.y = Math.random() * Math.PI * 2; this.cloudMaterials.push(material); this.cloudLayers.push({ mesh, texture: layerTexture, speedX: config.speedX, speedY: config.speedY, baseOpacity: config.opacity }); this.cloudGroup.add(mesh); } createCloudTexture() { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; const context = canvas.getContext('2d'); context.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < 28; i++) { const x = 28 + Math.random() * 200; const y = 52 + Math.random() * 152; const radius = 28 + Math.random() * 44; const gradient = context.createRadialGradient(x, y, 0, x, y, radius); gradient.addColorStop(0, 'rgba(255,255,255,0.92)'); gradient.addColorStop(0.28, 'rgba(255,255,255,0.78)'); gradient.addColorStop(0.62, 'rgba(255,255,255,0.22)'); gradient.addColorStop(1, 'rgba(255,255,255,0)'); context.fillStyle = gradient; context.fillRect(x - radius, y - radius, radius * 2, radius * 2); } const texture = new THREE.CanvasTexture(canvas); texture.colorSpace = THREE.SRGBColorSpace; texture.needsUpdate = true; return texture; } async initWater() { const waterGeometry = new THREE.PlaneGeometry(10000, 10000, 128, 128); const waterNormals = await new Promise((resolve) => { new THREE.TextureLoader().load( '/textures/waternormals.jpg', (texture) => { texture.wrapS = texture.wrapT = THREE.RepeatWrapping; resolve(texture); } ); }); this.water = new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: waterNormals, sunDirection: new THREE.Vector3(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 3.7, fog: true }); this.water.rotation.x = -Math.PI / 2; this.water.position.y = -0.15; this.scene.add(this.water); } async initTerrain() { const terrainGen = new TerrainGenerator({ size: 1200, // 地形平面尺寸 segments: 200, // 网格细分数,越高细节越多 maxHeight: 34, // 海面以上地形的最大起伏高度 waterLevel: 0, // 海平面基准高度 underwaterDepthBias: 4.5, // 整体压低海面以下地形,避免浅滩露出 landBias: 0.2, // 整体抬升陆地倾向,增大陆地露出面积 falloffStartRatio: 0.22, // 从中心向外开始下沉的起始比例 maxLandRatio: 0.46, // 大陆海岸线的大致外缘比例 edgeDepth: 12, // 海岸外侧向海底下沉的强度 coreRadiusRatio: 0.24, // 大陆核心高地区域半径比例 continentLift: 0.55, // 核心大陆的额外抬升强度 coastVariance: 0.05, // 海岸线形状起伏幅度,越大越不规则 outerShelfDepth: 4, // 大陆外侧陆架的额外下沉深度 seed: 42 // 固定随机种子,保证地形稳定复现 }); this.terrain = terrainGen.generate(); this.scene.add(this.terrain); this.terrainGenerator = terrainGen; } async initVegetation() { const vegSystem = new VegetationSystem(this.terrainGenerator, { grassCount: 28000, // 草簇数量,越大地表覆盖越密 shrubCount: 12, // 灌木和低矮植物数量 treeCount: 9, // 树木数量 terrainSize: 1200, // 植被允许分布的地形范围 waterLevel: 1 // 植被生成时参考的水位,避免贴近海边 }); this.vegetation = vegSystem.generate(); this.vegetationSystem = vegSystem; vegSystem.addToScene(this.scene); } initSunPosition() { this.updateSun(); } updateSun() { const phi = THREE.MathUtils.degToRad(90 - this.params.elevation); const theta = THREE.MathUtils.degToRad(this.params.azimuth); this.sun.setFromSphericalCoords(1, phi, theta); this.sky.material.uniforms['sunPosition'].value.copy(this.sun); this.water.material.uniforms['sunDirection'].value.copy(this.sun).normalize(); if (this.sunLight) { const sunDistance = 100; this.sunLight.position.set( this.sun.x * sunDistance, this.sun.y * sunDistance, this.sun.z * sunDistance ); } if (this.vegetationFillLight) { const fillDistance = 90; this.vegetationFillLight.position.set( -this.sun.x * fillDistance * 0.45 + 35, Math.max(18, this.sun.y * fillDistance * 0.28 + 24), -this.sun.z * fillDistance * 0.35 + 28 ); this.vegetationFillLight.intensity = THREE.MathUtils.lerp(0.7, 1.15, THREE.MathUtils.clamp((this.sun.y + 0.2) / 0.9, 0, 1)); } if (this.renderTarget) { this.renderTarget.dispose(); } const sceneEnv = new THREE.Scene(); sceneEnv.add(this.sky); this.renderTarget = this.pmremGenerator.fromScene(sceneEnv); this.scene.environment = this.renderTarget.texture; this.scene.add(this.sky); this.scene.fog.color.setHex(this.getFogColor()); this.updateClouds(); } getFogColor() { const elevation = this.params.elevation; if (elevation < 0) { return 0x1a2a3a; } else if (elevation < 10) { return 0x4a5a6a; } else if (elevation < 20) { return 0x8cb8d4; } else if (elevation < 45) { return 0x9ec5db; } else if (elevation < 70) { return 0xb8d4e8; } else { return 0xd4e8f4; } } initEventListeners() { window.addEventListener('resize', () => this.onWindowResize()); } onWindowResize() { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); if (this.composer) { this.composer.setSize(window.innerWidth, window.innerHeight); } } setSunElevation(value) { this.params.elevation = value; this.updateSun(); } setSunAzimuth(value) { this.params.azimuth = value; this.updateSun(); } setExposure(value) { this.params.exposure = value; this.renderer.toneMappingExposure = value; } setTurbidity(value) { this.params.turbidity = value; this.sky.material.uniforms['turbidity'].value = value; this.updateSun(); } setRayleigh(value) { this.params.rayleigh = value; this.sky.material.uniforms['rayleigh'].value = value; this.updateSun(); } setBloomStrength(value) { this.params.bloomStrength = value; if (this.bloomPass) { this.bloomPass.strength = value; } } setBloomRadius(value) { this.params.bloomRadius = value; if (this.bloomPass) { this.bloomPass.radius = value; } } setBloomThreshold(value) { this.params.bloomThreshold = value; if (this.bloomPass) { this.bloomPass.threshold = value; } } setCloudCoverage(value) { this.params.cloudCoverage = value; this.updateClouds(); } setCloudDensity(value) { this.params.cloudDensity = value; this.updateClouds(); } setCloudElevation(value) { this.params.cloudElevation = value; if (this.cloudGroup) { this.cloudGroup.position.y = THREE.MathUtils.lerp(-380, 260, value); } } updateClouds() { if (!this.cloudGroup) return; const sunMix = THREE.MathUtils.clamp((this.params.elevation + 10) / 100, 0, 1); const warmCloud = new THREE.Color(0xdab188); const dayCloud = new THREE.Color(0xd1dbe6); const cloudColor = warmCloud.lerp(dayCloud, sunMix); for (const layer of this.cloudLayers) { const coverageFactor = 0.15 + this.params.cloudCoverage * 1.15; const densityFactor = 0.2 + this.params.cloudDensity * 1.35; const opacity = layer.baseOpacity * coverageFactor * densityFactor; layer.mesh.material.opacity = opacity; layer.mesh.material.color.copy(cloudColor); layer.mesh.visible = opacity > 0.015; } } animate() { requestAnimationFrame(() => this.animate()); const time = this.clock.getElapsedTime(); if (this.water) { this.water.material.uniforms['time'].value += 1.0 / 60.0; } if (this.cloudGroup) { this.cloudGroup.rotation.y = time * 0.004; this.cloudLayers.forEach((layer) => { layer.texture.offset.x = time * layer.speedX; layer.texture.offset.y = time * layer.speedY; }); } if (this.vegetationSystem) { this.vegetationSystem.update(time); } this.controls.update(); if (this.composer) { this.composer.render(); } else { this.renderer.render(this.scene, this.camera); } this.frameCount++; const currentTime = performance.now(); if (currentTime - this.lastTime >= 1000) { const fps = Math.round(this.frameCount * 1000 / (currentTime - this.lastTime)); const fpsElement = document.getElementById('fps'); if (fpsElement) { fpsElement.textContent = fps; } this.frameCount = 0; this.lastTime = currentTime; } } hideLoading() { const loading = document.getElementById('loading'); if (loading) { loading.style.opacity = '0'; loading.style.transition = 'opacity 0.5s ease'; setTimeout(() => { loading.style.display = 'none'; }, 500); } } }