mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-19 17:16:08 +08:00
着色器特效
This commit is contained in:
parent
f197303a2f
commit
37a18c87c0
@ -399,7 +399,7 @@ var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d =
|
|||||||
1
|
1
|
||||||
],
|
],
|
||||||
"background": "winskin2.png",
|
"background": "winskin2.png",
|
||||||
"textfont": 16
|
"textfont": 14
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"shops": [
|
"shops": [
|
||||||
|
@ -37,7 +37,7 @@ main.floors.MT0=
|
|||||||
],
|
],
|
||||||
"9,13": [
|
"9,13": [
|
||||||
"这里会列出每一层所展示的插件名称。",
|
"这里会列出每一层所展示的插件名称。",
|
||||||
"1-5:点光源"
|
"1-5:点光源\n6-7:碎裂特效\n8-10:着色器特效"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"changeFloor": {
|
"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, 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, 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]
|
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||||
],
|
],
|
||||||
"bgmap": [
|
"bgmap": [
|
||||||
|
@ -1,45 +1,119 @@
|
|||||||
main.floors.MT8=
|
main.floors.MT8=
|
||||||
{
|
{
|
||||||
"floorId": "MT8",
|
"floorId": "MT8",
|
||||||
"title": "主塔 8 层",
|
"title": "主塔 8 层",
|
||||||
"name": "8",
|
"name": "8",
|
||||||
"width": 15,
|
"width": 15,
|
||||||
"height": 15,
|
"height": 15,
|
||||||
"canFlyTo": true,
|
"canFlyTo": true,
|
||||||
"canFlyFrom": true,
|
"canFlyFrom": true,
|
||||||
"canUseQuickShop": true,
|
"canUseQuickShop": true,
|
||||||
"cannotViewMap": false,
|
"cannotViewMap": false,
|
||||||
"images": [],
|
"images": [],
|
||||||
"ratio": 1,
|
"ratio": 1,
|
||||||
"defaultGround": "ground",
|
"defaultGround": "ground",
|
||||||
"bgm": "cave.mp3",
|
"bgm": "cave.mp3",
|
||||||
"firstArrive": [],
|
"firstArrive": [],
|
||||||
"eachArrive": [],
|
"eachArrive": [],
|
||||||
"parallelDo": "",
|
"parallelDo": "",
|
||||||
"events": {},
|
"events": {
|
||||||
"changeFloor": {},
|
"10,13": [
|
||||||
"beforeBattle": {},
|
"从本层开始将进入着色器特效插件的教学。",
|
||||||
"afterBattle": {},
|
"着色器特效插件是一个通用型特效插件,允许你使用gpu进行特效渲染,效果好,同时性能表现也好。",
|
||||||
"afterGetItem": {},
|
"插件的核心是片元着色器,接下来的几层也将以片元着色器为核心进行教学。"
|
||||||
"afterOpenDoor": {},
|
],
|
||||||
"autoEvent": {},
|
"4,13": [
|
||||||
"cannotMove": {},
|
"这里是第一个特效展示木牌。",
|
||||||
"cannotMoveIn": {},
|
"现在我要对背景层、事件层、前景层、勇士层进行特效处理。",
|
||||||
"map": [
|
" 首先我应该引入需要使用的内容,使用:\n\r[yellow]const { ShaderEffect, setTickerFor, replaceGameCanvas } = core.plugin.shaderEffect;\r",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 然后我需要创建一个着色器特效实例,并指定特效画布的背景色,例如我指定背景色为全透明:\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",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 下面我需要指定特效的作用画布,上面说了我要对哪些画布进行特效处理,现在我们就需要获取这些画布:\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",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 设置完作用画布之后,我们就可以编写着色器脚本了。着色器脚本使用\r[gold]glsl\r语言进行编写。\n首先我们来看顶点着色器。顶点着色器用于确定绘制顶点与图片顶点等,一般不需要我们编写,直接使用插件内置的脚本即可:\n\r[yellow]effect.vs(ShaderEffect.defaultVs);\r\n 其中\r[gold]vs\r函数便是设置顶点着色器的函数。",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 下面我们主要看片元着色器。片元着色器描述了每个像素的颜色,因此它是本插件的核心。在片元着色器脚本中,必须包含一个\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 这段着色器脚本乍一看貌似没有一处能看懂(),不过不要慌,我来解释一下它干了什么。",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 首先\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 的顺序进行获取,也就是把绿色与红色进行了互换。",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 这下,我们便将着色器特效最复杂的部分解决了,后面的事情就好办了,我们将片元着色器脚本传递给特效实例:\n\r[yellow]effect.fs(`\n void main() {\n gl_FragColor = texture2D(uSampler, vTextureCoord).grba;\n }\n`);\r\n 下面,我们就可以进行特效渲染了。",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 我们直接进行渲染:\n\r[yellow]effect.update(true);\r\n 其中\r[gold]update\r函数便是强制重新渲染特效的函数,后面的参数表示重新编译着色器脚本,因为我们还没编译过,因此需要传入true。一般我们更改了着色器脚本后都需要重新编译。",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 目前为止,我们渲染了一个静态的特效,我们需要每帧都渲染来让特效表现为动态的,我们直接使用插件自带的函数:\n\r[yellow]const ticker = setTickerFor(effect);\r\n 这样,特效就会自动每帧渲染,保持为动态了,其返回值\r[gold]ticker\r是高级动画插件中的\r[gold]Ticker\r实例。",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
" 下面,我们需要将特效展示在页面上。我们刚刚只对样板的系统画布进行了特效渲染,因此我们可以使用插件内置的函数进行这一操作:\n\r[yellow]const manager = replaceGameCanvas(effect, canvas);\r\n 这样,特效就会展示在页面中了。下面我们来看一下完整代码。",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
"\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",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
{
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
"type": "function",
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
"function": "function(){\nflags.lastShaderSample?.end();\nflags.lastShaderSample = core.shaderSample1();\n}"
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
},
|
||||||
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
"现在便是最终效果。"
|
||||||
|
],
|
||||||
|
"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 path from './plugin/fx/path';
|
||||||
import * as ani from 'mutate-animate';
|
import * as ani from 'mutate-animate';
|
||||||
import frag from './plugin/fx/frag';
|
import frag from './plugin/fx/frag';
|
||||||
|
import shader from './plugin/fx/shader';
|
||||||
|
|
||||||
function forward() {
|
function forward() {
|
||||||
const toForward: any[] = [
|
const toForward: any[] = [
|
||||||
@ -40,6 +41,7 @@ function forward() {
|
|||||||
completion(),
|
completion(),
|
||||||
path(),
|
path(),
|
||||||
frag(),
|
frag(),
|
||||||
|
shader()
|
||||||
];
|
];
|
||||||
|
|
||||||
// 初始化所有插件,并转发到core上
|
// 初始化所有插件,并转发到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