mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-08 21:47:05 +08:00
着色器特效
This commit is contained in:
parent
b2a243b567
commit
f15007513a
438
src/core/fx/shader.ts
Normal file
438
src/core/fx/shader.ts
Normal file
@ -0,0 +1,438 @@
|
||||
import { Ticker } from 'mutate-animate';
|
||||
import { EmitableEvent, EventEmitter } from '../common/eventEmitter';
|
||||
import { loading } from '../loader/load';
|
||||
|
||||
interface ShaderEvent extends EmitableEvent {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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 z = parseInt(canvas.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