mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-19 04:19:30 +08:00
粒子特效
This commit is contained in:
parent
29b4dd13e2
commit
6a667d4ff7
@ -54,7 +54,7 @@
|
|||||||
"<br>",
|
"<br>",
|
||||||
"游戏作者:古祠",
|
"游戏作者:古祠",
|
||||||
"<br>",
|
"<br>",
|
||||||
"本塔遵循MIT开源协议,你可随意使用本塔的任何代码,不需要作者授权,也可以随意用于商业用途。",
|
"本塔遵循MIT开源协议。<a href=\"LICENSE\" target=\"_blank\">查看开源协议</a>",
|
||||||
"<br>",
|
"<br>",
|
||||||
"BGM来源:网易云音乐等",
|
"BGM来源:网易云音乐等",
|
||||||
"<br>",
|
"<br>",
|
||||||
|
@ -3,6 +3,7 @@ import App from './App.vue';
|
|||||||
import App2 from './App2.vue';
|
import App2 from './App2.vue';
|
||||||
import './styles.less';
|
import './styles.less';
|
||||||
import 'ant-design-vue/dist/antd.dark.css';
|
import 'ant-design-vue/dist/antd.dark.css';
|
||||||
|
import './plugin/particle/render';
|
||||||
|
|
||||||
createApp(App).mount('#root');
|
createApp(App).mount('#root');
|
||||||
createApp(App2).mount('#root2');
|
createApp(App2).mount('#root2');
|
||||||
|
@ -1,11 +1,31 @@
|
|||||||
import { Matrix4 } from '../webgl/martrix';
|
import { TimingFn } from 'mutate-animate';
|
||||||
|
import { Matrix4 } from '../webgl/matrix';
|
||||||
import { Renderer } from './render';
|
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 {
|
export class Camera {
|
||||||
/** 视图矩阵 */
|
/** 视图矩阵 */
|
||||||
matrix!: Matrix4;
|
view!: Matrix4;
|
||||||
|
/** 投影矩阵 */
|
||||||
|
projection!: Matrix4;
|
||||||
/** 绑定的渲染器 */
|
/** 绑定的渲染器 */
|
||||||
renderer?: Renderer;
|
renderer?: Renderer;
|
||||||
|
|
||||||
@ -13,8 +33,12 @@ export class Camera {
|
|||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化视角矩阵
|
||||||
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
this.matrix = new Matrix4();
|
this.view = new Matrix4();
|
||||||
|
this.projection = new Matrix4();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +63,7 @@ export class Camera {
|
|||||||
* @param up 上方向
|
* @param up 上方向
|
||||||
*/
|
*/
|
||||||
lookAt(eye: Position3D, at: Position3D, up: Position3D) {
|
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 上方向
|
* @param up 上方向
|
||||||
*/
|
*/
|
||||||
transform(eye: Position3D, at: Position3D, up: Position3D) {
|
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
|
* @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[0] = [sx, sy, sz, 0];
|
||||||
matrix[1] = [ux, uy, uz, 0];
|
matrix[1] = [ux, uy, uz, 0];
|
||||||
matrix[2] = [-fx, -fy, -fz, 0];
|
matrix[2] = [-fx, -fy, -fz, 0];
|
||||||
matrix[4] = [0, 0, 0, 1];
|
matrix[3] = [0, 0, 0, 1];
|
||||||
|
|
||||||
matrix.translate(-eyeX, -eyeY, -eyeZ);
|
matrix.translate(-eyeX, -eyeY, -eyeZ);
|
||||||
return matrix;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,30 +3,47 @@ import { has } from '../utils';
|
|||||||
import { Camera } from './camera';
|
import { Camera } from './camera';
|
||||||
import { Renderer } from './render';
|
import { Renderer } from './render';
|
||||||
|
|
||||||
|
export type ParticleColor = [number, number, number, number];
|
||||||
|
|
||||||
interface ParticleThreshold {
|
interface ParticleThreshold {
|
||||||
radius: number;
|
radius: number;
|
||||||
color: number;
|
color: number;
|
||||||
position: number;
|
posX: number;
|
||||||
|
posY: number;
|
||||||
|
posZ: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParticleOne {
|
export interface ParticleOne {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
r: number;
|
r: number;
|
||||||
|
color: ParticleColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Loc3D extends Loc {
|
interface Loc3D extends Loc {
|
||||||
z: number;
|
z: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ParticleInfo {
|
||||||
|
pos: Loc3D;
|
||||||
|
density: number;
|
||||||
|
color: ParticleColor;
|
||||||
|
radius: number;
|
||||||
|
threshold: ParticleThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
export class Particle {
|
export class Particle {
|
||||||
/** 绑定的摄像机 */
|
/** 绑定的摄像机 */
|
||||||
camera?: Camera;
|
camera?: Camera;
|
||||||
/** 粒子中心位置 */
|
/** 粒子中心位置 */
|
||||||
pos: Loc3D;
|
pos: Loc3D = { x: 0, y: 0, z: 0 };
|
||||||
/** 粒子密度 */
|
/** 粒子密度,即粒子总数 */
|
||||||
density: number;
|
density: number = 50;
|
||||||
|
/** 粒子颜色 */
|
||||||
|
color: ParticleColor = [0, 0, 0, 0];
|
||||||
|
/** 每个粒子的半径 */
|
||||||
|
radius: number = 2;
|
||||||
/** 渲染器 */
|
/** 渲染器 */
|
||||||
renderer?: Renderer;
|
renderer?: Renderer;
|
||||||
|
|
||||||
@ -37,17 +54,22 @@ export class Particle {
|
|||||||
private needUpdate: boolean = false;
|
private needUpdate: boolean = false;
|
||||||
private ticker: Ticker = new Ticker();
|
private ticker: Ticker = new Ticker();
|
||||||
|
|
||||||
|
/** 设置信息前的信息 */
|
||||||
|
private originInfo: DeepPartial<ParticleInfo> = {};
|
||||||
|
|
||||||
/** 各个属性的阈值 */
|
/** 各个属性的阈值 */
|
||||||
threshold: ParticleThreshold = {
|
threshold: ParticleThreshold = {
|
||||||
radius: 2,
|
radius: 2,
|
||||||
color: 16,
|
color: 0.1,
|
||||||
position: 50
|
posX: 0.1,
|
||||||
|
posY: 0.1,
|
||||||
|
posZ: 0.1
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(density: number, x: number, y: number, z: number) {
|
constructor() {
|
||||||
this.pos = { x, y, z };
|
this.ticker.add(() => {
|
||||||
this.density = density;
|
this.updateParticleData.call(this);
|
||||||
this.ticker.add(this.updateParticleData);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,9 +77,53 @@ export class Particle {
|
|||||||
* @param x 横坐标
|
* @param x 横坐标
|
||||||
* @param y 纵坐标
|
* @param y 纵坐标
|
||||||
*/
|
*/
|
||||||
setPos(x?: number, y?: number): Particle {
|
setPos(x?: number, y?: number, z?: number): Particle {
|
||||||
has(x) && (this.pos.x = x);
|
this.originInfo.pos ??= {};
|
||||||
has(y) && (this.pos.y = y);
|
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;
|
this.needUpdate = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -67,19 +133,18 @@ export class Particle {
|
|||||||
* @param data 阈值信息
|
* @param data 阈值信息
|
||||||
*/
|
*/
|
||||||
setThreshold(data: Partial<ParticleThreshold>): Particle {
|
setThreshold(data: Partial<ParticleThreshold>): Particle {
|
||||||
const { radius, color, position } = data;
|
this.originInfo.threshold ??= {};
|
||||||
has(radius) && (this.threshold.radius = radius);
|
for (const [key, value] of Object.entries(data) as [
|
||||||
has(color) && (this.threshold.radius = color);
|
keyof ParticleThreshold,
|
||||||
has(position) && (this.threshold.radius = position);
|
any
|
||||||
|
][]) {
|
||||||
|
this.threshold[key] = value;
|
||||||
|
this.originInfo.threshold[key] = value;
|
||||||
|
}
|
||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成粒子
|
|
||||||
*/
|
|
||||||
generate() {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加到一个渲染器上
|
* 添加到一个渲染器上
|
||||||
* @param renderer 渲染器
|
* @param renderer 渲染器
|
||||||
@ -102,11 +167,137 @@ export class Particle {
|
|||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成粒子,注意该函数会删除当前的所有粒子,然后再重新生成
|
||||||
|
*/
|
||||||
|
generate() {
|
||||||
|
const particles = this.generateNewParticles(this.density);
|
||||||
|
this.list = particles;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每帧执行的粒子更新器
|
* 每帧执行的粒子更新器
|
||||||
*/
|
*/
|
||||||
private updateParticleData() {
|
private updateParticleData() {
|
||||||
if (!this.needUpdate) return;
|
if (!this.needUpdate || this.list.length === 0) return;
|
||||||
this.needUpdate = false;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { isWebGLSupported } from '../webgl/utils';
|
||||||
import { Camera } from './camera';
|
import { Camera, Position3D } from './camera';
|
||||||
import { Particle } from './particle';
|
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 {
|
export class Renderer {
|
||||||
/** 粒子列表 */
|
/** 粒子列表 */
|
||||||
@ -13,12 +57,43 @@ export class Renderer {
|
|||||||
camera?: Camera;
|
camera?: Camera;
|
||||||
/** 缩放比例 */
|
/** 缩放比例 */
|
||||||
ratio: number = devicePixelRatio;
|
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) {
|
if (!isWebGLSupported) {
|
||||||
throw new Error(`Your service or browser does not support webgl!`);
|
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.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;
|
if (index === -1) return;
|
||||||
this.particleList.splice(index, 1);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -87,6 +87,8 @@ export function createProgram(
|
|||||||
throw new Error(`Program link fail: ${err}`);
|
throw new Error(`Program link fail: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gl.useProgram(program);
|
||||||
|
|
||||||
return program;
|
return program;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +115,7 @@ export function loadShader(
|
|||||||
gl.compileShader(shader);
|
gl.compileShader(shader);
|
||||||
|
|
||||||
// 检查是否编译成功
|
// 检查是否编译成功
|
||||||
const compiled = gl.getShaderParameter(gl, gl.COMPILE_STATUS);
|
const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
|
||||||
if (!compiled) {
|
if (!compiled) {
|
||||||
const err = gl.getShaderInfoLog(shader);
|
const err = gl.getShaderInfoLog(shader);
|
||||||
throw new Error(`Shader compile fail: ${err}`);
|
throw new Error(`Shader compile fail: ${err}`);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import { has } from '../utils';
|
import { has } from '../utils';
|
||||||
|
|
||||||
export class Matrix extends Array<number[]> {
|
export class Matrix extends Array<number[]> {
|
||||||
@ -40,26 +41,31 @@ export class Matrix extends Array<number[]> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const n = this.length;
|
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 i = 0; i < n; i++) {
|
||||||
for (let j = 0; j < n; j++) {
|
for (let j = 0; j < n; j++) {
|
||||||
|
this[i][j] = 0;
|
||||||
for (let k = 0; k < n; k++) {
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Matrix4 extends Matrix {
|
export class Matrix4 extends Matrix {
|
||||||
constructor(...n: number[][]) {
|
constructor(...n: number[][]) {
|
||||||
n ??= [
|
if (n.length === 0) {
|
||||||
[1, 0, 0, 0],
|
n = [
|
||||||
[0, 1, 0, 0],
|
[1, 0, 0, 0],
|
||||||
[0, 0, 1, 0],
|
[0, 1, 0, 0],
|
||||||
[0, 0, 0, 1]
|
[0, 0, 1, 0],
|
||||||
];
|
[0, 0, 0, 1]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (n.length !== 4) {
|
if (n.length !== 4) {
|
||||||
throw new TypeError(`The length of delivered array must be 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 {
|
transpose(target: 'this' | 'new' = 'new'): Matrix4 {
|
||||||
const t = target === 'this' ? this : 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 i = 0; i < 4; i++) {
|
||||||
for (let j = 0; j < 4; j++) {
|
for (let j = 0; j < 4; j++) {
|
||||||
t[i][j] = arr[j][i];
|
t[i][j] = arr[j][i];
|
||||||
@ -155,6 +161,6 @@ export class Matrix4 extends Matrix {
|
|||||||
* 转换成列主序的Float32Array,用于webgl
|
* 转换成列主序的Float32Array,用于webgl
|
||||||
*/
|
*/
|
||||||
toWebGLFloat32Array(): Float32Array {
|
toWebGLFloat32Array(): Float32Array {
|
||||||
return new Float32Array(this.transpose().flat());
|
return new Float32Array(Array.from(this.transpose()).flat());
|
||||||
}
|
}
|
||||||
}
|
}
|
8
src/types/util.d.ts
vendored
8
src/types/util.d.ts
vendored
@ -822,12 +822,12 @@ type DeepReadonly<T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 深度可选一个对象,使其所有属性都
|
* 深度可选一个对象,使其所有属性都可选
|
||||||
*/
|
*/
|
||||||
type DeepPartial<T> = {
|
type DeepPartial<T> = {
|
||||||
[P in keyof T]?: T[P] extends number | string | boolean
|
[P in keyof T]?: T[P] extends number | string | boolean
|
||||||
? T[P]
|
? T[P]
|
||||||
: DeepReadonly<T[P]>;
|
: DeepPartial<T[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -836,7 +836,7 @@ type DeepPartial<T> = {
|
|||||||
type DeepRequired<T> = {
|
type DeepRequired<T> = {
|
||||||
[P in keyof T]-?: T[P] extends number | string | boolean
|
[P in keyof T]-?: T[P] extends number | string | boolean
|
||||||
? T[P]
|
? T[P]
|
||||||
: DeepReadonly<T[P]>;
|
: DeepRequired<T[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -852,7 +852,7 @@ type Writable<T> = {
|
|||||||
type DeepWritable<T> = {
|
type DeepWritable<T> = {
|
||||||
-readonly [P in keyof T]: T[P] extends number | string | boolean
|
-readonly [P in keyof T]: T[P] extends number | string | boolean
|
||||||
? T[P]
|
? T[P]
|
||||||
: DeepReadonly<T[P]>;
|
: DeepWritable<T[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user