mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-11-01 04:42:58 +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); | ||||
|                 if (o !== void 0) { | ||||
|                     // @ts-ignore
 | ||||
|                 gl.bufferData(gl.UNIFORM_BUFFER, srcData, usage, o, l); | ||||
|                 gl.uniformBlockBinding(program, location, 0); | ||||
|                 gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, buffer); | ||||
|                     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