diff --git a/src/core/fx/canvas2d.ts b/src/core/fx/canvas2d.ts index 9323326..5a7d3e5 100644 --- a/src/core/fx/canvas2d.ts +++ b/src/core/fx/canvas2d.ts @@ -97,7 +97,7 @@ export class MotaOffscreenCanvas2D extends EventEmitter { * 删除这个画布 */ delete() { - MotaCanvas2D.list.delete(this); + MotaOffscreenCanvas2D.list.delete(this); } /** diff --git a/src/core/render/container.ts b/src/core/render/container.ts index 1000ac2..aa62607 100644 --- a/src/core/render/container.ts +++ b/src/core/render/container.ts @@ -1,8 +1,18 @@ import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; -import { IRenderChildable, RenderItem, RenderItemPosition } from './item'; +import { + ERenderItemEvent, + IRenderChildable, + RenderItem, + RenderItemPosition +} from './item'; import { Transform } from './transform'; -export class Container extends RenderItem implements IRenderChildable { +export interface EContainerEvent extends ERenderItemEvent {} + +export class Container + extends RenderItem + implements IRenderChildable +{ children: RenderItem[] = []; sortedChildren: RenderItem[] = []; @@ -23,7 +33,6 @@ export class Container extends RenderItem implements IRenderChildable { transform: Transform ): void { const { ctx } = canvas; - // console.log(ctx.getTransform()); this.sortedChildren.forEach(v => { if (v.hidden) return; diff --git a/src/core/render/item.ts b/src/core/render/item.ts index edd62e2..aa6e821 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -155,7 +155,7 @@ export abstract class RenderItem anchorY: number = 0; /** 渲染模式,absolute表示绝对位置,static表示跟随摄像机移动 */ - type: 'absolute' | 'static' = 'static'; + type: RenderItemPosition = 'static'; /** 是否是高清画布 */ highResolution: boolean = true; /** 是否抗锯齿 */ @@ -172,9 +172,9 @@ export abstract class RenderItem transform: Transform = new Transform(); /** 渲染缓存信息 */ - private cache: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + protected cache: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); /** 是否需要更新缓存 */ - private cacheDirty: boolean = true; + protected cacheDirty: boolean = true; /** 是否启用缓存机制 */ readonly enableCache: boolean = true; @@ -183,6 +183,7 @@ export abstract class RenderItem this.enableCache = enableCache; this.type = type; + this.cache.withGameScale(true); } diff --git a/src/core/render/render.ts b/src/core/render/render.ts index 33bcaae..9dca107 100644 --- a/src/core/render/render.ts +++ b/src/core/render/render.ts @@ -8,6 +8,7 @@ import { FloorDamageExtends } from './preset/damage'; import { HeroRenderer } from './preset/hero'; import { Transform } from './transform'; import { Text } from './preset/misc'; +import { Shader } from './shader'; export class MotaRenderer extends Container { static list: Set = new Set(); @@ -74,6 +75,7 @@ Mota.require('var', 'hook').once('reset', () => { const transform = render.transform; render.mount(); + const shader = new Shader(); const layer = new LayerGroup(); ['bg', 'bg2', 'event', 'fg', 'fg2'].forEach(v => { @@ -87,7 +89,18 @@ Mota.require('var', 'hook').once('reset', () => { layer.extends(damage); layer.getLayer('event')?.extends(hero); binder.bindThis(); - render.appendChild(layer); + render.appendChild(shader); + shader.appendChild(layer); + shader.size(480, 480); + shader.fs(` + void main() { + vec2 coord = v_texCoord; + if (coord.y > 0.25 && coord.y < 0.35) { + coord.y = sin((coord.y - 0.25) * 3.1415926535 / 0.2) * 0.1 + 0.25; + } + gl_FragColor = texture2D(u_sampler, coord); + } + `); layer.requestAfterFrame(() => { hero.setImage(core.material.images.images['hero2.png']); diff --git a/src/core/render/shader.ts b/src/core/render/shader.ts new file mode 100644 index 0000000..0a0c5a8 --- /dev/null +++ b/src/core/render/shader.ts @@ -0,0 +1,521 @@ +import { logger } from '../common/logger'; +import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; +import { isWebGL2Supported } from '../fx/webgl'; +import { Container } from './container'; +import { ERenderItemEvent, RenderItemPosition } from './item'; +import { Transform } from './transform'; + +const SHADER_VERTEX_PREFIX_300 = /* glsl */ `#version 300 es +`; +const SHADER_VERTEX_PREFIX_100 = /* glsl */ ` +#ifdef GL_ES + precision highp float; +#endif + +attribute vec4 a_position; +attribute vec2 a_texCoord; + +varying highp vec2 v_texCoord; +`; + +const SHADER_FRAGMENT_PREFIX_300 = /* glsl */ `#version 300 es +`; +const SHADER_FRAGMENT_PREFIX_100 = /* glsl */ ` +#ifdef GL_ES + precision highp float; +#endif + +varying highp vec2 v_texCoord; + +uniform sampler2D u_sampler; +`; + +const DEFAULT_VS = /* glsl */ ` +void main() { + v_texCoord = a_texCoord; + gl_Position = a_position; +} +`; +const DEFAULT_FS = /* glsl */ ` +void main() { + gl_FragColor = texture2D(u_sampler, v_texCoord); +} +`; + +export type ShaderGLSLVersion = '100' | '300'; + +interface CompiledShader { + vertex: WebGLShader; + fragment: WebGLShader; +} + +interface ShaderBuffer { + position: WebGLBuffer; + texture: WebGLBuffer; + indices: WebGLBuffer; +} + +interface ShaderUniform { + u_sampler: WebGLUniformLocation; + + [x: string]: WebGLUniformLocation | null; +} + +interface ShaderAttribute { + a_texCoord: number; + a_position: number; + + [x: string]: number; +} + +interface ShaderProgram { + program: WebGLProgram | null; + shader: CompiledShader | null; + uniform: ShaderUniform | null; + attribute: ShaderAttribute | null; +} + +interface EShaderEvent extends ERenderItemEvent {} + +export class Shader extends Container { + /** 是否支持此组件 */ + static readonly support: boolean = isWebGL2Supported(); + + // 会用到的静态常量 + static readonly VERTEX_SHADER: number = 0b1; + static readonly FRAGMENT_SHADER: number = 0b10; + + private version: ShaderGLSLVersion = '100'; + + canvas: HTMLCanvasElement; + gl: WebGL2RenderingContext; + + /** 是否修改过着色器,用于特定场景优化 */ + private shaderModified: boolean = false; + /** 需要重新编译的着色器 */ + private shaderDirty: boolean = true; + /** 当前程序是否被外部调用过 */ + private programExterned: boolean = false; + + /** 顶点着色器 */ + private vertex: string = DEFAULT_VS; + /** 片元着色器 */ + private fragment: string = DEFAULT_FS; + + /** webgl使用的程序 */ + private program: WebGLProgram | null = null; + /** 子元素构成的纹理 */ + private texture: WebGLTexture | null = null; + /** 编译过的着色器,在切换着色器时会被删除 */ + private shader: CompiledShader | null = null; + /** 缓冲区 */ + private buffer: ShaderBuffer | null = null; + /** uniform */ + private uniform: ShaderUniform | null = null; + /** attribute */ + private attribute: ShaderAttribute | null = null; + + constructor( + type: RenderItemPosition = 'static', + version: ShaderGLSLVersion = '100' + ) { + super(type, !Shader.support); + + this.canvas = document.createElement('canvas'); + this.gl = this.canvas.getContext('webgl2')!; + this.setVersion(version); + if (!Shader.support) { + this.canvas.width = 0; + this.canvas.height = 0; + } + this.init(); + } + + protected render( + canvas: MotaOffscreenCanvas2D, + transform: Transform + ): void { + if (!Shader.support || !this.shaderModified) { + super.render(canvas, transform); + } else { + if (this.shaderDirty) { + this.cacheDirty = true; + this.compileShader(); + this.shaderDirty = false; + } + + if (this.cacheDirty) { + const { ctx } = this.cache; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.save(); + super.render(this.cache, transform); + ctx.restore(); + this.drawScene(); + this.cacheDirty = false; + } + + canvas.ctx.drawImage(this.canvas, 0, 0, this.width, this.height); + } + } + + setHD(hd: boolean): void { + super.setHD(hd); + this.sizeGL(this.width, this.height); + } + + size(width: number, height: number): void { + super.size(width, height); + this.sizeGL(width, height); + } + + private sizeGL(width: number, height: number) { + const ratio = this.highResolution ? devicePixelRatio : 1; + const scale = ratio * core.domStyle.scale; + this.canvas.width = width * scale; + this.canvas.height = height * scale; + } + + drawScene() { + const gl = this.gl; + if (!this.uniform || !gl) return; + // 清空画布 + gl.viewport(0, 0, this.canvas.width, this.canvas.height); + + gl.clearColor(0, 0, 0, 0); + 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.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.cache.canvas + ); + gl.uniform1i(this.uniform.uSampler, 0); + + // 绘制 + gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); + } + + /** + * 切换着色器程序 + * @param program 着色器程序 + */ + useProgram(program: ShaderProgram) { + this.program = program.program; + this.shader = program.shader; + this.uniform = program.uniform; + this.attribute = program.attribute; + this.gl?.useProgram(this.program); + this.programExterned = true; + } + + /** + * 获取当前的着色器程序 + */ + getProgram(): ShaderProgram { + this.programExterned = true; + return { + program: this.program, + shader: this.shader, + uniform: this.uniform, + attribute: this.attribute + }; + } + + /** + * 删除指定的着色器程序 + * @param program 着色器程序 + */ + deleteProgram(program: ShaderProgram) { + if (!this.gl) return; + if (this.program === program.program) { + this.gl.useProgram(null); + this.program = null; + this.shader = null; + this.uniform = null; + this.attribute = null; + } + if (program.program) { + this.gl.deleteProgram(program.program); + } + if (program.shader) { + this.gl.deleteShader(program.shader.vertex); + this.gl.deleteShader(program.shader.fragment); + } + } + + /** + * 设置着色器使用的glsl版本,默认使用100版本 + */ + setVersion(version: ShaderGLSLVersion) { + this.version = version; + } + + /** + * 设置顶点着色器 + * @param shader 着色器 + */ + vs(shader: string) { + this.vertex = shader; + this.shaderModified = true; + this.shaderDirty = true; + } + + /** + * 设置片元着色器 + * @param shader 着色器 + */ + fs(shader: string) { + this.fragment = shader; + this.shaderModified = true; + this.shaderDirty = true; + } + + /** + * 编译指定着色器并附加到新程序,老程序可以在调用此函数之前通过 {@link Shader.getProgram} 获取, + * 否则,老程序将会被删除 + */ + compileShader() { + if (!Shader.support) return; + + const program = Shader.compileProgram( + this, + this.version, + this.vertex, + this.fragment + ); + if (!program) return; + + if (!this.programExterned) { + Shader.deleteProgram(this, this.getProgram()); + } + this.programExterned = false; + + this.program = program.program; + this.shader = program.shader; + this.uniform = program.uniform; + this.attribute = program.attribute; + this.gl?.useProgram(this.program); + } + + // ----- 初始化部分 + + private init() { + if (!this.gl) return; + this.program = this.gl.createProgram(); + this.initTexture(); + this.initBuffers(); + } + + private initTexture() { + const gl = this.gl; + if (!gl) return; + + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.RGBA, + gl.UNSIGNED_BYTE, + this.cache.canvas + ); + gl.generateMipmap(gl.TEXTURE_2D); + + this.texture = texture; + } + + private setTextureAttrib() { + if (!this.attribute) return; + const gl = this.gl; + const pos = this.attribute.a_texCoord; + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer!.texture); + gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(pos); + } + + private setPositionAttrib() { + if (!this.attribute) return; + const gl = this.gl; + const pos = this.attribute.a_position; + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer!.position); + gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(pos); + } + + private initBuffers() { + 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); + + this.buffer = { + position: posBuffer!, + texture: textureBuffer!, + indices: this.initIndexBuffer()! + }; + } + + private initBuffer(pos: Float32Array) { + const gl = this.gl; + if (!gl) return; + 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; + if (!gl) return; + 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; + } + + // ----- 静态api部分 + + /** + * 传入着色器,编译出对应的程序,可以直接通过 {@link Shader.useProgram} 用于Shader组件 + * @param vs 顶点着色器 + * @param fs 片元着色器 + */ + static compileProgram( + shader: Shader, + version: ShaderGLSLVersion, + vs: string, + fs: string + ): ShaderProgram | null { + const gl = shader.gl; + if (!gl) return null; + + const program = gl.createProgram(); + if (!program) return null; + + const vsPrefix = + version === '100' + ? SHADER_VERTEX_PREFIX_100 + : SHADER_VERTEX_PREFIX_300; + const fsPrefix = + version === '100' + ? SHADER_FRAGMENT_PREFIX_100 + : SHADER_FRAGMENT_PREFIX_300; + + const vertexShader = this.compileShader( + gl, + gl.VERTEX_SHADER, + vsPrefix + vs + ); + const fragmentShader = this.compileShader( + gl, + gl.FRAGMENT_SHADER, + fsPrefix + fs + ); + + if (!vertexShader || !fragmentShader) return null; + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + const aTexCoord = gl.getAttribLocation(program, 'a_texCoord'); + const aPosition = gl.getAttribLocation(program, 'a_position'); + const uSampler = gl.getUniformLocation(program, 'u_sampler'); + + if (!uSampler) return null; + + return { + program, + attribute: { a_position: aPosition, a_texCoord: aTexCoord }, + uniform: { u_sampler: uSampler }, + shader: { vertex: vertexShader, fragment: fragmentShader } + }; + } + + private static compileShader( + gl: WebGL2RenderingContext, + type: number, + source: string + ): WebGLShader | null { + const shader = gl.createShader(type); + if (!shader) return null; + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + logger.error( + 13, + `Cannot compile ${ + type === gl.VERTEX_SHADER ? 'vertex' : 'fragment' + } shader. Error info: ${gl.getShaderInfoLog(shader)}` + ); + } + + return shader; + } + + /** + * 删除着色器程序 + * @param program 要删除的程序 + */ + static deleteProgram(shader: Shader, program: ShaderProgram) { + const gl = shader.gl; + if (!gl) return; + gl.deleteProgram(program.program); + gl.deleteShader(program.shader?.vertex ?? null); + gl.deleteShader(program.shader?.fragment ?? null); + } + + /** + * 获取一个程序中对应属性名的位置 + * @param program 要获取的程序 + * @param name 要获取的属性名 + */ + static getAttribute( + shader: Shader, + program: ShaderProgram, + name: string + ): number | null { + const gl = shader.gl; + if (!gl || !program.program || !program.attribute) return null; + if (program.attribute[name]) return program.attribute[name]; + const loc = gl.getAttribLocation(program.program, name); + return (program.attribute[name] = loc); + } + + /** + * 获取一个程序中对应uniform变量名的位置 + * @param program 要获取的程序 + * @param name 要获取的uniform变量名 + */ + static getUniform( + shader: Shader, + program: ShaderProgram, + name: string + ): WebGLUniformLocation | null { + const gl = shader.gl; + if (!gl || !program.program || !program.uniform) return null; + if (name in program.uniform) return program.uniform[name]; + const loc = gl.getUniformLocation(program.program, name); + return (program.uniform[name] = loc); + } +}