diff --git a/public/project/data.js b/public/project/data.js index eff3e55..8e1918e 100644 --- a/public/project/data.js +++ b/public/project/data.js @@ -399,7 +399,7 @@ var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d = 1 ], "background": "winskin2.png", - "textfont": 16 + "textfont": 14 } ], "shops": [ diff --git a/public/project/floors/MT0.js b/public/project/floors/MT0.js index c7bedd7..e9f9373 100644 --- a/public/project/floors/MT0.js +++ b/public/project/floors/MT0.js @@ -37,7 +37,7 @@ main.floors.MT0= ], "9,13": [ "这里会列出每一层所展示的插件名称。", - "1-5:点光源" + "1-5:点光源\n6-7:碎裂特效\n8-10:着色器特效" ] }, "changeFloor": { diff --git a/public/project/floors/MT6.js b/public/project/floors/MT6.js index 168160d..50d2896 100644 --- a/public/project/floors/MT6.js +++ b/public/project/floors/MT6.js @@ -209,7 +209,7 @@ main.floors.MT6= [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [ 1, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 1], - [ 1, 0, 0, 0,129, 0, 0, 88, 0, 94,129, 0, 0, 0, 1], + [ 1, 0, 0,10187,129, 0, 0, 88, 0, 94,129,10186, 0, 0, 1], [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ], "bgmap": [ diff --git a/public/project/floors/MT8.js b/public/project/floors/MT8.js index d79366a..917ea1e 100644 --- a/public/project/floors/MT8.js +++ b/public/project/floors/MT8.js @@ -1,45 +1,119 @@ main.floors.MT8= { -"floorId": "MT8", -"title": "主塔 8 层", -"name": "8", -"width": 15, -"height": 15, -"canFlyTo": true, -"canFlyFrom": true, -"canUseQuickShop": true, -"cannotViewMap": false, -"images": [], -"ratio": 1, -"defaultGround": "ground", -"bgm": "cave.mp3", -"firstArrive": [], -"eachArrive": [], -"parallelDo": "", -"events": {}, -"changeFloor": {}, -"beforeBattle": {}, -"afterBattle": {}, -"afterGetItem": {}, -"afterOpenDoor": {}, -"autoEvent": {}, -"cannotMove": {}, -"cannotMoveIn": {}, -"map": [ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + "floorId": "MT8", + "title": "主塔 8 层", + "name": "8", + "width": 15, + "height": 15, + "canFlyTo": true, + "canFlyFrom": true, + "canUseQuickShop": true, + "cannotViewMap": false, + "images": [], + "ratio": 1, + "defaultGround": "ground", + "bgm": "cave.mp3", + "firstArrive": [], + "eachArrive": [], + "parallelDo": "", + "events": { + "10,13": [ + "从本层开始将进入着色器特效插件的教学。", + "着色器特效插件是一个通用型特效插件,允许你使用gpu进行特效渲染,效果好,同时性能表现也好。", + "插件的核心是片元着色器,接下来的几层也将以片元着色器为核心进行教学。" + ], + "4,13": [ + "这里是第一个特效展示木牌。", + "现在我要对背景层、事件层、前景层、勇士层进行特效处理。", + " 首先我应该引入需要使用的内容,使用:\n\r[yellow]const { ShaderEffect, setTickerFor, replaceGameCanvas } = core.plugin.shaderEffect;\r", + " 然后我需要创建一个着色器特效实例,并指定特效画布的背景色,例如我指定背景色为全透明:\n\r[yellow]const effect = new ShaderEffect([0, 0, 0, 0]);\r\n 其中\r[gold][0, 0, 0, 0]\r便是颜色数组,每一项分别表示 rgba,范围均为\r[gold]0-1\r", + " 下面我需要指定特效的作用画布,上面说了我要对哪些画布进行特效处理,现在我们就需要获取这些画布:\n\r[yellow]const canvas = ['bg', 'bg2', 'event', 'fg', 'fg2', 'hero'];\r\n 这些便是所有画布的id,下面我们将其映射为画布:\n\r[yellow]const imgs = canvas.map(v => core.canvas[v].canvas);\r\n 这样,我们就获取到了所有的作用画布,下面将特效实例的作用画布设置为它们:\n\r[yellow]effect.baseImage(...imgs);\r", + " 设置完作用画布之后,我们就可以编写着色器脚本了。着色器脚本使用\r[gold]glsl\r语言进行编写。\n首先我们来看顶点着色器。顶点着色器用于确定绘制顶点与图片顶点等,一般不需要我们编写,直接使用插件内置的脚本即可:\n\r[yellow]effect.vs(ShaderEffect.defaultVs);\r\n 其中\r[gold]vs\r函数便是设置顶点着色器的函数。", + " 下面我们主要看片元着色器。片元着色器描述了每个像素的颜色,因此它是本插件的核心。在片元着色器脚本中,必须包含一个\r[gold]main\r函数,同时必须为\r[gold]gl_FragColor\r赋值,其中\r[gold]gl_FragColor\r便是当前像素的颜色。\n 我们直接看一个例子,例子中将当前像素的绿色与红色值进行了互换:\n\r[yellow]void main() {\n gl_FragColor = texture2D(uSampler, vTextureCoord).grba;\n}\r\n 这段着色器脚本乍一看貌似没有一处能看懂(),不过不要慌,我来解释一下它干了什么。", + " 首先\r[gold]glsl\r是一个语法与\r[gold]C语言\r很像的语言,同时\r[gold]js\r也是C系语言,因此大部分语法使用js进行判断是没有太大问题的。下面我们就来仔细看看这段着色器代码。\n 首先是\r[yellow]void main() { }\r,它的作用是声明了一个返回值为空的函数,\r[gold]void\r便是返回值类型,\r[gold]main\r便是函数名称,它与js的函数声明基本一致。\n 下面是\r[yellow]gl_FragColor = texture2D(uSampler, vTextureCoord).grba;\r,它表示将后面的值赋给\r[gold]gl_FragColor\r。下面我们看后面的值,\r[gold]texture2D表示获取一个纹理的信息,其中\r[gold]uSampler\r指的便是我们的作用画布,\r[gold]vTextureCoord\r便是当前像素的位置,这么写意思便是获取到作用画布在当前位置的颜色信息。后面的\r[gold].grba\r表示根据\r[gold]grba\r的顺序获取颜色信息。它们的位置关系为:r - 0, g - 1, b - 2, a - 3,也就是说,我写\r[gold].grba\r就表示了按 1023 的顺序进行获取,也就是把绿色与红色进行了互换。", + " 这下,我们便将着色器特效最复杂的部分解决了,后面的事情就好办了,我们将片元着色器脚本传递给特效实例:\n\r[yellow]effect.fs(`\n void main() {\n gl_FragColor = texture2D(uSampler, vTextureCoord).grba;\n }\n`);\r\n 下面,我们就可以进行特效渲染了。", + " 我们直接进行渲染:\n\r[yellow]effect.update(true);\r\n 其中\r[gold]update\r函数便是强制重新渲染特效的函数,后面的参数表示重新编译着色器脚本,因为我们还没编译过,因此需要传入true。一般我们更改了着色器脚本后都需要重新编译。", + " 目前为止,我们渲染了一个静态的特效,我们需要每帧都渲染来让特效表现为动态的,我们直接使用插件自带的函数:\n\r[yellow]const ticker = setTickerFor(effect);\r\n 这样,特效就会自动每帧渲染,保持为动态了,其返回值\r[gold]ticker\r是高级动画插件中的\r[gold]Ticker\r实例。", + " 下面,我们需要将特效展示在页面上。我们刚刚只对样板的系统画布进行了特效渲染,因此我们可以使用插件内置的函数进行这一操作:\n\r[yellow]const manager = replaceGameCanvas(effect, canvas);\r\n 这样,特效就会展示在页面中了。下面我们来看一下完整代码。", + "\r[yellow]const { ShaderEffect, setTickerFor, replaceGameCanvas } = core.plugin.shaderEffect;\nconst effect = new ShaderEffect([0, 0, 0, 0]);\nconst canvas = ['bg', 'bg2', 'event', 'fg', 'fg2', 'hero'];\nconst imgs = canvas.map(v => core.canvas[v].canvas);\neffect.baseImage(imgs);\neffect.vs(ShaderEffect.defaultVs);\neffect.fs(`\n void main() {\n gl_FragColor = texture2D(uSampler, \n vTextureCoord).grba;\n}\n`);\neffect.update(true);\n\nconst ticker = setTickerFor(effect);\nconst manager = replaceGameCanvas(effect, canvas);\r", + { + "type": "function", + "function": "function(){\nflags.lastShaderSample?.end();\nflags.lastShaderSample = core.shaderSample1();\n}" + }, + "现在便是最终效果。" + ], + "10,9": [ + "这里是第二个特效展示木牌。", + "下面我将对事件层和勇士层进行处理,任何不透明的像素都将变成完全黑色,否则完全透明。它的着色器脚本:\n\r[yellow]void main() {\n float alpha = texture2D(uSampler, vTextureCoord).a;\n gl_FragColor = vec4(0.0, 0.0, 0.0, alpha == 0 ? 0 : 1);\n}\r", + { + "type": "function", + "function": "function(){\nflags.lastShaderSample?.end();\nflags.lastShaderSample = core.shaderSample2();\n}" + } + ], + "10,5": [ + "这里是第三个特效展示木牌", + "下面我将对事件层和勇士层进行处理,然后再rgb三个通道上加上一定量的噪声:\n\r[yellow]float noise(float x) {\n float y = fract(sin(x)*100000.0);\n return y;\n}\n\nvoid main() {\n vec4 rgba = texture2D(uSampler, vTextureCoord);\n float r = rgba.r + noise(rgba.r) / 5.0;\n float g = rgba.g + noise(rgba.g) / 5.0;\n float b = rgba.b + noise(rgba.b) / 5.0;\n\n gl_FragColor = vec4(r, g, b, rgba.a);\n}\r", + { + "type": "function", + "function": "function(){\nflags.lastShaderSample?.end();\nflags.lastShaderSample = core.shaderSample3();\n}" + } + ], + "10,1": [ + "这里是第四个特效展示木牌,让我们来做一些复杂一点的特效", + "下面我将对事件层和勇士层进行处理,然后降低亮度,在竖直方向上添加水平偏移噪声,模拟老式电视的信号不好的效果:\n\r[yellow]float noise(float x) {\n float y = fract(sin(x) * 100000.0);\n return y;\n}\n\nvoid main() {\n float brigtness = -0.1;\n vec2 xy = vTextureCoord;\n float x = xy.x + noise(xy.y) / 100.0;\n float y = xy.y;\n vec4 color = texture2D(uSampler, vec2(x, y));\n vec4 color1 = vec4(color.rgb + vec3(brigtness), color.a);\n\n gl_FragColor = color1;\n}\r", + { + "type": "function", + "function": "function(){\nflags.lastShaderSample?.end();\nflags.lastShaderSample = core.shaderSample4();\n}" + } + ], + "4,9": [ + "不过我们发现到目前为止特效都不会动,只能保持为一定的状态,怎么让特效也能动呢?那就期待插件的下一次更新吧!" + ] + }, + "changeFloor": { + "7,13": { + "floorId": ":before", + "stair": "upFloor" + }, + "7,1": { + "floorId": ":next", + "stair": "downFloor" + } + }, + "beforeBattle": {}, + "afterBattle": {}, + "afterGetItem": {}, + "afterOpenDoor": {}, + "autoEvent": {}, + "cannotMove": {}, + "cannotMoveIn": {}, + "map": [ + [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [ 1, 0, 0, 0, 0, 0, 0, 87, 0, 0,129,10189, 0, 0, 1], + [ 1, 27, 50,606, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 28, 52,608, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,129,10188, 0, 0, 1], + [ 1, 29, 51,236, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 30, 47,273, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 0, 0,10193,129, 0, 0, 0, 0, 0,129,10187, 0, 0, 1], + [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [ 1, 0, 0,10186,129, 0, 0, 88, 0, 94,129,10185, 0, 0, 1], + [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ], + "bgmap": [ + +], + "fgmap": [ + +], + "bg2map": [ + +], + "fg2map": [ + +] } \ No newline at end of file diff --git a/src/initPlugin.ts b/src/initPlugin.ts index 4daaa10..120537f 100644 --- a/src/initPlugin.ts +++ b/src/initPlugin.ts @@ -18,6 +18,7 @@ import completion, { floors } from './plugin/completion'; import path from './plugin/fx/path'; import * as ani from 'mutate-animate'; import frag from './plugin/fx/frag'; +import shader from './plugin/fx/shader'; function forward() { const toForward: any[] = [ @@ -40,6 +41,7 @@ function forward() { completion(), path(), frag(), + shader() ]; // 初始化所有插件,并转发到core上 diff --git a/src/plugin/fx/shader.ts b/src/plugin/fx/shader.ts new file mode 100644 index 0000000..fbac5ef --- /dev/null +++ b/src/plugin/fx/shader.ts @@ -0,0 +1,522 @@ +import { Ticker } from 'mutate-animate'; + +const isWebGLSupported = (() => { + return !!document.createElement('canvas').getContext('webgl'); +})(); + +type ShaderColorArray = [number, number, number, number]; +type ShaderEffectImage = Exclude; + +interface ProgramInfo { + program: WebGLProgram; + attrib: Record; + uniform: Record; +} + +interface ShaderEffectBuffer { + position: WebGLBuffer; + texture: WebGLBuffer; + indices: WebGLBuffer; +} + +interface ShaderEffectShader { + vertex: WebGLShader; + fragment: WebGLShader; +} + +interface MixedImage { + canvas: HTMLCanvasElement; + update(): void; +} + +export default function init() { + return { + ShaderEffect, + setTickerFor, + replaceGameCanvas, + shaderSample1: sample1, + shaderSample2: sample2, + shaderSample3: sample3, + shaderSample4: sample4 + }; +} + +const sample1 = buildSample( + ['bg', 'bg2', 'event', 'fg', 'fg2', 'hero'], + ` + void main() { + gl_FragColor = texture2D(uSampler, vTextureCoord).grba; + } +` +); +const sample2 = buildSample( + ['event', 'hero'], + ` + void main() { + float a = texture2D(uSampler, vTextureCoord).a; + gl_FragColor = vec4(0.0, 0.0, 0.0, a == 0.0 ? 0.0 : 1.0); + } +` +); +const sample3 = buildSample( + ['event', 'hero'], + ` + float noise(float x) { + float y = fract(sin(x) * 100000.0); + return y; + } + + void main() { + vec4 rgba = texture2D(uSampler, vTextureCoord); + float r = rgba.r + noise(rgba.r) / 5.0; + float g = rgba.g + noise(rgba.g) / 5.0; + float b = rgba.b + noise(rgba.b) / 5.0; + + gl_FragColor = vec4(r, g, b, rgba.a); + } +` +); +const sample4 = buildSample( + ['event', 'hero'], + ` + float noise(float x) { + float y = fract(sin(x) * 100000.0); + return y; + } + + void main() { + float brigtness = -0.1; + vec2 xy = vTextureCoord; + float x = xy.x + noise(xy.y) / 100.0; + float y = xy.y; + vec4 color = texture2D(uSampler, vec2(x, y)); + vec4 color1 = vec4(color.rgb + vec3(brigtness), color.a); + + gl_FragColor = color1; + } +` +); + +function buildSample(canvas: string[], fs: string) { + return () => { + const effect = new ShaderEffect([0, 0, 0, 0]); + effect.baseImage(...canvas.map(v => core.canvas[v].canvas)); + effect.vs(ShaderEffect.defaultVs); + effect.fs(fs); + effect.update(true); + + const ticker = setTickerFor(effect); + const manager = replaceGameCanvas(effect, canvas); + + return { + end() { + ticker.destroy(); + manager.remove(); + } + }; + }; +} + +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 { + 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) { + 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) { + const gl = this.gl; + if (compile) { + 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); + } + this.textureCanvas?.update(); + this.drawScene(); + } + + /** + * 设置顶点着色器,使用 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); + } + + 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); + } + `; +} + +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; +} + +export function replaceGameCanvas(effect: ShaderEffect, canvas: string[]) { + 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); + } + }; +}