mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-31 23:29:27 +08:00
feat: 一些着色器
This commit is contained in:
parent
b3727080b2
commit
f01d185db3
@ -318,6 +318,7 @@ events.prototype.restart = function () {
|
||||
core.hideStatusBar();
|
||||
core.showStartAnimate();
|
||||
core.playBgm(main.startBgm);
|
||||
Mota.require('var', 'hook').emit('restart');
|
||||
};
|
||||
|
||||
////// 询问是否需要重新开始 //////
|
||||
|
@ -130,6 +130,7 @@ main.floors.MT16=
|
||||
]
|
||||
},
|
||||
"追逐战后录像会进行自动修复,不用担心录像问题",
|
||||
"如果逃脱失败,或者想重新开始追逐战,直接读取自动存档即可,会跳过前奏",
|
||||
{
|
||||
"type": "hideStatusBar",
|
||||
"toolbox": true
|
||||
@ -400,6 +401,9 @@ main.floors.MT16=
|
||||
{
|
||||
"type": "function",
|
||||
"function": "function(){\nMota.Plugin.require('chase_r').start(false);\n}"
|
||||
},
|
||||
{
|
||||
"type": "autoSave"
|
||||
}
|
||||
],
|
||||
"2,23": [
|
||||
|
@ -1,545 +0,0 @@
|
||||
import { Animation, Ticker, hyper } from 'mutate-animate';
|
||||
import { EventEmitter } from '../common/eventEmitter';
|
||||
import { ensureArray } from '@/plugin/utils';
|
||||
|
||||
interface ShaderEvent {}
|
||||
|
||||
const isWebGLSupported = (() => {
|
||||
return !!document.createElement('canvas').getContext('webgl');
|
||||
})();
|
||||
|
||||
type ShaderColorArray = [number, number, number, number];
|
||||
type ShaderEffectImage = Exclude<TexImageSource, VideoFrame | ImageData>;
|
||||
|
||||
interface ProgramInfo {
|
||||
program: WebGLProgram;
|
||||
attrib: Record<string, number>;
|
||||
uniform: Record<string, WebGLUniformLocation>;
|
||||
}
|
||||
|
||||
interface ShaderEffectBuffer {
|
||||
position: WebGLBuffer;
|
||||
texture: WebGLBuffer;
|
||||
indices: WebGLBuffer;
|
||||
}
|
||||
|
||||
interface ShaderEffectShader {
|
||||
vertex: WebGLShader;
|
||||
fragment: WebGLShader;
|
||||
}
|
||||
|
||||
interface MixedImage {
|
||||
canvas: HTMLCanvasElement;
|
||||
update(): void;
|
||||
}
|
||||
|
||||
type UniformBinderNum = 1 | 2 | 3 | 4;
|
||||
type UniformBinderType = 'f' | 'i';
|
||||
type UniformFunc<
|
||||
N extends UniformBinderNum,
|
||||
T extends UniformBinderType,
|
||||
V extends 'v' | ''
|
||||
> = `uniform${N}${T}${V}`;
|
||||
|
||||
type UniformBinderValue<N extends UniformBinderNum> = N extends 1
|
||||
? number
|
||||
: N extends 2
|
||||
? [number, number]
|
||||
: N extends 3
|
||||
? [number, number, number]
|
||||
: [number, number, number, number];
|
||||
|
||||
interface UniformBinder<
|
||||
N extends UniformBinderNum,
|
||||
T extends UniformBinderType,
|
||||
V extends 'v' | ''
|
||||
> {
|
||||
value: UniformBinderValue<N>;
|
||||
set(value: UniformBinderValue<N>): void;
|
||||
get(): UniformBinderValue<N>;
|
||||
}
|
||||
|
||||
const builtinVs = `
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
|
||||
attribute vec4 aVertexPosition;
|
||||
attribute vec2 aTextureCoord;
|
||||
|
||||
varying highp vec2 vTextureCoord;
|
||||
`;
|
||||
const builtinFs = `
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
|
||||
varying highp vec2 vTextureCoord;
|
||||
|
||||
uniform sampler2D uSampler;
|
||||
`;
|
||||
|
||||
export class ShaderEffect extends EventEmitter<ShaderEvent> {
|
||||
canvas: HTMLCanvasElement;
|
||||
gl: WebGLRenderingContext;
|
||||
program: WebGLProgram | null = null;
|
||||
texture: WebGLTexture | null = null;
|
||||
programInfo: ProgramInfo | null = null;
|
||||
buffer: ShaderEffectBuffer | null = null;
|
||||
shader: ShaderEffectShader | null = null;
|
||||
textureCanvas: MixedImage | null = null;
|
||||
|
||||
private baseImages: ShaderEffectImage[] = [];
|
||||
private background: ShaderColorArray = [0, 0, 0, 0];
|
||||
|
||||
private _vsSource: string = '';
|
||||
private _fsSource: string = '';
|
||||
|
||||
get vsSource() {
|
||||
return builtinVs + this._vsSource;
|
||||
}
|
||||
get fsSource() {
|
||||
return builtinFs + this._fsSource;
|
||||
}
|
||||
|
||||
constructor(background: ShaderColorArray) {
|
||||
super();
|
||||
this.canvas = document.createElement('canvas');
|
||||
if (!isWebGLSupported) {
|
||||
throw new Error(
|
||||
`Cannot initialize ShaderEffect, since your device does not support webgl.`
|
||||
);
|
||||
}
|
||||
this.gl = this.canvas.getContext('webgl')!;
|
||||
this.gl.clearColor(...background);
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||
this.background = background;
|
||||
const s = core.domStyle.scale * devicePixelRatio;
|
||||
this.canvas.width = s * core._PX_;
|
||||
this.canvas.height = s * core._PY_;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置特效作用于的图片,不会修改原图片,而是会在 ShaderEffect.canvas 画布元素中展现
|
||||
* @param img 特效作用于的图片
|
||||
*/
|
||||
baseImage(...img: ShaderEffectImage[]) {
|
||||
this.baseImages = img;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制重新渲染特效
|
||||
* @param compile 是否重新编译着色器脚本,并重新创建纹理
|
||||
*/
|
||||
update(compile: boolean = false) {
|
||||
if (compile) this.compile();
|
||||
this.textureCanvas?.update();
|
||||
this.drawScene();
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅重新编译着色器,不进行纹理创建和特效渲染
|
||||
*/
|
||||
compile() {
|
||||
const gl = this.gl;
|
||||
gl.deleteProgram(this.program);
|
||||
gl.deleteTexture(this.texture);
|
||||
gl.deleteBuffer(this.buffer?.position ?? null);
|
||||
gl.deleteBuffer(this.buffer?.texture ?? null);
|
||||
gl.deleteShader(this.shader?.vertex ?? null);
|
||||
gl.deleteShader(this.shader?.fragment ?? null);
|
||||
|
||||
this.program = this.createProgram();
|
||||
this.programInfo = this.getProgramInfo();
|
||||
this.buffer = this.initBuffers();
|
||||
this.texture = this.createTexture();
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
||||
gl.useProgram(this.program);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶点着色器,使用 glsl 编写,插件提供了一些新的 api
|
||||
* 着色器中必须包含 main 函数,同时为 gl_Position 赋值
|
||||
* @param shader 顶点着色器代码
|
||||
*/
|
||||
vs(shader: string) {
|
||||
this._vsSource = shader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置片元着色器,使用 glsl 编写,插件提供了一些新的 api
|
||||
* 着色器中必须包含 main 函数,同时为 gl_FragColor 赋值
|
||||
* @param shader 片元着色器代码
|
||||
*/
|
||||
fs(shader: string) {
|
||||
this._fsSource = shader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制特效
|
||||
*/
|
||||
drawScene() {
|
||||
// 清空画布
|
||||
const gl = this.gl;
|
||||
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
gl.clearColor(...this.background);
|
||||
gl.clearDepth(1);
|
||||
gl.enable(gl.DEPTH_TEST);
|
||||
gl.depthFunc(gl.LEQUAL);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||
|
||||
// 设置顶点信息
|
||||
this.setPositionAttrib();
|
||||
this.setTextureAttrib();
|
||||
|
||||
// 准备绘制
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffer!.indices);
|
||||
gl.useProgram(this.program);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
this.textureCanvas!.canvas
|
||||
);
|
||||
gl.uniform1i(this.programInfo!.uniform.uSampler, 0);
|
||||
|
||||
// 绘制
|
||||
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个全局变量绑定器,用于操作全局变量
|
||||
* @param uniform 全局变量的变量名
|
||||
* @param num 变量的元素数量,float和int视为1,vec2 vec3 vec4分别视为 2 3 4
|
||||
* @param type 数据类型,可以填'f',表示浮点型,或者填'i',表示整型
|
||||
* @param vector 是否为向量,可以填'v',表示是向量,或者填'',表示不是向量
|
||||
* @returns 一个uniform绑定器,用于操作全局变量uniform
|
||||
*/
|
||||
createUniformBinder<
|
||||
N extends UniformBinderNum,
|
||||
T extends UniformBinderType,
|
||||
V extends 'v' | ''
|
||||
>(uniform: string, num: N, type: T, vector: V): UniformBinder<N, T, V> {
|
||||
if (!this.program) {
|
||||
throw new Error(
|
||||
`Uniform binder should be use when the program initialized.`
|
||||
);
|
||||
}
|
||||
|
||||
const suffix = `${num}${type}${vector ? 'v' : ''}`;
|
||||
const func = `uniform${suffix}` as UniformFunc<N, T, V>;
|
||||
const value = (
|
||||
num === 1 ? 0 : Array(num).fill(0)
|
||||
) as UniformBinderValue<N>;
|
||||
|
||||
const loc = this.gl.getUniformLocation(this.program, uniform);
|
||||
const gl = this.gl;
|
||||
|
||||
return {
|
||||
value,
|
||||
set(value) {
|
||||
this.value = value;
|
||||
let v;
|
||||
if (vector === 'v') {
|
||||
let _v = ensureArray(value);
|
||||
if (type === 'f') {
|
||||
v = new Float32Array(_v);
|
||||
} else {
|
||||
v = new Int32Array(_v);
|
||||
}
|
||||
} else {
|
||||
v = ensureArray(value);
|
||||
}
|
||||
// 对uniform赋值
|
||||
if (vector === 'v') {
|
||||
// @ts-ignore
|
||||
gl[func](loc, v);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
gl[func](loc, ...v);
|
||||
}
|
||||
},
|
||||
get() {
|
||||
return this.value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private createProgram() {
|
||||
const gl = this.gl;
|
||||
const vs = this.loadShader(gl.VERTEX_SHADER, this.vsSource);
|
||||
const fs = this.loadShader(gl.FRAGMENT_SHADER, this.fsSource);
|
||||
|
||||
this.shader = {
|
||||
vertex: vs,
|
||||
fragment: fs
|
||||
};
|
||||
|
||||
const program = gl.createProgram()!;
|
||||
gl.attachShader(program, vs);
|
||||
gl.attachShader(program, fs);
|
||||
gl.linkProgram(program);
|
||||
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
throw new Error(
|
||||
`Cannot initialize shader program. Error info: ${gl.getProgramInfoLog(
|
||||
program
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
private loadShader(type: number, source: string) {
|
||||
const gl = this.gl;
|
||||
const shader = gl.createShader(type)!;
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
throw new Error(
|
||||
`Cannot compile ${
|
||||
type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'
|
||||
} shader. Error info: ${gl.getShaderInfoLog(shader)}`
|
||||
);
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
private createTexture() {
|
||||
const gl = this.gl;
|
||||
|
||||
const img = mixImage(this.baseImages);
|
||||
this.textureCanvas = img;
|
||||
const texture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
img.canvas
|
||||
);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
private initBuffers(): ShaderEffectBuffer {
|
||||
const positions = new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]);
|
||||
const posBuffer = this.initBuffer(positions);
|
||||
const textureCoord = new Float32Array([1, 1, 0, 1, 1, 0, 0, 0]);
|
||||
const textureBuffer = this.initBuffer(textureCoord);
|
||||
|
||||
return (this.buffer = {
|
||||
position: posBuffer,
|
||||
texture: textureBuffer,
|
||||
indices: this.initIndexBuffer()
|
||||
});
|
||||
}
|
||||
|
||||
private initBuffer(pos: Float32Array) {
|
||||
const gl = this.gl;
|
||||
const posBuffer = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, pos, gl.STATIC_DRAW);
|
||||
|
||||
return posBuffer;
|
||||
}
|
||||
|
||||
private initIndexBuffer() {
|
||||
const gl = this.gl;
|
||||
const buffer = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
|
||||
const indices = new Uint16Array([0, 1, 2, 2, 3, 1]);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private getProgramInfo(): ProgramInfo | null {
|
||||
if (!this.program) return null;
|
||||
const gl = this.gl;
|
||||
const pro = this.program;
|
||||
return (this.programInfo = {
|
||||
program: pro,
|
||||
attrib: {
|
||||
vertexPosition: gl.getAttribLocation(pro, 'aVertexPosition'),
|
||||
textureCoord: gl.getAttribLocation(pro, 'aTextureCoord')
|
||||
},
|
||||
uniform: {
|
||||
uSampler: gl.getUniformLocation(pro, 'uSampler')!
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setTextureAttrib() {
|
||||
const gl = this.gl;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer!.texture);
|
||||
gl.vertexAttribPointer(
|
||||
this.programInfo!.attrib.textureCoord,
|
||||
2,
|
||||
gl.FLOAT,
|
||||
false,
|
||||
0,
|
||||
0
|
||||
);
|
||||
gl.enableVertexAttribArray(this.programInfo!.attrib.textureCoord);
|
||||
}
|
||||
|
||||
private setPositionAttrib() {
|
||||
const gl = this.gl;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer!.position);
|
||||
gl.vertexAttribPointer(
|
||||
this.programInfo!.attrib.vertexPosition,
|
||||
2,
|
||||
gl.FLOAT,
|
||||
false,
|
||||
0,
|
||||
0
|
||||
);
|
||||
gl.enableVertexAttribArray(this.programInfo!.attrib.vertexPosition);
|
||||
}
|
||||
|
||||
static defaultVs: string = `
|
||||
void main() {
|
||||
vTextureCoord = aTextureCoord;
|
||||
gl_Position = aVertexPosition;
|
||||
}
|
||||
`;
|
||||
static defaultFs: string = `
|
||||
void main() {
|
||||
gl_FragColor = texture2D(uSampler, vTextureCoord);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
interface GameCanvasReplacer {
|
||||
recover(): void;
|
||||
append(): void;
|
||||
remove(): void;
|
||||
update(compile?: boolean): void;
|
||||
}
|
||||
|
||||
function floorPower2(value: number) {
|
||||
return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 webgl 纹理图片,规范成2的幂的形式
|
||||
* @param img 要被规范化的图片
|
||||
*/
|
||||
function normalizeTexture(img: ShaderEffectImage) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = floorPower2(img.width);
|
||||
canvas.height = floorPower2(img.height);
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function mixImage(imgs: ShaderEffectImage[]): MixedImage {
|
||||
// todo: 直接使用webgl纹理进行图片混合
|
||||
if (imgs.length === 0) {
|
||||
throw new Error(`Cannot mix images whose count is 0.`);
|
||||
}
|
||||
if (
|
||||
imgs.some(v => v.width !== imgs[0].width || v.height !== imgs[0].height)
|
||||
) {
|
||||
throw new Error(`Cannot mix images with different size.`);
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = floorPower2(imgs[0].width);
|
||||
canvas.height = floorPower2(imgs[0].height);
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
imgs.forEach(v => {
|
||||
const img = normalizeTexture(v);
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
});
|
||||
return {
|
||||
canvas,
|
||||
update() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
imgs.forEach(v => {
|
||||
const img = normalizeTexture(v);
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 为一个着色器特效创建每帧更新的 ticker,部分条件下性能表现可能较差
|
||||
* @param effect 要每帧更新的着色器特效
|
||||
*/
|
||||
export function setTickerFor(effect: ShaderEffect) {
|
||||
const ticker = new Ticker();
|
||||
ticker.add(() => {
|
||||
effect.update();
|
||||
});
|
||||
|
||||
return ticker;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用着色器特效画布替换样板画布
|
||||
* @param effect 着色器特效实例
|
||||
* @param canvas 要替换的画布列表
|
||||
* @returns 特效控制器,用于控制特效的显示
|
||||
*/
|
||||
export function replaceGameCanvas(
|
||||
effect: ShaderEffect,
|
||||
canvas: string[]
|
||||
): GameCanvasReplacer {
|
||||
let zIndex = 0;
|
||||
canvas.forEach(v => {
|
||||
const canvas = core.canvas[v].canvas;
|
||||
const style = getComputedStyle(canvas);
|
||||
const z = parseInt(style.zIndex);
|
||||
if (z > zIndex) zIndex = z;
|
||||
canvas.style.display = 'none';
|
||||
});
|
||||
const gl = effect.canvas;
|
||||
gl.style.left = '0';
|
||||
gl.style.top = '0';
|
||||
gl.style.position = 'absolute';
|
||||
gl.style.zIndex = zIndex.toString();
|
||||
gl.style.display = 'block';
|
||||
gl.style.width = `${core._PX_ * core.domStyle.scale}px`;
|
||||
gl.style.height = `${core._PY_ * core.domStyle.scale}px`;
|
||||
core.dom.gameDraw.appendChild(gl);
|
||||
|
||||
return {
|
||||
recover() {
|
||||
canvas.forEach(v => {
|
||||
const canvas = core.canvas[v].canvas;
|
||||
canvas.style.display = 'block';
|
||||
});
|
||||
gl.style.display = 'none';
|
||||
},
|
||||
append() {
|
||||
canvas.forEach(v => {
|
||||
const canvas = core.canvas[v].canvas;
|
||||
canvas.style.display = 'none';
|
||||
});
|
||||
gl.style.display = 'block';
|
||||
},
|
||||
remove() {
|
||||
this.recover();
|
||||
gl.remove();
|
||||
},
|
||||
update(compile?: boolean) {
|
||||
effect.update(compile);
|
||||
}
|
||||
};
|
||||
}
|
@ -600,13 +600,14 @@ export class CameraAnimation extends EventEmitter<CameraAnimationEvent> {
|
||||
if (!end) return;
|
||||
const t = end.start + end.time;
|
||||
if (t > endTime) endTime = t;
|
||||
const cam = this.camera;
|
||||
|
||||
if (ope.type === 'translate') {
|
||||
this.camera.applyTranslateAnimation(ope, exe.animation, t + 50);
|
||||
cam.applyTranslateAnimation(ope, exe.animation, t + 100);
|
||||
} else if (ope.type === 'rotate') {
|
||||
this.camera.applyRotateAnimation(ope, exe.animation, t + 50);
|
||||
cam.applyRotateAnimation(ope, exe.animation, t + 100);
|
||||
} else {
|
||||
this.camera.applyScaleAnimation(ope, exe.animation, t + 50);
|
||||
cam.applyScaleAnimation(ope, exe.animation, t + 100);
|
||||
}
|
||||
});
|
||||
this.endTime = endTime + this.startTime;
|
||||
|
@ -14,7 +14,7 @@ import { Container } from './container';
|
||||
|
||||
let main: MotaRenderer;
|
||||
|
||||
Mota.require('var', 'loading').once('loaded', () => {
|
||||
Mota.require('var', 'loading').once('coreInit', () => {
|
||||
const render = new MotaRenderer();
|
||||
main = render;
|
||||
render.mount();
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
import { logger } from '../common/logger';
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { isWebGL2Supported } from '../fx/webgl';
|
||||
@ -7,9 +6,7 @@ import { ERenderItemEvent, RenderItem, RenderItemPosition } from './item';
|
||||
import { Transform } from './transform';
|
||||
|
||||
const SHADER_VERTEX_PREFIX_300 = /* glsl */ `#version 300 es
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
precision highp float;
|
||||
|
||||
in vec4 a_position;
|
||||
in vec2 a_texCoord;
|
||||
@ -17,9 +14,7 @@ in vec2 a_texCoord;
|
||||
out highp vec2 v_texCoord;
|
||||
`;
|
||||
const SHADER_VERTEX_PREFIX_100 = /* glsl */ `
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
precision highp float;
|
||||
|
||||
attribute vec4 a_position;
|
||||
attribute vec2 a_texCoord;
|
||||
@ -28,18 +23,14 @@ varying highp vec2 v_texCoord;
|
||||
`;
|
||||
|
||||
const SHADER_FRAGMENT_PREFIX_300 = /* glsl */ `#version 300 es
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
precision highp float;
|
||||
|
||||
in highp vec2 v_texCoord;
|
||||
|
||||
uniform sampler2D u_sampler;
|
||||
`;
|
||||
const SHADER_FRAGMENT_PREFIX_100 = /* glsl */ `
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
precision highp float;
|
||||
|
||||
varying highp vec2 v_texCoord;
|
||||
|
||||
@ -288,6 +279,12 @@ export class Shader extends Container<EShaderEvent> {
|
||||
gl.depthFunc(gl.LEQUAL);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||
|
||||
const pre = this.preDraw();
|
||||
if (!pre) {
|
||||
this.postDraw();
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置顶点信息
|
||||
this.setPositionAttrib();
|
||||
this.setTextureAttrib();
|
||||
@ -310,8 +307,24 @@ export class Shader extends Container<EShaderEvent> {
|
||||
|
||||
// 绘制
|
||||
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
||||
|
||||
this.postDraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在本着色器内部渲染之前执行的渲染,如果返回false,则表示不进行内部渲染,但依然会执行 {@link postDraw}
|
||||
* 继承本类,并复写此方法即可实现前置渲染功能
|
||||
*/
|
||||
protected preDraw(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在本着色器内部渲染之后执行的渲染,即使preDraw返回false,本函数也会执行
|
||||
* 继承本类,并复写此方法即可实现后置渲染功能
|
||||
*/
|
||||
protected postDraw() {}
|
||||
|
||||
/**
|
||||
* 切换着色器程序
|
||||
* @param program 着色器程序
|
||||
@ -542,13 +555,9 @@ interface ShaderUniformMatrix {
|
||||
interface ShaderUniformBlock {
|
||||
location: GLuint;
|
||||
buffer: WebGLBuffer;
|
||||
set(srcData: AllowSharedBufferSource | null, usage: GLenum): void;
|
||||
set(
|
||||
srcData: ArrayBufferView,
|
||||
usage: GLenum,
|
||||
srcOffset: number,
|
||||
length?: GLuint
|
||||
): void;
|
||||
size: number;
|
||||
set(srcData: AllowSharedBufferSource | null): void;
|
||||
set(srcData: ArrayBufferView, srcOffset: number, length?: GLuint): void;
|
||||
}
|
||||
|
||||
type UniformDefineFn = {
|
||||
@ -713,7 +722,7 @@ const attribDefine: AttribDefineFn = {
|
||||
}
|
||||
};
|
||||
|
||||
class ShaderProgram {
|
||||
export class ShaderProgram {
|
||||
/** 顶点着色器 */
|
||||
private vertex: string = DEFAULT_VS;
|
||||
/** 片元着色器 */
|
||||
@ -923,10 +932,20 @@ class ShaderProgram {
|
||||
/**
|
||||
* 定义一个 uniform block,例如 UBO,并存入本着色器程序的 uniform block 映射
|
||||
* 用于一次性向着色器传输大量数据
|
||||
* @param uniform uniform block 名称
|
||||
* @param block uniform block 名称
|
||||
* @param size 数据量,即数据长度,例如一个vec4就是4个长度
|
||||
* @param usage 缓冲区用途,例如 gl.STATIC_DRAW 是指会频繁读取但不会频繁写入
|
||||
* 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData
|
||||
* 的 `usage` 参数
|
||||
* @param binding uniform block 的索引,例如这是你设置的第一个uniform block,就可以填0,第二个填1,以此类推
|
||||
* @returns uniform block 的操作对象,可用于设置其值
|
||||
*/
|
||||
defineUniformBlock(block: string): ShaderUniformBlock | null {
|
||||
defineUniformBlock(
|
||||
block: string,
|
||||
size: number,
|
||||
usage: number,
|
||||
binding: number
|
||||
): ShaderUniformBlock | null {
|
||||
if (this.version === this.element.VERSION_ES_100) {
|
||||
logger.warn(24);
|
||||
return null;
|
||||
@ -935,18 +954,33 @@ class ShaderProgram {
|
||||
const gl = this.element.gl;
|
||||
if (!program || !gl) return null;
|
||||
const location = gl.getUniformBlockIndex(program, block);
|
||||
if (!location) return null;
|
||||
if (location === -1) return null;
|
||||
const buffer = gl.createBuffer();
|
||||
if (!buffer) return null;
|
||||
const data = new Float32Array(size);
|
||||
data.fill(0);
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
|
||||
gl.bufferData(gl.UNIFORM_BUFFER, data, usage);
|
||||
gl.uniformBlockBinding(program, location, binding);
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, binding, buffer);
|
||||
const obj: ShaderUniformBlock = {
|
||||
location,
|
||||
buffer,
|
||||
set: (srcData, usage, o?: number, l?: number) => {
|
||||
size,
|
||||
set: (data, o?: number, l?: number) => {
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
|
||||
// @ts-ignore
|
||||
gl.bufferData(gl.UNIFORM_BUFFER, srcData, usage, o, l);
|
||||
gl.uniformBlockBinding(program, location, 0);
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, buffer);
|
||||
if (o !== void 0) {
|
||||
// @ts-ignore
|
||||
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, data, o, l);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, data);
|
||||
}
|
||||
// gl.uniformBlockBinding(program, location, binding);
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, binding, buffer);
|
||||
// const array = new Float32Array(160);
|
||||
// gl.getBufferSubData(gl.UNIFORM_BUFFER, 0, array);
|
||||
// console.log(array[0]);
|
||||
}
|
||||
};
|
||||
this.block.set(block, obj);
|
||||
@ -977,7 +1011,7 @@ class ShaderProgram {
|
||||
}
|
||||
|
||||
private compile() {
|
||||
this.shaderDirty = true;
|
||||
this.shaderDirty = false;
|
||||
this.clearProgram();
|
||||
|
||||
const shader = this.element;
|
||||
|
@ -203,12 +203,15 @@ export class Transform {
|
||||
static readonly identity = new Transform();
|
||||
}
|
||||
|
||||
function multiplyVec3(mat: ReadonlyMat3, vec: ReadonlyVec3) {
|
||||
return vec3.fromValues(
|
||||
mat[0] * vec[0] + mat[3] * vec[1] + mat[6] * vec[2],
|
||||
mat[1] * vec[0] + mat[4] * vec[1] + mat[7] * vec[2],
|
||||
mat[2] * vec[0] + mat[5] * vec[1] + mat[8] * vec[2]
|
||||
);
|
||||
function multiplyVec3(mat: ReadonlyMat3, vec: ReadonlyVec3): vec3 {
|
||||
const v0 = vec[0];
|
||||
const v1 = vec[1];
|
||||
const v2 = vec[2];
|
||||
return [
|
||||
mat[0] * v0 + mat[3] * v1 + mat[6] * v2,
|
||||
mat[1] * v0 + mat[4] * v1 + mat[7] * v2,
|
||||
mat[2] * v0 + mat[5] * v1 + mat[8] * v2
|
||||
];
|
||||
}
|
||||
|
||||
function getTranslation(mat: ReadonlyMat3): vec2 {
|
||||
|
@ -48,6 +48,7 @@
|
||||
"22": "There is already an active camera for delivered render item. Consider using 'Camera.for' or diable the active camera to avoid some exceptions.",
|
||||
"23": "Render item with id of '$1' has already exists.",
|
||||
"24": "Uniform block can only be used in glsl version es 300.",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
|
||||
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
|
||||
}
|
||||
}
|
@ -91,6 +91,8 @@ export interface GameEvent {
|
||||
];
|
||||
/** Emitted in game/enemy/damage.ts */
|
||||
enemyExtract: [col: EnemyCollection];
|
||||
/** Emitted in lib/events.js restart */
|
||||
restart: [];
|
||||
}
|
||||
|
||||
export const hook = new EventEmitter<GameEvent>();
|
||||
|
@ -2,6 +2,7 @@ import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { CameraAnimation } from '@/core/render/camera';
|
||||
import { LayerGroup } from '@/core/render/preset/layer';
|
||||
import { MotaRenderer } from '@/core/render/render';
|
||||
import { Shader } from '@/core/render/shader';
|
||||
import { Sprite } from '@/core/render/sprite';
|
||||
import { disableViewport, enableViewport } from '@/core/render/utils';
|
||||
import type { HeroMover, MoveStep } from '@/game/state/move';
|
||||
@ -55,6 +56,8 @@ interface ChaseEvent {
|
||||
}
|
||||
|
||||
export class Chase extends EventEmitter<ChaseEvent> {
|
||||
static shader: Shader;
|
||||
|
||||
/** 本次追逐战的数据 */
|
||||
private readonly data: ChaseData;
|
||||
|
||||
@ -315,6 +318,11 @@ export class Chase extends EventEmitter<ChaseEvent> {
|
||||
floorTime.sort((a, b) => a.time - b.time);
|
||||
}
|
||||
this.onTimeListener.sort((a, b) => a.time - b.time);
|
||||
const render = MotaRenderer.get('render-main')!;
|
||||
const mapDraw = render.getElementById('map-draw')!;
|
||||
render.appendChild(Chase.shader);
|
||||
mapDraw.remove();
|
||||
mapDraw.append(Chase.shader);
|
||||
this.emit('start');
|
||||
}
|
||||
|
||||
@ -327,6 +335,19 @@ export class Chase extends EventEmitter<ChaseEvent> {
|
||||
this.layer.removeTicker(this.delegation);
|
||||
this.pathSprite?.destroy();
|
||||
this.heroMove.off('stepEnd', this.onStepEnd);
|
||||
const render = MotaRenderer.get('render-main')!;
|
||||
const mapDraw = render.getElementById('map-draw')!;
|
||||
mapDraw.remove();
|
||||
Chase.shader.remove();
|
||||
mapDraw.append(render);
|
||||
this.emit('end', success);
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Mota.require('var', 'loading').once('coreInit', () => {
|
||||
const shader = new Shader();
|
||||
Chase.shader = shader;
|
||||
shader.size(480, 480);
|
||||
shader.setHD(true);
|
||||
});
|
||||
|
@ -6,6 +6,8 @@ import { LayerGroup } from '@/core/render/preset/layer';
|
||||
import { MotaRenderer } from '@/core/render/render';
|
||||
import { Sprite } from '@/core/render/sprite';
|
||||
import { bgm } from '@/core/audio/bgm';
|
||||
import { Shader, ShaderProgram, UniformType } from '@/core/render/shader';
|
||||
import { PointEffect, PointEffectType } from '../fx/pointShader';
|
||||
|
||||
const path: Partial<Record<FloorIds, LocArr[]>> = {
|
||||
MT16: [
|
||||
@ -99,6 +101,11 @@ const path: Partial<Record<FloorIds, LocArr[]>> = {
|
||||
};
|
||||
|
||||
let back: Sprite | undefined;
|
||||
const effect = new PointEffect();
|
||||
|
||||
Mota.require('var', 'loading').once('loaded', () => {
|
||||
effect.create(Chase.shader, 10);
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化并开始这个追逐战
|
||||
@ -116,6 +123,7 @@ export function initChase(): IChaseController {
|
||||
const animation16 = new CameraAnimation(camera);
|
||||
const animation15 = new CameraAnimation(camera);
|
||||
const animation14 = new CameraAnimation(camera);
|
||||
effect.setTransform(layer.camera);
|
||||
|
||||
const data: ChaseData = {
|
||||
path,
|
||||
@ -205,22 +213,28 @@ export function initChase(): IChaseController {
|
||||
|
||||
Mota.Plugin.require('chase_g').chaseInit1();
|
||||
|
||||
chase.on('end', () => {
|
||||
effect.end();
|
||||
camera.destroy();
|
||||
});
|
||||
|
||||
const controller: IChaseController = {
|
||||
chase,
|
||||
start(fromSave) {
|
||||
core.setFlag('onChase', true);
|
||||
core.setFlag('chaseId', 1);
|
||||
core.autosave();
|
||||
chase.start();
|
||||
camera.enable();
|
||||
wolfMove(chase);
|
||||
effect.use();
|
||||
effect.start();
|
||||
if (fromSave) {
|
||||
initFromSave(chase);
|
||||
}
|
||||
testEffect(chase);
|
||||
},
|
||||
end(success) {
|
||||
chase.end(success);
|
||||
camera.destroy();
|
||||
},
|
||||
initAudio(fromSave) {
|
||||
if (fromSave) initFromSave(chase);
|
||||
@ -230,6 +244,26 @@ export function initChase(): IChaseController {
|
||||
return controller;
|
||||
}
|
||||
|
||||
function testEffect(chase: Chase) {
|
||||
// effect.addEffect(
|
||||
// PointEffectType.CircleContrast,
|
||||
// Date.now(),
|
||||
// 100000,
|
||||
// [7 * 32 + 16, 17 * 32 + 16, 200, 150],
|
||||
// [1, 0, 0, 0]
|
||||
// );
|
||||
effect.addEffect(
|
||||
PointEffectType.CircleWarpTangetial,
|
||||
Date.now(),
|
||||
100000,
|
||||
[7 * 32 + 16, 17 * 32 + 16, 150, 120],
|
||||
[Math.PI / 2, 0, 0, 0]
|
||||
);
|
||||
chase.on('frame', () => {
|
||||
effect.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
function initAudio(chase: Chase) {
|
||||
playAudio(0, chase);
|
||||
}
|
||||
|
620
src/plugin/fx/pointShader.ts
Normal file
620
src/plugin/fx/pointShader.ts
Normal file
@ -0,0 +1,620 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { Shader, ShaderProgram, UniformType } 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-1,0.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
|
||||
}
|
||||
|
||||
type EffectData = [x0: number, x1: number, x2: number, x3: number];
|
||||
|
||||
const warpEffect = new Set<PointEffectType>();
|
||||
warpEffect.add(PointEffectType.CircleWarp);
|
||||
|
||||
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();
|
||||
program.setVersion(shader.VERSION_ES_300);
|
||||
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);
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在下一帧更新特效数据
|
||||
*/
|
||||
requestUpdate() {
|
||||
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, now);
|
||||
|
||||
if (now > startTime) {
|
||||
this.addEffectToList(data);
|
||||
} else {
|
||||
// 如果还没开始,那么添加至预备队列
|
||||
this.toAdd.add(data);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 立刻删除一个特效
|
||||
* @param id 要删除的特效的id
|
||||
*/
|
||||
deleteEffect(id: number) {
|
||||
this.removeEffect(this.findIndexById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个特效的数据,注意特效数据必须填四位,不足者补0
|
||||
* @param id 特效id
|
||||
* @param data1 第一组自定义数据,可选
|
||||
* @param data2 第二组自定义数据,可选
|
||||
* @param data3 第三组自定义数据,可选
|
||||
*/
|
||||
setEffect(
|
||||
id: number,
|
||||
data1?: EffectData,
|
||||
data2?: EffectData,
|
||||
data3?: EffectData
|
||||
) {
|
||||
const index = this.findIndexById(id);
|
||||
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;
|
||||
const id = data[1];
|
||||
if (warpEffect.has(type)) {
|
||||
list.copyWithin(16, 0, 16);
|
||||
list.set(data, 0);
|
||||
this.dataPointer++;
|
||||
this.idIndexMap.clear();
|
||||
} else {
|
||||
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) 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束计时器
|
||||
*/
|
||||
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 scale = transform.scaleX * core.domStyle.scale;
|
||||
if (this.shader?.highResolution) scale *= devicePixelRatio;
|
||||
const scaleTransform = new Transform();
|
||||
scaleTransform.scale(scale, scale);
|
||||
const scaled = scaleTransform.multiply(transform);
|
||||
const fixedHeight = core._PY_ * scale;
|
||||
|
||||
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.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;
|
||||
// 我草了这句continue,调试的时候直接硬控我俩小时
|
||||
// if (now < 0.0 || now > end) continue;
|
||||
|
||||
// 下面开始实施对应的着色器特效
|
||||
|
||||
// 反色,data1: x y radius decay;data2: 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 decay;data2: 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 decay;data2: 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 decay;data2: 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 decay;data2: 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;
|
||||
}
|
||||
`;
|
||||
}
|
Loading…
Reference in New Issue
Block a user