diff --git a/src/core/render/shader.ts b/src/core/render/shader.ts index c210ed5..def216a 100644 --- a/src/core/render/shader.ts +++ b/src/core/render/shader.ts @@ -12,7 +12,7 @@ precision highp float; in vec4 a_position; in vec2 a_texCoord; -out highp vec2 v_texCoord; +out vec2 v_texCoord; `; const SHADER_VERTEX_PREFIX_100 = /* glsl */ ` precision highp float; @@ -20,20 +20,20 @@ precision highp float; attribute vec4 a_position; attribute vec2 a_texCoord; -varying highp vec2 v_texCoord; +varying vec2 v_texCoord; `; const SHADER_FRAGMENT_PREFIX_300 = /* glsl */ `#version 300 es precision highp float; -in highp vec2 v_texCoord; +in vec2 v_texCoord; uniform sampler2D u_sampler; `; const SHADER_FRAGMENT_PREFIX_100 = /* glsl */ ` precision highp float; -varying highp vec2 v_texCoord; +varying vec2 v_texCoord; uniform sampler2D u_sampler; `; @@ -268,9 +268,7 @@ export class Shader extends Container { } update(item?: RenderItem): void { - const isSelf = item === this && !this.cacheDirty; super.update(item); - if (isSelf) this.cacheDirty = false; this.shaderRenderDirty = true; } @@ -283,7 +281,6 @@ export class Shader extends Container { const ready = dr && program.ready(); if (!ready) return; const indices = program.usingIndices; - if (!indices) return; const param = program.getDrawParams(program.renderMode); if (!param) return; @@ -293,22 +290,22 @@ export class Shader extends Container { gl.clearDepth(1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - const pre = this.preDraw(); + const pre = this.preDraw(gl, program, param, indices); if (!pre) { - this.postDraw(); + this.postDraw(gl, program, param, indices); return; } this.draw(gl, program, param, indices); - this.postDraw(); + this.postDraw(gl, program, param, indices); } - private draw( + draw( gl: WebGL2RenderingContext, program: ShaderProgram, param: DrawParamsMap[keyof DrawParamsMap], - indices: IShaderIndices + indices: IShaderIndices | null ) { switch (program.renderMode) { case RenderMode.Arrays: { @@ -316,6 +313,7 @@ export class Shader extends Container { gl.drawArrays(mode, first, count); } case RenderMode.Elements: { + if (!indices) return; const { mode, count, type, offset } = param as DrawElementsParam; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indices.data); @@ -327,6 +325,7 @@ export class Shader extends Container { gl.drawArraysInstanced(mode, first, count, instanceCount); } case RenderMode.ElementsInstanced: { + if (!indices) return; const { mode, count, @@ -344,7 +343,12 @@ export class Shader extends Container { * 在本着色器内部渲染之前执行的渲染,如果返回false,则表示不进行内部渲染,但依然会执行 {@link postDraw}。 * 继承本类,并复写此方法即可实现前置渲染功能 */ - protected preDraw(): boolean { + protected preDraw( + gl: WebGL2RenderingContext, + program: ShaderProgram, + param: DrawParamsMap[keyof DrawParamsMap], + indices: IShaderIndices | null + ): boolean { return true; } @@ -352,9 +356,18 @@ export class Shader extends Container { * 在本着色器内部渲染之后执行的渲染,即使preDraw返回false,本函数也会执行 * 继承本类,并复写此方法即可实现后置渲染功能 */ - protected postDraw() {} + protected postDraw( + gl: WebGL2RenderingContext, + program: ShaderProgram, + param: DrawParamsMap[keyof DrawParamsMap], + indices: IShaderIndices | null + ) {} - private defaultReady(): boolean { + /** + * 默认的准备函数 + * @returns 是否准备成功 + */ + protected defaultReady(): boolean { const program = this.program; if (!program) return false; const tex = program.getTexture('u_sampler'); @@ -491,6 +504,8 @@ export class Shader extends Container { const gl = this.gl; if (!gl) return; gl.enable(gl.DEPTH_TEST); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.depthFunc(gl.LEQUAL); } } @@ -544,7 +559,7 @@ interface AttribSetFn { [AttribType.AttribI4uiv]: _A; } -interface IShaderUniform { +export interface IShaderUniform { /** 这个 uniform 变量的内存位置 */ readonly location: WebGLUniformLocation; /** 这个 uniform 变量的类型 */ @@ -559,7 +574,7 @@ interface IShaderUniform { set(...params: UniformSetFn[T]): void; } -interface IShaderAttrib { +export interface IShaderAttrib { /** 这个 attribute 常量的内存位置 */ readonly location: number; /** 这个 attribute 常量的类型 */ @@ -575,7 +590,7 @@ interface IShaderAttrib { set(...params: AttribSetFn[T]): void; } -interface IShaderAttribArray { +export interface IShaderAttribArray { /** 这个 attribute 常量的内存位置 */ readonly location: number; /** 这个 attribute 所用的缓冲区信息 */ @@ -670,7 +685,7 @@ interface IShaderAttribArray { disable(): void; } -interface IShaderIndices { +export interface IShaderIndices { /** 这个顶点索引所用的缓冲区信息 */ readonly data: WebGLBuffer; /** 这个量所处的着色器程序 */ @@ -719,7 +734,7 @@ interface IShaderIndices { ): void; } -interface IShaderUniformMatrix { +export interface IShaderUniformMatrix { /** 矩阵的内存位置 */ readonly location: WebGLUniformLocation; /** 矩阵类型 */ @@ -741,7 +756,7 @@ interface IShaderUniformMatrix { ): void; } -interface IShaderUniformBlock { +export interface IShaderUniformBlock { /** 这个 uniform block 的内存地址 */ readonly location: GLuint; /** 与这个 uniform block 所绑定的缓冲区 */ @@ -764,7 +779,7 @@ interface IShaderUniformBlock { set(srcData: ArrayBufferView, srcOffset: number, length?: number): void; } -interface IShaderTexture2D { +export interface IShaderTexture2D { /** 纹理对象 */ readonly texture: WebGLTexture; /** 宽度 */ @@ -1235,7 +1250,7 @@ interface DrawElementsInstancedParam { instanceCount: number; } -interface DrawParamsMap { +export interface DrawParamsMap { [RenderMode.Arrays]: DrawArraysParam; [RenderMode.ArraysInstanced]: DrawArraysInstancedParam; [RenderMode.Elements]: DrawElementsParam; @@ -1684,6 +1699,7 @@ export class ShaderProgram extends EventEmitter { const buffer = gl.createBuffer(); if (!buffer) return null; const location = gl.getAttribLocation(program, name); + if (location === -1) return null; const obj = new ShaderAttribArray(buffer, location, gl, this); this.attribArray.set(name, obj); return obj; @@ -1870,7 +1886,7 @@ export class ShaderProgram extends EventEmitter { const sampler = this.defineTexture('u_sampler', 0); const indices = this.defineIndices('defalutIndices'); if (!tex || !position || !sampler || !indices) { - return false; + return true; } position.buffer( new Float32Array([1, -1, -1, -1, 1, 1, -1, 1]), diff --git a/src/module/weather/rain.ts b/src/module/weather/rain.ts new file mode 100644 index 0000000..745d0eb --- /dev/null +++ b/src/module/weather/rain.ts @@ -0,0 +1,250 @@ +import { + DrawParamsMap, + IShaderIndices, + IShaderUniform, + Shader, + ShaderProgram, + UniformType +} from '@/core/render/shader'; +import { IWeather, WeatherController } from './weather'; +import { MotaRenderer } from '@/core/render/render'; +import { Container } from '@/core/render/container'; + +const rainVs = /* glsl */ ` +in vec2 a_rainVertex; +in vec2 a_offset; // 雨滴的中心位置 +in vec4 a_data; // x: 雨滴宽度; y: 雨滴长度; z: 雨滴旋转角度,0表示向下,逆时针为正; + // w: 属于哪一种雨,需要两种雨反复循环,一种无法实现循环,0表示第一种,1表示第二种 + +uniform float u_progress; // 下雨进度,从最上落到最下是0.5个进度,以保证不会出现奇怪的问题 + +out vec2 v_center; +out vec2 v_data; // 雨滴宽度与高度 +out vec2 v_pos; + +mat2 createScaleMatrix(float x, float y) { + return mat2( + x, 0, + 0, y + ); +} + +vec2 getOffsetByProgress(vec2 offset) { + if (a_data.w == 0.0) { + if (u_progress < 0.5) { + return offset * u_progress * 2.0; + } else { + return offset * (u_progress - 1.0) * 2.0; + } + } else { + return offset * u_progress * 2.0; + } +} + +void main() { + float cosTheta = cos(a_data.z); + float sinTheta = sin(a_data.z); + mat2 rotate = mat2( + cosTheta, -sinTheta, + sinTheta, cosTheta + ); + vec2 offset = getOffsetByProgress(vec2(-sinTheta * 2.0, -cosTheta * 2.0)); + mat2 scale = createScaleMatrix(a_data.x, a_data.y); + vec2 off = a_offset + offset; + vec2 pos = rotate * scale * a_rainVertex + off; + v_center = off; + v_pos = pos; + gl_Position = vec4(pos, 0.0, 1.0); +} +`; + +const rainFs = /* glsl */ ` +in vec2 v_center; +in vec2 v_data; // 雨滴的宽度与长度 +in vec2 v_pos; + +uniform vec4 u_color; // 雨滴的颜色 + +out vec4 outColor; + +float random(vec2 uv) { + return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453); +} + +void main() { + float dis = distance(v_pos, v_center); + float alpha = 1.0; + float decay = v_data.y * 0.5 * 0.5; + if (dis > decay) { + alpha = 2.0 * (dis - decay) / decay; + } + float ran = random(v_pos); + vec2 pos = vec2(v_pos.x + ran * 0.01, v_pos.y); + vec2 texPos = (pos + 1.0) / 2.0; + texPos.y = 1.0 - texPos.y; + vec4 tex = texture(u_sampler, texPos); + outColor = mix(u_color, tex, 0.9); +} +`; + +/** 雨滴顶点坐标 */ +const vertex = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]); + +Mota.require('var', 'loading').once('coreInit', () => { + const shader = new RainShader(); + const gl = shader.gl; + shader.size(480, 480); + shader.setHD(true); + RainWeather.shader = shader; + const program = shader.createProgram(); + program.setVersion(shader.VERSION_ES_300); + program.fs(rainFs); + program.vs(rainVs); + program.requestCompile(); + program.useDefault(false); + const pos = program.defineAttribArray('a_rainVertex'); + program.defineAttribArray('a_offset'); + program.defineAttribArray('a_data'); + program.defineUniform('u_progress', shader.UNIFORM_1f); + program.defineUniform('u_color', shader.UNIFORM_4f); + program.mode(shader.DRAW_ARRAYS_INSTANCED); + RainShader.rainProgram = program; + shader.useProgram(program); + + if (pos) { + pos.buffer(vertex, gl.STATIC_DRAW); + pos.pointer(2, gl.FLOAT, false, 0, 0); + pos.enable(); + } + + const back = shader.createProgram(); + back.requestCompile(); + back.modified = true; + back.useDefault(false); + RainShader.backProgram = back; + shader.useProgram(back); +}); + +export class RainWeather implements IWeather { + static id: string = 'rain'; + + static shader: RainShader; + + private progress: IShaderUniform | null = null; + + constructor(readonly level: number = 5) {} + + activate(): void { + const render = MotaRenderer.get('render-main'); + const layer = render?.getElementById('layer-main'); + const draw = render?.getElementById('map-draw') as Container; + if (!layer || !draw) return; + const shader = RainWeather.shader; + layer.append(shader); + shader.append(draw); + + const gl = shader.gl; + const program = RainShader.rainProgram; + program.paramArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, 100 * this.level); + + this.progress = program.getUniform('u_progress'); + shader.generateRainPath( + this.level * 100, + (((Math.random() - 0.5) * Math.PI) / 30) * this.level, + (Math.PI / 180) * (12 - this.level) + ); + } + + frame(): void { + RainWeather.shader.update(RainWeather.shader); + const time = 5000 - 400 * this.level; + const progress = (Date.now() % time) / time; + // console.log(progress); + + RainWeather.shader.useProgram(RainShader.rainProgram); + this.progress?.set(progress); + } + + deactivate(): void { + const render = MotaRenderer.get('render-main'); + const layer = render?.getElementById('layer-main'); + const draw = render?.getElementById('map-draw') as Container; + if (!layer || !draw) return; + const shader = RainWeather.shader; + layer.append(draw); + shader.remove(); + } +} + +WeatherController.register(RainWeather); + +class RainShader extends Shader { + static rainProgram: ShaderProgram; + static backProgram: ShaderProgram; + + /** + * 生成雨滴 + * @param num 雨滴数量 + */ + generateRainPath(num: number, angle: number, deviation: number) { + const program = RainShader.rainProgram; + RainWeather.shader.useProgram(program); + const aOffset = program.getAttribArray('a_offset'); + const aData = program.getAttribArray('a_data'); + const color = program.getUniform('u_color'); + const gl = this.gl; + if (!aOffset || !aData) return; + + const tan = Math.tan(angle); + + const offset = new Float32Array(num * 2); + const data = new Float32Array(num * 4); + const half = num / 2; + for (let i = 0; i < half; i++) { + const ox = Math.random() * 3 - 1.5; + const oy = Math.random() * 2 - 1; + const rad = angle + (Math.random() - 0.5) * Math.PI * deviation; + const length = Math.random() * 0.05 + 0.03; + const width = Math.random() * 0.002 + 0.002; + offset.set([ox, oy], i * 2); + data.set([width, length, rad, 0], i * 4); + } + for (let i = half; i < num; i++) { + const ox = Math.random() * 3 - 1.5 + tan * 2; + const oy = Math.random() * 2 + 1; + const rad = angle + (Math.random() - 0.5) * Math.PI * deviation; + const length = Math.random() * 0.1 + 0.05; + const width = Math.random() * 0.002 + 0.002; + offset.set([ox, oy], i * 2); + data.set([width, length, rad, 1], i * 4); + } + + aOffset.buffer(offset, gl.STATIC_DRAW); + aData.buffer(data, gl.STATIC_DRAW); + aOffset.pointer(2, gl.FLOAT, false, 0, 0); + aOffset.divisor(1); + aOffset.enable(); + aData.pointer(4, gl.FLOAT, false, 0, 0); + aData.divisor(1); + aData.enable(); + + program.paramArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, num); + color?.set(1, 1, 1, 0.1); + } + + protected override preDraw(gl: WebGL2RenderingContext): boolean { + const back = RainShader.backProgram; + const rain = RainShader.rainProgram; + this.useProgram(back); + const ready = this.defaultReady(); + const param = back.getDrawParams(this.DRAW_ELEMENTS); + const rainParam = rain.getDrawParams(this.DRAW_ARRAYS_INSTANCED); + if (!ready || !param || !rainParam) return false; + + this.draw(gl, back, param, back.usingIndices); + this.useProgram(rain); + this.draw(gl, rain, rainParam, null); + + return false; + } +} diff --git a/src/module/weather/weather.ts b/src/module/weather/weather.ts new file mode 100644 index 0000000..885b20a --- /dev/null +++ b/src/module/weather/weather.ts @@ -0,0 +1,75 @@ +import { logger } from '@/core/common/logger'; +import { Ticker } from 'mutate-animate'; + +export interface IWeather { + /** + * 初始化天气,当天气被添加时会被立刻调用 + */ + activate(): void; + + /** + * 每帧执行的函数 + */ + frame(): void; + + /** + * 摧毁这个天气,当天气被删除时会执行 + */ + deactivate(): void; +} + +interface Weather { + id: string; + new (level?: number): IWeather; +} + +export class WeatherController { + static list: Map = new Map(); + + /** 当前的所有天气 */ + active: Set = new Set(); + ticker: Ticker = new Ticker(); + + private tick = () => { + this.active.forEach(v => { + v.frame(); + }); + }; + + /** + * 添加一个天气,如果天气不存在则抛出警告。注意虽然原则上允许天气重复,但一些天气在实现时,并不允许重复 + * @param id 天气的id + * @param level 天气的等级 + * @returns 天气实例,可以操作天气的效果,也可以用来删除 + */ + activate(id: string, level?: number) { + const Weather = WeatherController.list.get(id); + if (!Weather) { + logger.warn(25, id); + return; + } + const weather = new Weather(level); + this.active.add(weather); + weather.activate(); + if (!this.ticker.funcs.has(this.tick)) { + this.ticker.add(this.tick); + } + return weather; + } + + /** + * 删除一个天气 + * @param weather 要删除的天气 + */ + deactivate(weather: IWeather) { + this.active.delete(weather); + if (this.active.size === 0) { + this.ticker.remove(this.tick); + } + weather.deactivate(); + } + + static register(weather: Weather) { + this.list.set(weather.id, weather); + } +}