feat: Shader着色器组件

This commit is contained in:
unanmed 2024-08-24 00:43:22 +08:00
parent d7dcbe9d04
commit 2561d7f23d
5 changed files with 552 additions and 8 deletions

View File

@ -97,7 +97,7 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
* *
*/ */
delete() { delete() {
MotaCanvas2D.list.delete(this); MotaOffscreenCanvas2D.list.delete(this);
} }
/** /**

View File

@ -1,8 +1,18 @@
import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { IRenderChildable, RenderItem, RenderItemPosition } from './item'; import {
ERenderItemEvent,
IRenderChildable,
RenderItem,
RenderItemPosition
} from './item';
import { Transform } from './transform'; import { Transform } from './transform';
export class Container extends RenderItem implements IRenderChildable { export interface EContainerEvent extends ERenderItemEvent {}
export class Container<E extends EContainerEvent = EContainerEvent>
extends RenderItem<E | EContainerEvent>
implements IRenderChildable
{
children: RenderItem[] = []; children: RenderItem[] = [];
sortedChildren: RenderItem[] = []; sortedChildren: RenderItem[] = [];
@ -23,7 +33,6 @@ export class Container extends RenderItem implements IRenderChildable {
transform: Transform transform: Transform
): void { ): void {
const { ctx } = canvas; const { ctx } = canvas;
// console.log(ctx.getTransform());
this.sortedChildren.forEach(v => { this.sortedChildren.forEach(v => {
if (v.hidden) return; if (v.hidden) return;

View File

@ -155,7 +155,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
anchorY: number = 0; anchorY: number = 0;
/** 渲染模式absolute表示绝对位置static表示跟随摄像机移动 */ /** 渲染模式absolute表示绝对位置static表示跟随摄像机移动 */
type: 'absolute' | 'static' = 'static'; type: RenderItemPosition = 'static';
/** 是否是高清画布 */ /** 是否是高清画布 */
highResolution: boolean = true; highResolution: boolean = true;
/** 是否抗锯齿 */ /** 是否抗锯齿 */
@ -172,9 +172,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
transform: Transform = new Transform(); 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; readonly enableCache: boolean = true;
@ -183,6 +183,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.enableCache = enableCache; this.enableCache = enableCache;
this.type = type; this.type = type;
this.cache.withGameScale(true); this.cache.withGameScale(true);
} }

View File

@ -8,6 +8,7 @@ import { FloorDamageExtends } from './preset/damage';
import { HeroRenderer } from './preset/hero'; import { HeroRenderer } from './preset/hero';
import { Transform } from './transform'; import { Transform } from './transform';
import { Text } from './preset/misc'; import { Text } from './preset/misc';
import { Shader } from './shader';
export class MotaRenderer extends Container { export class MotaRenderer extends Container {
static list: Set<MotaRenderer> = new Set(); static list: Set<MotaRenderer> = new Set();
@ -74,6 +75,7 @@ Mota.require('var', 'hook').once('reset', () => {
const transform = render.transform; const transform = render.transform;
render.mount(); render.mount();
const shader = new Shader();
const layer = new LayerGroup(); const layer = new LayerGroup();
['bg', 'bg2', 'event', 'fg', 'fg2'].forEach(v => { ['bg', 'bg2', 'event', 'fg', 'fg2'].forEach(v => {
@ -87,7 +89,18 @@ Mota.require('var', 'hook').once('reset', () => {
layer.extends(damage); layer.extends(damage);
layer.getLayer('event')?.extends(hero); layer.getLayer('event')?.extends(hero);
binder.bindThis(); 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(() => { layer.requestAfterFrame(() => {
hero.setImage(core.material.images.images['hero2.png']); hero.setImage(core.material.images.images['hero2.png']);

521
src/core/render/shader.ts Normal file
View File

@ -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<EShaderEvent> {
/** 是否支持此组件 */
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);
}
}