HumanBreak/src/plugin/fx/pointShader.ts
2024-11-18 22:16:29 +08:00

654 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { logger } from '@/core/common/logger';
import { UniformType } from '@/core/render/gl2';
import { Shader, ShaderProgram } from '@/core/render/shader';
import { Transform } from '@/core/render/transform';
export const enum PointEffectType {
/**
* 无特效,在此之后的所有特效将会被截断,因此不要在空特效之后添加特效,也不要手动添加空特效
*/
None,
/**
* 反色特效,可与任意特效叠加\
* 参数分别为:\
* `data1: x, y, radius, decay` | 横坐标,纵坐标,半径,衰减开始半径\
* `data2: ratio, _, _, _` | 反色强度,空,空,空
*/
CircleInvert,
/**
* 灰度特效,可与任意特效叠加\
* 参数分别为:\
* `data1: x, y, radius, decay` | 横坐标,纵坐标,半径,衰减开始半径\
* `data2: ratio, _, _, _` | 黑白强度,空,空,空
*/
CircleGray,
/**
* 对比度特效,可与任意特效叠加\
* 参数分别为:\
* `data1: x, y, radius, decay` | 横坐标,纵坐标,半径,衰减开始半径\
* `data2: ratio, _, _, _` | 对比度强度0表示不变-1表示灰色1表示2倍对比度
*/
CircleContrast,
/**
* 饱和度特效,可与任意特效叠加\
* 参数分别为:\
* `data1: x, y, radius, decay` | 横坐标,纵坐标,半径,衰减开始半径\
* `data2: ratio, _, _, _` | 对比度强度0表示不变-1表示灰色1表示2倍饱和度
*/
CircleSaturate,
/**
* 饱和度特效,可与任意特效叠加\
* 参数分别为:\
* `data1: x, y, radius, decay` | 横坐标,纵坐标,半径,衰减开始半径\
* `data2: angle, _, _, _` | 旋转角度0表示旋转0度1表示旋转360度
*/
CircleHue,
/**
* 圆形扭曲特效\
* 参数分别为:\
* `data1: x, y, maxRaius, waveRadius` | 中心横坐标,中心纵坐标,波纹最大传播距离,波纹环的半径\
* `data2: amplitude, attenuation, linear, tangential` \
*   * `amplitude`: 震动幅度1表示幅度为1个屏幕受摄像机缩放影响\
*   * `attenuation`: 衰减比例,即衰减速度\
*   * `linear`: 开始线性衰减时间(为确保波纹最终归于平静,在衰减时,会首先平方反比衰减,
* 然后到此值时转为线性衰减。此参数范围0-10.5表示从一半时间处开始线性衰减)\
*   * `tangential`: 切向扭曲比例1表示与振动幅度一致\
* `data3: startPhase, endPhase, _, _` | 起始位置相位(靠近波纹中心的位置),终止位置相位(远离波纹中心位置),空,空,
* 其中相位一个周期为2 * PI填 Math.PI * 2 则表示相位从一个周期处开始
*/
CircleWarp,
/**
* 圆形切向扭曲特效\
* 参数分别为:\
* `data1: x, y, minRadius, maxRadius` | 中心横坐标,中心纵坐标,扭曲环内圈半径,扭曲环外圈半径
* 1表示扭曲了相位角的程度例如Math.PI的相位幅度为1表示旋转整整一圈\
* `data2: startPhase, endPhase, _, _` 起始位置相位(靠近波纹中心的位置),终止位置相位(远离波纹中心的位置),空,空
*/
CircleWarpTangetial,
/**
* 圆形亮度特效,可与任何特效叠加\
* 参数分别为:\
* `data1: x, y, radius, decay` | 中心横坐标,中心纵坐标,半径,衰减开始半径\
* `data2: ratio, _, _, _` | 亮度0表示不变1表示2倍亮度
*/
CircleBrightness
}
type EffectData = [x0: number, x1: number, x2: number, x3: number];
const warpEffect = new Set<PointEffectType>();
warpEffect
.add(PointEffectType.CircleWarp)
.add(PointEffectType.CircleWarpTangetial);
export class PointEffect {
/** 着色器程序 */
private program?: ShaderProgram;
/** 着色器渲染元素 */
private shader?: Shader;
/** 是否开始计时器 */
private started: boolean = false;
/** 计时器委托id */
private delegation: number = -1;
/** 特效id计数器用于修改对应特效的数据 */
private effectId: number = 0;
/** 特效id到特效索引的映射 */
private idIndexMap: Map<number, number> = new Map();
/** 变换矩阵 */
private transform?: Transform;
/** 特效最大数量 */
private effectCount: number = 0;
/** 特效数据 */
private dataList: Float32Array = new Float32Array();
/** 经过矩阵变换操作后的特效数据 */
private transformed: Float32Array = new Float32Array();
/** 当前的数据指针,也就是下一次添加特效应该添加至哪 */
private dataPointer: number = 0;
/** 是否需要更新数据 */
private needUpdateData: boolean = false;
/** 需要在之后添加的特效 */
private toAdd: Set<number[]> = new Set();
/** 每个特效的开始时间 */
private startTime: Map<number, number> = new Map();
/**
* 为着色器创建程序
* @param shader 着色器渲染元素
* @param itemCount 最大效果数量
*/
create(shader: Shader, itemCount: number) {
if (this.program || this.shader || this.started) return;
const program = shader.createProgram(ShaderProgram);
program.fs(generateFragment(itemCount));
program.requestCompile();
// 声明变量
program.defineUniform('u_count', shader.UNIFORM_1i);
program.defineUniform('u_screen', shader.UNIFORM_2f);
program.defineUniformBlock(
'EffectInfo',
itemCount * 16,
shader.gl.DYNAMIC_DRAW,
0
);
this.program = program;
this.shader = shader;
this.effectCount = itemCount;
this.dataList = new Float32Array(itemCount * 16);
this.transformed = new Float32Array(itemCount * 16);
this.transform = shader.transform;
return program;
}
/**
* 在下一帧更新特效数据
*/
requestUpdate() {
if (this.dataList[0] !== PointEffectType.None) {
this.needUpdateData = true;
}
if (this.shader) this.shader.update(this.shader);
}
/**
* 设置本特效实例的变换矩阵,变换矩阵可以在设置特效位置时自动进行变换,配合摄像机视角
* @param tranform 变换矩阵
*/
setTransform(tranform: Transform) {
this.transform = tranform;
}
/**
* 添加一个特效如果特效还未开始那么会在其开始时添加特效注意特效数据必须填四位不足者补0
* @param type 特效类型
* @param startTime 特效开始时间
* @param time 特效持续时间
* @param data1 第一组自定义数据,可选
* @param data2 第二组自定义数据,可选
* @param data3 第三组自定义数据,可选
* @returns 这个特效的唯一id可用于设置特效
*/
addEffect(
type: PointEffectType,
startTime: number,
time: number,
data1: EffectData = [0, 0, 0, 0],
data2: EffectData = [0, 0, 0, 0],
data3: EffectData = [0, 0, 0, 0]
) {
if (type === PointEffectType.None) return -1;
const now = Date.now();
// 如果已经结束,那么什么都不干
if (now > time + startTime) return -1;
// 填充 0 是因为 std140 布局中,每个数据都会占据 16 的倍数个字节
// 不过第二项填充一个整数,是为了能够对每个特效进行标识,从而能够对某个特效进行修改
const id = this.effectId++;
// 第三项为特效的进度
const data = [type, id, 0, time, ...data1, ...data2, ...data3];
this.startTime.set(id, startTime);
if (now >= startTime) {
this.addEffectToList(data);
} else {
// 如果还没开始,那么添加至预备队列
this.toAdd.add(data);
}
return id;
}
/**
* 立刻删除一个特效
* @param id 要删除的特效的id
*/
deleteEffect(id: number) {
this.removeEffect(this.findIndexById(id));
}
/**
* 清除所有特性
*/
clearEffect() {
this.dataList.fill(0);
this.dataPointer = 0;
this.needUpdateData = true;
}
/**
* 设置一个特效的数据注意特效数据必须填四位不足者补0
* @param id 特效id
* @param data1 第一组自定义数据,可选
* @param data2 第二组自定义数据,可选
* @param data3 第三组自定义数据,可选
*/
setEffect(
id: number,
data1?: EffectData,
data2?: EffectData,
data3?: EffectData
) {
const index = this.findIndexById(id);
if (index >= this.dataPointer || index === -1) return;
const list = this.dataList;
if (data1) {
list.set(data1, index * 16 + 4);
}
if (data2) {
list.set(data2, index * 16 + 8);
}
if (data3) {
list.set(data3, index * 16 + 12);
}
this.needUpdateData = true;
}
private findIndexById(id: number) {
const map = this.idIndexMap.get(id);
if (map !== void 0) return map;
let index = -1;
const list = this.dataList;
for (let i = 0; i < this.effectCount; i++) {
const realIndex = i * 16 + 1;
if (list[realIndex] === id) {
index = i;
break;
}
}
if (index !== -1) {
this.idIndexMap.set(id, index);
}
return index;
}
private addEffectToList(data: number[]) {
if (this.dataPointer >= this.effectCount) {
logger.warn(1101);
return;
}
const type = data[0];
const list = this.dataList;
if (warpEffect.has(type)) {
list.copyWithin(16, 0);
list.set(data, 0);
this.dataPointer++;
this.idIndexMap.clear();
} else {
const id = data[1];
list.set(data, this.dataPointer * 16);
this.idIndexMap.set(id, this.dataPointer);
this.dataPointer++;
}
this.needUpdateData = true;
}
private removeEffect(index: number) {
if (index >= this.effectCount) return;
if (this.dataPointer === 0) return;
const list = this.dataList;
const id = list[index * 16 + 1];
this.startTime.delete(id);
list.copyWithin(index * 16, index * 16 + 16);
list.fill(0, -16, -1);
this.dataPointer--;
this.needUpdateData = true;
this.idIndexMap.clear();
}
/**
* 使用这个特效作为着色器程序
*/
use() {
if (!this.shader || !this.program) return;
this.shader.useProgram(this.program);
const uCount =
this.program.getUniform<UniformType.Uniform1f>('u_count');
uCount?.set(this.effectCount);
}
/**
* 开始计时器
*/
start() {
if (!this.shader || !this.program) return;
this.started = true;
const block = this.program.getUniformBlock('EffectInfo')!;
const screen =
this.program.getUniform<UniformType.Uniform2f>('u_screen');
const { width, height } = this.shader;
const scale = core.domStyle.scale * devicePixelRatio;
screen?.set(width * scale, height * scale);
// 不知道性能怎么样,只能试试看了
this.delegation = this.shader.delegateTicker(() => {
if (!this.started) {
this.shader?.removeTicker(this.delegation);
return;
}
const now = Date.now();
const toDelete = new Set<number[]>();
this.toAdd.forEach(v => {
const id = v[1];
const startTime = this.startTime.get(id);
if (!startTime) return;
const time = v[3];
if (now > startTime + time) {
toDelete.add(v);
} else if (now >= startTime) {
this.addEffectToList(v);
toDelete.add(v);
}
});
toDelete.forEach(v => this.toAdd.delete(v));
const toRemove: number[] = [];
const list = this.dataList;
// 倒序以保证删除时不会影响到其他删除
for (let i = this.effectCount - 1; i >= 0; i--) {
const type = list[i * 16];
if (type === PointEffectType.None) continue;
const id = list[i * 16 + 1];
const start = this.startTime.get(id);
if (!start) {
toRemove.push(i);
continue;
}
const time = list[i * 16 + 3];
const progress = (now - start) / time;
if (progress > 1) toRemove.push(i);
list[i * 16 + 2] = progress;
}
toRemove.forEach(v => {
this.removeEffect(v);
});
if (this.needUpdateData) {
const list = this.dataList;
const transformed = this.transformed;
transformed.set(list);
this.transformData();
block.set(transformed);
this.needUpdateData = false;
}
});
}
/**
* 结束计时器
*/
end() {
if (!this.started || !this.shader || !this.program) return;
this.shader.removeTicker(this.delegation);
this.started = false;
this.dataList.fill(0);
this.dataPointer = 0;
}
destroy() {
this.end();
if (this.shader && this.program) {
this.shader.deleteProgram(this.program);
}
}
private transformData() {
const transform = this.transform;
if (!transform) return;
const count = this.effectCount;
const list = this.dataList;
const transformed = this.transformed;
let ratio = core.domStyle.scale;
if (this.shader?.highResolution) ratio *= devicePixelRatio;
const scale = transform.scaleX * ratio;
const scaleTransform = new Transform();
scaleTransform.scale(ratio, ratio);
const scaled = scaleTransform.multiply(transform);
const fixedHeight = core._PY_ * ratio;
const transformXY = (index: number) => {
const x = list[index + 4];
const y = list[index + 5];
const [tx, ty] = Transform.transformed(scaled, x, y);
transformed[index + 4] = tx;
transformed[index + 5] = fixedHeight - ty;
};
for (let i = 0; i < count; i++) {
const index = i * 16;
const type = list[index];
switch (type) {
case PointEffectType.CircleGray:
case PointEffectType.CircleInvert:
case PointEffectType.CircleContrast:
case PointEffectType.CircleSaturate:
case PointEffectType.CircleHue:
case PointEffectType.CircleBrightness:
case PointEffectType.CircleWarpTangetial: {
transformXY(index);
transformed[index + 6] *= scale;
transformed[index + 7] *= scale;
break;
}
case PointEffectType.CircleWarp: {
transformXY(index);
transformed[index + 6] *= scale;
transformed[index + 7] *= scale;
transformed[index + 8] *= scale;
break;
}
case PointEffectType.None: {
return;
}
}
}
}
}
function generateFragment(itemCount: number) {
return /* glsl */ `
uniform int u_count;
uniform vec2 u_screen; // 画布长宽
out vec4 outColor;
struct Effect {
vec2 type; // 效果类型
vec2 time; // 开始时间,持续时长
vec4 info1; // 第一组信息,表示自定义参数
vec4 info2; // 第二组信息,表示自定义参数
vec4 info3; // 第三组信息,表示自定义参数
};
layout (std140) uniform EffectInfo {
Effect effects[${itemCount}];
};
// Helper function: RGB to HSL conversion
vec3 rgb2hsl(vec3 color) {
float maxC = max(max(color.r, color.g), color.b);
float minC = min(min(color.r, color.g), color.b);
float delta = maxC - minC;
float h = 0.0;
float s = 0.0;
float l = (maxC + minC) * 0.5;
if (delta != 0.0) {
s = (l < 0.5) ? delta / (maxC + minC) : delta / (2.0 - maxC - minC);
if (maxC == color.r) {
h = (color.g - color.b) / delta + (color.g < color.b ? 6.0 : 0.0);
} else if (maxC == color.g) {
h = (color.b - color.r) / delta + 2.0;
} else {
h = (color.r - color.g) / delta + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
// Helper function: Hue to RGB conversion
float hue2rgb(float p, float q, float t) {
if (t < 0.0) t += 1.0;
if (t > 1.0) t -= 1.0;
if (t < 1.0 / 6.0) return p + (q - p) * 6.0 * t;
if (t < 1.0 / 2.0) return q;
if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
return p;
}
// Helper function: HSL to RGB conversion
vec3 hsl2rgb(vec3 hsl) {
float h = hsl.x;
float s = hsl.y;
float l = hsl.z;
float r, g, b;
if (s == 0.0) {
r = g = b = l; // Achromatic (gray)
} else {
float q = (l < 0.5) ? (l * (1.0 + s)) : (l + s - l * s);
float p = 2.0 * l - q;
r = hue2rgb(p, q, h + 1.0 / 3.0);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1.0 / 3.0);
}
return vec3(r, g, b);
}
// x: 横坐标 y: 纵坐标 z: 半径 w: 衰减开始半径
// 计算圆形效果的衰减程度
float calCircleDecay(vec4 data) {
float dis = distance(data.xy, gl_FragCoord.xy);
if (dis <= data.w) return 1.0;
if (dis >= data.z) return 0.0;
if (data.z <= data.w) return 1.0;
return 1.0 - (dis - data.w) / (data.z - data.w);
}
// data1: x, y, maxRadius, waveRadius
// data2: amplitude, attenuation, linear, _
// 计算波纹扭曲的衰减程度,从 x = 1 的平方反比函数开始,衰减至 x = 1 + attenuation然后线性衰减
float calWarpDecay(float progress, vec4 data1, vec4 data2) {
if (progress >= data2.z) {
float end = 1.0 / pow(1.0 + data2.y, 2.0);
return end - end * (progress - data2.z) / (1.0 - data2.z);
} else {
return 1.0 / pow(1.0 + (progress / data2.z) * data2.y, 2.0);
}
}
void main() {
vec2 coord = v_texCoord;
vec4 color = texture(u_sampler, v_texCoord);
for (int i = 0; i < u_count; i++) {
Effect effect = effects[i];
int effectType = int(effect.type.x);
vec2 timeInfo = effect.time;
vec4 data1 = effect.info1;
vec4 data2 = effect.info2;
vec4 data3 = effect.info3;
if (effectType == ${PointEffectType.None}) break;
float progress = timeInfo.x;
// 下面开始实施对应的着色器特效
// 反色data1: x y radius decaydata2: ratio _ _ _
if (effectType == ${PointEffectType.CircleInvert}) {
float ratio = data2.x * calCircleDecay(data1);
if (ratio > 0.0) {
vec3 delta = (1.0 - 2.0 * color.rgb) * ratio;
color.rgb = clamp(color.rgb + delta, 0.0, 1.0);
}
}
// 灰度data1: x y radius decaydata2: ratio _ _ _
else if (effectType == ${PointEffectType.CircleGray}) {
float ratio = data2.x * calCircleDecay(data1);
if (ratio > 0.0) {
float gray = dot(color.rgb, vec3(0.2126, 0.7125, 0.0722));
vec3 grayed = color.rgb - gray;
color = vec4(color.rgb - grayed * ratio, 1.0);
}
}
// 亮度data1: x y radius decaydata2: ratio _ _ _
else if (effectType == ${PointEffectType.CircleBrightness}) {
float ratio = data2.x * calCircleDecay(data1) + 1.0;
if (ratio > 0.0) {
color.rgb *= ratio;
}
}
// 对比度data1: x y radius decaydata2: ratio _ _ _
else if (effectType == ${PointEffectType.CircleContrast}) {
float ratio = data2.x * calCircleDecay(data1) + 1.0;
if (ratio > 0.0) {
color.rgb = mix(vec3(0.5), color.rgb, ratio);
}
}
// 饱和度data1: x y radius decaydata2: ratio _ _ _
else if (effectType == ${PointEffectType.CircleSaturate}) {
float ratio = data2.x * calCircleDecay(data1) + 1.0;
if (ratio > 0.0) {
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = mix(vec3(gray), color.rgb, ratio);
}
}
// 色相旋转data1: x y radius decaydata2: angle _ _ _
else if (effectType == ${PointEffectType.CircleHue}) {
float ratio = data2.x * calCircleDecay(data1);
if (ratio > 0.0) {
vec3 hsl = rgb2hsl(color.rgb);
hsl.x = mod(hsl.x + data2.x * ratio, 1.0);
color.rgb = hsl2rgb(hsl);
}
}
// 扭曲data1: x, y, maxRadius, waveRadius; data2: amplitude, attenuation, linear, _
// data3: startPhase, endPhase, _, _
else if (effectType == ${PointEffectType.CircleWarp}) {
float dis = distance(data1.xy, gl_FragCoord.xy);
// 当前半径
float radius = progress * data1.z;
if (dis > radius + data1.w || dis < radius - data1.w) continue;
float ratio = data2.x * calWarpDecay(progress, data1, data2);
float halfRadius = data1.w / 2.0;
if (ratio > 0.0 && abs(dis - radius) <= halfRadius) {
// 计算当前相位
float x = ((dis - radius) / data1.w + 0.5) * (data3.y - data3.x) + data3.x;
float wave = sin(x) * ratio;
float xRatio = (gl_FragCoord.x - data1.x) / dis;
float yRatio = (gl_FragCoord.y - data1.y) / dis;
coord.x += wave * xRatio + wave * yRatio * data2.w;
coord.y += wave * yRatio + wave * xRatio * data2.w;
color = texture(u_sampler, coord);
}
}
// 切向扭曲data1: x, y, minRadius, maxRadius; data2: startPhase, endPhase, _, _
else if (effectType == ${PointEffectType.CircleWarpTangetial}) {
float dis = distance(data1.xy, gl_FragCoord.xy);
float ratio = (dis - data1.z) / (data1.w - data1.z);
if (ratio <= 1.0 && ratio > 0.0) {
float phase = ratio * (data2.y - data2.x) + data2.x;
float waveSin = sin(phase);
float waveCos = cos(phase);
// 相对坐标
vec2 relCoord = gl_FragCoord.xy - data1.xy;
coord.x = (data1.x + relCoord.x * waveCos - relCoord.y * waveSin) / u_screen.x;
coord.y = 1.0 - (data1.y + relCoord.x * waveSin + relCoord.y * waveCos) / u_screen.y;
color = texture(u_sampler, coord);
}
}
}
outColor = color;
}
`;
}