Files
TkAstral3D/packages/sdk/lib/core/objects/WaterPool.js
T

1563 lines
56 KiB
JavaScript

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