feat: fog weather & refactor: rain weather

This commit is contained in:
AncTe committed 2025-09-17 16:16:35 +08:00
1 parent 1b8624041b
commit df8328c33b
23 files changed
+296 -188

No files matched your search

@@ -1,6 +1,8 @@
import { logger } from '@motajs/common';
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-elements';
import {
MotaOffscreenCanvas2D,
SizedCanvasImageSource
} from '@motajs/render-core';
// 经过测试(https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading)
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
@@ -1,7 +1,6 @@
import { RenderAdapter } from '@motajs/render-core';
import { RenderAdapter, SizedCanvasImageSource } from '@motajs/render-core';
import { logger } from '@motajs/common';
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
import { SizedCanvasImageSource } from '@motajs/render-elements';
import EventEmitter from 'eventemitter3';
import { texture } from './cache';
import { TimingFn } from 'mutate-animate';
@@ -4,9 +4,9 @@ import {
RenderItem,
RenderItemPosition,
MotaOffscreenCanvas2D,
Transform
Transform,
SizedCanvasImageSource
} from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-elements';
import { isNil } from 'lodash-es';
import { RenderableData, AutotileRenderable, texture } from './cache';
import { IAnimateFrame, renderEmits } from './frame';
@@ -1,5 +1,5 @@
import { BaseProps, TagDefine } from '@motajs/render-vue';
import { Transform } from '@motajs/render-core';
import { CanvasStyle, Transform } from '@motajs/render-core';
import {
ILayerGroupRenderExtends,
FloorLayer,
@@ -10,7 +10,6 @@ import {
import { EAnimateEvent } from './animate';
import { EIconEvent, EWinskinEvent } from './misc';
import { IEnemyCollection } from '@motajs/types';
import { CanvasStyle } from '@motajs/render-elements';
export interface AnimateProps extends BaseProps {}
@@ -22,6 +22,7 @@ export abstract class EffectBase<T> {
this.program = program;
this.shader = shader;
shader.useProgram(program);
this.initProgram(program, options);
}
@@ -8,7 +8,14 @@ import {
onTick
} from '@motajs/render';
import { WeatherController } from '../weather';
import { defineComponent, onMounted, onUnmounted, reactive, ref } from 'vue';
import {
defineComponent,
onMounted,
onUnmounted,
reactive,
ref,
shallowRef
} from 'vue';
import { Textbox, Tip } from '../components';
import { GameUI } from '@motajs/system-ui';
import {
@@ -76,7 +83,7 @@ const MainScene = defineComponent(() => {
width: 480
};
const map = ref<LayerGroup>();
const map = shallowRef<LayerGroup>();
const hideStatus = ref(false);
const locked = ref(false);
const weather = new WeatherController();
@@ -1,10 +1,11 @@
import { WeatherController } from './controller';
import { CloudWeather, RainWeather, SunWeather } from './presets';
import { CloudWeather, FogWeather, RainWeather, SunWeather } from './presets';
export function createWeather() {
WeatherController.register('cloud', CloudWeather);
WeatherController.register('rain', RainWeather);
WeatherController.register('sun', SunWeather);
WeatherController.register('fog', FogWeather);
// WeatherController.register('snow', SnowWeather);
}
@@ -1,83 +1,9 @@
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
import { Weather } from '../weather';
import { CloudLike } from './cloudLike';
import { SizedCanvasImageSource } from '@motajs/render-core';
export class CloudWeather extends Weather<Sprite> {
/** 云层的不透明度 */
private alpha: number = 0;
/** 水平速度 */
private vx: number = 0;
/** 竖直速度 */
private vy: number = 0;
/** 水平位置 */
private cx: number = 0;
/** 竖直位置 */
private cy: number = 0;
/** 云层移动的最大速度 */
private maxSpeed: number = 1;
/** 云层图像 */
private image: HTMLImageElement | null = null;
/** 上一次执行速度变换的时刻 */
private lastTick = 0;
private drawCloud(canvas: MotaOffscreenCanvas2D) {
const ctx = canvas.ctx;
if (!this.image) return;
ctx.globalAlpha = this.alpha;
const { width, height } = this.image;
for (let x = -1; x < 2; x++) {
for (let y = -1; y < 2; y++) {
const dx = x * width + this.cx;
const dy = y * height + this.cy;
if (dx > canvas.width || dy > canvas.height) continue;
if (dx + width < 0 || dy + height < 0) continue;
ctx.drawImage(this.image, dx, dy, width, height);
}
}
}
tick(time: number): void {
if (!this.element || !this.image) return;
this.element.update();
const dt = time - this.lastTick;
this.lastTick = time;
if (dt > 100) return;
const dvx = (Math.random() - 0.5) * this.level * 10;
const dvy = (Math.random() - 0.5) * this.level * 10;
const addx = (dvx * dt) / 1000;
const addy = (dvy * dt) / 1000;
if (Math.sign(addx) === Math.sign(this.vx)) {
const ratio = Math.sqrt(
(this.maxSpeed - Math.abs(this.vx)) / this.maxSpeed
);
const value = Math.abs(addx) * ratio;
this.vx += value * Math.sign(addx);
} else {
this.vx += addx;
}
if (Math.sign(addy) === Math.sign(this.vy)) {
const ratio = Math.sqrt(
(this.maxSpeed - Math.abs(this.vy)) / this.maxSpeed
);
const value = Math.abs(addy) * ratio;
this.vy += value * Math.sign(addy);
} else {
this.vy += addy;
}
this.cx += (this.vx * dt) / 1000;
this.cy += (this.vy * dt) / 1000;
this.cx %= this.image.width;
this.cy %= this.image.height;
}
createElement(level: number): Sprite {
const element = new Sprite('static', true);
element.setRenderFn(canvas => this.drawCloud(canvas));
this.maxSpeed = Math.sqrt(level) * 100;
this.vx = ((Math.random() - 0.5) * this.maxSpeed) / 2;
this.vy = ((Math.random() - 0.5) * this.maxSpeed) / 2;
this.alpha = Math.sqrt(level) / 10;
this.image = core.material.images.images['cloud.png'];
return element;
export class CloudWeather extends CloudLike {
getImage(): SizedCanvasImageSource | null {
return core.material.images.images['cloud.png'] ?? null;
}
onDestroy(): void {}
@@ -0,0 +1,88 @@
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
import { Weather } from '../weather';
import { SizedCanvasImageSource } from '@motajs/render-core';
export abstract class CloudLike extends Weather<Sprite> {
/** 不透明度 */
private alpha: number = 0;
/** 水平速度 */
private vx: number = 0;
/** 竖直速度 */
private vy: number = 0;
/** 水平位置 */
private cx: number = 0;
/** 竖直位置 */
private cy: number = 0;
/** 移动的最大速度 */
private maxSpeed: number = 1;
/** 上一次执行速度变换的时刻 */
private lastTick = 0;
/** 绘制天气使用的图片 */
private image: SizedCanvasImageSource | null = null;
/**
* 获取类多云天气所使用的图片
*/
abstract getImage(): SizedCanvasImageSource | null;
private drawImage(canvas: MotaOffscreenCanvas2D) {
const ctx = canvas.ctx;
if (!this.image) return;
ctx.globalAlpha = this.alpha;
const { width, height } = this.image;
for (let x = -1; x < 2; x++) {
for (let y = -1; y < 2; y++) {
const dx = x * width + this.cx;
const dy = y * height + this.cy;
if (dx > canvas.width || dy > canvas.height) continue;
if (dx + width < 0 || dy + height < 0) continue;
ctx.drawImage(this.image, dx, dy, width, height);
}
}
}
tick(time: number): void {
if (!this.element || !this.image) return;
this.element.update();
const dt = time - this.lastTick;
this.lastTick = time;
if (dt > 100) return;
const dvx = (Math.random() - 0.5) * this.level * 10;
const dvy = (Math.random() - 0.5) * this.level * 10;
const addx = (dvx * dt) / 1000;
const addy = (dvy * dt) / 1000;
if (Math.sign(addx) === Math.sign(this.vx)) {
const ratio = Math.sqrt(
(this.maxSpeed - Math.abs(this.vx)) / this.maxSpeed
);
const value = Math.abs(addx) * ratio;
this.vx += value * Math.sign(addx);
} else {
this.vx += addx;
}
if (Math.sign(addy) === Math.sign(this.vy)) {
const ratio = Math.sqrt(
(this.maxSpeed - Math.abs(this.vy)) / this.maxSpeed
);
const value = Math.abs(addy) * ratio;
this.vy += value * Math.sign(addy);
} else {
this.vy += addy;
}
this.cx += (this.vx * dt) / 1000;
this.cy += (this.vy * dt) / 1000;
this.cx %= this.image.width;
this.cy %= this.image.height;
}
createElement(level: number): Sprite {
const element = new Sprite('static', true);
element.setRenderFn(canvas => this.drawImage(canvas));
this.maxSpeed = Math.sqrt(level) * 100;
this.vx = ((Math.random() - 0.5) * this.maxSpeed) / 2;
this.vy = ((Math.random() - 0.5) * this.maxSpeed) / 2;
this.alpha = Math.sqrt(level) / 10;
this.image = this.getImage();
return element;
}
}
@@ -0,0 +1,35 @@
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
import { CloudLike } from './cloudLike';
import { SizedCanvasImageSource } from '@motajs/render-core';
export class FogWeather extends CloudLike {
/** 雾天气的图像比较小,因此将四个进行合并 */
private static mergedFog: MotaOffscreenCanvas2D | null = null;
getImage(): SizedCanvasImageSource | null {
if (FogWeather.mergedFog) {
return FogWeather.mergedFog.canvas;
} else {
return FogWeather.mergeFog();
}
}
onDestroy(): void {}
/**
* 将雾天气的图片 2x2 合并
*/
static mergeFog() {
const image = core.material.images.images['fog.png'];
if (!image) return null;
const { width, height } = image;
this.mergedFog = new MotaOffscreenCanvas2D();
this.mergedFog.size(width * 2, height * 2);
const ctx = this.mergedFog.ctx;
ctx.drawImage(image, 0, 0, width, width);
ctx.drawImage(image, width, 0, width, width);
ctx.drawImage(image, 0, height, width, width);
ctx.drawImage(image, width, height, width, width);
return this.mergedFog.canvas;
}
}
@@ -1,4 +1,6 @@
export * from './cloud';
export * from './cloudLike';
export * from './fog';
export * from './rain';
export * from './snow';
export * from './sun';
@@ -1,8 +1,9 @@
import {
Shader,
ShaderProgram,
IShaderUniform,
UniformType
UniformType,
MotaOffscreenCanvas2D,
GL2
} from '@motajs/render';
import { Weather } from '../weather';
@@ -73,24 +74,61 @@ void main() {
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);
outColor = mix(u_color, tex, 0.8);
}
`;
/** 雨滴顶点坐标 */
const vertex = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
export class RainWeather extends Weather<Shader> {
/** 下雨流程的 uniform 变量 */
private progress: IShaderUniform<UniformType.Uniform1f> | null = null;
/** 使用的着色器程序 */
private program: ShaderProgram | null = null;
interface RainCreateData {
/** 进度变量 */
readonly uProgress: IShaderUniform<UniformType.Uniform1f> | null;
/** 下雨着色器程序 */
readonly program: ShaderProgram;
}
class RainShader extends GL2 {
/** 下雨程序 */
private rainProgram: ShaderProgram | null = null;
/** 背景程序 */
private backProgram: ShaderProgram | null = null;
create(level: number): RainCreateData {
const gl = this.gl;
const program = this.createProgram(ShaderProgram);
program.fs(rainFs);
program.vs(rainVs);
program.requestCompile();
this.useProgram(program);
const pos = program.defineAttribArray('a_rainVertex');
program.defineAttribArray('a_offset');
program.defineAttribArray('a_data');
program.defineUniform('u_color', this.UNIFORM_4f);
const uProgress = program.defineUniform('u_progress', this.UNIFORM_1f);
program.mode(this.DRAW_ARRAYS_INSTANCED);
if (pos) {
pos.buffer(vertex, gl.STATIC_DRAW);
pos.pointer(2, gl.FLOAT, false, 0, 0);
pos.enable();
}
program.paramArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, 100 * level);
this.generateRainPath(level, program);
this.rainProgram = program;
this.backProgram = this.createProgram(ShaderProgram);
this.backProgram.requestCompile();
return { uProgress, program };
}
/**
* 生成雨滴
* @param num 雨滴数量
*/
generateRainPath(level: number, program: ShaderProgram, shader: Shader) {
generateRainPath(level: number, program: ShaderProgram) {
const num = level * 100;
const angle = (((Math.random() - 0.5) * Math.PI) / 30) * level;
const deviation = (Math.PI / 180) * (12 - level);
@@ -98,8 +136,8 @@ export class RainWeather extends Weather<Shader> {
const aOffset = program.getAttribArray('a_offset');
const aData = program.getAttribArray('a_data');
const color = program.getUniform<UniformType.Uniform4f>('u_color');
const gl = shader.gl;
if (!aOffset || !aData) return;
const gl = this.gl;
if (!aOffset || !aData || !color) return;
const tan = Math.tan(angle);
@@ -135,52 +173,47 @@ export class RainWeather extends Weather<Shader> {
aData.enable();
program.paramArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, num);
color?.set(1, 1, 1, 0.1);
color.set(1, 1, 1, 0.1);
}
createElement(level: number): Shader {
const shader = new Shader();
const gl = shader.gl;
shader.size(480, 480);
protected drawScene(
canvas: MotaOffscreenCanvas2D,
gl: WebGL2RenderingContext
): void {
const program1 = this.backProgram;
const program2 = this.rainProgram;
if (!program1 || !program2) return;
this.useProgram(program1);
program1.texTexture('u_sampler', canvas.canvas);
this.draw(gl, program1);
this.useProgram(program2);
program2.texTexture('u_sampler', canvas.canvas);
this.draw(gl, program2);
}
}
export class RainWeather extends Weather<GL2> {
/** 下雨流程的 uniform 变量 */
private progress: IShaderUniform<UniformType.Uniform1f> | null = null;
/** 下雨着色器程序 */
private program: ShaderProgram | null = null;
createElement(level: number): GL2 {
const shader = new RainShader();
shader.setHD(true);
shader.setZIndex(100);
const program = shader.createProgram(ShaderProgram);
program.fs(rainFs);
program.vs(rainVs);
program.requestCompile();
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);
shader.useProgram(program);
if (pos) {
pos.buffer(vertex, gl.STATIC_DRAW);
pos.pointer(2, gl.FLOAT, false, 0, 0);
pos.enable();
}
program.paramArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, 100 * this.level);
this.progress = program.getUniform<UniformType.Uniform1f>('u_progress');
this.generateRainPath(level, program, shader);
const { uProgress, program } = shader.create(level);
this.progress = uProgress;
this.program = program;
return shader;
}
tick(timestamp: number): void {
if (!this.element) return;
if (!this.element || !this.program) return;
this.element.update();
const time = 5000 - 400 * this.level;
const progress = (timestamp % time) / time;
this.progress?.set(progress);
}
onDestroy(): void {
if (!this.element || !this.program) return;
this.element.deleteProgram(this.program);
}
onDestroy(): void {}
}