TkAstral3D/packages/sdk/lib/core/objects/WaterPool.js
2026-04-08 15:34:43 +08:00

1563 lines
56 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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