mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-13 09:34:47 +08:00
354 lines
14 KiB
TypeScript
354 lines
14 KiB
TypeScript
import type {
|
||
BufferGeometry,
|
||
IUniform,
|
||
OrthographicCamera,
|
||
Texture,
|
||
Vector2,
|
||
WebGLRenderer,
|
||
WebGLRenderTarget,
|
||
} from 'three'
|
||
|
||
type ThreeModule = typeof import('three')
|
||
|
||
interface GlassFluidFieldUniforms extends Record<string, IUniform> {
|
||
uDecay: IUniform<number>
|
||
uInjection: IUniform<number>
|
||
uPointer: IUniform<Vector2>
|
||
uPrevious: IUniform<Texture | null>
|
||
uTexelSize: IUniform<Vector2>
|
||
uVelocity: IUniform<Vector2>
|
||
uViewportAspect: IUniform<number>
|
||
}
|
||
|
||
interface CreateGlassFluidDynamicsOptions {
|
||
camera: OrthographicCamera
|
||
geometry: BufferGeometry
|
||
pointer: Vector2
|
||
renderer: WebGLRenderer
|
||
three: ThreeModule
|
||
velocity: Vector2
|
||
}
|
||
|
||
export interface GlassFluidDynamics {
|
||
/** 清除当前输入包络;下一帧会把时序场收敛到中性值。 */
|
||
clearInput(): void
|
||
/** 释放 fluid 私有 shader 和两个 ping-pong target。 */
|
||
dispose(): void
|
||
/** 清除当前帧注入,避免非输入绘制重复写入同一能量。 */
|
||
finishFrame(): void
|
||
/** 调整 fluid field;尺寸只来自主 renderer 已提交的 buffer。 */
|
||
resize(bufferWidth: number, bufferHeight: number, viewportWidth: number, viewportHeight: number): void
|
||
/** 更新当前帧的衰减与注入参数,不自行调度动画。 */
|
||
setFrameParameters(decay: number, injection: number): void
|
||
/** 推进一次 field 并返回主材质应采样的最新纹理。 */
|
||
step(): Texture
|
||
}
|
||
|
||
export const GLASS_FLUID_DYNAMIC_RANGE_SCALE = 0.52
|
||
export const GLASS_FLUID_DYNAMIC_RANGE_DENSITY = 1 / GLASS_FLUID_DYNAMIC_RANGE_SCALE ** 2
|
||
const GLASS_FLUID_BUFFER_SCALE = 0.25
|
||
|
||
const GLASS_FLUID_VERTEX_SHADER = `
|
||
varying vec2 vUv;
|
||
|
||
void main() {
|
||
vUv = position.xy * 0.5 + 0.5;
|
||
gl_Position = vec4(position.xy, 0.0, 1.0);
|
||
}
|
||
`
|
||
|
||
export const GLASS_FLUID_FIELD_FRAGMENT_SHADER = `
|
||
precision highp float;
|
||
|
||
uniform sampler2D uPrevious;
|
||
uniform vec2 uPointer;
|
||
uniform vec2 uVelocity;
|
||
uniform vec2 uTexelSize;
|
||
uniform float uInjection;
|
||
uniform float uDecay;
|
||
uniform float uViewportAspect;
|
||
varying vec2 vUv;
|
||
|
||
void main() {
|
||
vec4 previous = (
|
||
texture2D(uPrevious, vUv) * 0.5 +
|
||
texture2D(uPrevious, vUv + vec2(uTexelSize.x, 0.0)) * 0.125 +
|
||
texture2D(uPrevious, vUv - vec2(uTexelSize.x, 0.0)) * 0.125 +
|
||
texture2D(uPrevious, vUv + vec2(0.0, uTexelSize.y)) * 0.125 +
|
||
texture2D(uPrevious, vUv - vec2(0.0, uTexelSize.y)) * 0.125
|
||
);
|
||
float previousEnergy = previous.z;
|
||
vec2 flow = previousEnergy < 0.001 ? vec2(0.0) : (previous.xy * 2.0 - 1.0) * uDecay;
|
||
float energy = previousEnergy * uDecay;
|
||
vec2 delta = vUv - uPointer;
|
||
delta.x *= uViewportAspect;
|
||
float distanceSquared = dot(delta, delta);
|
||
float injection = exp(-distanceSquared * ${(70 * GLASS_FLUID_DYNAMIC_RANGE_DENSITY).toFixed(3)}) * uInjection;
|
||
float speed = length(uVelocity);
|
||
vec2 direction = speed > 0.0001 ? uVelocity / speed : vec2(0.0, -1.0);
|
||
vec2 perpendicular = vec2(-direction.y, direction.x);
|
||
float shear = dot(delta, perpendicular) * exp(-distanceSquared * ${(42 * GLASS_FLUID_DYNAMIC_RANGE_DENSITY).toFixed(3)});
|
||
|
||
flow += (direction * min(speed * 9.0, 0.9) - perpendicular * shear * 0.85) * injection * 0.44;
|
||
energy = max(energy, injection);
|
||
|
||
gl_FragColor = vec4(flow * 0.5 + 0.5, energy, 1.0);
|
||
}
|
||
`
|
||
|
||
/** fluid 主材质的全局临时量;由共享 shader 在原位置逐字拼装。 */
|
||
export const GLASS_FLUID_FRAGMENT_SETUP = ` vec2 wakeDirection = length(uWakeDirection) > 0.0001 ? normalize(uWakeDirection) : vec2(0.0, -1.0);
|
||
vec2 wakePerpendicular = vec2(-wakeDirection.y, wakeDirection.x);
|
||
vec2 trailRefraction = vec2(0.0);
|
||
float trailEnergy = 0.0;
|
||
float trailSpatialSpan = 0.0;
|
||
float motionRangeCompression = mix(1.0, 1.34, uMotionExpansion);
|
||
const float dynamicRangeScale = ${GLASS_FLUID_DYNAMIC_RANGE_SCALE.toFixed(2)};
|
||
const float dynamicRangeDensity = ${GLASS_FLUID_DYNAMIC_RANGE_DENSITY.toFixed(3)};`
|
||
|
||
/** fluid 的 trail 与高质量 temporal field 响应。 */
|
||
export const GLASS_FLUID_FRAGMENT_TRAIL_AND_FIELD = ` for (int trailIndex = 0; trailIndex < 4; trailIndex++) {
|
||
if (trailIndex >= uTrailCount) break;
|
||
|
||
vec4 trail = uTrail[trailIndex];
|
||
vec2 trailDelta = vUv - trail.xy;
|
||
trailDelta *= uPresentationSize / max(uVisibleViewportSize.y, 1.0) * motionRangeCompression;
|
||
vec2 trailSpanDelta = trail.xy - uPointer;
|
||
trailSpanDelta *= uPresentationSize / max(uVisibleViewportSize.y, 1.0) * motionRangeCompression;
|
||
trailSpatialSpan = max(trailSpatialSpan, length(trailSpanDelta) * trail.z);
|
||
float along = dot(trailDelta, wakeDirection);
|
||
float across = dot(trailDelta, wakePerpendicular);
|
||
float trailAlongDensity = mix(42.0, 22.0, uMotionExpansion) * dynamicRangeDensity;
|
||
float trailAcrossDensity = mix(210.0, 86.0, uMotionExpansion) * dynamicRangeDensity;
|
||
float lobe =
|
||
exp(-(along * along * trailAlongDensity + across * across * trailAcrossDensity)) * trail.z * uMotion;
|
||
float wake = mix(0.88, 0.58, float(trailIndex) / 3.0);
|
||
|
||
trailRefraction +=
|
||
(wakeDirection * 0.0048 + wakePerpendicular * across * 0.018) *
|
||
lobe *
|
||
uDeformationStrength *
|
||
uFlowStrength;
|
||
trailEnergy += lobe * wake * mix(0.72, 0.42, float(trailIndex) / 3.0);
|
||
}
|
||
|
||
vec4 flowSample = uHasFlowTexture > 0.5 ? texture2D(uFlowTexture, vUv) : vec4(0.5, 0.5, 0.0, 1.0);
|
||
vec2 temporalFlow =
|
||
uHasFlowTexture > 0.5
|
||
? (flowSample.xy * 2.0 - 1.0) *
|
||
flowSample.z *
|
||
uMotion *
|
||
uDeformationStrength *
|
||
uFlowStrength
|
||
: vec2(0.0);
|
||
float flowSurfaceDetail = 0.0;
|
||
if (uQuality > 0.5 && uHasFlowTexture > 0.5) {
|
||
vec2 flowTexel = vec2(3.0) / max(uPresentationSize, vec2(1.0));
|
||
vec3 flowLeft = texture2D(uFlowTexture, vUv - vec2(flowTexel.x, 0.0)).xyz;
|
||
vec3 flowRight = texture2D(uFlowTexture, vUv + vec2(flowTexel.x, 0.0)).xyz;
|
||
vec3 flowBottom = texture2D(uFlowTexture, vUv - vec2(0.0, flowTexel.y)).xyz;
|
||
vec3 flowTop = texture2D(uFlowTexture, vUv + vec2(0.0, flowTexel.y)).xyz;
|
||
float flowGradient = length(flowRight.xy - flowLeft.xy) + length(flowTop.xy - flowBottom.xy);
|
||
float energyGradient = abs(flowRight.z - flowLeft.z) + abs(flowTop.z - flowBottom.z);
|
||
flowSurfaceDetail = smoothstep(0.015, 0.24, flowGradient + energyGradient * 0.72) * uMotion;
|
||
}`
|
||
|
||
/** 单个 surface 内的 fluid 指针、方向、wake 与能量形态。 */
|
||
export const GLASS_FLUID_FRAGMENT_SURFACE_SHAPE = ` vec2 pointerDelta = uPointer - vUv;
|
||
vec2 pointerDeltaAspect = pointerDelta;
|
||
pointerDeltaAspect *= uPresentationSize / max(uVisibleViewportSize.y, 1.0) * motionRangeCompression;
|
||
// 三材质共享指针几何足迹;磨砂身份由位移幅度、低通扩散和材质合成表达。
|
||
float pointerSpread = mix(26.0, 17.0, uQuality);
|
||
pointerSpread *= dynamicRangeDensity * mix(1.0, 0.46, uMotionExpansion);
|
||
float sharedDirectionality = smoothstep(0.015, 0.18, trailSpatialSpan);
|
||
float pointerAlong = dot(-pointerDeltaAspect, wakeDirection);
|
||
float pointerAcross = dot(-pointerDeltaAspect, wakePerpendicular);
|
||
float sharedWakeTravel =
|
||
0.08 * sharedDirectionality * mix(0.86, 1.18, uMotionExpansion);
|
||
float radialPointerShape = exp(-dot(pointerDeltaAspect, pointerDeltaAspect) * pointerSpread);
|
||
float directionalPointerShape =
|
||
exp(-(
|
||
pow(pointerAlong + sharedWakeTravel * 0.45, 2.0) * pointerSpread * 0.72 +
|
||
pointerAcross * pointerAcross * pointerSpread * 1.35
|
||
));
|
||
float pointerEnergy =
|
||
clamp(mix(radialPointerShape, directionalPointerShape, sharedDirectionality) * uMotion, 0.0, 1.0);
|
||
float sharedWaveDensity = mix(2.81, 1.63, uMotionExpansion);
|
||
float radialSharedWave =
|
||
exp(-dot(pointerDeltaAspect, pointerDeltaAspect) * sharedWaveDensity);
|
||
float directionalSharedWave =
|
||
exp(-(
|
||
pow(pointerAlong + sharedWakeTravel, 2.0) * sharedWaveDensity * 0.62 +
|
||
pointerAcross * pointerAcross * sharedWaveDensity * 2.2
|
||
));
|
||
float sharedWaveEnergy =
|
||
mix(radialSharedWave, directionalSharedWave, sharedDirectionality) *
|
||
clamp(length(uPointerVelocity) * 14.0 * uTranslationStrength, 0.0, 1.0) *
|
||
mix(1.0, 0.78, sharedDirectionality) *
|
||
uMotion *
|
||
uMotion;
|
||
vec2 wakeDelta = vUv - uPointer;
|
||
wakeDelta *= uPresentationSize / max(uVisibleViewportSize.y, 1.0) * motionRangeCompression;
|
||
float wakeAlong = dot(wakeDelta, wakeDirection);
|
||
float wakeAcross = dot(wakeDelta, wakePerpendicular);
|
||
float wakeTravel =
|
||
0.014 * dynamicRangeScale *
|
||
mix(0.82, 1.18, uQuality) *
|
||
mix(1.0, 1.45, uMotionExpansion);
|
||
float wakeWidth =
|
||
mix(0.027, 0.044, uQuality) * dynamicRangeScale * mix(1.0, 1.72, uMotionExpansion);
|
||
float wakeCoordinate = (wakeAlong + wakeTravel) / wakeWidth;
|
||
float wakeShape = wakeCoordinate * exp(-0.5 * wakeCoordinate * wakeCoordinate);
|
||
float wakeEnvelope =
|
||
exp(
|
||
-wakeAcross *
|
||
wakeAcross *
|
||
mix(280.0, 145.0, uQuality) *
|
||
dynamicRangeDensity *
|
||
mix(1.0, 0.44, uMotionExpansion)
|
||
);
|
||
vec2 wakeRefraction =
|
||
wakeDirection *
|
||
wakeShape *
|
||
wakeEnvelope *
|
||
mix(0.0045, 0.0075, uQuality) *
|
||
uMotion *
|
||
uDeformationStrength *
|
||
uFlowStrength;
|
||
float wakeEnergy = abs(wakeShape) * wakeEnvelope * uMotion;
|
||
// 覆盖能量比位移核更快收敛,避免高斯尾部把真实折射扩成整块色调覆盖。
|
||
float coverageDirectionality = max(
|
||
sharedDirectionality,
|
||
smoothstep(0.001, 0.012, length(uPointerVelocity))
|
||
);
|
||
float coverageWakeTravel =
|
||
0.08 * coverageDirectionality * mix(0.86, 1.18, uMotionExpansion);
|
||
float directionalCoverageAlong = pointerAlong + coverageWakeTravel * 0.45;
|
||
float directionalCoverageShape = exp(-(
|
||
directionalCoverageAlong * directionalCoverageAlong * pointerSpread * 0.55 +
|
||
pointerAcross * pointerAcross * pointerSpread * 2.8
|
||
));
|
||
float pointerCoverageShape = mix(
|
||
radialPointerShape,
|
||
directionalCoverageShape,
|
||
coverageDirectionality
|
||
);
|
||
float pointerCoverageEnergy = pow(clamp(pointerCoverageShape * uMotion, 0.0, 1.0), 1.15);
|
||
float liquidEnergy = clamp(max(
|
||
pointerCoverageEnergy,
|
||
max(min(1.0, trailEnergy) * 0.68, wakeEnergy * 0.82)
|
||
), 0.0, 1.0);`
|
||
|
||
/** 单个 surface 内的 fluid 高光与焦散响应。 */
|
||
export const GLASS_FLUID_FRAGMENT_SURFACE_OPTICS = ` float pointerStrength = mix(mix(0.0055, 0.008, uQuality), mix(0.0085, 0.012, uQuality), frosted);
|
||
float trailStrength = mix(mix(0.78, 1.08, uQuality), mix(0.96, 1.3, uQuality), frosted);
|
||
float temporalStrength = mix(0.032, 0.042, frosted) * uQuality * (1.0 + flowSurfaceDetail * 0.5);
|
||
vec2 specularDelta =
|
||
vUv - (uPointer - wakeDirection * mix(0.006, 0.022, uMotionExpansion) * dynamicRangeScale);
|
||
specularDelta *= uPresentationSize / max(uVisibleViewportSize.y, 1.0) * motionRangeCompression;
|
||
float specularAlong = dot(specularDelta, wakeDirection);
|
||
float specularAcross = dot(specularDelta, wakePerpendicular);
|
||
float singleSpecular =
|
||
exp(-(
|
||
specularAlong * specularAlong * mix(58.0, 25.0, uMotionExpansion) * dynamicRangeDensity +
|
||
specularAcross * specularAcross * mix(190.0, 78.0, uMotionExpansion) * dynamicRangeDensity
|
||
)) *
|
||
uMotion *
|
||
mix(1.0, 1.24, uMotionExpansion);
|
||
float localCaustic = singleSpecular * rectMask * surfaceDynamic * interactionMask;`
|
||
|
||
/** fluid 对共享 dynamicRefraction 的贡献;静态透镜和 ripple 响应仍由主材质合成。 */
|
||
export const GLASS_FLUID_FRAGMENT_SURFACE_REFRACTION = ` vec2 sampleTranslation =
|
||
uPointerVelocity *
|
||
mix(0.055, 0.075, uQuality) *
|
||
uMotion *
|
||
uTranslationStrength;
|
||
dynamicRefraction += (
|
||
sampleTranslation +
|
||
// 收紧高斯半径时补偿向量峰值,避免范围缩小同时削弱用户设置的形变强度。
|
||
pointerDelta * pointerEnergy * pointerStrength * uDeformationStrength / dynamicRangeScale +
|
||
trailRefraction * trailStrength +
|
||
temporalFlow * temporalStrength +
|
||
wakeRefraction
|
||
) * rectMask * surfaceDynamic * interactionMask;`
|
||
|
||
/** 创建仅由高质量 fluid 模式持有的时序位移场。 */
|
||
export function createGlassFluidDynamics(options: CreateGlassFluidDynamicsOptions): GlassFluidDynamics {
|
||
const { camera, geometry, pointer, renderer, three, velocity } = options
|
||
let disposed = false
|
||
const createTarget = () =>
|
||
new three.WebGLRenderTarget(1, 1, {
|
||
depthBuffer: false,
|
||
magFilter: three.LinearFilter,
|
||
minFilter: three.LinearFilter,
|
||
stencilBuffer: false,
|
||
})
|
||
let readTarget: WebGLRenderTarget = createTarget()
|
||
let writeTarget: WebGLRenderTarget = createTarget()
|
||
const uniforms: GlassFluidFieldUniforms = {
|
||
uDecay: { value: 1 },
|
||
uInjection: { value: 0 },
|
||
uPointer: { value: pointer },
|
||
uPrevious: { value: null },
|
||
uTexelSize: { value: new three.Vector2(1, 1) },
|
||
uVelocity: { value: velocity },
|
||
uViewportAspect: { value: window.innerWidth / Math.max(window.innerHeight, 1) },
|
||
}
|
||
const material = new three.ShaderMaterial({
|
||
depthTest: false,
|
||
depthWrite: false,
|
||
fragmentShader: GLASS_FLUID_FIELD_FRAGMENT_SHADER,
|
||
uniforms,
|
||
vertexShader: GLASS_FLUID_VERTEX_SHADER,
|
||
})
|
||
const scene = new three.Scene()
|
||
const mesh = new three.Mesh(geometry, material)
|
||
mesh.frustumCulled = false
|
||
scene.add(mesh)
|
||
|
||
return {
|
||
clearInput() {
|
||
uniforms.uDecay.value = 0
|
||
uniforms.uInjection.value = 0
|
||
},
|
||
dispose() {
|
||
if (disposed) return
|
||
disposed = true
|
||
material.dispose()
|
||
readTarget.dispose()
|
||
writeTarget.dispose()
|
||
},
|
||
finishFrame() {
|
||
uniforms.uInjection.value = 0
|
||
},
|
||
resize(bufferWidth, bufferHeight, viewportWidth, viewportHeight) {
|
||
if (disposed) return
|
||
const width = Math.max(96, Math.round(bufferWidth * GLASS_FLUID_BUFFER_SCALE))
|
||
const height = Math.max(96, Math.round(bufferHeight * GLASS_FLUID_BUFFER_SCALE))
|
||
if (readTarget.width !== width || readTarget.height !== height) {
|
||
readTarget.setSize(width, height)
|
||
writeTarget.setSize(width, height)
|
||
}
|
||
uniforms.uTexelSize.value.set(1 / width, 1 / height)
|
||
uniforms.uViewportAspect.value = viewportWidth / Math.max(viewportHeight, 1)
|
||
},
|
||
setFrameParameters(decay, injection) {
|
||
uniforms.uDecay.value = decay
|
||
uniforms.uInjection.value = injection
|
||
},
|
||
step() {
|
||
if (disposed) return readTarget.texture
|
||
renderer.setScissorTest(false)
|
||
uniforms.uPrevious.value = readTarget.texture
|
||
renderer.setRenderTarget(writeTarget)
|
||
renderer.render(scene, camera)
|
||
renderer.setRenderTarget(null)
|
||
const previousReadTarget = readTarget
|
||
readTarget = writeTarget
|
||
writeTarget = previousReadTarget
|
||
|
||
return readTarget.texture
|
||
},
|
||
}
|
||
}
|