feat: 一些着色器

This commit is contained in:
unanmed 2024-10-15 15:55:55 +08:00
parent b3727080b2
commit f01d185db3
12 changed files with 764 additions and 588 deletions

View File

@ -318,6 +318,7 @@ events.prototype.restart = function () {
core.hideStatusBar();
core.showStartAnimate();
core.playBgm(main.startBgm);
Mota.require('var', 'hook').emit('restart');
};
////// 询问是否需要重新开始 //////

View File

@ -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": [

View File

@ -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视为1vec2 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);
}
};
}

View File

@ -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;

View File

@ -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();

View File

@ -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 block01
* @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;

View File

@ -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 {

View File

@ -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."
}
}

View File

@ -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>();

View File

@ -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);
});

View File

@ -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);
}

View 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-112
*/
CircleContrast,
/**
* \
* \
* `data1: x, y, radius, decay` | \
* `data2: ratio, _, _, _` | 0-112
*/
CircleSaturate,
/**
* \
* \
* `data1: x, y, radius, decay` | \
* `data2: angle, _, _, _` | 001360
*/
CircleHue,
/**
* \
* \
* `data1: x, y, maxRaius, waveRadius` | \
* `data2: amplitude, attenuation, linear, tangential` \
* &ensp;&ensp;* `amplitude`: 11\
* &ensp;&ensp;* `attenuation`: \
* &ensp;&ensp;* `linear`: 线
* 线0-10.5线\
* &ensp;&ensp;* `tangential`: 1\
* `data3: startPhase, endPhase, _, _` |
* 2 * PI Math.PI * 2
*/
CircleWarp,
/**
* \
* \
* `data1: x, y, minRadius, maxRadius` |
* 1Math.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 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.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;
}
`;
}