mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-10-31 12:12:58 +08:00 
			
		
		
		
	着色器特效
This commit is contained in:
		
							parent
							
								
									f197303a2f
								
							
						
					
					
						commit
						37a18c87c0
					
				| @ -399,7 +399,7 @@ var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d = | ||||
| 					1 | ||||
| 				], | ||||
| 				"background": "winskin2.png", | ||||
| 				"textfont": 16 | ||||
| 				"textfont": 14 | ||||
| 			} | ||||
| 		], | ||||
| 		"shops": [ | ||||
|  | ||||
| @ -37,7 +37,7 @@ main.floors.MT0= | ||||
|         ], | ||||
|         "9,13": [ | ||||
|             "这里会列出每一层所展示的插件名称。", | ||||
|             "1-5:点光源" | ||||
|             "1-5:点光源\n6-7:碎裂特效\n8-10:着色器特效" | ||||
|         ] | ||||
|     }, | ||||
|     "changeFloor": { | ||||
|  | ||||
| @ -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": [ | ||||
|  | ||||
| @ -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": [ | ||||
| 
 | ||||
| ] | ||||
| } | ||||
| @ -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上
 | ||||
|  | ||||
							
								
								
									
										522
									
								
								src/plugin/fx/shader.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										522
									
								
								src/plugin/fx/shader.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<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; | ||||
| } | ||||
| 
 | ||||
| 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); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user