UNPKG

ng-cw-v12

Version:

Angular UI Component Library

1,187 lines 182 kB
import { Component, ViewChild, Input, HostListener } from '@angular/core'; import * as THREE from 'three'; import * as i0 from "@angular/core"; // ─── 顶点着色器(公用)─────────────────────────────────────────────────────── const face_vert = ` attribute vec3 position; uniform vec2 px; uniform vec2 boundarySpace; varying vec2 uv; precision highp float; void main(){ vec3 pos = position; vec2 scale = 1.0 - boundarySpace * 2.0; pos.xy = pos.xy * scale; uv = vec2(0.5) + (pos.xy) * 0.5; gl_Position = vec4(pos, 1.0); } `; // ─── 边界线顶点着色器 ────────────────────────────────────────────────────── const line_vert = ` attribute vec3 position; uniform vec2 px; precision highp float; varying vec2 uv; void main(){ vec3 pos = position; uv = 0.5 + pos.xy * 0.5; vec2 n = sign(pos.xy); pos.xy = abs(pos.xy) - px * 1.0; pos.xy *= n; gl_Position = vec4(pos, 1.0); } `; // ─── 鼠标力顶点着色器 ────────────────────────────────────────────────────── const mouse_vert = ` precision highp float; attribute vec3 position; attribute vec2 uv; uniform vec2 center; uniform vec2 scale; uniform vec2 px; varying vec2 vUv; void main(){ vec2 pos = position.xy * scale * 2.0 * px + center; vUv = uv; gl_Position = vec4(pos, 0.0, 1.0); } `; // ─── 平流片元着色器(BFECC)──────────────────────────────────────────────── const advection_frag = ` precision highp float; uniform sampler2D velocity; uniform float dt; uniform bool isBFECC; uniform vec2 fboSize; uniform vec2 px; varying vec2 uv; void main(){ vec2 ratio = max(fboSize.x, fboSize.y) / fboSize; if(isBFECC == false){ vec2 vel = texture2D(velocity, uv).xy; vec2 uv2 = uv - vel * dt * ratio; vec2 newVel = texture2D(velocity, uv2).xy; gl_FragColor = vec4(newVel, 0.0, 0.0); } else { vec2 spot_new = uv; vec2 vel_old = texture2D(velocity, uv).xy; vec2 spot_old = spot_new - vel_old * dt * ratio; vec2 vel_new1 = texture2D(velocity, spot_old).xy; vec2 spot_new2 = spot_old + vel_new1 * dt * ratio; vec2 error = spot_new2 - spot_new; vec2 spot_new3 = spot_new - error / 2.0; vec2 vel_2 = texture2D(velocity, spot_new3).xy; vec2 spot_old2 = spot_new3 - vel_2 * dt * ratio; vec2 newVel2 = texture2D(velocity, spot_old2).xy; gl_FragColor = vec4(newVel2, 0.0, 0.0); } } `; // ─── 颜色输出片元着色器 ──────────────────────────────────────────────────── const color_frag = ` precision highp float; uniform sampler2D velocity; uniform sampler2D palette; uniform vec4 bgColor; varying vec2 uv; void main(){ vec2 vel = texture2D(velocity, uv).xy; float lenv = clamp(length(vel), 0.0, 1.0); vec3 c = texture2D(palette, vec2(lenv, 0.5)).rgb; vec3 outRGB = mix(bgColor.rgb, c, lenv); float outA = mix(bgColor.a, 1.0, lenv); gl_FragColor = vec4(outRGB, outA); } `; // ─── 散度片元着色器 ──────────────────────────────────────────────────────── const divergence_frag = ` precision highp float; uniform sampler2D velocity; uniform float dt; uniform vec2 px; varying vec2 uv; void main(){ float x0 = texture2D(velocity, uv - vec2(px.x, 0.0)).x; float x1 = texture2D(velocity, uv + vec2(px.x, 0.0)).x; float y0 = texture2D(velocity, uv - vec2(0.0, px.y)).y; float y1 = texture2D(velocity, uv + vec2(0.0, px.y)).y; float divergence = (x1 - x0 + y1 - y0) / 2.0; gl_FragColor = vec4(divergence / dt); } `; // ─── 外力片元着色器 ──────────────────────────────────────────────────────── const externalForce_frag = ` precision highp float; uniform vec2 force; uniform vec2 center; uniform vec2 scale; uniform vec2 px; varying vec2 vUv; void main(){ vec2 circle = (vUv - 0.5) * 2.0; float d = 1.0 - min(length(circle), 1.0); d *= d; gl_FragColor = vec4(force * d, 0.0, 1.0); } `; // ─── 泊松压力片元着色器 ──────────────────────────────────────────────────── const poisson_frag = ` precision highp float; uniform sampler2D pressure; uniform sampler2D divergence; uniform vec2 px; varying vec2 uv; void main(){ float p0 = texture2D(pressure, uv + vec2(px.x * 2.0, 0.0)).r; float p1 = texture2D(pressure, uv - vec2(px.x * 2.0, 0.0)).r; float p2 = texture2D(pressure, uv + vec2(0.0, px.y * 2.0)).r; float p3 = texture2D(pressure, uv - vec2(0.0, px.y * 2.0)).r; float div = texture2D(divergence, uv).r; float newP = (p0 + p1 + p2 + p3) / 4.0 - div; gl_FragColor = vec4(newP); } `; // ─── 压力梯度修正片元着色器 ──────────────────────────────────────────────── const pressure_frag = ` precision highp float; uniform sampler2D pressure; uniform sampler2D velocity; uniform vec2 px; uniform float dt; varying vec2 uv; void main(){ float step = 1.0; float p0 = texture2D(pressure, uv + vec2(px.x * step, 0.0)).r; float p1 = texture2D(pressure, uv - vec2(px.x * step, 0.0)).r; float p2 = texture2D(pressure, uv + vec2(0.0, px.y * step)).r; float p3 = texture2D(pressure, uv - vec2(0.0, px.y * step)).r; vec2 v = texture2D(velocity, uv).xy; vec2 gradP = vec2(p0 - p1, p2 - p3) * 0.5; v = v - gradP * dt; gl_FragColor = vec4(v, 0.0, 1.0); } `; // ─── 粘性扩散片元着色器 ──────────────────────────────────────────────────── const viscous_frag = ` precision highp float; uniform sampler2D velocity; uniform sampler2D velocity_new; uniform float v; uniform vec2 px; uniform float dt; varying vec2 uv; void main(){ vec2 old = texture2D(velocity, uv).xy; vec2 new0 = texture2D(velocity_new, uv + vec2(px.x * 2.0, 0.0)).xy; vec2 new1 = texture2D(velocity_new, uv - vec2(px.x * 2.0, 0.0)).xy; vec2 new2 = texture2D(velocity_new, uv + vec2(0.0, px.y * 2.0)).xy; vec2 new3 = texture2D(velocity_new, uv - vec2(0.0, px.y * 2.0)).xy; vec2 newv = 4.0 * old + v * dt * (new0 + new1 + new2 + new3); newv /= 4.0 * (1.0 + v * dt); gl_FragColor = vec4(newv, 0.0, 0.0); } `; // ─── 边界处理片元着色器:速度反弹 ────────────────────────────────────────── const boundary_vel_frag = ` precision highp float; uniform sampler2D velocity; uniform vec2 px; varying vec2 uv; void main(){ vec2 n = sign(uv - 0.5); vec2 offset = n * px; vec2 vel = texture2D(velocity, uv - offset).xy; if(abs(n.x) > 0.5) vel.x *= -1.0; if(abs(n.y) > 0.5) vel.y *= -1.0; gl_FragColor = vec4(vel, 0.0, 1.0); } `; // ─── 边界处理片元着色器:压力一致 ────────────────────────────────────────── const boundary_pres_frag = ` precision highp float; uniform sampler2D pressure; uniform vec2 px; varying vec2 uv; void main(){ vec2 n = sign(uv - 0.5); vec2 offset = n * px; float p = texture2D(pressure, uv - offset).r; gl_FragColor = vec4(p, 0.0, 0.0, 1.0); } `; export class LiquidEtherBackgroundComponent { constructor(ngZone) { this.ngZone = ngZone; /** 背景颜色 */ this.ncBgColor = 'black'; /** 颜色数组,用于映射流体速度场的渲染颜色 */ this.ncColors = ['#5227FF', '#FF9FFC', '#B19EEF']; /** 鼠标拖动产生的流体力度(0-60) */ this.ncMouseForce = 20; /** 鼠标影响半径(单位:像素 10-300) */ this.ncCursorSize = 100; /** 是否启用粘性模拟(启用后运动更平滑、更厚重) */ this._isViscous = false; /** 粘性系数(仅在 ncIsViscous 为 true 时生效 1-100) */ this.ncViscous = 30; /** 粘性扩散的迭代次数(仅在ncIsViscous为true时生效 1-64,次数越多,迭代越平滑,速度越慢)) */ this.ncIterationsViscous = 32; /** 强制不可压缩性所需的压力泊松迭代次数(2-64) */ this.ncIterationsPoisson = 32; /** 在对流/扩散过程中使用固定的模拟时间步长,数值越小越精确但性能开销更高 */ this.ncDt = 0.014; /** 启用BFECC平流(误差补偿)可获得更清晰的流场;禁用可略微提高性能 */ this._bfecc = true; /** 模拟纹理相对于画布大小的比例(0~1,越小性能越好,模糊程度越高) */ this.ncResolution = 0.5; /** 是否开启边界反弹模式(流体在边界处反弹而非穿透) */ this._isBounce = false; /** 是否开启自动演示模式(无用户操作时自动驱动流体运动) */ this._autoDemo = true; /** 自动指针运动速度(标准化单位/秒,0-1) */ this.ncAutoSpeed = 0.5; /** 在自动模式下,对速度增量应用乘数(0-4) */ this.ncAutoIntensity = 2.2; /** 用户移动鼠标时,从自动指针到实际光标的插值时间(秒) */ this.ncTakeoverDuration = 0.25; /** 自动模式恢复前,短暂的无操作时间(毫秒) */ this.ncAutoResumeDelay = 1000; /** 激活后,自动移动速度从0加速到全速所需的时间(秒) */ this.ncAutoRampDuration = 0.6; this.webgl = null; this.rafId = null; this.resizeRafId = null; this.isVisible = true; } set ncIsViscous(val) { this._isViscous = val !== null && val !== undefined && val !== false && val !== 'false'; } get ncIsViscous() { return this._isViscous; } set ncBFECC(val) { this._bfecc = val !== null && val !== undefined && val !== false && val !== 'false'; } get ncBFECC() { return this._bfecc; } set ncIsBounce(val) { this._isBounce = val !== null && val !== undefined && val !== false && val !== 'false'; } get ncIsBounce() { return this._isBounce; } set ncAutoDemo(val) { this._autoDemo = val !== null && val !== undefined && val !== false && val !== 'false'; } get ncAutoDemo() { return this._autoDemo; } ngOnInit() { } ngAfterViewInit() { this.ngZone.runOutsideAngular(() => { this.initWebGL(); }); } ngOnDestroy() { this.cleanup(); } ngOnChanges(changes) { if (!this.webgl) return; // ncColors 变化需要重建调色板纹理,整体重新初始化 if (changes['ncColors']) { this.cleanup(); this.initWebGL(); return; } // 其余参数变化时,直接写入 simulation.options this.applyOptions(); } // ─── 初始化入口 ────────────────────────────────────────────────────────── initWebGL() { const container = this.containerRef.nativeElement; this.webgl = this.createWebGLManager(container); this.webgl.start(); this.setupResizeObserver(container); this.setupIntersectionObserver(container); } // ─── 构建调色板纹理 ─────────────────────────────────────────────────────── makePaletteTexture(stops) { let arr; if (Array.isArray(stops) && stops.length > 0) { arr = stops.length === 1 ? [stops[0], stops[0]] : stops; } else { arr = ['#ffffff', '#ffffff']; } const w = arr.length; const data = new Uint8Array(w * 4); for (let i = 0; i < w; i++) { const c = new THREE.Color(arr[i]); data[i * 4 + 0] = Math.round(c.r * 255); data[i * 4 + 1] = Math.round(c.g * 255); data[i * 4 + 2] = Math.round(c.b * 255); data[i * 4 + 3] = 255; } const tex = new THREE.DataTexture(data, w, 1, THREE.RGBAFormat); tex.magFilter = THREE.LinearFilter; tex.minFilter = THREE.LinearFilter; tex.wrapS = THREE.ClampToEdgeWrapping; tex.wrapT = THREE.ClampToEdgeWrapping; tex.generateMipmaps = false; tex.needsUpdate = true; return tex; } // ─── 创建 WebGL 管理器(包含所有仿真子类) ────────────────────────────── createWebGLManager(container) { const self = this; const paletteTex = this.makePaletteTexture(this.ncColors); const bgVec4 = new THREE.Vector4(0, 0, 0, 0); // 始终透明背景 // ── Common:WebGL 渲染器与时钟 ────────────────────────────────────── class CommonClass { constructor() { this.width = 0; this.height = 0; this.aspect = 1; this.pixelRatio = 1; this.fboWidth = null; this.fboHeight = null; this.time = 0; this.delta = 0; this.container = null; this.renderer = null; this.clock = null; } init(c) { this.container = c; this.pixelRatio = Math.min(window.devicePixelRatio || 1, 2); this.resize(); this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); this.renderer.autoClear = false; this.renderer.setClearColor(new THREE.Color(0x000000), 0); this.renderer.setPixelRatio(this.pixelRatio); this.renderer.setSize(this.width, this.height); this.renderer.domElement.style.width = '100%'; this.renderer.domElement.style.height = '100%'; this.renderer.domElement.style.display = 'block'; this.clock = new THREE.Clock(); this.clock.start(); } resize() { if (!this.container) return; const rect = this.container.getBoundingClientRect(); this.width = Math.max(1, Math.floor(rect.width)); this.height = Math.max(1, Math.floor(rect.height)); this.aspect = this.width / this.height; if (this.renderer) this.renderer.setSize(this.width, this.height, false); } update() { this.delta = this.clock.getDelta(); this.time += this.delta; } } const Common = new CommonClass(); // ── Mouse:鼠标与触控事件 + 自动演示平滑过渡 ────────────────────── class MouseClass { constructor() { this.mouseMoved = false; this.coords = new THREE.Vector2(); this.coords_old = new THREE.Vector2(); this.diff = new THREE.Vector2(); this.timer = null; this.container = null; this.docTarget = null; this.listenerTarget = null; this.isHoverInside = false; this.hasUserControl = false; this.isAutoActive = false; this.autoIntensity = 2.0; this.takeoverActive = false; this.takeoverStartTime = 0; this.takeoverDuration = 0.25; this.takeoverFrom = new THREE.Vector2(); this.takeoverTo = new THREE.Vector2(); this.onInteract = null; } init(c) { this.container = c; } dispose() { this.container = null; } isPointInside(clientX, clientY) { if (!this.container) return false; const rect = this.container.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return false; return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom; } updateHoverState(clientX, clientY) { this.isHoverInside = this.isPointInside(clientX, clientY); return this.isHoverInside; } setCoords(x, y) { if (!this.container) return; if (this.timer) window.clearTimeout(this.timer); const rect = this.container.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; const nx = (x - rect.left) / rect.width; const ny = (y - rect.top) / rect.height; this.coords.set(nx * 2 - 1, -(ny * 2 - 1)); this.mouseMoved = true; this.timer = window.setTimeout(() => { this.mouseMoved = false; }, 100); } setNormalized(nx, ny) { this.coords.set(nx, ny); this.mouseMoved = true; } onMouseMove(event) { if (!this.updateHoverState(event.clientX, event.clientY)) return; if (this.onInteract) this.onInteract(); if (this.isAutoActive && !this.hasUserControl && !this.takeoverActive) { if (!this.container) return; const rect = this.container.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; const nx = (event.clientX - rect.left) / rect.width; const ny = (event.clientY - rect.top) / rect.height; this.takeoverFrom.copy(this.coords); this.takeoverTo.set(nx * 2 - 1, -(ny * 2 - 1)); this.takeoverStartTime = performance.now(); this.takeoverActive = true; this.hasUserControl = true; this.isAutoActive = false; return; } this.setCoords(event.clientX, event.clientY); this.hasUserControl = true; } onTouchStart(event) { if (event.touches.length !== 1) return; const t = event.touches[0]; if (!this.updateHoverState(t.clientX, t.clientY)) return; if (this.onInteract) this.onInteract(); this.setCoords(t.clientX, t.clientY); this.hasUserControl = true; } onTouchMove(event) { if (event.touches.length !== 1) return; const t = event.touches[0]; if (!this.updateHoverState(t.clientX, t.clientY)) return; if (this.onInteract) this.onInteract(); this.setCoords(t.clientX, t.clientY); } onTouchEnd() { this.isHoverInside = false; } onMouseLeave() { this.isHoverInside = false; } update() { if (this.takeoverActive) { const t = (performance.now() - this.takeoverStartTime) / (this.takeoverDuration * 1000); if (t >= 1) { this.takeoverActive = false; this.coords.copy(this.takeoverTo); this.coords_old.copy(this.coords); this.diff.set(0, 0); } else { const k = t * t * (3 - 2 * t); this.coords.copy(this.takeoverFrom).lerp(this.takeoverTo, k); } } this.diff.subVectors(this.coords, this.coords_old); this.coords_old.copy(this.coords); if (this.coords_old.x === 0 && this.coords_old.y === 0) this.diff.set(0, 0); if (this.isAutoActive && !this.takeoverActive) this.diff.multiplyScalar(this.autoIntensity); } } const Mouse = new MouseClass(); // ── AutoDriver:无人操作时自动漫游 ───────────────────────────────── class AutoDriver { constructor(mouse, manager, opts) { this.active = false; this.current = new THREE.Vector2(0, 0); this.target = new THREE.Vector2(); this.lastTime = performance.now(); this.activationTime = 0; this.margin = 0.2; this._tmpDir = new THREE.Vector2(); this.mouse = mouse; this.manager = manager; this.enabled = opts.enabled; this.speed = opts.speed; this.resumeDelay = opts.resumeDelay || 3000; this.rampDurationMs = (opts.rampDuration || 0) * 1000; this.pickNewTarget(); } pickNewTarget() { const r = Math.random; this.target.set((r() * 2 - 1) * (1 - this.margin), (r() * 2 - 1) * (1 - this.margin)); } forceStop() { this.active = false; this.mouse.isAutoActive = false; } update() { if (!this.enabled) return; const now = performance.now(); const idle = now - this.manager.lastUserInteraction; if (idle < this.resumeDelay) { if (this.active) this.forceStop(); return; } if (this.mouse.isHoverInside) { if (this.active) this.forceStop(); return; } if (!this.active) { this.active = true; this.current.copy(this.mouse.coords); this.lastTime = now; this.activationTime = now; } this.mouse.isAutoActive = true; let dtSec = (now - this.lastTime) / 1000; this.lastTime = now; if (dtSec > 0.2) dtSec = 0.016; const dir = this._tmpDir.subVectors(this.target, this.current); const dist = dir.length(); if (dist < 0.01) { this.pickNewTarget(); return; } dir.normalize(); let ramp = 1; if (this.rampDurationMs > 0) { const t = Math.min(1, (now - this.activationTime) / this.rampDurationMs); ramp = t * t * (3 - 2 * t); } const step = this.speed * dtSec * ramp; const move = Math.min(step, dist); this.current.addScaledVector(dir, move); this.mouse.setNormalized(this.current.x, this.current.y); } } // ── ShaderPass:通用着色器渲染通道基类 ──────────────────────────── class ShaderPass { constructor(props) { var _a; this.material = null; this.geometry = null; this.plane = null; this.props = props || {}; this.uniforms = (_a = this.props.material) === null || _a === void 0 ? void 0 : _a.uniforms; } init() { this.scene = new THREE.Scene(); this.camera = new THREE.Camera(); if (this.uniforms) { this.material = new THREE.RawShaderMaterial(this.props.material); this.geometry = new THREE.PlaneGeometry(2.0, 2.0); this.plane = new THREE.Mesh(this.geometry, this.material); this.scene.add(this.plane); } } update(props) { Common.renderer.setRenderTarget(this.props.output || null); Common.renderer.render(this.scene, this.camera); Common.renderer.setRenderTarget(null); } } // ── Advection:速度场平流(含边界反弹线段) ──────────────────────── class Advection extends ShaderPass { constructor(simProps) { super({ material: { vertexShader: face_vert, fragmentShader: advection_frag, uniforms: { boundarySpace: { value: simProps.cellScale }, px: { value: simProps.cellScale }, fboSize: { value: simProps.fboSize }, velocity: { value: simProps.src.texture }, dt: { value: simProps.dt }, isBFECC: { value: true } } }, output: simProps.dst }); this.uniforms = this.props.material.uniforms; this.init(); } init() { super.init(); const boundaryG = new THREE.BufferGeometry(); const verts = new Float32Array([-1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0, -1, -1, 0]); boundaryG.setAttribute('position', new THREE.BufferAttribute(verts, 3)); const boundaryM = new THREE.RawShaderMaterial({ vertexShader: line_vert, fragmentShader: boundary_vel_frag, uniforms: this.uniforms }); this.line = new THREE.LineSegments(boundaryG, boundaryM); this.scene.add(this.line); } update({ dt, isBounce, BFECC }) { this.uniforms.dt.value = dt; this.line.visible = isBounce; this.uniforms.isBFECC.value = BFECC; super.update(); } } // ── ExternalForce:鼠标施力 ──────────────────────────────────────── class ExternalForce extends ShaderPass { constructor(simProps) { super({ output: simProps.dst }); this.initForce(simProps); } initForce(simProps) { super.init(); const mouseG = new THREE.PlaneGeometry(1, 1); const mouseM = new THREE.RawShaderMaterial({ vertexShader: mouse_vert, fragmentShader: externalForce_frag, blending: THREE.AdditiveBlending, depthWrite: false, uniforms: { px: { value: simProps.cellScale }, force: { value: new THREE.Vector2(0, 0) }, center: { value: new THREE.Vector2(0, 0) }, scale: { value: new THREE.Vector2(simProps.cursor_size, simProps.cursor_size) } } }); this.mouse = new THREE.Mesh(mouseG, mouseM); this.scene.add(this.mouse); } update(props) { const forceX = (Mouse.diff.x / 2) * props.mouse_force; const forceY = (Mouse.diff.y / 2) * props.mouse_force; const cursorSizeX = props.cursor_size * props.cellScale.x; const cursorSizeY = props.cursor_size * props.cellScale.y; const centerX = Math.min(Math.max(Mouse.coords.x, -1 + cursorSizeX + props.cellScale.x * 2), 1 - cursorSizeX - props.cellScale.x * 2); const centerY = Math.min(Math.max(Mouse.coords.y, -1 + cursorSizeY + props.cellScale.y * 2), 1 - cursorSizeY - props.cellScale.y * 2); const u = this.mouse.material.uniforms; u.force.value.set(forceX, forceY); u.center.value.set(centerX, centerY); u.scale.value.set(props.cursor_size, props.cursor_size); super.update(); } } // ── Viscous:粘性扩散(雅可比迭代) ────────────────────────────── class Viscous extends ShaderPass { constructor(simProps) { super({ material: { vertexShader: face_vert, fragmentShader: viscous_frag, uniforms: { boundarySpace: { value: simProps.boundarySpace }, velocity: { value: simProps.src.texture }, velocity_new: { value: simProps.dst_.texture }, v: { value: simProps.viscous }, px: { value: simProps.cellScale }, dt: { value: simProps.dt } } }, output: simProps.dst, output0: simProps.dst_, output1: simProps.dst }); this.init(); } init() { super.init(); const boundaryG = new THREE.BufferGeometry(); const verts = new Float32Array([-1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0, -1, -1, 0]); boundaryG.setAttribute('position', new THREE.BufferAttribute(verts, 3)); const boundaryM = new THREE.RawShaderMaterial({ vertexShader: line_vert, fragmentShader: boundary_vel_frag, uniforms: this.uniforms }); this.line = new THREE.LineSegments(boundaryG, boundaryM); this.scene.add(this.line); } update({ viscous, iterations, dt, isBounce }) { let fbo_in, fbo_out; this.uniforms.v.value = viscous; this.line.visible = isBounce; for (let i = 0; i < iterations; i++) { if (i % 2 === 0) { fbo_in = this.props.output0; fbo_out = this.props.output1; } else { fbo_in = this.props.output1; fbo_out = this.props.output0; } this.uniforms.velocity_new.value = fbo_in.texture; this.props.output = fbo_out; this.uniforms.dt.value = dt; super.update(); } return fbo_out; } } // ── Divergence:散度计算 ────────────────────────────────────────── class Divergence extends ShaderPass { constructor(simProps) { super({ material: { vertexShader: face_vert, fragmentShader: divergence_frag, uniforms: { boundarySpace: { value: simProps.boundarySpace }, velocity: { value: simProps.src.texture }, px: { value: simProps.cellScale }, dt: { value: simProps.dt } } }, output: simProps.dst }); this.init(); } update({ vel }) { this.uniforms.velocity.value = vel.texture; super.update(); } } // ── Poisson:压力求解(雅可比迭代) ────────────────────────────── class Poisson extends ShaderPass { constructor(simProps) { super({ material: { vertexShader: face_vert, fragmentShader: poisson_frag, uniforms: { boundarySpace: { value: simProps.boundarySpace }, pressure: { value: simProps.dst_.texture }, divergence: { value: simProps.src.texture }, px: { value: simProps.cellScale } } }, output: simProps.dst, output0: simProps.dst_, output1: simProps.dst }); this.init(); } init() { super.init(); const boundaryG = new THREE.BufferGeometry(); const verts = new Float32Array([-1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0, -1, -1, 0]); boundaryG.setAttribute('position', new THREE.BufferAttribute(verts, 3)); const boundaryM = new THREE.RawShaderMaterial({ vertexShader: line_vert, fragmentShader: boundary_pres_frag, uniforms: this.uniforms }); this.line = new THREE.LineSegments(boundaryG, boundaryM); this.scene.add(this.line); } update({ iterations, isBounce }) { let p_in, p_out; this.line.visible = isBounce; for (let i = 0; i < iterations; i++) { if (i % 2 === 0) { p_in = this.props.output0; p_out = this.props.output1; } else { p_in = this.props.output1; p_out = this.props.output0; } this.uniforms.pressure.value = p_in.texture; this.props.output = p_out; super.update(); } return p_out; } } // ── Pressure:压力梯度修正 ──────────────────────────────────────── class Pressure extends ShaderPass { constructor(simProps) { super({ material: { vertexShader: face_vert, fragmentShader: pressure_frag, uniforms: { boundarySpace: { value: simProps.boundarySpace }, pressure: { value: simProps.src_p.texture }, velocity: { value: simProps.src_v.texture }, px: { value: simProps.cellScale }, dt: { value: simProps.dt } } }, output: simProps.dst }); this.init(); } init() { super.init(); const boundaryG = new THREE.BufferGeometry(); const verts = new Float32Array([-1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0, -1, -1, 0]); boundaryG.setAttribute('position', new THREE.BufferAttribute(verts, 3)); const boundaryM = new THREE.RawShaderMaterial({ vertexShader: line_vert, fragmentShader: boundary_vel_frag, uniforms: this.uniforms }); this.line = new THREE.LineSegments(boundaryG, boundaryM); this.scene.add(this.line); } update({ vel, pressure, isBounce }) { this.uniforms.velocity.value = vel.texture; this.uniforms.pressure.value = pressure.texture; this.line.visible = isBounce; super.update(); } } // ── Simulation:整合流体仿真管线 ───────────────────────────────── class Simulation { constructor(options) { this.fboSize = new THREE.Vector2(); this.cellScale = new THREE.Vector2(); this.boundarySpace = new THREE.Vector2(); this.options = Object.assign({ iterations_poisson: 32, iterations_viscous: 32, mouse_force: 20, resolution: 0.5, cursor_size: 100, viscous: 30, isBounce: false, dt: 0.014, isViscous: false, BFECC: true }, (options || {})); this.fbos = { vel_0: null, vel_1: null, vel_viscous0: null, vel_viscous1: null, div: null, pressure_0: null, pressure_1: null }; this.init(); } init() { this.calcSize(); this.createAllFBO(); this.createShaderPass(); } getFloatType() { const isIOS = /(iPad|iPhone|iPod)/i.test(navigator.userAgent); return isIOS ? THREE.HalfFloatType : THREE.FloatType; } createAllFBO() { const type = this.getFloatType(); const opts = { type, depthBuffer: false, stencilBuffer: false, minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, wrapS: THREE.ClampToEdgeWrapping, wrapT: THREE.ClampToEdgeWrapping }; for (const key in this.fbos) { this.fbos[key] = new THREE.WebGLRenderTarget(this.fboSize.x, this.fboSize.y, opts); } } createShaderPass() { this.advection = new Advection({ cellScale: this.cellScale, fboSize: this.fboSize, dt: this.options.dt, src: this.fbos.vel_0, dst: this.fbos.vel_1 }); this.externalForce = new ExternalForce({ cellScale: this.cellScale, cursor_size: this.options.cursor_size, dst: this.fbos.vel_1 }); this.viscous = new Viscous({ cellScale: this.cellScale, boundarySpace: this.boundarySpace, viscous: this.options.viscous, src: this.fbos.vel_1, dst: this.fbos.vel_viscous1, dst_: this.fbos.vel_viscous0, dt: this.options.dt }); this.divergence = new Divergence({ cellScale: this.cellScale, boundarySpace: this.boundarySpace, src: this.fbos.vel_viscous0, dst: this.fbos.div, dt: this.options.dt }); this.poisson = new Poisson({ cellScale: this.cellScale, boundarySpace: this.boundarySpace, src: this.fbos.div, dst: this.fbos.pressure_1, dst_: this.fbos.pressure_0 }); this.pressure = new Pressure({ cellScale: this.cellScale, boundarySpace: this.boundarySpace, src_p: this.fbos.pressure_0, src_v: this.fbos.vel_viscous0, dst: this.fbos.vel_0, dt: this.options.dt }); } calcSize() { const width = Math.max(1, Math.round(this.options.resolution * Common.width)); const height = Math.max(1, Math.round(this.options.resolution * Common.height)); this.cellScale.set(1.0 / width, 1.0 / height); this.fboSize.set(width, height); } resize() { this.calcSize(); for (const key in this.fbos) { this.fbos[key].setSize(this.fboSize.x, this.fboSize.y); } } update() { if (this.options.isBounce) this.boundarySpace.set(0, 0); else this.boundarySpace.copy(this.cellScale); this.advection.update({ dt: this.options.dt, isBounce: this.options.isBounce, BFECC: this.options.BFECC }); this.externalForce.update({ cursor_size: this.options.cursor_size, mouse_force: this.options.mouse_force, cellScale: this.cellScale }); let vel = this.fbos.vel_1; if (this.options.isViscous) { vel = this.viscous.update({ viscous: this.options.viscous, iterations: this.options.iterations_viscous, dt: this.options.dt, isBounce: this.options.isBounce }); } this.divergence.update({ vel }); const pressure = this.poisson.update({ iterations: this.options.iterations_poisson, isBounce: this.options.isBounce }); this.pressure.update({ vel, pressure, isBounce: this.options.isBounce }); } } // ── Output:最终颜色输出场景 ────────────────────────────────────── class Output { constructor() { this.init(); } init() { this.simulation = new Simulation(); this.scene = new THREE.Scene(); this.camera = new THREE.Camera(); this.output = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), new THREE.RawShaderMaterial({ vertexShader: face_vert, fragmentShader: color_frag, transparent: true, depthWrite: false, uniforms: { velocity: { value: this.simulation.fbos.vel_0.texture }, boundarySpace: { value: new THREE.Vector2() }, palette: { value: paletteTex }, bgColor: { value: bgVec4 } } })); this.scene.add(this.output); } resize() { this.simulation.resize(); } render() { Common.renderer.setRenderTarget(null); Common.renderer.render(this.scene, this.camera); } update() { this.simulation.update(); this.render(); } } // ── WebGLManager:顶层管理器,驱动动画循环 ─────────────────────── class WebGLManager { constructor(props) { this.lastUserInteraction = performance.now(); this.running = false; this.props = props; Common.init(props.$wrapper); this.mouse = Mouse; this.mouse.init(props.$wrapper); Mouse.autoIntensity = props.autoIntensity; Mouse.takeoverDuration = props.takeoverDuration; Mouse.onInteract = () => { this.lastUserInteraction = performance.now(); if (this.autoDriver) this.autoDriver.forceStop(); }; this.autoDriver = new AutoDriver(Mouse, this, { enabled: props.autoDemo, speed: props.autoSpeed, resumeDelay: props.autoResumeDelay, rampDuration: props.autoRampDuration }); this.init(); this._loop = this.loop.bind(this); this._resize = this.resize.bind(this); this._onVisibility = () => { if (document.hidden) { this.pause(); } else if (self.isVisible) { self.ngZone.runOutsideAngular(() => { this.start(); }); } }; window.addEventListener('resize', this._resize); document.addEventListener('visibilitychange', this._onVisibility); } init() { this.props.$wrapper.prepend(Common.renderer.domElement); this.output = new Output(); } resize() { Common.resize(); this.output.resize(); } render() { if (this.autoDriver) this.autoDriver.update(); Mouse.update(); Common.update(); this.output.update(); } loop() { if (!this.running) return; this.render(); self.rafId = requestAnimationFrame(this._loop); } start() { if (this.running) return; this.running = true; this._loop(); } pause() { this.running = false; if (self.rafId !== null) { cancelAnimationFrame(self.rafId); self.rafId = null; } } dispose() { try { window.removeEventListener('resize', this._resize); document.removeEventListener('visibilitychange', this._onVisibility); this.mouse.dispose(); if (Common.renderer) { const canvas = Common.renderer.domElement; if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas); Common.renderer.dispose(); } } catch (e) { void 0; } } } // ─── 实例化并配置 ──────────────────────────────────────────────── const webgl = new WebGLManager({ $wrapper: container, autoDemo: self.ncAutoDemo, autoSpeed: self.ncAutoSpeed, autoIntensity: self.ncAutoIntensity, takeoverDuration: self.ncTakeoverDuration, autoResumeDelay: self.ncAutoResumeDelay, autoRampDuration: self.ncAutoRampDuration }); this.applyOptionsToSimulation(webgl.output.simulation); return webgl; } // ─── 将 @Input 参数写入 simulation.options ─────────────────────────────── applyOptions() { var _a, _b; if (!((_b = (_a = this.webgl) === null || _a === void 0 ? void 0 : _a.output) === null || _b === void 0 ? void 0 : _b.simulation)) return; this.applyOptionsToSimulation(this.webgl.output.simulation); } applyOptionsToSimulation(sim) { var _a; const prevRes = sim.options.resolution; Object.assign(sim.options, { mouse_force: this.ncMouseForce, cursor_size: this.ncCursorSize, isViscous: this.ncIsViscous, viscous: this.ncViscous, iterations_viscous: this.ncIterationsViscous, iterations_poisson: this.ncIterationsPoisson, dt: this.ncDt, BFECC: this.ncBFECC, resolution: this.ncResolution, isBounce: this.ncIsBounce }); if (this.ncResolution !== prevRes) { sim.resize(); } // 同步 autoDriver 参数 if ((_a = this.webgl) === null || _a === void 0 ? void 0 : _a.autoDriver) { this.webgl.autoDriver.enabled = this.ncAutoDemo; this.webgl.autoDriver.speed = this.ncAutoSpeed; this.webgl.autoDriver.resumeDelay = this.ncAutoResumeDelay; this.webgl.autoDriver.rampDurationMs = this.ncAutoRampDuration * 1000; if (this.webgl.autoDriver.mouse) { this.webgl.autoDriver.mouse.autoIntensity = this.ncAutoIntensity; this.webgl.autoDriver.mouse.takeoverDuration = this.ncTakeoverDuration; } } } // ─── ResizeObserver:容器尺寸变化时重新计算 ────────────────────────────── setupResizeObserver(container) { this.resizeObserver = new ResizeObserver(() => { if (!this.webgl) return; if (this.resizeRafId !== null) cancelAnimationFrame(this.resizeRafId); this.ngZone.runOutsideAngular(() => { this.resizeRafId = requestAnimationFrame(() => { if (!this.webgl) return; this.webgl.resize(); this.resizeRafId = null; }); }); }); this.resizeObserver.observe(container); } // ─── IntersectionObserver:不可见时暂停渲染以节省性能 ──────────────────── setupIntersectionObserver(container) { this.intersectionObserver = new IntersectionObserver(entries => { const entry = entries[0]; this.isVisible = entry.isIntersecting && entry.intersectionRatio > 0; if (!this.webgl) return; if (this.isVisible && !document.hidden) { this.ngZone.runOutsideAngular(() => { this.webgl.start(); }); } else { this.webgl.pause(); } }, { threshold: [0, 0.01, 0.1] }); this.intersectionObserver.observe(container); } // ─── 清理资源 ───────────────────────────────────────────────────────────── cleanup() { var _a, _b; if (this.rafId !== null) { cancelAnimationFrame(this.rafId); this.rafId = null; } if (this.resizeRafId !== null) { cancelAnimationFrame(this.resizeRafId); this.resizeRafId = null; } try { (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect(); } catch (e) { void 0; } try { (_b = this.intersectionObserver) === null || _b === void 0 ? void 0 : _b.disconnect(); } catch (e) { void 0; } if (this.webgl) { this.webgl.dispose(); this.webgl = null; } } // ─── HostListeners for Mouse/Touch Interaction ────────────────────────── onMouseMove(event) { var _a, _b; (_b = (_a = this.webgl) === null || _a === void 0 ? void 0 : _a.mouse) === null || _b === void 0 ? void 0 : _b.onMouseMove(event); } onTouchStart(event) { var _a, _b; (_b = (_a = this.webgl) === null || _a === void 0 ? void 0 : _a.mouse) === null || _b === void 0 ? void 0 : _b.onTouchStart(event); } onTouchMove(event) { var _a, _b; (_b = (_a = this.webgl) === null || _a === vo