mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-11-04 15:12:58 +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