粒子特效

This commit is contained in:
unanmed 2023-02-15 13:19:05 +08:00
parent 29b4dd13e2
commit 6a667d4ff7
8 changed files with 666 additions and 50 deletions

View File

@ -54,7 +54,7 @@
"<br>",
"游戏作者:古祠",
"<br>",
"本塔遵循MIT开源协议,你可随意使用本塔的任何代码,不需要作者授权,也可以随意用于商业用途。",
"本塔遵循MIT开源协议<a href=\"LICENSE\" target=\"_blank\">查看开源协议</a>",
"<br>",
"BGM来源网易云音乐等",
"<br>",

View File

@ -3,6 +3,7 @@ import App from './App.vue';
import App2 from './App2.vue';
import './styles.less';
import 'ant-design-vue/dist/antd.dark.css';
import './plugin/particle/render';
createApp(App).mount('#root');
createApp(App2).mount('#root2');

View File

@ -1,11 +1,31 @@
import { Matrix4 } from '../webgl/martrix';
import { TimingFn } from 'mutate-animate';
import { Matrix4 } from '../webgl/matrix';
import { Renderer } from './render';
type Position3D = [number, number, number];
export type Position3D = [number, number, number];
type OneParameterAnimationType = 'x' | 'y' | 'z';
type TwoParameterAnimationType = 'xy' | 'yx' | 'xz' | 'zx' | 'yz' | 'zy';
type ThreeParameterAnimationType =
| 'xyz'
| 'xzy'
| 'yxz'
| 'yzx'
| 'zxy'
| 'zyx';
type CameraAnimationTarget = 'eye' | 'at' | 'up';
interface CameraAnimationType {
1: OneParameterAnimationType;
2: TwoParameterAnimationType;
3: ThreeParameterAnimationType;
}
export class Camera {
/** 视图矩阵 */
matrix!: Matrix4;
view!: Matrix4;
/** 投影矩阵 */
projection!: Matrix4;
/** 绑定的渲染器 */
renderer?: Renderer;
@ -13,8 +33,12 @@ export class Camera {
this.reset();
}
/**
*
*/
reset() {
this.matrix = new Matrix4();
this.view = new Matrix4();
this.projection = new Matrix4();
}
/**
@ -39,7 +63,7 @@ export class Camera {
* @param up
*/
lookAt(eye: Position3D, at: Position3D, up: Position3D) {
this.matrix = this.calLookAt(eye, at, up);
this.view = this.calLookAt(eye, at, up);
}
/**
@ -49,9 +73,62 @@ export class Camera {
* @param up
*/
transform(eye: Position3D, at: Position3D, up: Position3D) {
this.matrix.multipy(this.calLookAt(eye, at, up));
this.view.multipy(this.calLookAt(eye, at, up));
}
/**
*
* @param fov
* @param aspect
* @param near
* @param far
*/
setPerspective(fov: number, aspect: number, near: number, far: number) {
this.projection = this.calPerspective(fov, aspect, near, far);
}
/**
*
* @param left
* @param right
* @param bottom
* @param top
* @param near
* @param far
*/
setOrthogonal(
left: number,
right: number,
bottom: number,
top: number,
near: number,
far: number
) {
this.projection = this.calOrthogonal(
left,
right,
bottom,
top,
near,
far
);
}
/**
*
*/
update() {
this.renderer?.render();
}
applyAnimate<N extends keyof CameraAnimationType = 1>(
target: CameraAnimationTarget,
type: CameraAnimationType[N],
time: number = 1000,
timing?: TimingFn<N>,
relative: boolean = false
) {}
/**
*
* @see https://github.com/bad4iz/cuon-matrix/blob/main/src/Matrix4/Matrix4.ts
@ -92,9 +169,90 @@ export class Camera {
matrix[0] = [sx, sy, sz, 0];
matrix[1] = [ux, uy, uz, 0];
matrix[2] = [-fx, -fy, -fz, 0];
matrix[4] = [0, 0, 0, 1];
matrix[3] = [0, 0, 0, 1];
matrix.translate(-eyeX, -eyeY, -eyeZ);
return matrix;
}
/**
*
* @see https://github.com/bad4iz/cuon-matrix/blob/main/src/Matrix4/Matrix4.ts
* @param fovy
* @param aspect
* @param near
* @param far
*/
private calPerspective(
fov: number,
aspect: number,
near: number,
far: number
) {
if (near === far || aspect === 0) {
throw new Error(
`No sence can be set, because near === far or aspect === 0.`
);
}
if (near <= 0 || far <= 0) {
throw new Error(`near and far must be positive.`);
}
fov = (Math.PI * fov) / 180 / 2;
const s = Math.sin(fov);
if (s === 0) {
throw new Error(
`Cannot set perspectivity, because sin(fov) === 0.`
);
}
const rd = 1 / (far - near);
const ct = Math.cos(fov) / s;
const matrix = new Matrix4();
matrix[0] = [ct / aspect, 0, 0, 0];
matrix[1] = [0, ct, 0, 0];
matrix[2] = [0, 0, -(far + near) * rd, -2 * near * far * rd];
matrix[3] = [0, 0, -1, 0];
return matrix;
}
/**
*
* @param left
* @param right
* @param bottom
* @param top
* @param near
* @param far
*/
private calOrthogonal(
left: number,
right: number,
bottom: number,
top: number,
near: number,
far: number
) {
if (left === right || bottom === top || near === far) {
throw new Error(
`Cannot set Orthogonality, because left === right or top === bottom or near === far.`
);
}
const rw = 1 / (right - left);
const rh = 1 / (top - bottom);
const rd = 1 / (far - near);
const matrix = new Matrix4();
matrix[0] = [2 * rw, 0, 0, -(right + left) * rw];
matrix[1] = [0, 2 * rh, 0, -(top + bottom) * rh];
matrix[2] = [0, 0, -2 * rd, -(far + near) * rd];
matrix[3] = [0, 0, 0, 1];
return matrix;
}
}

View File

@ -3,30 +3,47 @@ import { has } from '../utils';
import { Camera } from './camera';
import { Renderer } from './render';
export type ParticleColor = [number, number, number, number];
interface ParticleThreshold {
radius: number;
color: number;
position: number;
posX: number;
posY: number;
posZ: number;
}
interface ParticleOne {
export interface ParticleOne {
x: number;
y: number;
z: number;
r: number;
color: ParticleColor;
}
interface Loc3D extends Loc {
z: number;
}
interface ParticleInfo {
pos: Loc3D;
density: number;
color: ParticleColor;
radius: number;
threshold: ParticleThreshold;
}
export class Particle {
/** 绑定的摄像机 */
camera?: Camera;
/** 粒子中心位置 */
pos: Loc3D;
/** 粒子密度 */
density: number;
pos: Loc3D = { x: 0, y: 0, z: 0 };
/** 粒子密度,即粒子总数 */
density: number = 50;
/** 粒子颜色 */
color: ParticleColor = [0, 0, 0, 0];
/** 每个粒子的半径 */
radius: number = 2;
/** 渲染器 */
renderer?: Renderer;
@ -37,17 +54,22 @@ export class Particle {
private needUpdate: boolean = false;
private ticker: Ticker = new Ticker();
/** 设置信息前的信息 */
private originInfo: DeepPartial<ParticleInfo> = {};
/** 各个属性的阈值 */
threshold: ParticleThreshold = {
radius: 2,
color: 16,
position: 50
color: 0.1,
posX: 0.1,
posY: 0.1,
posZ: 0.1
};
constructor(density: number, x: number, y: number, z: number) {
this.pos = { x, y, z };
this.density = density;
this.ticker.add(this.updateParticleData);
constructor() {
this.ticker.add(() => {
this.updateParticleData.call(this);
});
}
/**
@ -55,9 +77,53 @@ export class Particle {
* @param x
* @param y
*/
setPos(x?: number, y?: number): Particle {
has(x) && (this.pos.x = x);
has(y) && (this.pos.y = y);
setPos(x?: number, y?: number, z?: number): Particle {
this.originInfo.pos ??= {};
if (has(x)) {
this.pos.x = x;
this.originInfo.pos.x = x;
}
if (has(y)) {
this.pos.y = y;
this.originInfo.pos.y = y;
}
if (has(z)) {
this.pos.z = z;
this.originInfo.pos.z = z;
}
this.needUpdate = true;
return this;
}
/**
*
* @param density
*/
setDensity(density: number): Particle {
this.density = density;
this.originInfo.density = density;
this.needUpdate = true;
return this;
}
/**
*
* @param color
*/
setColor(color: ParticleColor) {
this.color = color;
this.originInfo.color = color;
this.needUpdate = true;
return this;
}
/**
*
* @param radius
*/
setRadius(radius: number) {
this.radius = radius;
this.originInfo.radius = radius;
this.needUpdate = true;
return this;
}
@ -67,19 +133,18 @@ export class Particle {
* @param data
*/
setThreshold(data: Partial<ParticleThreshold>): Particle {
const { radius, color, position } = data;
has(radius) && (this.threshold.radius = radius);
has(color) && (this.threshold.radius = color);
has(position) && (this.threshold.radius = position);
this.originInfo.threshold ??= {};
for (const [key, value] of Object.entries(data) as [
keyof ParticleThreshold,
any
][]) {
this.threshold[key] = value;
this.originInfo.threshold[key] = value;
}
this.needUpdate = true;
return this;
}
/**
*
*/
generate() {}
/**
*
* @param renderer
@ -102,11 +167,137 @@ export class Particle {
this.needUpdate = true;
}
/**
*
*/
generate() {
const particles = this.generateNewParticles(this.density);
this.list = particles;
}
/**
*
*/
private updateParticleData() {
if (!this.needUpdate) return;
if (!this.needUpdate || this.list.length === 0) return;
this.needUpdate = false;
// check number
if (this.list.length > this.density) {
this.list.splice(this.density);
} else if (this.list.length < this.density) {
this.list.push(
...this.generateNewParticles(this.density - this.list.length)
);
}
// check radius
if (has(this.originInfo.radius)) {
if (this.radius !== this.originInfo.radius) {
const delta = this.radius - this.originInfo.radius;
this.list.forEach(v => {
v.r += delta;
});
}
}
// check color
if (has(this.originInfo.color)) {
if (!core.same(this.color, this.originInfo.color)) {
const r = this.color[0] - this.originInfo.color[0]!;
const g = this.color[1] - this.originInfo.color[1]!;
const b = this.color[2] - this.originInfo.color[2]!;
const a = this.color[3] - this.originInfo.color[3]!;
this.list.forEach(v => {
v.color[0] += r;
v.color[1] += g;
v.color[2] += b;
v.color[3] += a;
});
}
}
// check position
if (has(this.originInfo.pos)) {
if (!core.same(this.pos, this.originInfo.pos)) {
const x = this.pos.x - this.originInfo.pos.x!;
const y = this.pos.y - this.originInfo.pos.y!;
const z = this.pos.z - this.originInfo.pos.z!;
this.list.forEach(v => {
v.x += x;
v.y += y;
v.z += z;
});
}
}
// check threshold
if (has(this.originInfo.threshold)) {
for (const [key, v] of Object.entries(this.threshold) as [
keyof ParticleThreshold,
any
][]) {
const now = v;
const origin = this.originInfo.threshold[key];
if (origin === now || !has(origin)) {
continue;
}
const ratio = now / origin;
if (key === 'posX') {
this.list.forEach(v => {
v.x = (v.x - this.pos.x) * ratio + this.pos.x;
});
} else if (key === 'posY') {
this.list.forEach(v => {
v.y = (v.y - this.pos.y) * ratio + this.pos.y;
});
} else if (key === 'posZ') {
this.list.forEach(v => {
v.z = (v.z - this.pos.z) * ratio + this.pos.z;
});
} else if (key === 'radius') {
this.list.forEach(v => {
v.r = (v.r - this.radius) * ratio + this.radius;
});
} else {
this.list.forEach(v => {
v.color = v.color.map((v, i) => {
return (v - this.color[i]) * ratio + this.color[i];
}) as ParticleColor;
});
}
}
}
this.render();
}
/**
*
* @param num
*/
private generateNewParticles(num: number): ParticleOne[] {
const res: ParticleOne[] = new Array(num);
const { posX, posY, posZ, radius, color } = this.threshold;
for (let i = 0; i < num; i++) {
const p: ParticleOne = {
x: this.pos.x + (Math.random() - 0.5) * 2 * posX,
y: this.pos.y + (Math.random() - 0.5) * 2 * posY,
z: this.pos.z + (Math.random() - 0.5) * 2 * posZ,
r: this.radius + (Math.random() - 0.5) * 2 * radius,
color: [0, 0, 0, 0].map(
(v, i) => this.color[i] + (Math.random() - 0.5) * 2 * color
) as ParticleColor
};
res[i] = p;
}
return res;
}
/**
*
*/
private render() {
this.renderer?.render(this);
}
}

View File

@ -1,6 +1,50 @@
import { circle, sleep, Ticker } from 'mutate-animate';
import { has } from '../utils';
import { createProgram } from '../webgl/canvas';
import { Matrix4 } from '../webgl/matrix';
import { isWebGLSupported } from '../webgl/utils';
import { Camera } from './camera';
import { Particle } from './particle';
import { Camera, Position3D } from './camera';
import { Particle, ParticleColor, ParticleOne } from './particle';
// 顶点着色器与片元着色器
// 很像C对吧但这不是C是glsl
const vshader = `
attribute vec4 position;
attribute vec4 color;
attribute vec2 radius;
uniform mat4 camera;
uniform mat4 projection;
varying vec4 vColor;
varying vec4 vPosition;
varying float vRadius;
void main() {
vec4 p = projection * camera * position;
gl_Position = p;
vColor = color;
vPosition = p;
vRadius = radius.x;
gl_PointSize = vRadius;
}
`;
const fshader = `
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 vColor;
varying vec4 vPosition;
varying float vRadius;
void main() {
vec2 position = gl_PointCoord.xy;
if (distance(position, vec2(0.5)) > 0.5) {
discard;
} else {
gl_FragColor = vColor;
}
}
`;
export class Renderer {
/** 粒子列表 */
@ -13,12 +57,43 @@ export class Renderer {
camera?: Camera;
/** 缩放比例 */
ratio: number = devicePixelRatio;
/** gl的程序对象 */
program: WebGLProgram;
constructor() {
/** 画布缓冲区 */
private buffer: WebGLBuffer;
/** 各个attribute的内存地址 */
private attribLocation: Record<string, number> = {};
/** 各个uniform的内存地址 */
private uniformLocation: Record<string, WebGLUniformLocation> = {};
private static readonly attributes: string[] = [
'position',
'color',
'radius'
];
private static readonly uniforms: string[] = ['camera', 'projection'];
constructor(width?: number, height?: number) {
if (!isWebGLSupported) {
throw new Error(`Your service or browser does not support webgl!`);
}
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
if (has(width)) {
this.canvas.width = width * devicePixelRatio;
}
if (has(height)) {
this.canvas.height = height * devicePixelRatio;
}
this.gl = this.canvas.getContext('webgl')!;
this.program = createProgram(this.gl, vshader, fshader);
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.buffer = this.bindBuffer();
this.getGLVariblesLocation();
this.gl.enable(this.gl.BLEND);
this.gl.enable(this.gl.DEPTH_TEST);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
}
/**
@ -80,4 +155,187 @@ export class Renderer {
if (index === -1) return;
this.particleList.splice(index, 1);
}
/**
*
* @param color
*/
setBackground(color: ParticleColor) {
this.gl.clearColor(...color);
}
/**
*
*/
render(particle?: Particle | number) {
const { position, color } = this.attribLocation;
const { camera } = this.uniformLocation;
if (!has(position) || !has(color)) {
throw new Error(`Unexpected unset of attribute location`);
}
if (!has(camera)) {
throw new Error(`Unexpected unset of uniform location`);
}
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
if (!has(particle)) this.particleList.forEach(v => this.renderOne(v));
else {
const p =
typeof particle === 'number'
? this.particleList[particle]
: particle;
this.renderOne(p);
}
}
/**
*
* @returns
*/
private bindBuffer() {
const buffer = this.gl.createBuffer();
if (!buffer) throw this.notSupport();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
return buffer;
}
/**
*
* @param array
*/
private updateOneParticleBufferData(array: ParticleOne[]) {
const particleArray = new Float32Array(
array
.map(v => {
const [r, g, b, a] = v.color;
return [v.x, v.y, v.z, r, g, b, a, v.r, 0];
})
.flat()
);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
particleArray,
this.gl.DYNAMIC_DRAW
);
return particleArray;
}
/**
* gl变量的内存地址
*/
private getGLVariblesLocation() {
Renderer.attributes.forEach(v => {
this.attribLocation[v] = this.gl.getAttribLocation(this.program, v);
});
Renderer.uniforms.forEach(v => {
const loc = this.gl.getUniformLocation(this.program, v);
if (!loc) {
throw new Error(`Cannot get the location of uniform '${v}'`);
}
this.uniformLocation[v] = loc;
});
}
/**
*
* @param particle
*/
private renderOne(particle: Particle) {
const arr = this.updateOneParticleBufferData(particle.list);
const size = arr.BYTES_PER_ELEMENT;
const { position, color, radius } = this.attribLocation;
const { camera, projection } = this.uniformLocation;
// 给gl变量赋值
this.gl.vertexAttribPointer(
position,
3,
this.gl.FLOAT,
false,
size * 9,
0
);
this.gl.vertexAttribPointer(
color,
4,
this.gl.FLOAT,
false,
size * 9,
size * 3
);
this.gl.vertexAttribPointer(
radius,
2,
this.gl.FLOAT,
false,
size * 9,
size * 7
);
this.gl.enableVertexAttribArray(position);
this.gl.enableVertexAttribArray(color);
this.gl.enableVertexAttribArray(radius);
const matrix = new Matrix4();
const c =
this.camera?.view.toWebGLFloat32Array() ??
matrix.toWebGLFloat32Array();
const p =
this.camera?.projection.toWebGLFloat32Array() ??
matrix.toWebGLFloat32Array();
this.gl.uniformMatrix4fv(camera, false, c);
this.gl.uniformMatrix4fv(projection, false, p);
// 绘制
this.gl.drawArrays(this.gl.POINTS, 0, particle.list.length);
}
private notSupport() {
throw new Error(`Your service or browser does not support webgl!`);
}
}
window.addEventListener('load', async () => {
const renderer = new Renderer(
480 * core.domStyle.scale,
480 * core.domStyle.scale
);
const particle = new Particle();
const camera = new Camera();
renderer.bindCamera(camera);
particle.appendTo(renderer);
renderer.append(core.dom.gameDraw);
camera.lookAt([1, 1, 5], [0, 0, 0], [0, 1, 0]);
camera.setPerspective(20, 1, 1, 100);
console.log(camera.view, camera.projection);
particle.setColor([0.3, 0.6, 0.7, 0.7]);
particle.setRadius(3);
particle.setDensity(1000);
particle.setThreshold({
posX: 0.2,
posY: 0.2,
posZ: 10,
radius: 0,
color: 0
});
particle.generate();
renderer.canvas.style.position = 'absolute';
renderer.canvas.style.zIndex = '160';
renderer.render();
await sleep(5000);
const now: Position3D = [1, 1, 5];
const path = circle(1, 1000, [0, 0]);
let f = 0;
new Ticker().add(() => {
camera.lookAt(now, [0, 0, 0], [0, 1, 0]);
const [x, y] = path(f / 1000 / 2000);
f++;
now[0] = x;
now[1] = y;
renderer.render();
});
});

View File

@ -87,6 +87,8 @@ export function createProgram(
throw new Error(`Program link fail: ${err}`);
}
gl.useProgram(program);
return program;
}
@ -113,7 +115,7 @@ export function loadShader(
gl.compileShader(shader);
// 检查是否编译成功
const compiled = gl.getShaderParameter(gl, gl.COMPILE_STATUS);
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
const err = gl.getShaderInfoLog(shader);
throw new Error(`Shader compile fail: ${err}`);

View File

@ -1,3 +1,4 @@
import { cloneDeep } from 'lodash';
import { has } from '../utils';
export class Matrix extends Array<number[]> {
@ -40,26 +41,31 @@ export class Matrix extends Array<number[]> {
);
}
const n = this.length;
const arr = this.map(v => v.slice());
const arr = Array.from(this).map(v => v.slice());
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
this[i][j] = 0;
for (let k = 0; k < n; k++) {
this[i][j] = arr[i][k] * matrix[k][j];
this[i][j] += arr[i][k] * matrix[k][j];
}
}
}
return this;
}
}
export class Matrix4 extends Matrix {
constructor(...n: number[][]) {
n ??= [
if (n.length === 0) {
n = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
}
if (n.length !== 4) {
throw new TypeError(`The length of delivered array must be 4.`);
}
@ -142,7 +148,7 @@ export class Matrix4 extends Matrix {
*/
transpose(target: 'this' | 'new' = 'new'): Matrix4 {
const t = target === 'this' ? this : new Matrix4();
const arr = this.map(v => v.slice());
const arr = Array.from(this).map(v => v.slice());
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
t[i][j] = arr[j][i];
@ -155,6 +161,6 @@ export class Matrix4 extends Matrix {
* Float32Arraywebgl
*/
toWebGLFloat32Array(): Float32Array {
return new Float32Array(this.transpose().flat());
return new Float32Array(Array.from(this.transpose()).flat());
}
}

8
src/types/util.d.ts vendored
View File

@ -822,12 +822,12 @@ type DeepReadonly<T> = {
};
/**
* 使
* 使
*/
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends number | string | boolean
? T[P]
: DeepReadonly<T[P]>;
: DeepPartial<T[P]>;
};
/**
@ -836,7 +836,7 @@ type DeepPartial<T> = {
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends number | string | boolean
? T[P]
: DeepReadonly<T[P]>;
: DeepRequired<T[P]>;
};
/**
@ -852,7 +852,7 @@ type Writable<T> = {
type DeepWritable<T> = {
-readonly [P in keyof T]: T[P] extends number | string | boolean
? T[P]
: DeepReadonly<T[P]>;
: DeepWritable<T[P]>;
};
/**