wip(type): add type to particles

This commit is contained in:
pissang 2022-05-07 21:45:53 +08:00
parent 9b93debbf5
commit 42a6df0852
7 changed files with 411 additions and 482 deletions

View File

@ -6,7 +6,7 @@ import Vector4 from './Vector4';
* Random or constant 1d, 2d, 3d vector generator
*/
interface Value<T> {
export interface Value<T> {
get(out?: T): T;
}

View File

@ -1,68 +1,64 @@
// @ts-nocheck
import Base from '../core/Base';
import Vector3 from '../math/Vector3';
import Particle from './Particle';
import Value from '../math/Value';
import * as Value from '../math/Value';
/**
* @constructor clay.particle.Emitter
* @extends clay.core.Base
*/
const Emitter = Base.extend(
/** @lends clay.particle.Emitter# */ {
/**
* Maximum number of particles created by this emitter
* @type {number}
*/
max: 1000,
/**
* Number of particles created by this emitter each shot
* @type {number}
*/
amount: 20,
interface ParticleEmitterOpts {
/**
* Maximum number of particles created by this emitter
*/
max: number;
/**
* Number of particles created by this emitter each shot
*/
amount: number;
// Init status for each particle
/**
* Particle life generator
* @type {?clay.Value.<number>}
*/
life: null,
/**
* Particle position generator
* @type {?clay.Value.<clay.Vector3>}
*/
position: null,
/**
* Particle rotation generator
* @type {?clay.Value.<clay.Vector3>}
*/
rotation: null,
/**
* Particle velocity generator
* @type {?clay.Value.<clay.Vector3>}
*/
velocity: null,
/**
* Particle angular velocity generator
* @type {?clay.Value.<clay.Vector3>}
*/
angularVelocity: null,
/**
* Particle sprite size generator
* @type {?clay.Value.<number>}
*/
spriteSize: null,
/**
* Particle weight generator
* @type {?clay.Value.<number>}
*/
weight: null,
// Init status for each particle
/**
* Particle life generator
*/
life?: Value.Value<number>;
/**
* Particle position generator
*/
position?: Value.Value<Vector3>;
/**
* Particle rotation generator
*/
rotation?: Value.Value<Vector3>;
/**
* Particle velocity generator
*/
velocity?: Value.Value<Vector3>;
/**
* Particle angular velocity generator
*/
angularVelocity?: Value.Value<Vector3>;
/**
* Particle sprite size generator
*/
spriteSize?: Value.Value<number>;
/**
* Particle weight generator
*/
weight?: Value.Value<number>;
}
_particlePool: null
},
function () {
this._particlePool = [];
interface ParticleEmitter extends ParticleEmitterOpts {}
class ParticleEmitter {
/** @lends clay.particle.Emitter# */
/**
* Maximum number of particles created by this emitter
* @type {number}
*/
max = 1000;
/*
* Number of particles created by this emitter each shot
* @type {number}
*/
amount = 20;
_particlePool: Particle[] = [];
constructor() {
// TODO Reduce heap memory
for (let i = 0; i < this.max; i++) {
const particle = new Particle();
@ -76,84 +72,74 @@ const Emitter = Base.extend(
particle.angularVelocity = new Vector3();
}
}
},
/** @lends clay.particle.Emitter.prototype */
{
/**
* Emitter number of particles and push them to a given particle list. Emmit number is defined by amount property
* @param {Array.<clay.particle.Particle>} out
*/
emit: function (out) {
const amount = Math.min(this._particlePool.length, this.amount);
}
/**
* Emitter number of particles and push them to a given particle list. Emmit number is defined by amount property
*/
emit(out: Particle[]) {
const amount = Math.min(this._particlePool.length, this.amount);
let particle;
for (let i = 0; i < amount; i++) {
particle = this._particlePool.pop();
// Initialize particle status
if (this.position) {
this.position.get(particle.position);
}
if (this.rotation) {
this.rotation.get(particle.rotation);
}
if (this.velocity) {
this.velocity.get(particle.velocity);
}
if (this.angularVelocity) {
this.angularVelocity.get(particle.angularVelocity);
}
if (this.life) {
particle.life = this.life.get();
}
if (this.spriteSize) {
particle.spriteSize = this.spriteSize.get();
}
if (this.weight) {
particle.weight = this.weight.get();
}
particle.age = 0;
out.push(particle);
let particle;
for (let i = 0; i < amount; i++) {
particle = this._particlePool.pop()!;
// Initialize particle status
if (this.position) {
this.position.get(particle.position);
}
},
/**
* Kill a dead particle and put it back in the pool
* @param {clay.particle.Particle} particle
*/
kill: function (particle) {
this._particlePool.push(particle);
if (this.rotation) {
this.rotation.get(particle.rotation);
}
if (this.velocity) {
this.velocity.get(particle.velocity);
}
if (this.angularVelocity) {
this.angularVelocity.get(particle.angularVelocity);
}
if (this.life) {
particle.life = this.life.get();
}
if (this.spriteSize) {
particle.spriteSize = this.spriteSize.get();
}
if (this.weight) {
particle.weight = this.weight.get();
}
particle.age = 0;
out.push(particle);
}
}
);
/**
* Kill a dead particle and put it back in the pool
*/
kill(particle: Particle) {
this._particlePool.push(particle);
}
/**
* Create a constant 1d value generator. Alias for {@link clay.Value.constant}
* @function clay.particle.Emitter.constant
*/
static constant = Value.constant;
/**
* Create a constant 1d value generator. Alias for {@link clay.Value.constant}
* @function clay.particle.Emitter.constant
*/
Emitter.constant = Value.constant;
/**
* Create a constant vector value(2d or 3d) generator. Alias for {@link clay.Value.vector}
*/
static vector = Value.vector;
/**
* Create a constant vector value(2d or 3d) generator. Alias for {@link clay.Value.vector}
* @function clay.particle.Emitter.vector
*/
Emitter.vector = Value.vector;
/**
* Create a random 1d value generator. Alias for {@link clay.Value.random1D}
*/
static random1D = Value.random1D;
/**
* Create a random 1d value generator. Alias for {@link clay.Value.random1D}
* @function clay.particle.Emitter.random1D
*/
Emitter.random1D = Value.random1D;
/**
* Create a random 2d value generator. Alias for {@link clay.Value.random2D}
*/
static random2D = Value.random2D;
/**
* Create a random 2d value generator. Alias for {@link clay.Value.random2D}
* @function clay.particle.Emitter.random2D
*/
Emitter.random2D = Value.random2D;
/**
* Create a random 3d value generator. Alias for {@link clay.Value.random3D}
*/
static random3D = Value.random3D;
}
/**
* Create a random 3d value generator. Alias for {@link clay.Value.random3D}
* @function clay.particle.Emitter.random3D
*/
Emitter.random3D = Value.random3D;
export default Emitter;
export default ParticleEmitter;

View File

@ -1,22 +1,13 @@
// @ts-nocheck
import Base from '../core/Base';
/**
* @constructor clay.particle.Field
* @extends clay.core.Base
*/
const Field = Base.extend(
{},
{
/**
* Apply a field to the particle and update the particle velocity
* @param {clay.Vector3} velocity
* @param {clay.Vector3} position
* @param {number} weight
* @param {number} deltaTime
* @memberOf clay.particle.Field.prototype
*/
applyTo: function (velocity, position, weight, deltaTime) {}
}
);
import Vector3 from '../math/Vector3';
export default Field;
export default interface ParticleField {
/**
* Apply a field to the particle and update the particle velocity
*/
applyTo(
velocity: Vector3 | undefined,
position: Vector3,
weight: number | undefined,
deltaTime: number
): void;
}

View File

@ -1,25 +1,17 @@
// @ts-nocheck
import Field from './Field';
import ParticleField from './Field';
import Vector3 from '../math/Vector3';
import vec3 from '../glmatrix/vec3';
import * as vec3 from '../glmatrix/vec3';
/**
* @constructor clay.particle.ForceField
* @extends clay.particle.Field
*/
const ForceField = Field.extend(
function () {
return {
force: new Vector3()
};
},
{
applyTo: function (velocity, position, weight, deltaTime) {
if (weight > 0) {
vec3.scaleAndAdd(velocity.array, velocity.array, this.force.array, deltaTime / weight);
}
export class ForceParticleField implements ParticleField {
force: Vector3;
constructor(force: Vector3) {
this.force = force;
}
applyTo(velocity: Vector3, position: Vector3, weight: number, deltaTime: number) {
if (weight > 0) {
vec3.scaleAndAdd(velocity.array, velocity.array, this.force.array, deltaTime / weight);
}
}
);
}
export default ForceField;
export default ForceParticleField;

View File

@ -1,75 +1,45 @@
// @ts-nocheck
import Vector3 from '../math/Vector3';
import vec3 from '../glmatrix/vec3';
import * as vec3 from '../glmatrix/vec3';
import ParticleEmitter from './Emitter';
/**
* @constructor
* @alias clay.particle.Particle
*/
const Particle = function () {
/**
* @type {clay.Vector3}
*/
this.position = new Vector3();
class Particle {
position = new Vector3();
/**
* Use euler angle to represent particle rotation
* @type {clay.Vector3}
*/
this.rotation = new Vector3();
rotation = new Vector3();
velocity?: Vector3;
angularVelocity?: Vector3;
life = 1;
age = 0;
spriteSize: = 1;
weight = 1;
emitter?: ParticleEmitter;
/**
* @type {?clay.Vector3}
* Update particle position
*/
this.velocity = null;
/**
* @type {?clay.Vector3}
*/
this.angularVelocity = null;
/**
* @type {number}
*/
this.life = 1;
/**
* @type {number}
*/
this.age = 0;
/**
* @type {number}
*/
this.spriteSize = 1;
/**
* @type {number}
*/
this.weight = 1;
/**
* @type {clay.particle.Emitter}
*/
this.emitter = null;
};
/**
* Update particle position
* @param {number} deltaTime
*/
Particle.prototype.update = function (deltaTime) {
if (this.velocity) {
vec3.scaleAndAdd(this.position.array, this.position.array, this.velocity.array, deltaTime);
update(deltaTime: number) {
if (this.velocity) {
vec3.scaleAndAdd(this.position.array, this.position.array, this.velocity.array, deltaTime);
}
if (this.angularVelocity) {
vec3.scaleAndAdd(
this.rotation.array,
this.rotation.array,
this.angularVelocity.array,
deltaTime
);
}
}
if (this.angularVelocity) {
vec3.scaleAndAdd(
this.rotation.array,
this.rotation.array,
this.angularVelocity.array,
deltaTime
);
}
};
}
export default Particle;

View File

@ -1,11 +1,15 @@
// @ts-nocheck
import Renderable from '../Renderable';
import Renderable, { RenderableOpts } from '../Renderable';
import Geometry from '../Geometry';
import Material from '../Material';
import Shader from '../Shader';
import particleEssl from './particle.glsl.js';
import ParticleEmitter from './Emitter';
import ParticleField from './Field';
import Particle from './Particle';
import { optional } from '../core/util';
import Renderer from '../Renderer';
Shader.import(particleEssl);
const particleShader = new Shader(
@ -13,10 +17,21 @@ const particleShader = new Shader(
Shader.source('clay.particle.fragment')
);
interface ParticleRenderableOpts extends RenderableOpts {
loop: boolean;
oneshot: boolean;
/**
* Duration of particle system in milliseconds
*/
duration: number;
// UV Animation
spriteAnimationTileX: number;
spriteAnimationTileY: number;
spriteAnimationRepeat: number;
}
/**
* @constructor clay.particle.ParticleRenderable
* @extends clay.Renderable
*
* @example
* const particleRenderable = new clay.particle.ParticleRenderable({
* spriteAnimationTileX: 4,
@ -45,50 +60,31 @@ const particleShader = new Shader(
* renderer.render(scene, camera);
* });
*/
const ParticleRenderable = Renderable.extend(
/** @lends clay.particle.ParticleRenderable# */ {
/**
* @type {boolean}
*/
loop: true,
/**
* @type {boolean}
*/
oneshot: false,
/**
* Duration of particle system in milliseconds
* @type {number}
*/
duration: 1,
interface ParticleRenderable extends ParticleRenderableOpts {}
class ParticleRenderable extends Renderable {
mode = Renderable.POINTS;
// UV Animation
/**
* @type {number}
*/
spriteAnimationTileX: 1,
/**
* @type {number}
*/
spriteAnimationTileY: 1,
/**
* @type {number}
*/
spriteAnimationRepeat: 0,
ignorePicking = true;
mode: Renderable.POINTS,
culling = false;
frustumCulling = false;
castShadow = false;
receiveShadow = false;
ignorePicking: true,
_elapsedTime = 0;
_emitting = true;
private _emitters: ParticleEmitter[] = [];
private _fields: ParticleField[] = [];
private _particles: Particle[] = [];
_elapsedTime: 0,
_emitting: true
},
function () {
constructor(opts: Partial<ParticleRenderableOpts>) {
super(opts);
Object.assign(this, opts);
this.geometry = new Geometry({
dynamic: true
});
if (!this.material) {
if (!opts.material) {
this.material = new Material({
shader: particleShader,
transparent: true,
@ -98,210 +94,205 @@ const ParticleRenderable = Renderable.extend(
this.material.enableTexture('sprite');
}
this._particles = [];
this._fields = [];
this._emitters = [];
},
/** @lends clay.particle.ParticleRenderable.prototype */
{
culling: false,
frustumCulling: false,
castShadow: false,
receiveShadow: false,
/**
* Add emitter
* @param {clay.particle.Emitter} emitter
*/
addEmitter: function (emitter) {
this._emitters.push(emitter);
},
/**
* Remove emitter
* @param {clay.particle.Emitter} emitter
*/
removeEmitter: function (emitter) {
this._emitters.splice(this._emitters.indexOf(emitter), 1);
},
/**
* Add field
* @param {clay.particle.Field} field
*/
addField: function (field) {
this._fields.push(field);
},
/**
* Remove field
* @param {clay.particle.Field} field
*/
removeField: function (field) {
this._fields.splice(this._fields.indexOf(field), 1);
},
/**
* Reset the particle system.
*/
reset: function () {
// Put all the particles back
for (let i = 0; i < this._particles.length; i++) {
const p = this._particles[i];
p.emitter.kill(p);
}
this._particles.length = 0;
this._elapsedTime = 0;
this._emitting = true;
},
/**
* @param {number} deltaTime
*/
updateParticles: function (deltaTime) {
// MS => Seconds
deltaTime /= 1000;
this._elapsedTime += deltaTime;
const particles = this._particles;
if (this._emitting) {
for (let i = 0; i < this._emitters.length; i++) {
this._emitters[i].emit(particles);
}
if (this.oneshot) {
this._emitting = false;
}
}
// Aging
let len = particles.length;
for (let i = 0; i < len; ) {
const p = particles[i];
p.age += deltaTime;
if (p.age >= p.life) {
p.emitter.kill(p);
particles[i] = particles[len - 1];
particles.pop();
len--;
} else {
i++;
}
}
for (let i = 0; i < len; i++) {
// Update
const p = particles[i];
if (this._fields.length > 0) {
for (let j = 0; j < this._fields.length; j++) {
this._fields[j].applyTo(p.velocity, p.position, p.weight, deltaTime);
}
}
p.update(deltaTime);
}
this._updateVertices();
},
_updateVertices: function () {
const geometry = this.geometry;
// If has uv animation
const animTileX = this.spriteAnimationTileX;
const animTileY = this.spriteAnimationTileY;
const animRepeat = this.spriteAnimationRepeat;
const nUvAnimFrame = animTileY * animTileX * animRepeat;
const hasUvAnimation = nUvAnimFrame > 1;
let positions = geometry.attributes.position.value;
// Put particle status in normal
let normals = geometry.attributes.normal.value;
let uvs = geometry.attributes.texcoord0.value;
let uvs2 = geometry.attributes.texcoord1.value;
const len = this._particles.length;
if (!positions || positions.length !== len * 3) {
// TODO Optimize
positions = geometry.attributes.position.value = new Float32Array(len * 3);
normals = geometry.attributes.normal.value = new Float32Array(len * 3);
if (hasUvAnimation) {
uvs = geometry.attributes.texcoord0.value = new Float32Array(len * 2);
uvs2 = geometry.attributes.texcoord1.value = new Float32Array(len * 2);
}
}
const invAnimTileX = 1 / animTileX;
for (let i = 0; i < len; i++) {
const particle = this._particles[i];
const offset = i * 3;
for (let j = 0; j < 3; j++) {
positions[offset + j] = particle.position.array[j];
normals[offset] = particle.age / particle.life;
// normals[offset + 1] = particle.rotation;
normals[offset + 1] = 0;
normals[offset + 2] = particle.spriteSize;
}
const offset2 = i * 2;
if (hasUvAnimation) {
// TODO
const p = particle.age / particle.life;
const stage = Math.round(p * (nUvAnimFrame - 1)) * animRepeat;
const v = Math.floor(stage * invAnimTileX);
const u = stage - v * animTileX;
uvs[offset2] = u / animTileX;
uvs[offset2 + 1] = 1 - v / animTileY;
uvs2[offset2] = (u + 1) / animTileX;
uvs2[offset2 + 1] = 1 - (v + 1) / animTileY;
}
}
geometry.dirty();
},
/**
* @return {boolean}
*/
isFinished: function () {
return this._elapsedTime > this.duration && !this.loop;
},
/**
* @param {clay.Renderer} renderer
*/
dispose: function (renderer) {
// Put all the particles back
for (let i = 0; i < this._particles.length; i++) {
const p = this._particles[i];
p.emitter.kill(p);
}
this.geometry.dispose(renderer);
// TODO Dispose texture ?
},
/**
* @return {clay.particle.ParticleRenderable}
*/
clone: function () {
const particleRenderable = new ParticleRenderable({
material: this.material
});
particleRenderable.loop = this.loop;
particleRenderable.duration = this.duration;
particleRenderable.oneshot = this.oneshot;
particleRenderable.spriteAnimationRepeat = this.spriteAnimationRepeat;
particleRenderable.spriteAnimationTileY = this.spriteAnimationTileY;
particleRenderable.spriteAnimationTileX = this.spriteAnimationTileX;
particleRenderable.position.copy(this.position);
particleRenderable.rotation.copy(this.rotation);
particleRenderable.scale.copy(this.scale);
for (let i = 0; i < this._children.length; i++) {
particleRenderable.add(this._children[i].clone());
}
return particleRenderable;
}
opts = opts || {};
this.loop = optional(opts.loop, true);
this.oneshot = optional(opts.oneshot, false);
this.duration = optional(opts.duration, 1);
this.spriteAnimationTileX = optional(opts.spriteAnimationTileX, 1);
this.spriteAnimationTileY = optional(opts.spriteAnimationTileY, 1);
this.spriteAnimationRepeat = optional(opts.spriteAnimationRepeat, 0);
}
);
/**
* Add emitter
* @param {clay.particle.Emitter} emitter
*/
addEmitter(emitter: ParticleEmitter) {
this._emitters.push(emitter);
}
/**
* Remove emitter
* @param {clay.particle.Emitter} emitter
*/
removeEmitter(emitter: ParticleEmitter) {
this._emitters.splice(this._emitters.indexOf(emitter), 1);
}
/**
* Add field
* @param {clay.particle.Field} field
*/
addField(field: ParticleField) {
this._fields.push(field);
}
/**
* Remove field
* @param {clay.particle.Field} field
*/
removeField(field: ParticleField) {
this._fields.splice(this._fields.indexOf(field), 1);
}
/**
* Reset the particle system.
*/
reset() {
// Put all the particles back
for (let i = 0; i < this._particles.length; i++) {
const p = this._particles[i];
p.emitter!.kill(p);
}
this._particles.length = 0;
this._elapsedTime = 0;
this._emitting = true;
}
/**
* @param {number} deltaTime
*/
updateParticles(deltaTime: number) {
// MS => Seconds
deltaTime /= 1000;
this._elapsedTime += deltaTime;
const particles = this._particles;
if (this._emitting) {
for (let i = 0; i < this._emitters.length; i++) {
this._emitters[i].emit(particles);
}
if (this.oneshot) {
this._emitting = false;
}
}
// Aging
let len = particles.length;
for (let i = 0; i < len; ) {
const p = particles[i];
p.age += deltaTime;
if (p.age >= p.life) {
p.emitter!.kill(p);
particles[i] = particles[len - 1];
particles.pop();
len--;
} else {
i++;
}
}
for (let i = 0; i < len; i++) {
// Update
const p = particles[i];
if (this._fields.length > 0) {
for (let j = 0; j < this._fields.length; j++) {
this._fields[j].applyTo(p.velocity, p.position, p.weight, deltaTime);
}
}
p.update(deltaTime);
}
this._updateVertices();
}
_updateVertices() {
const geometry = this.geometry;
// If has uv animation
const animTileX = this.spriteAnimationTileX;
const animTileY = this.spriteAnimationTileY;
const animRepeat = this.spriteAnimationRepeat;
const nUvAnimFrame = animTileY * animTileX * animRepeat;
const hasUvAnimation = nUvAnimFrame > 1;
let positions = geometry.attributes.position.value;
// Put particle status in normal
let normals = geometry.attributes.normal.value!;
let uvs = geometry.attributes.texcoord0.value!;
let uvs2 = geometry.attributes.texcoord1.value!;
const len = this._particles.length;
if (!positions || positions.length !== len * 3) {
// TODO Optimize
positions = geometry.attributes.position.value = new Float32Array(len * 3);
normals = geometry.attributes.normal.value = new Float32Array(len * 3);
if (hasUvAnimation) {
uvs = geometry.attributes.texcoord0.value = new Float32Array(len * 2);
uvs2 = geometry.attributes.texcoord1.value = new Float32Array(len * 2);
}
}
const invAnimTileX = 1 / animTileX;
for (let i = 0; i < len; i++) {
const particle = this._particles[i];
const offset = i * 3;
for (let j = 0; j < 3; j++) {
positions[offset + j] = particle.position.array[j];
normals[offset] = particle.age / particle.life;
// normals[offset + 1] = particle.rotation;
normals[offset + 1] = 0;
normals[offset + 2] = particle.spriteSize;
}
const offset2 = i * 2;
if (hasUvAnimation) {
// TODO
const p = particle.age / particle.life;
const stage = Math.round(p * (nUvAnimFrame - 1)) * animRepeat;
const v = Math.floor(stage * invAnimTileX);
const u = stage - v * animTileX;
uvs[offset2] = u / animTileX;
uvs[offset2 + 1] = 1 - v / animTileY;
uvs2[offset2] = (u + 1) / animTileX;
uvs2[offset2 + 1] = 1 - (v + 1) / animTileY;
}
}
geometry.dirty();
}
/**
* @return {boolean}
*/
isFinished() {
return this._elapsedTime > this.duration && !this.loop;
}
/**
* @param {clay.Renderer} renderer
*/
dispose(renderer: Renderer) {
// Put all the particles back
for (let i = 0; i < this._particles.length; i++) {
const p = this._particles[i];
p.emitter!.kill(p);
}
this.geometry.dispose(renderer);
// TODO Dispose texture ?
}
/**
* @return {clay.particle.ParticleRenderable}
*/
clone() {
const particleRenderable = new ParticleRenderable({
material: this.material
});
particleRenderable.loop = this.loop;
particleRenderable.duration = this.duration;
particleRenderable.oneshot = this.oneshot;
particleRenderable.spriteAnimationRepeat = this.spriteAnimationRepeat;
particleRenderable.spriteAnimationTileY = this.spriteAnimationTileY;
particleRenderable.spriteAnimationTileX = this.spriteAnimationTileX;
particleRenderable.position.copy(this.position);
particleRenderable.rotation.copy(this.rotation);
particleRenderable.scale.copy(this.scale);
for (let i = 0; i < this._children.length; i++) {
particleRenderable.add(this._children[i].clone());
}
return particleRenderable;
}
}
export default ParticleRenderable;

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import calcAmbientSHLightEssl from './calcAmbientSHLight.glsl.js';
const uniformVec3Prefix = 'uniform vec3 ';