import * as THREE from 'three'; import App from '@/core/app/App'; import Loader from '@/core/loader/Loader'; const DEFAULT_LIGHT = [0.7559289460184544, 0.7559289460184544, -0.3779644730092272]; const COLOR_BLACK = new THREE.Color('black'); const WATERPOOL_CLEAR_COLOR = 0x272727; const WATERPOOL_PICK_MATERIAL = new THREE.MeshBasicMaterial({ colorWrite: false, depthWrite: false, depthTest: false, transparent: true, opacity: 0, side: THREE.DoubleSide, }); const waterPoolSet = new Set(); let waterPoolRenderingRegistered = false; let waterPoolRenderingPending = false; const waterPoolSceneState = { texture: null, resolution: new THREE.Vector2(), renderId: 0, lastCaptureId: -1, }; function markWaterPoolRenderFrame() { waterPoolSceneState.renderId += 1; } function ensureWaterPoolSceneTexture(renderer) { if (!renderer) return; renderer.getSize(waterPoolSceneState.resolution); const w = waterPoolSceneState.resolution.x; const h = waterPoolSceneState.resolution.y; const texture = waterPoolSceneState.texture; const image = texture ? texture.image : null; if (!texture || !image || image.width !== w || image.height !== h) { if (texture) texture.dispose(); waterPoolSceneState.texture = new THREE.FramebufferTexture(w, h); waterPoolSceneState.texture.minFilter = THREE.LinearFilter; waterPoolSceneState.texture.magFilter = THREE.LinearFilter; } } function captureWaterPoolSceneTexture(renderer) { if (!renderer) return; if (waterPoolSceneState.lastCaptureId === waterPoolSceneState.renderId) { return; } ensureWaterPoolSceneTexture(renderer); if (!waterPoolSceneState.texture) return; renderer.copyFramebufferToTexture(waterPoolSceneState.texture); waterPoolSceneState.lastCaptureId = waterPoolSceneState.renderId; } function registerWaterPoolRendering() { if (waterPoolRenderingRegistered) return; const viewer = App?.viewer; if (!viewer) { waterPoolRenderingPending = true; return; } waterPoolRenderingPending = false; viewer.addEventListener("beforeRender", () => { markWaterPoolRenderFrame(); }); viewer.addEventListener("afterAnimation", (e) => { if (waterPoolSet.size === 0) return; const renderer = App.viewer?.renderer; if (!renderer) return; waterPoolSet.forEach((pool) => { if (!pool || typeof pool.step !== 'function') return; pool.step(renderer); }); e.toBeRender(true); const prevClearColor = renderer.getClearColor(new THREE.Color()); const prevClearAlpha = renderer.getClearAlpha(); renderer.setRenderTarget(null); renderer.setClearColor(WATERPOOL_CLEAR_COLOR, 0); renderer.clear(); renderer.setClearColor(prevClearColor, prevClearAlpha); }); waterPoolRenderingRegistered = true; } function registerWaterPool(pool) { if (!pool) return; if (pool._waterPoolRegistered || pool._waterPoolRegistering) return; if (pool.loaded && typeof pool.loaded.then === 'function') { pool._waterPoolRegistering = true; pool.loaded.then(() => { pool._waterPoolRegistering = false; if (pool._waterPoolRemoved) return; if (pool._waterPoolRegistered) return; pool._waterPoolRegistered = true; waterPoolSet.add(pool); registerWaterPoolRendering(); }).catch(() => { pool._waterPoolRegistering = false; }); return; } pool._waterPoolRegistered = true; waterPoolSet.add(pool); registerWaterPoolRendering(); } function unregisterWaterPool(pool) { if (!pool) return; pool._waterPoolRegistered = false; pool._waterPoolRegistering = false; waterPoolSet.delete(pool); } function serializeTexture(texture) { if (!texture) return null; if (!texture.isTexture || !texture.image) return null; if (typeof document === 'undefined') return null; const image = texture.image; try { if (image instanceof HTMLCanvasElement) { return { type: 'texture', dataUrl: image.toDataURL('image/png') }; } } catch (error) { return null; } const canvas = document.createElement('canvas'); const width = image.width || image.videoWidth; const height = image.height || image.videoHeight; if (!width || !height) return null; canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) return null; try { if (image instanceof ImageData) { ctx.putImageData(image, 0, 0); } else if (image.data && image.width && image.height) { const data = image.data instanceof Uint8ClampedArray ? image.data : new Uint8ClampedArray(image.data); const imageData = new ImageData(data, image.width, image.height); ctx.putImageData(imageData, 0, 0); } else { ctx.drawImage(image, 0, 0, width, height); } return { type: 'texture', dataUrl: canvas.toDataURL('image/png') }; } catch (error) { return null; } } function restoreTexture(serialized) { if (!serialized) return null; if (serialized.type === 'texture' && serialized.dataUrl) { const tex = new THREE.TextureLoader().load(serialized.dataUrl); tex.wrapS = THREE.RepeatWrapping; tex.wrapT = THREE.RepeatWrapping; return tex; } return null; } function loadText(url) { return new Promise((resolve, reject) => { const loader = new THREE.FileLoader(); const _url = new URL(import.meta.env.BASE_URL + url, import.meta.url).href; loader.load(_url, resolve, undefined, reject); }); } let shaderChunksPromise = null; function ensureShaderChunks() { if (THREE.ShaderChunk['utils'] && THREE.ShaderChunk['cylinder_utils']) { return Promise.resolve(); } if (shaderChunksPromise) { return shaderChunksPromise; } shaderChunksPromise = Promise.all([ loadText('resource/shaders/water/utils.glsl'), loadText('resource/shaders/water/cylinder_utils.glsl') ]).then(([utils, cylinderUtils]) => { THREE.ShaderChunk['utils'] = utils; THREE.ShaderChunk['cylinder_utils'] = cylinderUtils; }).catch((error) => { shaderChunksPromise = null; throw error; }); return shaderChunksPromise; } function createWallMaterial(type, light, tiles, wallOpacity, wallMode, volumeColor) { const vertexPromise = loadText('resource/shaders/water/pool/vertex.glsl'); const fragmentPromise = type === 'cylinder' ? loadText('resource/shaders/water/cylinder_pool/fragment.glsl') : loadText('resource/shaders/water/pool/fragment.glsl'); return Promise.all([vertexPromise, fragmentPromise]).then(([vertexShader, fragmentShader]) => { const material = new THREE.RawShaderMaterial({ uniforms: { light: { value: light }, tiles: { value: tiles }, useTiles: { value: tiles ? 1 : 0 }, tilesColor: { value: volumeColor }, water: { value: null }, causticTex: { value: null }, sceneTexture: { value: null }, resolution: { value: new THREE.Vector2(1, 1) }, useSceneRefraction: { value: 1 }, surfaceTransmittance: { value: 0.65 }, normalStrength: { value: 0.6 }, refractionStrength: { value: 0.035 }, wallMode: { value: wallMode }, wallOpacity: { value: wallOpacity }, volumeColor: { value: volumeColor }, poolHeight: { value: 1 }, poolHalfSize: { value: new THREE.Vector2(1, 1) }, poolRadius: { value: 1 }, }, vertexShader: vertexShader, fragmentShader: fragmentShader, glslVersion: THREE.GLSL3, }); material.side = THREE.DoubleSide; if (wallOpacity < 1) { material.transparent = true; material.depthWrite = false; } return material; }); } class WaterSimulation { constructor(size = 256) { this._camera = new THREE.OrthographicCamera(0, 1, 1, 0, 0, 2000); this._geometry = new THREE.PlaneGeometry(2, 2); this._textureA = new THREE.WebGLRenderTarget(size, size, { type: THREE.FloatType }); this._textureB = new THREE.WebGLRenderTarget(size, size, { type: THREE.FloatType }); this.texture = this._textureA; const shadersPromises = [ loadText('resource/shaders/water/simulation/vertex.glsl'), loadText('resource/shaders/water/simulation/drop_fragment.glsl'), loadText('resource/shaders/water/simulation/normal_fragment.glsl'), loadText('resource/shaders/water/simulation/update_fragment.glsl'), ]; const delta = 1 / size; this.loaded = Promise.all(shadersPromises) .then(([vertexShader, dropFragmentShader, normalFragmentShader, updateFragmentShader]) => { const dropMaterial = new THREE.RawShaderMaterial({ uniforms: { center: { value: [0, 0] }, radius: { value: 0 }, strength: { value: 0 }, tInput: { value: null }, }, vertexShader: vertexShader, fragmentShader: dropFragmentShader, glslVersion: THREE.GLSL3, }); const normalMaterial = new THREE.RawShaderMaterial({ uniforms: { delta: { value: [delta, delta] }, tInput: { value: null }, }, vertexShader: vertexShader, fragmentShader: normalFragmentShader, glslVersion: THREE.GLSL3, }); const updateMaterial = new THREE.RawShaderMaterial({ uniforms: { delta: { value: [delta, delta] }, tInput: { value: null }, }, vertexShader: vertexShader, fragmentShader: updateFragmentShader, glslVersion: THREE.GLSL3, }); this._dropMesh = new THREE.Mesh(this._geometry, dropMaterial); this._normalMesh = new THREE.Mesh(this._geometry, normalMaterial); this._updateMesh = new THREE.Mesh(this._geometry, updateMaterial); }); } addDrop(renderer, x, y, radius, strength) { this._dropMesh.material.uniforms['center'].value = [x, y]; this._dropMesh.material.uniforms['radius'].value = radius; this._dropMesh.material.uniforms['strength'].value = strength; this._render(renderer, this._dropMesh); } stepSimulation(renderer) { this._render(renderer, this._updateMesh); } updateNormals(renderer) { this._render(renderer, this._normalMesh); } _render(renderer, mesh) { const oldTexture = this.texture; const newTexture = this.texture === this._textureA ? this._textureB : this._textureA; mesh.material.uniforms['tInput'].value = oldTexture.texture; renderer.setRenderTarget(newTexture); renderer.render(mesh, this._camera); this.texture = newTexture; } } class Caustics { constructor(lightFrontGeometry, light, size = 1024) { this._camera = new THREE.OrthographicCamera(0, 1, 1, 0, 0, 2000); this._geometry = lightFrontGeometry; this.texture = new THREE.WebGLRenderTarget(size, size, { type: THREE.UnsignedByteType }); const shadersPromises = [ loadText('resource/shaders/water/caustics/vertex.glsl'), loadText('resource/shaders/water/caustics/fragment.glsl') ]; this.loaded = Promise.all(shadersPromises) .then(([vertexShader, fragmentShader]) => { const material = new THREE.RawShaderMaterial({ uniforms: { light: { value: light }, water: { value: null }, poolHeight: { value: 1 }, poolHalfSize: { value: new THREE.Vector2(1, 1) }, poolRadius: { value: 1 }, }, vertexShader: vertexShader, fragmentShader: fragmentShader, glslVersion: THREE.GLSL3, }); this._causticMesh = new THREE.Mesh(this._geometry, material); }); } update(renderer, waterTexture) { this._causticMesh.material.uniforms['water'].value = waterTexture; renderer.setRenderTarget(this.texture); renderer.setClearColor(COLOR_BLACK, 0); renderer.clear(); renderer.render(this._causticMesh, this._camera); } } class CylinderCaustics { constructor(lightFrontGeometry, light, size = 1024) { this._camera = new THREE.OrthographicCamera(0, 1, 1, 0, 0, 2000); this._geometry = lightFrontGeometry; this.texture = new THREE.WebGLRenderTarget(size, size, { type: THREE.UnsignedByteType }); const shadersPromises = [ loadText('resource/shaders/water/cylinder_caustics/vertex.glsl'), loadText('resource/shaders/water/cylinder_caustics/fragment.glsl') ]; this.loaded = Promise.all(shadersPromises) .then(([vertexShader, fragmentShader]) => { const material = new THREE.RawShaderMaterial({ uniforms: { light: { value: light }, water: { value: null }, poolHeight: { value: 1 }, poolHalfSize: { value: new THREE.Vector2(1, 1) }, poolRadius: { value: 1 }, }, vertexShader: vertexShader, fragmentShader: fragmentShader, glslVersion: THREE.GLSL3, }); this._causticMesh = new THREE.Mesh(this._geometry, material); }); } update(renderer, waterTexture) { this._causticMesh.material.uniforms['water'].value = waterTexture; renderer.setRenderTarget(this.texture); renderer.setClearColor(COLOR_BLACK, 0); renderer.clear(); renderer.render(this._causticMesh, this._camera); } } class Water { constructor({ light, tiles, sky, segments }) { this.geometry = new THREE.PlaneGeometry(2, 2, segments, segments); const shadersPromises = [ loadText('resource/shaders/water/water/vertex.glsl'), loadText('resource/shaders/water/water/fragment.glsl') ]; this.loaded = Promise.all(shadersPromises) .then(([vertexShader, fragmentShader]) => { const baseUniforms = { light: { value: light }, tiles: { value: tiles }, useTiles: { value: tiles ? 1 : 0 }, tilesColor: { value: new THREE.Color(0x2c5965) }, sky: { value: sky }, water: { value: null }, causticTex: { value: null }, underwater: { value: false }, wallMode: { value: 0 }, volumeColor: { value: new THREE.Color(0x2c5965) }, surfaceColor: { value: new THREE.Color(0x4a8aa0) }, useSky: { value: 0 }, useSceneRefraction: { value: 1 }, surfaceTransmittance: { value: 0.65 }, normalStrength: { value: 0.6 }, refractionStrength: { value: 0.035 }, hasWall: { value: 1 }, poolHeight: { value: 1 }, poolHalfSize: { value: new THREE.Vector2(1, 1) }, poolRadius: { value: 1 }, sceneTexture: { value: null }, resolution: { value: new THREE.Vector2(1, 1) }, }; const frontUniforms = THREE.UniformsUtils.clone(baseUniforms); const backUniforms = THREE.UniformsUtils.clone(baseUniforms); this.materialFront = new THREE.RawShaderMaterial({ uniforms: frontUniforms, vertexShader: vertexShader, fragmentShader: fragmentShader, glslVersion: THREE.GLSL3, }); this.materialBack = new THREE.RawShaderMaterial({ uniforms: backUniforms, vertexShader: vertexShader, fragmentShader: fragmentShader, glslVersion: THREE.GLSL3, }); this.materialFront.side = THREE.FrontSide; this.materialBack.side = THREE.BackSide; this.materialFront.uniforms['underwater'].value = true; this.materialBack.uniforms['underwater'].value = false; this.materials = [this.materialFront, this.materialBack]; this.meshFront = new THREE.Mesh(this.geometry, this.materialFront); this.meshBack = new THREE.Mesh(this.geometry, this.materialBack); this.meshes = [this.meshFront, this.meshBack]; this.material = this.materialFront; this.mesh = this.meshFront; }); } getMaterials() { if (this.materials && this.materials.length) { return this.materials; } if (this.material) { return [this.material]; } return []; } _applyTextures(waterTexture, causticsTexture) { const materials = this.getMaterials(); if (!materials.length) { return; } materials.forEach((material) => { if (!material || !material.uniforms) { return; } material.uniforms['water'].value = waterTexture; material.uniforms['causticTex'].value = causticsTexture; }); } draw(renderer, camera, waterTexture, causticsTexture) { this._applyTextures(waterTexture, causticsTexture); if (this.meshFront) { renderer.render(this.meshFront, camera); } if (this.meshBack) { renderer.render(this.meshBack, camera); } } } class CylinderWater { constructor({ light, tiles, sky, segments }) { this.geometry = new THREE.PlaneGeometry(2, 2, segments, segments); const shadersPromises = [ loadText('resource/shaders/water/water/vertex.glsl'), loadText('resource/shaders/water/cylinder_water/fragment.glsl') ]; this.loaded = Promise.all(shadersPromises) .then(([vertexShader, fragmentShader]) => { const baseUniforms = { light: { value: light }, tiles: { value: tiles }, useTiles: { value: tiles ? 1 : 0 }, tilesColor: { value: new THREE.Color(0x2c5965) }, sky: { value: sky }, water: { value: null }, causticTex: { value: null }, underwater: { value: false }, wallMode: { value: 0 }, volumeColor: { value: new THREE.Color(0x2c5965) }, surfaceColor: { value: new THREE.Color(0x4a8aa0) }, useSky: { value: 0 }, useSceneRefraction: { value: 1 }, surfaceTransmittance: { value: 0.65 }, normalStrength: { value: 0.6 }, refractionStrength: { value: 0.035 }, hasWall: { value: 1 }, poolHeight: { value: 1 }, poolHalfSize: { value: new THREE.Vector2(1, 1) }, poolRadius: { value: 1 }, sceneTexture: { value: null }, resolution: { value: new THREE.Vector2(1, 1) }, }; const frontUniforms = THREE.UniformsUtils.clone(baseUniforms); const backUniforms = THREE.UniformsUtils.clone(baseUniforms); this.materialFront = new THREE.RawShaderMaterial({ uniforms: frontUniforms, vertexShader: vertexShader, fragmentShader: fragmentShader, glslVersion: THREE.GLSL3, }); this.materialBack = new THREE.RawShaderMaterial({ uniforms: backUniforms, vertexShader: vertexShader, fragmentShader: fragmentShader, glslVersion: THREE.GLSL3, }); this.materialFront.side = THREE.FrontSide; this.materialBack.side = THREE.BackSide; this.materialFront.uniforms['underwater'].value = true; this.materialBack.uniforms['underwater'].value = false; this.materials = [this.materialFront, this.materialBack]; this.meshFront = new THREE.Mesh(this.geometry, this.materialFront); this.meshBack = new THREE.Mesh(this.geometry, this.materialBack); this.meshes = [this.meshFront, this.meshBack]; this.material = this.materialFront; this.mesh = this.meshFront; }); } getMaterials() { if (this.materials && this.materials.length) { return this.materials; } if (this.material) { return [this.material]; } return []; } _applyTextures(waterTexture, causticsTexture) { const materials = this.getMaterials(); if (!materials.length) { return; } materials.forEach((material) => { if (!material || !material.uniforms) { return; } material.uniforms['water'].value = waterTexture; material.uniforms['causticTex'].value = causticsTexture; }); } draw(renderer, camera, waterTexture, causticsTexture) { this._applyTextures(waterTexture, causticsTexture); if (this.meshFront) { renderer.render(this.meshFront, camera); } if (this.meshBack) { renderer.render(this.meshBack, camera); } } } class SquarePoolWall { constructor(material) { this._geometry = new THREE.BufferGeometry(); const vertices = new Float32Array([ -1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, -1, 1, 1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1 ]); const indices = new Uint32Array([ 0, 1, 2, 2, 1, 3, 4, 5, 6, 6, 5, 7, 12, 13, 14, 14, 13, 15, 16, 17, 18, 18, 17, 19, 20, 21, 22, 22, 21, 23 ]); this._geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); this._geometry.setIndex(new THREE.BufferAttribute(indices, 1)); this._material = material; this.mesh = new THREE.Mesh(this._geometry, this._material); } draw(renderer, camera, waterTexture, causticsTexture) { this._applyUniforms(waterTexture, causticsTexture); renderer.render(this.mesh, camera); } _applyUniforms(waterTexture, causticsTexture) { if (!this._material || !this._material.uniforms) { return; } if (this._material.uniforms.water) { this._material.uniforms.water.value = waterTexture; } if (this._material.uniforms.causticTex) { this._material.uniforms.causticTex.value = causticsTexture; } } } class CylinderPoolWall { constructor(material, segments = 64) { this._geometry = this._buildGeometry(segments); this._material = material; this.mesh = new THREE.Mesh(this._geometry, this._material); } _buildGeometry(segments) { const geometry = new THREE.BufferGeometry(); const vertices = []; const indices = []; for (let i = 0; i <= segments; i++) { const angle = (i / segments) * Math.PI * 2; const x = Math.cos(angle); const z = Math.sin(angle); vertices.push(x, -1, z); vertices.push(x, 1, z); } for (let i = 0; i < segments; i++) { const base = i * 2; indices.push(base, base + 1, base + 2); indices.push(base + 2, base + 1, base + 3); } // 移除顶部盖面,避免与水面重叠产生伪影 geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3)); geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1)); return geometry; } draw(renderer, camera, waterTexture, causticsTexture) { this._applyUniforms(waterTexture, causticsTexture); renderer.render(this.mesh, camera); } _applyUniforms(waterTexture, causticsTexture) { if (!this._material || !this._material.uniforms) { return; } if (this._material.uniforms.water) { this._material.uniforms.water.value = waterTexture; } if (this._material.uniforms.causticTex) { this._material.uniforms.causticTex.value = causticsTexture; } } } function buildSquareRaycastTarget() { const targetGeometry = new THREE.PlaneGeometry(2, 2); const posAttr = targetGeometry.getAttribute('position'); for (let i = 0; i < posAttr.count; i++) { const y = posAttr.getY(i); posAttr.setZ(i, -y); posAttr.setY(i, 0); } posAttr.needsUpdate = true; const targetMesh = new THREE.Mesh(targetGeometry, WATERPOOL_PICK_MATERIAL); targetMesh.visible = true; targetMesh.updateMatrixWorld(true); return targetMesh; } function buildCylinderRaycastTarget() { const targetGeometry = new THREE.CircleGeometry(1, 64); const posAttr = targetGeometry.getAttribute('position'); for (let i = 0; i < posAttr.count; i++) { const y = posAttr.getY(i); posAttr.setZ(i, -y); posAttr.setY(i, 0); } posAttr.needsUpdate = true; const targetMesh = new THREE.Mesh(targetGeometry, WATERPOOL_PICK_MATERIAL); targetMesh.visible = true; targetMesh.updateMatrixWorld(true); return targetMesh; } export class WaterPool extends THREE.Group { static register(pool) { registerWaterPool(pool); } static unregister(pool) { unregisterWaterPool(pool); } static registerRendering() { registerWaterPoolRendering(); } constructor(options = {}) { super(); this.type = 'WaterPool'; this.isWaterPool = true; this.name = options.name || 'WaterPool'; this.poolType = options.type === 'cylinder' ? 'cylinder' : 'square'; this.light = options.light || DEFAULT_LIGHT; this.tiles = options.tiles || null; this.sky = options.sky || null; const wallMode = (options.wallMode === 'wall' || options.wallMode === 'volume' || options.wallMode === 'none') ? options.wallMode : null; this.hasWall = wallMode ? wallMode !== 'none' : options.wall === true; this.wallMode = wallMode || (this.hasWall ? 'wall' : 'none'); this.wallOpacity = Number.isFinite(options.wallOpacity) ? THREE.MathUtils.clamp(options.wallOpacity, 0, 1) : 1; // 体积水模式下默认关闭屏幕空间折射,避免水面发黑 this.useSceneRefraction = options.useSceneRefraction !== undefined ? options.useSceneRefraction : (this.wallMode === 'volume' ? 0 : 1); // 顶部水面透视强度(0~1),用于控制看到水下物体的程度 this.surfaceTransmittance = Number.isFinite(options.surfaceTransmittance) ? THREE.MathUtils.clamp(options.surfaceTransmittance, 0, 1) : 0.65; // 波纹法线强度与折射偏移强度,降低边缘过强的明暗对比 this.normalStrength = Number.isFinite(options.normalStrength) ? THREE.MathUtils.clamp(options.normalStrength, 0, 1) : 0.6; // 屏幕空间折射强度 this.refractionStrength = Number.isFinite(options.refractionStrength) ? THREE.MathUtils.clamp(options.refractionStrength, 0, 1) : 0.035; // 体积水颜色,避免 wallMode 时出现“发光蓝” this.volumeColor = (options.volumeColor instanceof THREE.Color) ? options.volumeColor : new THREE.Color(options.volumeColor !== undefined ? options.volumeColor : 0x2c5965); // 水面颜色默认与体积色一致,保证水面/水体颜色接近 this.surfaceColor = (options.surfaceColor instanceof THREE.Color) ? options.surfaceColor : new THREE.Color(options.surfaceColor !== undefined ? options.surfaceColor : this.volumeColor.getHex()); // 是否有天空盒,避免 sky 为空导致水面发黑 this.useSky = !!this.sky; this._halfWidth = 1; this._halfDepth = 1; this._poolHeight = 1; this._radius = 1; this._uniformHalfSize = new THREE.Vector2(1, 1); this._tempMat4 = new THREE.Matrix4(); this._tempVec3 = new THREE.Vector3(); this._disturbance = null; this._sceneTexture = null; this._resolution = new THREE.Vector2(); this._waterPoolRegistered = false; this._waterPoolRegistering = false; this._waterPoolRemoved = false; this._renderOrderBase = Number.isFinite(options.renderOrder) ? options.renderOrder : 1000; this._lastSceneCaptureFrame = -1; this._beforeRenderHandler = this._handleBeforeRender.bind(this); this._addedHandler = () => { this._waterPoolRemoved = false; WaterPool.register(this); if (waterPoolRenderingPending) { WaterPool.registerRendering(); } }; this._removedHandler = () => { this._waterPoolRemoved = true; WaterPool.unregister(this); }; this.addEventListener('added', this._addedHandler); this.addEventListener('removed', this._removedHandler); const simulationSize = options.simulationSize || 256; const causticsSize = options.causticsSize || 1024; const waterSegments = options.waterSegments || 200; const wallSegments = options.wallSegments || 64; this.simulationSize = simulationSize; this.causticsSize = causticsSize; this.waterSegments = waterSegments; this.wallSegments = wallSegments; this._updateSize(options); this.simulation = new WaterSimulation(simulationSize); this.water = this.poolType === 'cylinder' ? new CylinderWater({ light: this.light, tiles: this.tiles, sky: this.sky, segments: waterSegments }) : new Water({ light: this.light, tiles: this.tiles, sky: this.sky, segments: waterSegments }); this.caustics = this.poolType === 'cylinder' ? new CylinderCaustics(this.water.geometry, this.light, causticsSize) : new Caustics(this.water.geometry, this.light, causticsSize); this.wall = null; this.raycastTarget = this.poolType === 'cylinder' ? buildCylinderRaycastTarget() : buildSquareRaycastTarget(); const tasks = [ ensureShaderChunks(), this.simulation.loaded, this.water.loaded, this.caustics.loaded, ]; if (this.hasWall) { const wallModeValue = this.wallMode === 'volume' ? 1 : 0; const wallTask = createWallMaterial( this.poolType, this.light, this.tiles, this.wallOpacity, wallModeValue, this.volumeColor ).then((material) => { this.wall = this.poolType === 'cylinder' ? new CylinderPoolWall(material, wallSegments) : new SquarePoolWall(material); }); tasks.push(wallTask); } this.loaded = Promise.all(tasks).then(() => { this.waterMeshFront = this.water.meshFront || this.water.mesh; this.waterMeshBack = this.water.meshBack || null; this.waterMesh = this.waterMeshFront || this.water.mesh || null; this.waterMeshes = this._getWaterMeshes(); if (this.wall) { this.wallMesh = this.wall.mesh; } this._applyScale(); this._attachMeshesToGroup(); this._applyRenderOrder(); this._installRenderHooks(); return this; }); } _updateSize(options = {}) { if (this.poolType === 'cylinder') { const diameter = options.diameter || (options.radius ? options.radius * 2 : 2); const height = options.height || 2; const radius = options.radius || diameter / 2; this.diameter = diameter; this.radius = radius; this.width = diameter; this.depth = diameter; this.height = height; this._radius = radius; this._halfWidth = radius; this._halfDepth = radius; this._poolHeight = height; } else { const width = options.width || options.size || 2; const depth = options.depth || options.size || width; const height = options.height || 2; this.width = width; this.depth = depth; this.height = height; this._halfWidth = width / 2; this._halfDepth = depth / 2; this._poolHeight = height; } } _getWaterMeshes() { if (this.waterMeshes && this.waterMeshes.length) { return this.waterMeshes; } if (this.water && Array.isArray(this.water.meshes) && this.water.meshes.length) { return this.water.meshes; } if (this.waterMesh) { return [this.waterMesh]; } if (this.water && this.water.mesh) { return [this.water.mesh]; } return []; } _getWaterMaterials() { if (!this.water) { return []; } if (typeof this.water.getMaterials === 'function') { return this.water.getMaterials(); } if (Array.isArray(this.water.materials) && this.water.materials.length) { return this.water.materials; } if (this.water.material) { return [this.water.material]; } return []; } _attachMeshesToGroup() { if (this.wallMesh && this.wallMesh.parent !== this) { this.add(this.wallMesh); } const waterMeshes = this._getWaterMeshes(); waterMeshes.forEach((mesh) => { if (mesh && mesh.parent !== this) { this.add(mesh); } }); if (this.raycastTarget && this.raycastTarget.parent !== this) { this.raycastTarget.ignore = false; this.add(this.raycastTarget); } if (this.wallMesh) { this.wallMesh.ignore = true; } waterMeshes.forEach((mesh) => { if (mesh) { mesh.ignore = true; } }); this._applyProxyToMeshes(); } _applyProxyToMeshes() { this.traverse((child) => { if (child && child.isMesh) { child.proxy = this; //child.ignore = true; } }); } _applyRenderOrder() { const baseOrder = Number.isFinite(this._renderOrderBase) ? this._renderOrderBase : 0; if (this.wallMesh) { this.wallMesh.renderOrder = baseOrder; } if (this.waterMeshFront) { this.waterMeshFront.renderOrder = baseOrder + 1; } if (this.waterMeshBack) { this.waterMeshBack.renderOrder = baseOrder + 2; } if (!this.waterMeshFront && this.waterMesh) { this.waterMesh.renderOrder = baseOrder + 1; } } _installRenderHooks() { const waterMeshes = this._getWaterMeshes(); if (!waterMeshes.length) { if (this.wallMesh) { this.wallMesh.onBeforeRender = this._beforeRenderHandler; } return; } waterMeshes.forEach((mesh) => { if (!mesh) { return; } mesh.onBeforeRender = this._beforeRenderHandler; }); if (this.wallMesh) { this.wallMesh.onBeforeRender = this._beforeRenderHandler; } } _handleBeforeRender(renderer) { this._prepareSceneTexture(renderer); } _prepareSceneTexture(renderer) { if (!renderer) { return; } this._ensureSceneTexture(renderer); captureWaterPoolSceneTexture(renderer); this._applySceneTextureUniforms(); } _applySceneTextureUniforms() { const materials = this._getWaterMaterials(); if (!materials.length) { if (this.wall && this.wall._material && this.wall._material.uniforms) { if (this.wall._material.uniforms.sceneTexture) { this.wall._material.uniforms.sceneTexture.value = this._sceneTexture; } if (this.wall._material.uniforms.resolution) { this.wall._material.uniforms.resolution.value.copy(this._resolution); } } return; } materials.forEach((material) => { if (!material || !material.uniforms) { return; } if (material.uniforms.sceneTexture) { material.uniforms.sceneTexture.value = this._sceneTexture; } if (material.uniforms.resolution) { material.uniforms.resolution.value.copy(this._resolution); } }); if (this.wall && this.wall._material && this.wall._material.uniforms) { if (this.wall._material.uniforms.sceneTexture) { this.wall._material.uniforms.sceneTexture.value = this._sceneTexture; } if (this.wall._material.uniforms.resolution) { this.wall._material.uniforms.resolution.value.copy(this._resolution); } } } _applyWaterTextures(waterTexture, causticsTexture) { const materials = this._getWaterMaterials(); materials.forEach((material) => { if (!material || !material.uniforms) { return; } if (material.uniforms.water) { material.uniforms.water.value = waterTexture; } if (material.uniforms.causticTex) { material.uniforms.causticTex.value = causticsTexture; } }); if (this.wall && this.wall._material && this.wall._material.uniforms) { if (this.wall._material.uniforms.water) { this.wall._material.uniforms.water.value = waterTexture; } if (this.wall._material.uniforms.causticTex) { this.wall._material.uniforms.causticTex.value = causticsTexture; } } } _applyScale() { const waterMeshes = this._getWaterMeshes(); if (waterMeshes.length) { waterMeshes.forEach((mesh) => { if (mesh) { mesh.scale.set(1, 1, 1); } }); const geometry = waterMeshes[0].geometry; if (geometry) { // Update bounding sphere to match shader-transformed geometry. // The vertex shader scales vertices by poolHalfSize, so the actual // rendered extent exceeds the original PlaneGeometry bounding sphere. const r = this.poolType === 'cylinder' ? this._radius : Math.sqrt(this._halfWidth * this._halfWidth + this._halfDepth * this._halfDepth); geometry.boundingSphere = new THREE.Sphere( new THREE.Vector3(0, 0, 0), r ); } } if (this.wallMesh) { this.wallMesh.scale.set(1, 1, 1); // 墙体在着色器中按尺寸缩放,需要手动更新包围体避免视锥裁剪错误 this._updateWallBounds(); } if (this.raycastTarget) { const scaleX = this.poolType === 'cylinder' ? this._radius : this._halfWidth; const scaleZ = this.poolType === 'cylinder' ? this._radius : this._halfDepth; this.raycastTarget.scale.set(scaleX, 1, scaleZ); this.raycastTarget.updateMatrixWorld(true); } this._applyUniforms(); } _updateWallBounds() { if (!this.wallMesh || !this.wallMesh.geometry) { return; } const minY = -this._poolHeight; // 墙体顶部与水面齐平,避免超出水面 const maxY = 0; const min = new THREE.Vector3(-this._halfWidth, minY, -this._halfDepth); const max = new THREE.Vector3(this._halfWidth, maxY, this._halfDepth); const box = new THREE.Box3(min, max); const sphere = new THREE.Sphere(); box.getBoundingSphere(sphere); this.wallMesh.geometry.boundingBox = box; this.wallMesh.geometry.boundingSphere = sphere; } setSize(options = {}) { this._updateSize(options); this._applyScale(); } _applyUniforms() { this._uniformHalfSize.set(this._halfWidth, this._halfDepth); this._updateMaterialUniforms(this._getWaterMaterials()); this._updateMaterialUniforms(this.wall ? this.wall._material : null); this._updateMaterialUniforms(this._getCausticsMaterial()); } _updateMaterialUniforms(material) { if (Array.isArray(material)) { material.forEach((item) => this._updateMaterialUniforms(item)); return; } if (!material || !material.uniforms) { return; } if (material.uniforms.tiles) { material.uniforms.tiles.value = this.tiles; } if (material.uniforms.useTiles) { material.uniforms.useTiles.value = this.tiles ? 1 : 0; } if (material.uniforms.tilesColor) { material.uniforms.tilesColor.value = this.volumeColor; } if (material.uniforms.wallMode) { material.uniforms.wallMode.value = this.wallMode === 'volume' ? 1 : 0; } if (material.uniforms.wallOpacity) { material.uniforms.wallOpacity.value = this.wallOpacity; } if (material.uniforms.volumeColor) { material.uniforms.volumeColor.value = this.volumeColor; } if (material.uniforms.surfaceColor) { material.uniforms.surfaceColor.value = this.surfaceColor; } if (material.uniforms.useSky) { material.uniforms.useSky.value = this.useSky ? 1 : 0; } if (material.uniforms.useSceneRefraction) { material.uniforms.useSceneRefraction.value = this.useSceneRefraction ? 1 : 0; } if (material.uniforms.hasWall) { material.uniforms.hasWall.value = this.hasWall ? 1 : 0; } if (material.uniforms.surfaceTransmittance) { material.uniforms.surfaceTransmittance.value = this.surfaceTransmittance; } if (material.uniforms.normalStrength) { material.uniforms.normalStrength.value = this.normalStrength; } if (material.uniforms.refractionStrength) { material.uniforms.refractionStrength.value = this.refractionStrength; } if (material.uniforms.poolHalfSize) { material.uniforms.poolHalfSize.value = this._uniformHalfSize; } if (material.uniforms.poolHeight) { material.uniforms.poolHeight.value = this._poolHeight; } if (material.uniforms.poolRadius) { material.uniforms.poolRadius.value = this._radius; } } _getCausticsMaterial() { if (!this.caustics || !this.caustics._causticMesh) { return null; } return this.caustics._causticMesh.material || null; } setPosition(x, y, z) { this.position.set(x, y, z); const waterMeshes = this._getWaterMeshes(); waterMeshes.forEach((mesh) => { if (mesh) { mesh.position.set(0, 0, 0); } }); if (this.wallMesh) { this.wallMesh.position.set(0, 0, 0); } if (this.raycastTarget) { this.raycastTarget.position.set(0, 0, 0); this.raycastTarget.updateMatrixWorld(true); } this.updateMatrixWorld(true); } getDropCoordsFromWorld(point, out = new THREE.Vector2()) { if (!this.raycastTarget) { return out.set(0, 0); } this.raycastTarget.updateMatrixWorld(true); const local = this._tempVec3.copy(point) .applyMatrix4(this._tempMat4.copy(this.raycastTarget.matrixWorld).invert()); return out.set(local.x, local.z); } addDrop(renderer, x, y, radius, strength) { this.simulation.addDrop(renderer, x, y, radius, strength); } startDisturbance(options = {}) { const mode = options.mode === 'uniform' ? 'uniform' : 'drag'; const travelRadius = Number.isFinite(options.travelRadius) ? options.travelRadius : 0.85; const driftSpeed = Number.isFinite(options.driftSpeed) ? options.driftSpeed : 0.015; const jitter = Number.isFinite(options.jitter) ? options.jitter : 0.01; const spread = Number.isFinite(options.spread) ? options.spread : 0.08; const angle = Math.random() * Math.PI * 2; this._disturbance = { mode, dropsPerStep: options.dropsPerStep || 1, radiusMin: options.radiusMin || 0.01, radiusMax: options.radiusMax || 0.05, strengthMin: options.strengthMin || 0.005, strengthMax: options.strengthMax || 0.02, // “拖动式”随机扰动的运动参数,避免波纹过于平均 travelRadius, driftSpeed, jitter, spread, x: Math.cos(angle) * travelRadius * 0.5, y: Math.sin(angle) * travelRadius * 0.5, vx: Math.cos(angle) * driftSpeed, vy: Math.sin(angle) * driftSpeed, }; } stopDisturbance() { this._disturbance = null; } step(renderer) { if (this._disturbance) { const d = this._disturbance; if (d.mode === 'uniform') { for (let i = 0; i < d.dropsPerStep; i++) { const angle = Math.random() * Math.PI * 2; const r = Math.random() * 0.8; const radius = d.radiusMin + Math.random() * (d.radiusMax - d.radiusMin); const strength = (Math.random() > 0.5 ? 1 : -1) * (d.strengthMin + Math.random() * (d.strengthMax - d.strengthMin)); this.simulation.addDrop( renderer, Math.cos(angle) * r, Math.sin(angle) * r, radius, strength ); } } else { // 随机游走 + 抖动,模拟鼠标来回滑动的水波 d.vx += (Math.random() - 0.5) * d.jitter; d.vy += (Math.random() - 0.5) * d.jitter; const speed = Math.hypot(d.vx, d.vy); if (speed > d.driftSpeed) { const scale = d.driftSpeed / speed; d.vx *= scale; d.vy *= scale; } d.x += d.vx; d.y += d.vy; const dist2 = d.x * d.x + d.y * d.y; const limit = d.travelRadius; if (dist2 > limit * limit) { const len = Math.sqrt(dist2) || 1; d.x = (d.x / len) * limit; d.y = (d.y / len) * limit; d.vx = -d.vx; d.vy = -d.vy; } for (let i = 0; i < d.dropsPerStep; i++) { const radius = d.radiusMin + Math.random() * (d.radiusMax - d.radiusMin); const strength = (Math.random() > 0.5 ? 1 : -1) * (d.strengthMin + Math.random() * (d.strengthMax - d.strengthMin)); const ox = (Math.random() - 0.5) * d.spread; const oy = (Math.random() - 0.5) * d.spread; this.simulation.addDrop(renderer, d.x + ox, d.y + oy, radius, strength); } } } this.simulation.stepSimulation(renderer); this.simulation.updateNormals(renderer); this.caustics.update(renderer, this.simulation.texture.texture); this._applyWaterTextures(this.simulation.texture.texture, this.caustics.texture.texture); } _ensureSceneTexture(renderer) { const effect = App?.viewer?.effect; const composer = effect?.composer; const outputBuffer = effect?.enabled ? composer?.outputBuffer : null; if (outputBuffer && outputBuffer.texture) { this._sceneTexture = outputBuffer.texture; this._resolution.set(outputBuffer.width, outputBuffer.height); return; } ensureWaterPoolSceneTexture(renderer); this._sceneTexture = waterPoolSceneState.texture; if (this._sceneTexture) { this._resolution.copy(waterPoolSceneState.resolution); } } render(renderer, camera) { const waterTexture = this.simulation.texture.texture; const causticsTexture = this.caustics.texture.texture; if (!THREE.ShaderChunk['utils'] || !THREE.ShaderChunk['cylinder_utils']) { return; } this._applyWaterTextures(waterTexture, causticsTexture); this._prepareSceneTexture(renderer); if (this.parent) { return; } // 先渲染墙体,让帧缓冲包含场景+墙体 if (this.wall) { this.wall.draw(renderer, camera, waterTexture, causticsTexture); } // 最后渲染水面 this.water.draw(renderer, camera, waterTexture, causticsTexture); } clone(recursive = true) { const self = super.clone(false); self.matrix = self.matrix.toArray(); self.up = self.up.toArray(); const options = { name: this.name, type: this.poolType === 'cylinder' ? 'cylinder' : 'square', light: Array.isArray(this.light) ? [...this.light] : DEFAULT_LIGHT, tiles: this.tiles || null, sky: this.sky || null, wallMode: this.wallMode, wall: !!this.hasWall, wallOpacity: this.wallOpacity, useSceneRefraction: this.useSceneRefraction ? 1 : 0, surfaceTransmittance: this.surfaceTransmittance, normalStrength: this.normalStrength, refractionStrength: this.refractionStrength, volumeColor: this.volumeColor ? this.volumeColor.clone() : new THREE.Color(0x2c5965), surfaceColor: this.surfaceColor ? this.surfaceColor.clone() : new THREE.Color(0x4a8aa0), simulationSize: this.simulationSize, causticsSize: this.causticsSize, waterSegments: this.waterSegments, wallSegments: this.wallSegments, diameter: this.diameter, width: this.width, depth: this.depth, height: this.height, renderOrder: this._renderOrderBase, }; const cloned = new WaterPool(options); const _uuid = cloned.uuid; Loader.objectLoader.copyAttrByData(cloned, self); cloned.uuid = _uuid; if (this._disturbance) { const disturbance = { mode: this._disturbance.mode, dropsPerStep: this._disturbance.dropsPerStep, radiusMin: this._disturbance.radiusMin, radiusMax: this._disturbance.radiusMax, strengthMin: this._disturbance.strengthMin, strengthMax: this._disturbance.strengthMax, travelRadius: this._disturbance.travelRadius, driftSpeed: this._disturbance.driftSpeed, jitter: this._disturbance.jitter, spread: this._disturbance.spread, }; cloned._disturbance = { ...disturbance }; if (cloned.loaded && typeof cloned.loaded.then === 'function') { cloned.loaded.then(() => { cloned.startDisturbance(disturbance); }); } else { cloned.startDisturbance(disturbance); } } return cloned; } toJSON(meta) { const internalChildren = this.children.slice(); internalChildren.forEach((child) => this.remove(child)); const data = super.toJSON(meta); const options = { name: this.name, type: this.poolType === 'cylinder' ? 'cylinder' : 'square', light: Array.isArray(this.light) ? [...this.light] : DEFAULT_LIGHT, tiles: serializeTexture(this.tiles), wallMode: this.wallMode, wall: !!this.hasWall, wallOpacity: this.wallOpacity, useSceneRefraction: this.useSceneRefraction ? 1 : 0, surfaceTransmittance: this.surfaceTransmittance, normalStrength: this.normalStrength, refractionStrength: this.refractionStrength, volumeColor: this.volumeColor ? `#${this.volumeColor.getHexString()}` : '#2c5965', surfaceColor: this.surfaceColor ? `#${this.surfaceColor.getHexString()}` : '#4a8aa0', simulationSize: this.simulationSize, causticsSize: this.causticsSize, waterSegments: this.waterSegments, wallSegments: this.wallSegments, diameter: this.diameter, width: this.width, depth: this.depth, height: this.height, disturbance: this._disturbance ? { enabled: true, mode: this._disturbance.mode, dropsPerStep: this._disturbance.dropsPerStep, radiusMin: this._disturbance.radiusMin, radiusMax: this._disturbance.radiusMax, strengthMin: this._disturbance.strengthMin, strengthMax: this._disturbance.strengthMax, travelRadius: this._disturbance.travelRadius, driftSpeed: this._disturbance.driftSpeed, jitter: this._disturbance.jitter, spread: this._disturbance.spread, } : { enabled: false }, }; data.object.type = this.type; data.object.options = options; data.object.children = []; internalChildren.forEach((child) => this.add(child)); return data; } static fromJSON(json = {}) { const options = json.options ? { ...json.options } : {}; if (options.tiles && typeof options.tiles === 'object') { options.tiles = restoreTexture(options.tiles); } const sceneEnv = App?.viewer?.scene?.environment || null; if (sceneEnv) { options.sky = sceneEnv; } const pool = new WaterPool(options); if (pool.loaded && typeof pool.loaded.then === 'function') { pool.loaded.then(() => { pool.startDisturbance(options.disturbance); }); } else { pool.startDisturbance(options.disturbance); } return pool; } }