HumanBreak/src/plugin/particle/particle.ts

335 lines
9.2 KiB
TypeScript
Raw Normal View History

2023-02-10 16:39:53 +08:00
import { Ticker } from 'mutate-animate';
import { has } from '../utils';
import { Camera } from './camera';
import { Renderer } from './render';
2023-02-15 13:19:05 +08:00
export type ParticleColor = [number, number, number, number];
2023-02-10 16:39:53 +08:00
interface ParticleThreshold {
radius: number;
color: number;
2023-02-15 13:19:05 +08:00
posX: number;
posY: number;
posZ: number;
2023-02-10 16:39:53 +08:00
}
2023-02-15 13:19:05 +08:00
export interface ParticleOne {
2023-02-10 16:39:53 +08:00
x: number;
y: number;
z: number;
r: number;
2023-02-15 13:19:05 +08:00
color: ParticleColor;
2023-02-10 16:39:53 +08:00
}
interface Loc3D extends Loc {
z: number;
}
2023-02-15 13:19:05 +08:00
interface ParticleInfo {
pos: Loc3D;
density: number;
color: ParticleColor;
radius: number;
threshold: ParticleThreshold;
}
2023-02-10 16:39:53 +08:00
export class Particle {
/** 绑定的摄像机 */
camera?: Camera;
/** 粒子中心位置 */
2023-02-15 13:19:05 +08:00
pos: Loc3D = { x: 0, y: 0, z: 0 };
/** 粒子密度,即粒子总数 */
density: number = 50;
/** 粒子颜色 */
color: ParticleColor = [0, 0, 0, 0];
/** 每个粒子的半径 */
radius: number = 2;
2023-02-10 16:39:53 +08:00
/** 渲染器 */
renderer?: Renderer;
/** 需要渲染的粒子列表 */
list: ParticleOne[] = [];
2023-02-15 21:15:33 +08:00
/** 是否需要更新缓冲区数据 */
needUpdateBuffer: boolean = false;
/** 当前缓存信息 */
cache?: Float32Array;
2023-02-10 16:39:53 +08:00
/** 是否需要更新 */
private needUpdate: boolean = false;
private ticker: Ticker = new Ticker();
2023-02-15 13:19:05 +08:00
/** 设置信息前的信息 */
private originInfo: DeepPartial<ParticleInfo> = {};
2023-02-10 16:39:53 +08:00
/** 各个属性的阈值 */
threshold: ParticleThreshold = {
radius: 2,
2023-02-15 13:19:05 +08:00
color: 0.1,
posX: 0.1,
posY: 0.1,
posZ: 0.1
2023-02-10 16:39:53 +08:00
};
2023-02-15 13:19:05 +08:00
constructor() {
this.ticker.add(() => {
this.updateParticleData.call(this);
});
2023-02-10 16:39:53 +08:00
}
/**
*
* @param x
* @param y
*/
2023-02-15 13:19:05 +08:00
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;
}
2023-02-10 16:39:53 +08:00
this.needUpdate = true;
return this;
}
/**
2023-02-15 13:19:05 +08:00
*
* @param density
2023-02-10 16:39:53 +08:00
*/
2023-02-15 13:19:05 +08:00
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;
2023-02-10 16:39:53 +08:00
this.needUpdate = true;
return this;
}
/**
2023-02-15 13:19:05 +08:00
*
* @param data
2023-02-10 16:39:53 +08:00
*/
2023-02-15 13:19:05 +08:00
setThreshold(data: Partial<ParticleThreshold>): Particle {
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;
}
2023-02-10 16:39:53 +08:00
/**
*
* @param renderer
*/
appendTo(renderer: Renderer) {
renderer.addParticle(this);
}
/**
*
*/
remove() {
this.renderer?.removeParticle(this);
}
/**
*
*/
update() {
this.needUpdate = true;
}
2023-02-15 13:19:05 +08:00
/**
*
*/
generate() {
const particles = this.generateNewParticles(this.density);
this.list = particles;
}
2023-02-15 21:15:33 +08:00
/**
* Float32Array信息
*/
getArrayInfo() {
if (!this.cache || this.needUpdateBuffer) {
const array = this.list;
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.cache = particleArray;
return particleArray;
} else {
return this.cache;
}
}
2023-02-10 16:39:53 +08:00
/**
*
*/
private updateParticleData() {
2023-02-15 13:19:05 +08:00
if (!this.needUpdate || this.list.length === 0) return;
2023-02-10 16:39:53 +08:00
this.needUpdate = false;
2023-02-15 13:19:05 +08:00
// check number
if (this.list.length > this.density) {
this.list.splice(this.density);
2023-02-15 21:15:33 +08:00
this.needUpdateBuffer = true;
2023-02-15 13:19:05 +08:00
} else if (this.list.length < this.density) {
this.list.push(
...this.generateNewParticles(this.density - this.list.length)
);
2023-02-15 21:15:33 +08:00
this.needUpdateBuffer = true;
2023-02-15 13:19:05 +08:00
}
// 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;
});
2023-02-15 21:15:33 +08:00
this.needUpdateBuffer = true;
2023-02-15 13:19:05 +08:00
}
}
// 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;
});
2023-02-15 21:15:33 +08:00
this.needUpdateBuffer = true;
2023-02-15 13:19:05 +08:00
}
}
// 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;
});
2023-02-15 21:15:33 +08:00
this.needUpdateBuffer = true;
2023-02-15 13:19:05 +08:00
}
}
// 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;
});
}
2023-02-15 21:15:33 +08:00
this.needUpdateBuffer = true;
2023-02-15 13:19:05 +08:00
}
}
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() {
2023-02-15 21:15:33 +08:00
this.renderer?.render();
2023-02-10 16:39:53 +08:00
}
}