mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-18 20:09:27 +08:00
粒子特效
This commit is contained in:
parent
29b4dd13e2
commit
6a667d4ff7
@ -54,7 +54,7 @@
|
||||
"<br>",
|
||||
"游戏作者:古祠",
|
||||
"<br>",
|
||||
"本塔遵循MIT开源协议,你可随意使用本塔的任何代码,不需要作者授权,也可以随意用于商业用途。",
|
||||
"本塔遵循MIT开源协议。<a href=\"LICENSE\" target=\"_blank\">查看开源协议</a>",
|
||||
"<br>",
|
||||
"BGM来源:网易云音乐等",
|
||||
"<br>",
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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}`);
|
||||
|
@ -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 ??= [
|
||||
[1, 0, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 1]
|
||||
];
|
||||
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 {
|
||||
* 转换成列主序的Float32Array,用于webgl
|
||||
*/
|
||||
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> = {
|
||||
[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]>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user