1563 lines
56 KiB
JavaScript
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;
|
||
}
|
||
}
|