@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
1,177 lines • 49.5 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { AxesHelper, BackSide, FrontSide, LinearSRGBColorSpace, Material, Mesh, MeshBasicMaterial, NormalBlending, Object3D, PlaneGeometry, Quaternion, Vector3 } from "three";
import { BatchedParticleRenderer, ConstantColor, ConstantValue, ParticleSystem as _ParticleSystem, RenderMode, TrailParticle, Vector4 as QVector4 } from "three.quarks";
import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
import { Mathf } from "../../engine/engine_math.js";
// https://github.dev/creativelifeform/three-nebula
// import System, { Emitter, Position, Life, SpriteRenderer, Particle, Body, MeshRenderer, } from 'three-nebula.js';
import { serializable } from "../../engine/engine_serialization.js";
import { assign } from "../../engine/engine_serialization_core.js";
import { createFlatTexture } from "../../engine/engine_shaders.js";
import { getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale } from "../../engine/engine_three_utils.js";
import { getParam } from "../../engine/engine_utils.js";
import { NEEDLE_progressive } from "../../engine/extensions/NEEDLE_progressive.js";
import { RGBAColor } from "../../engine/js-extensions/index.js";
import { Behaviour, GameObject } from "../Component.js";
import { ColorBySpeedModule, ColorOverLifetimeModule, EmissionModule, InheritVelocityModule, LimitVelocityOverLifetimeModule, MainModule, NoiseModule, ParticleBurst, ParticleSystemRenderMode, ParticleSystemScalingMode, ParticleSystemSimulationSpace, RotationBySpeedModule, RotationOverLifetimeModule, ShapeModule, SizeBySpeedModule, SizeOverLifetimeModule, TextureSheetAnimationModule, TrailModule, VelocityOverLifetimeModule } from "./ParticleSystemModules.js";
import { ParticleSubEmitter } from "./ParticleSystemSubEmitter.js";
const debug = getParam("debugparticles");
const suppressProgressiveLoading = getParam("noprogressive");
const debugProgressiveLoading = getParam("debugprogressive");
export var SubEmitterType;
(function (SubEmitterType) {
SubEmitterType[SubEmitterType["Birth"] = 0] = "Birth";
SubEmitterType[SubEmitterType["Collision"] = 1] = "Collision";
SubEmitterType[SubEmitterType["Death"] = 2] = "Death";
SubEmitterType[SubEmitterType["Trigger"] = 3] = "Trigger";
SubEmitterType[SubEmitterType["Manual"] = 4] = "Manual";
})(SubEmitterType || (SubEmitterType = {}));
/** @internal */
export class ParticleSystemRenderer extends Behaviour {
renderMode;
particleMaterial;
trailMaterial;
// @serializable(Mesh)
particleMesh;
maxParticleSize;
minParticleSize;
velocityScale;
cameraVelocityScale;
lengthScale;
start() {
if (this.maxParticleSize !== .5 && this.minParticleSize !== 0) {
if (isDevEnvironment()) {
const msg = `ParticleSystem \"${this.name}\" has non-default min/max particle size. This may not render correctly. Please set min size to 0 and the max size to 0.5 and use the \"StartSize\" setting instead`;
console.warn(msg);
// showBalloonWarning(msg);
}
}
}
get transparent() {
const res = this.particleMaterial?.transparent ?? false;
// console.log(res, this.particleMaterial);
return res;
}
getMaterial(trailEnabled = false) {
let material = (trailEnabled === true && this.trailMaterial) ? this.trailMaterial : this.particleMaterial;
if (material) {
if (material.type === "MeshStandardMaterial") {
if (debug)
console.debug("ParticleSystemRenderer.getMaterial: MeshStandardMaterial detected, converting to MeshBasicMaterial. See https://github.com/Alchemist0823/three.quarks/issues/101");
if ("map" in material && material.map) {
material.map.colorSpace = LinearSRGBColorSpace;
material.map.premultiplyAlpha = false;
}
const newMaterial = new MeshBasicMaterial();
newMaterial.copy(material);
if (trailEnabled)
this.trailMaterial = newMaterial;
else
this.particleMaterial = newMaterial;
}
if (material.map) {
material.map.colorSpace = LinearSRGBColorSpace;
material.map.premultiplyAlpha = false;
}
if (trailEnabled) {
// the particle material for trails must be DoubleSide or BackSide (since otherwise the trail is invisible)
if (material.side === FrontSide) {
// don't modify the assigned material
material = material.clone();
material.side = BackSide;
if (trailEnabled)
this.trailMaterial = material;
else
this.particleMaterial = material;
}
}
}
// progressive load on start
// TODO: figure out how to do this before particle system rendering so we only load textures for visible materials
if (material && !suppressProgressiveLoading && material["_didRequestTextureLOD"] === undefined) {
material["_didRequestTextureLOD"] = 0;
if (debugProgressiveLoading) {
console.log("Load material LOD", material.name);
}
NEEDLE_progressive.assignTextureLOD(material, 0);
}
return material;
}
getMesh(_renderMode) {
let geo = null;
if (!geo) {
if (this.particleMesh instanceof Mesh) {
geo = this.particleMesh.geometry;
}
if (geo === null) {
geo = new PlaneGeometry(1, 1);
// Flip UVs horizontally
const uv = geo.attributes.uv;
for (let i = 0; i < uv.count; i++) {
uv.setX(i, 1 - uv.getX(i));
}
}
}
const res = new Mesh(geo, this.getMaterial());
return res;
}
}
__decorate([
serializable()
], ParticleSystemRenderer.prototype, "renderMode", void 0);
__decorate([
serializable(Material)
], ParticleSystemRenderer.prototype, "particleMaterial", void 0);
__decorate([
serializable(Material)
], ParticleSystemRenderer.prototype, "trailMaterial", void 0);
__decorate([
serializable()
], ParticleSystemRenderer.prototype, "maxParticleSize", void 0);
__decorate([
serializable()
], ParticleSystemRenderer.prototype, "minParticleSize", void 0);
__decorate([
serializable()
], ParticleSystemRenderer.prototype, "velocityScale", void 0);
__decorate([
serializable()
], ParticleSystemRenderer.prototype, "cameraVelocityScale", void 0);
__decorate([
serializable()
], ParticleSystemRenderer.prototype, "lengthScale", void 0);
class MinMaxCurveFunction {
_curve;
_factor;
constructor(curve, factor = 1) {
this._curve = curve;
this._factor = factor;
}
type = "function";
startGen(_memory) {
// ...
}
genValue(_memory, t) {
return this._curve.evaluate(t, Math.random()) * this._factor;
}
toJSON() {
throw new Error("Method not implemented.");
}
clone() {
throw new Error("Method not implemented.");
}
}
class MinMaxGradientFunction {
_curve;
constructor(curve) { this._curve = curve; }
type = "function";
startGen(_memory) {
throw new Error("Method not implemented.");
}
genColor(_memory, color, t) {
const col = this._curve.evaluate(t, Math.random());
// TODO: incoming color should probably be blended?
color.set(col.r, col.g, col.b, "alpha" in col ? col.alpha : 1);
return color;
}
toJSON() {
throw new Error("Method not implemented.");
}
clone() {
throw new Error("Method not implemented.");
}
}
class BaseValueGenerator {
type = "value";
toJSON() {
throw new Error("Method not implemented.");
}
clone() {
throw new Error("Method not implemented.");
}
startGen(_memory) { }
system;
constructor(system) {
this.system = system;
}
}
class TextureSheetStartFrameGenerator extends BaseValueGenerator {
genValue() {
return this.system.textureSheetAnimation.getStartIndex();
}
}
class ParticleSystemEmissionOverTime extends BaseValueGenerator {
_lastPosition = new Vector3();
_lastDistance = 0;
update() {
const currentPosition = getWorldPosition(this.system.gameObject);
this._lastDistance = this._lastPosition.distanceTo(currentPosition);
this._lastPosition.copy(currentPosition);
}
genValue() {
if (!this.system.isPlaying)
return 0;
if (!this.system.emission.enabled)
return 0;
if (this.system.currentParticles >= this.system.maxParticles)
return 0;
// emission over time
let emission = this.system.emission.rateOverTime.evaluate(this.system.time / this.system.duration, Math.random());
// if(this.system.currentParticles + emission > this.system.maxParticles)
// emission = (this.system.maxParticles - this.system.currentParticles);
// const res = Mathf.clamp(emission, 0, this.system.maxParticles - this.system.currentParticles);
if (this.system.deltaTime > 0) {
const distanceEmission = this.system.emission.rateOverDistance.evaluate(this.system.time / this.system.duration, Math.random());
const meterPerSecond = this._lastDistance / this.system.deltaTime;
let distanceEmissionValue = meterPerSecond * distanceEmission;
if (!Number.isFinite(distanceEmissionValue))
distanceEmissionValue = 0;
emission += distanceEmissionValue;
}
const burst = this.system.emission.getBurst();
if (burst > 0)
emission += burst / this.system.deltaTime;
const maxEmission = (this.system.maxParticles - this.system.currentParticles);
return Mathf.clamp(emission, 0, maxEmission / this.system.deltaTime);
}
}
class ParticleSystemEmissionOverDistance extends BaseValueGenerator {
genValue() {
if (!this.system.isPlaying)
return 0;
// this seems not be called yet
return 0;
// if (this.system.currentParticles >= this.system.maxParticles) return 0;
// const emission = this.system.emission.rateOverDistance.evaluate(this.system.time / this.system.duration, Math.random());
// return emission;
}
}
export class ParticleSystemBaseBehaviour {
system;
get context() { return this.system.context; }
constructor(ps) {
this.type = Object.getPrototypeOf(this).constructor.name || "ParticleSystemBaseBehaviour";
if (ps)
this.system = ps;
}
type;
initialize(_particle) { }
update(_particle, _delta) { }
frameUpdate(_delta) { }
toJSON() { throw new Error("Method not implemented."); }
clone() { throw new Error("Method not implemented."); }
reset() { }
}
const $startFrame = Symbol("startFrame");
class TextureSheetAnimationBehaviour extends ParticleSystemBaseBehaviour {
type = "NeedleTextureSheet";
// initialize(_particle: Particle): void {
// _particle[$startFrame] = this.system.textureSheetAnimation.getStartIndex();
// }
update(particle, _delta) {
const sheet = this.system.textureSheetAnimation;
if (sheet.enabled) {
const t01 = particle.age / particle.life;
const index = sheet.evaluate(t01);
if (index !== undefined)
particle.uvTile = index;
}
}
}
const $particleRotation = Symbol("particleRotation");
class RotationBehaviour extends ParticleSystemBaseBehaviour {
type = "NeedleRotation";
initialize(particle) {
particle[$particleRotation] = Math.random();
}
update(particle, delta) {
if (particle.rotation === undefined)
return;
const t = particle.age / particle.life;
if (typeof particle.rotation === "number") {
if (this.system.rotationOverLifetime.enabled) {
particle.rotation += this.system.rotationOverLifetime.evaluate(t, particle[$particleRotation]) * delta;
}
else {
if (this.system.renderer.renderMode === ParticleSystemRenderMode.Billboard)
particle.rotation = Math.PI;
}
if (this.system.rotationBySpeed.enabled) {
const speed = particle.velocity.length();
particle.rotation += this.system.rotationBySpeed.evaluate(t, speed) * delta;
}
}
else {
// const quat = particle.rotation as Quaternion;
// TODO: implement rotation by speed for quaternions
}
}
}
const $sizeLerpFactor = Symbol("sizeLerpFactor");
const localScaleVec3 = new Vector3();
class SizeBehaviour extends ParticleSystemBaseBehaviour {
type = "NeedleSize";
_minSize = 0;
_maxSize = 1;
initialize(particle) {
particle[$sizeLerpFactor] = Math.random();
this._minSize = this.system.renderer.minParticleSize;
this._maxSize = this.system.renderer.maxParticleSize;
}
update(particle, _delta) {
const age01 = particle.age / particle.life;
let size = 1;
if (this.system.sizeOverLifetime.enabled)
size *= this.system.sizeOverLifetime.evaluate(age01, undefined, particle[$sizeLerpFactor]).x;
let scaleFactor = 1;
if (this.system.renderer.renderMode !== ParticleSystemRenderMode.Mesh)
scaleFactor = this.system.worldScale.x / this.system.cameraScale;
const newSize = getTempVector(particle.startSize).multiplyScalar(size * scaleFactor);
particle.size.set(newSize.x, newSize.y, newSize.z);
if (this.system.localspace) {
const scale = getLocalSimulationScale(this.system, localScaleVec3);
particle.size.x *= scale.x;
particle.size.y *= scale.y;
particle.size.z *= scale.z;
}
// in Unity this is viewport size, we don't really support this yet (and the renderer is logging a warning)
// so for now it's disabled again
// particle.size = Mathf.clamp(particle.size, this._minSize, this._maxSize);
}
}
export const $particleLife = Symbol("particleLife");
const $trailLifetime = Symbol("trailLifetime");
const $trailStartLength = Symbol("trailStartLength");
const $trailWidthRandom = Symbol("trailWidthRandom");
class TrailBehaviour extends ParticleSystemBaseBehaviour {
type = "NeedleTrail";
initialize(particle) {
if (particle instanceof TrailParticle) {
particle[$particleLife] = particle.life;
if (this.system.trails.enabled && this.system.trails.dieWithParticles === false) {
particle[$trailLifetime] = this.system.trails.lifetime.evaluate(Math.random(), Math.random());
particle.life += particle[$trailLifetime];
}
particle[$trailStartLength] = particle.length;
particle[$trailWidthRandom] = Math.random();
}
}
update(particle) {
if (this.system.trails?.enabled && particle instanceof TrailParticle) {
const trailParticle = particle;
const age01 = particle.age / particle[$particleLife];
const iter = particle.previous.values();
const length = particle.previous.length;
// const maxAge = this.system.trails.lifetime.
for (let i = 0; i < length; i++) {
const cur = iter.next();
const state = cur.value;
const pos01 = 1 - (i / (length - 1));
const size = particle.size;
if (size.x <= 0 && !this.system.trails.sizeAffectsWidth) {
// Not sure where we get to 100* from, tested in SOC trong com
const newSize = 20 * this.system.trails.widthOverTrail.evaluate(.5, trailParticle[$trailWidthRandom]);
size.x = newSize;
size.y = newSize;
size.z = newSize;
}
state.size = this.system.trails.getWidth(size.x, age01, pos01, trailParticle[$trailWidthRandom]);
state.color.copy(particle.color);
this.system.trails.getColor(state.color, age01, pos01);
}
// particle.life = particle.age + .1;
if (particle.age > particle[$particleLife]) {
particle.velocity.set(0, 0, 0);
const t = (particle.age - particle[$particleLife]) / particle[$trailLifetime];
trailParticle.length = Mathf.lerp(particle[$trailStartLength], 0, t);
}
}
}
}
const $startVelocity = Symbol("startVelocity");
const $gravityFactor = Symbol("gravityModifier");
const $gravitySpeed = Symbol("gravitySpeed");
const $velocityLerpFactor = Symbol("velocity lerp factor");
const temp3 = new Vector3();
const temp4 = new Quaternion();
class VelocityBehaviour extends ParticleSystemBaseBehaviour {
type = "NeedleVelocity";
_gravityDirection = new Vector3();
initialize(particle) {
const simulationSpeed = this.system.main.simulationSpeed;
particle.startSpeed = this.system.main.startSpeed.evaluate(Math.random(), Math.random());
const dir = this.system.shape.getDirection(particle, particle.position);
particle.velocity.x = dir.x * particle.startSpeed;
particle.velocity.y = dir.y * particle.startSpeed;
particle.velocity.z = dir.z * particle.startSpeed;
if (this.system.inheritVelocity?.enabled) {
this.system.inheritVelocity.applyInitial(particle.velocity);
}
if (!particle[$startVelocity])
particle[$startVelocity] = particle.velocity.clone();
else
particle[$startVelocity].copy(particle.velocity);
const gravityFactor = this.system.main.gravityModifier.evaluate(Math.random(), Math.random());
particle[$gravityFactor] = gravityFactor * simulationSpeed;
particle[$gravitySpeed] = gravityFactor * simulationSpeed * .5;
particle[$velocityLerpFactor] = Math.random();
this.system.velocityOverLifetime?.init(particle);
this._gravityDirection.set(0, -1, 0);
if (this.system.main.simulationSpace === ParticleSystemSimulationSpace.Local)
this._gravityDirection.applyQuaternion(this.system.worldQuaternionInverted).normalize();
}
update(particle, delta) {
//////////////////////
// calculate speed
const baseVelocity = particle[$startVelocity];
const gravityFactor = particle[$gravityFactor];
if (gravityFactor !== 0) {
const factor = gravityFactor * particle[$gravitySpeed];
temp3.copy(this._gravityDirection).multiplyScalar(factor);
particle[$gravitySpeed] += delta * .05;
baseVelocity.add(temp3);
}
particle.velocity.copy(baseVelocity);
const t01 = particle.age / particle.life;
if (this.system.inheritVelocity?.enabled) {
this.system.inheritVelocity.applyCurrent(particle.velocity, t01, particle[$velocityLerpFactor]);
}
const noise = this.system.noise;
if (noise.enabled) {
noise.apply(0, particle.position, particle.velocity, delta, particle.age, particle.life);
}
//////////////////////
// evaluate by speed modules
const sizeBySpeed = this.system.sizeBySpeed;
if (sizeBySpeed?.enabled) {
particle.size = sizeBySpeed.evaluate(particle.velocity, t01, particle[$velocityLerpFactor], particle.size);
}
const colorBySpeed = this.system.colorBySpeed;
if (colorBySpeed?.enabled) {
colorBySpeed.evaluate(particle.velocity, particle[$velocityLerpFactor], particle.color);
}
//////////////////////
// limit or modify speed
const velocity = this.system.velocityOverLifetime;
if (velocity.enabled) {
velocity.apply(particle, 0, particle.position, particle.velocity, delta, particle.age, particle.life);
}
const limitVelocityOverLifetime = this.system.limitVelocityOverLifetime;
if (limitVelocityOverLifetime.enabled) {
// const factor = this.system.worldScale.x;
limitVelocityOverLifetime.apply(particle.position, baseVelocity, particle.velocity, particle.size, t01, delta, 1);
}
if (this.system.worldspace) {
const ws = this.system.worldScale;
particle.velocity.x *= ws.x;
particle.velocity.y *= ws.y;
particle.velocity.z *= ws.z;
}
}
}
const $colorLerpFactor = Symbol("colorLerpFactor");
const tempColor = new RGBAColor(1, 1, 1, 1);
const col = new RGBAColor(1, 1, 1, 1);
class ColorBehaviour extends ParticleSystemBaseBehaviour {
type = "NeedleColor";
initialize(_particle) {
}
_init(particle) {
const materialColor = this.system.renderer.particleMaterial;
col.copy(this.system.main.startColor.evaluate(Math.random()));
if (materialColor?.color) {
tempColor.copy(materialColor.color);
col.multiply(tempColor);
}
col.convertLinearToSRGB();
particle.startColor.set(col.r, col.g, col.b, col.alpha);
particle.color.copy(particle.startColor);
particle[$colorLerpFactor] = Math.random();
}
update(particle, _delta) {
if (particle.age === 0)
this._init(particle);
if (this.system.colorOverLifetime.enabled) {
const t = particle.age / particle.life;
const col = this.system.colorOverLifetime.color.evaluate(t, particle[$colorLerpFactor]);
particle.color.set(col.r, col.g, col.b, "alpha" in col ? col.alpha : 1).multiply(particle.startColor);
}
else {
particle.color.copy(particle.startColor);
}
}
}
class ParticleSystemInterface {
system;
emission;
get anim() {
return this.system.textureSheetAnimation;
}
constructor(system) {
this.system = system;
this.emission = new ParticleSystemEmissionOverTime(this.system);
}
get prewarm() { return false; } // force disable three.quark prewarm, we have our own!
get material() { return this.system.renderer.getMaterial(this.system.trails.enabled); }
get layers() { return this.system.gameObject.layers; }
update() {
this.emission.update();
}
autoDestroy;
get looping() { return this.system.main.loop; }
get duration() { return this.system.duration; }
get shape() { return this.system.shape; }
get startLife() { return new MinMaxCurveFunction(this.system.main.startLifetime); }
get startSpeed() { return new MinMaxCurveFunction(this.system.main.startSpeed); }
get startRotation() { return new MinMaxCurveFunction(this.system.main.startRotation); }
get startSize() { return new MinMaxCurveFunction(this.system.main.startSize); }
startLength; /** start length is for trails */
get startColor() { return new ConstantColor(new QVector4(1, 1, 1, 1)); }
get emissionOverTime() { return this.emission; }
/** this is not supported yet */
get emissionOverDistance() { return new ParticleSystemEmissionOverDistance(this.system); }
/** not used - burst is controled via emissionOverTime */
emissionBursts;
onlyUsedByOther;
behaviors = [];
get instancingGeometry() {
return this.system.renderer.getMesh(this.system.renderer.renderMode).geometry;
}
get renderMode() {
if (this.system.trails["enabled"] === true) {
return RenderMode.Trail;
}
switch (this.system.renderer.renderMode) {
case ParticleSystemRenderMode.Billboard: return RenderMode.BillBoard;
case ParticleSystemRenderMode.Stretch: return RenderMode.StretchedBillBoard;
case ParticleSystemRenderMode.HorizontalBillboard: return RenderMode.HorizontalBillBoard;
case ParticleSystemRenderMode.VerticalBillboard: return RenderMode.VerticalBillBoard;
case ParticleSystemRenderMode.Mesh: return RenderMode.Mesh;
}
return RenderMode.BillBoard;
}
rendererEmitterSettings = {
startLength: new ConstantValue(220),
followLocalOrigin: false,
};
get speedFactor() {
let factor = this.system.main.simulationSpeed;
if (this.system.renderer?.renderMode === ParticleSystemRenderMode.Stretch) {
factor *= this.system.renderer.velocityScale ?? 1;
}
return factor;
}
flatWhiteTexture;
clonedTexture = { original: undefined, clone: undefined };
get texture() {
const mat = this.material;
if (mat && mat["map"]) {
const original = mat["map"];
// cache the last original one so we're not creating tons of clones
if (this.clonedTexture.original !== original || !this.clonedTexture.clone) {
const tex = original.clone();
tex.premultiplyAlpha = false;
tex.colorSpace = LinearSRGBColorSpace;
this.clonedTexture.original = original;
this.clonedTexture.clone = tex;
}
return this.clonedTexture.clone;
}
if (!this.flatWhiteTexture)
this.flatWhiteTexture = createFlatTexture(new RGBAColor(1, 1, 1, 1), 1);
return this.flatWhiteTexture;
}
get startTileIndex() { return new TextureSheetStartFrameGenerator(this.system); }
get uTileCount() { return this.anim.enabled ? this.anim?.numTilesX : undefined; }
get vTileCount() { return this.anim.enabled ? this.anim?.numTilesY : undefined; }
get renderOrder() { return 1; }
get blending() { return this.system.renderer.particleMaterial?.blending ?? NormalBlending; }
get transparent() { return this.system.renderer.transparent; }
get worldSpace() { return this.system.main.simulationSpace === ParticleSystemSimulationSpace.World; }
}
class ParticlesEmissionState {
burstParticleIndex = 0;
burstParticleCount = 0;
isBursting = false;
travelDistance = 0;
previousWorldPos;
burstIndex = 0;
burstWaveIndex = 0;
time = 0;
waitEmiting = 0;
}
/**
* The ParticleSystem component efficiently handles the motion and rendering of many individual particles.
*
* You can add custom behaviours to the particle system to fully customize the behaviour of the particles. See {@link ParticleSystemBaseBehaviour} and {@link ParticleSystem.addBehaviour} for more information.
*
* Needle Engine uses [three.quarks](https://github.com/Alchemist0823/three.quarks) under the hood to handle particles.
*
* @category Rendering
* @group Components
*/
export class ParticleSystem extends Behaviour {
play(includeChildren = false) {
if (includeChildren) {
GameObject.foreachComponent(this.gameObject, comp => {
if (comp instanceof ParticleSystem && comp !== this) {
comp.play(false);
}
}, true);
}
this._isPlaying = true;
// https://github.com/Alchemist0823/three.quarks/pull/35
if (this._particleSystem) {
this._particleSystem["emissionState"].time = 0;
this._particleSystem["emitEnded"] = false;
}
this.emission?.reset();
}
pause(includeChildren = true) {
if (includeChildren) {
GameObject.foreachComponent(this.gameObject, comp => {
if (comp instanceof ParticleSystem && comp !== this) {
comp.pause(false);
}
}, true);
}
this._isPlaying = false;
}
/** clear=true removes all emitted particles */
stop(includeChildren = true, clear = false) {
if (includeChildren) {
GameObject.foreachComponent(this.gameObject, comp => {
if (comp instanceof ParticleSystem && comp !== this) {
comp.stop(false, clear);
}
}, true);
}
this._isPlaying = false;
this._time = 0;
if (clear)
this.reset();
}
/** remove emitted particles and reset time */
reset() {
this._time = 0;
if (this._particleSystem) {
this._particleSystem.particleNum = 0;
this._particleSystem["emissionState"].time = 0;
this._particleSystem["emitEnded"] = false;
this.emission?.reset();
}
}
_state;
emit(count) {
if (this._particleSystem) {
// we need to call update the matrices etc e.g. if we call emit from a physics callback
this.onUpdate();
count = Math.min(count, this.maxParticles - this.currentParticles);
if (!this._state)
this._state = new ParticlesEmissionState();
this._state.waitEmiting = count;
this._state.time = 0;
const emitEndedState = this._particleSystem["emitEnded"];
this._particleSystem["emitEnded"] = false;
this._particleSystem.emit(this.deltaTime, this._state, this._particleSystem.emitter.matrixWorld);
this._particleSystem["emitEnded"] = emitEndedState;
}
}
get playOnAwake() {
return this.main.playOnAwake;
}
set playOnAwake(val) {
this.main.playOnAwake = val;
}
colorOverLifetime;
main;
emission;
sizeOverLifetime;
shape;
noise;
trails;
velocityOverLifetime;
limitVelocityOverLifetime;
inheritVelocity;
colorBySpeed;
textureSheetAnimation;
rotationOverLifetime;
rotationBySpeed;
sizeBySpeed;
get renderer() {
return this._renderer;
}
get isPlaying() { return this._isPlaying; }
get currentParticles() {
return this._particleSystem?.particleNum ?? 0;
}
get maxParticles() {
return this.main.maxParticles;
}
get time() {
return this._time;
}
get duration() {
return this.main.duration;
}
get deltaTime() {
return this.context.time.deltaTime * this.main.simulationSpeed;
}
get scale() {
return this.gameObject.scale.x;
}
get cameraScale() {
return this._cameraScale;
}
_cameraScale = 1;
get container() {
return this._container;
}
get worldspace() {
return this.main.simulationSpace === ParticleSystemSimulationSpace.World;
}
get localspace() {
return this.main.simulationSpace === ParticleSystemSimulationSpace.Local;
}
__worldQuaternion = new Quaternion();
get worldQuaternion() {
return this.__worldQuaternion;
}
_worldQuaternionInverted = new Quaternion();
get worldQuaternionInverted() {
return this._worldQuaternionInverted;
}
_worldScale = new Vector3();
get worldScale() {
return this._worldScale;
}
_worldPositionFrame = -1;
_worldPos = new Vector3();
get worldPos() {
if (this._worldPositionFrame !== this.context.time.frame) {
this._worldPositionFrame = this.context.time.frame;
getWorldPosition(this.gameObject, this._worldPos);
}
return this._worldPos;
}
get matrixWorld() {
return this._container.matrixWorld;
}
get isSubsystem() {
return this._isUsedAsSubsystem;
}
/** Add a custom quarks behaviour to the particle system.
* You can add a quarks.Behaviour type or derive from {@link ParticleSystemBaseBehaviour}
* @link https://github.com/Alchemist0823/three.quarks
* @example
* ```typescript
* class MyBehaviour extends ParticleSystemBaseBehaviour {
* initialize(particle: Particle) {
* // initialize the particle
* }
* update(particle: Particle, delta: number) {
* // do something with the particle
* }
* }
*
* const system = gameObject.getComponent(ParticleSystem);
* system.addBehaviour(new MyBehaviour());
* ```
*/
addBehaviour(particleSystemBehaviour) {
if (!this._particleSystem) {
return false;
}
if (particleSystemBehaviour instanceof ParticleSystemBaseBehaviour) {
particleSystemBehaviour.system = this;
}
if (debug)
console.debug("Add custom ParticleSystem Behaviour", particleSystemBehaviour);
this._particleSystem.addBehavior(particleSystemBehaviour);
return true;
}
/** Remove a custom quarks behaviour from the particle system. **/
removeBehaviour(particleSystemBehaviour) {
if (!this._particleSystem) {
return false;
}
const behaviours = this._particleSystem.behaviors;
const index = behaviours.indexOf(particleSystemBehaviour);
if (index !== -1) {
if (isDevEnvironment() || debug)
console.debug("Remove custom ParticleSystem Behaviour", index, particleSystemBehaviour);
behaviours.splice(index, 1);
return true;
}
return true;
}
/** Removes all behaviours from the particle system
* **Note:** this will also remove the default behaviours like SizeBehaviour, ColorBehaviour etc.
*/
removeAllBehaviours() {
if (!this._particleSystem) {
return false;
}
this._particleSystem.behaviors.length = 0;
return true;
}
/** Get the underlying three.quarks particle system behaviours. This can be used to fully customize the behaviour of the particles. */
get behaviours() {
if (!this._particleSystem)
return null;
return this._particleSystem.behaviors;
}
/** Get access to the underlying quarks particle system if you need more control
* @link https://github.com/Alchemist0823/three.quarks
*/
get particleSystem() {
return this._particleSystem ?? null;
}
_renderer;
_batchSystem;
_particleSystem;
_interface;
// private _system!: System;
// private _emitter: Emitter;
// private _size!: SizeBehaviour;
_container;
_time = 0;
_isPlaying = true;
_isUsedAsSubsystem = false;
_didPreWarm = false;
/** called from deserialization */
set bursts(arr) {
for (let i = 0; i < arr.length; i++) {
const burst = arr[i];
if ((burst instanceof ParticleBurst) === false) {
const instance = new ParticleBurst();
assign(instance, burst);
arr[i] = instance;
}
}
this._bursts = arr;
}
_bursts;
/** called from deserialization */
set subEmitterSystems(arr) {
for (let i = 0; i < arr.length; i++) {
const sub = arr[i];
if ((sub instanceof SubEmitterSystem) === false) {
const instance = new SubEmitterSystem();
assign(instance, sub);
arr[i] = instance;
}
}
if (debug && arr.length > 0) {
console.log("SubEmitters: ", arr, this);
}
this._subEmitterSystems = arr;
}
_subEmitterSystems;
/** @internal */
onAfterDeserialize(_) {
// doing this here to get a chance to resolve the subemitter guid
if (this._subEmitterSystems && Array.isArray(this._subEmitterSystems)) {
for (const sub of this._subEmitterSystems) {
sub._deserialize(this.context, this.gameObject);
}
}
}
/** @internal */
awake() {
this._worldPositionFrame = -1;
this._renderer = this.gameObject.getComponent(ParticleSystemRenderer);
if (!this.main) {
throw new Error("Not Supported: ParticleSystem needs a serialized MainModule. Creating new particle systems at runtime is currently not supported.");
}
this._container = new Object3D();
this._container.matrixAutoUpdate = false;
// if (this.main.simulationSpace == ParticleSystemSimulationSpace.Local) {
// this.gameObject.add(this._container);
// }
// else
{
this.context.scene.add(this._container);
}
// else this._container = this.context.scene;
this._batchSystem = new BatchedParticleRenderer();
this._batchSystem.name = this.gameObject.name;
this._container.add(this._batchSystem);
this._interface = new ParticleSystemInterface(this);
this._particleSystem = new _ParticleSystem(this._interface);
this._particleSystem.addBehavior(new SizeBehaviour(this));
this._particleSystem.addBehavior(new ColorBehaviour(this));
this._particleSystem.addBehavior(new TextureSheetAnimationBehaviour(this));
this._particleSystem.addBehavior(new RotationBehaviour(this));
this._particleSystem.addBehavior(new VelocityBehaviour(this));
this._particleSystem.addBehavior(new TrailBehaviour(this));
this._batchSystem.addSystem(this._particleSystem);
const emitter = this._particleSystem.emitter;
this.context.scene.add(emitter);
if (this.inheritVelocity.system && this.inheritVelocity.system !== this) {
this.inheritVelocity = this.inheritVelocity.clone();
}
this.inheritVelocity.awake(this);
if (debug) {
console.log(this);
this.gameObject.add(new AxesHelper(1));
}
}
/** @internal */
start() {
this.addSubParticleSystems();
this.updateLayers();
if (this.renderer.particleMesh instanceof Mesh && this._interface.renderMode == RenderMode.Mesh) {
NEEDLE_progressive.assignMeshLOD(this.renderer.particleMesh, 0).then(geo => {
if (geo && this.particleSystem && this._interface.renderMode == RenderMode.Mesh) {
this.particleSystem.instancingGeometry = geo;
}
});
}
}
/** @internal */
onDestroy() {
this._container?.removeFromParent();
this._batchSystem?.removeFromParent();
this._particleSystem?.emitter.removeFromParent();
this._particleSystem?.dispose();
}
/** @internal */
onEnable() {
if (!this.main)
return;
if (this.inheritVelocity)
this.inheritVelocity.system = this;
if (this._batchSystem)
this._batchSystem.visible = true;
if (this.playOnAwake)
this.play();
this._isPlaying = this.playOnAwake;
}
onDisable() {
if (this._batchSystem)
this._batchSystem.visible = false;
}
/** @internal */
onBeforeRender() {
if (!this.main)
return;
if (this._didPreWarm === false && this.main?.prewarm === true) {
this._didPreWarm = true;
this.preWarm();
}
this.onUpdate();
this.onSimulate(this.deltaTime);
}
preWarm() {
if (!this.emission?.enabled)
return;
const emission = this.emission.rateOverTime.getMax();
if (emission <= 0)
return;
const dt = 1 / 60;
const duration = this.main.duration;
const lifetime = this.main.startLifetime.getMax();
const maxDurationToPrewarm = 1000;
const timeToSimulate = Math.min(Math.max(duration, lifetime) / Math.max(.01, this.main.simulationSpeed), maxDurationToPrewarm);
const framesToSimulate = Math.ceil(timeToSimulate / dt);
const startTime = Date.now();
if (debug)
console.log(`Particles ${this.name} - Prewarm for ${framesToSimulate} frames (${timeToSimulate} sec). Duration: ${duration}, Lifetime: ${lifetime}`);
for (let i = 0; i < framesToSimulate; i++) {
if (this.currentParticles >= this.maxParticles)
break;
const timePassed = Date.now() - startTime;
if (timePassed > 2000) {
console.warn(`Particles ${this.name} - Prewarm took too long. Aborting: ${timePassed}`);
break;
}
this.onUpdate();
this.onSimulate(dt);
}
}
_lastBatchesCount = -1;
onSimulate(dt) {
if (this._batchSystem) {
let needsUpdate = this.context.time.frameCount % 60 === 0;
if (this._lastBatchesCount !== this._batchSystem.batches.length) {
this._lastBatchesCount = this._batchSystem.batches.length;
needsUpdate = true;
}
// Updating layers on batches
// TODO: figure out a better way to do this
// Issue: https://github.com/Alchemist0823/three.quarks/issues/49
if (needsUpdate) {
this.updateLayers();
}
this._batchSystem.update(dt);
}
this._time += dt;
if (this._time > this.duration)
this._time = 0;
}
updateLayers() {
if (this._batchSystem) {
for (let i = 0; i < this._batchSystem.batches.length; i++) {
const batch = this._batchSystem.batches[i];
batch.layers.disableAll();
const layer = this.layer;
batch.layers.mask = 1 << layer;
}
}
}
// private lastMaterialVersion: number = -1;
onUpdate() {
if (this._bursts) {
this.emission.bursts = this._bursts;
delete this._bursts;
}
if (!this._isPlaying)
return;
// sprite materials must be scaled in AR
const cam = this.context.mainCamera;
if (cam) {
const scale = getWorldScale(cam);
this._cameraScale = scale.x;
}
const isLocalSpace = !this.worldspace;
const source = this.gameObject;
getWorldQuaternion(source, this.__worldQuaternion);
this._worldQuaternionInverted.copy(this.__worldQuaternion).invert();
getWorldScale(this.gameObject, this._worldScale);
// Handle LOCALSPACE
if (isLocalSpace && this._container && this.gameObject?.parent) {
const scale = getLocalSimulationScale(this, temp3);
this._container.matrix.makeScale(scale.x, scale.y, scale.z);
this._container.matrix.makeRotationFromQuaternion(this.__worldQuaternion);
this._container.matrix.setPosition(this.worldPos);
this._container.matrix.scale(this.gameObject.scale);
}
this.emission.system = this;
this._interface.update();
this.shape.onUpdate(this, this.context, this.main.simulationSpace, this.gameObject);
this.noise.update(this.context);
this.inheritVelocity?.update(this.context);
this.velocityOverLifetime.update(this);
}
addSubParticleSystems() {
if (this._subEmitterSystems && this._particleSystem) {
for (const sys of this._subEmitterSystems) {
// Make sure the particle system is created
if (sys.particleSystem) {
if (sys.particleSystem.__internalAwake)
sys.particleSystem.__internalAwake();
else if (isDevEnvironment())
console.warn("SubParticleSystem serialization issue(?)", sys.particleSystem, sys);
}
const system = sys.particleSystem?._particleSystem;
if (system) {
sys.particleSystem._isUsedAsSubsystem = true;
// sys.particleSystem!.main.simulationSpace = ParticleSystemSimulationSpace.World;
const sub = new ParticleSubEmitter(this, this._particleSystem, sys.particleSystem, system);
sub.emitterType = sys.type;
sub.emitterProbability = sys.emitProbability;
this._particleSystem.addBehavior(sub);
}
else if (debug)
console.warn("Could not add SubParticleSystem", sys, this);
}
}
}
}
__decorate([
serializable(ColorOverLifetimeModule)
], ParticleSystem.prototype, "colorOverLifetime", void 0);
__decorate([
serializable(MainModule)
], ParticleSystem.prototype, "main", void 0);
__decorate([
serializable(EmissionModule)
], ParticleSystem.prototype, "emission", void 0);
__decorate([
serializable(SizeOverLifetimeModule)
], ParticleSystem.prototype, "sizeOverLifetime", void 0);
__decorate([
serializable(ShapeModule)
], ParticleSystem.prototype, "shape", void 0);
__decorate([
serializable(NoiseModule)
], ParticleSystem.prototype, "noise", void 0);
__decorate([
serializable(TrailModule)
], ParticleSystem.prototype, "trails", void 0);
__decorate([
serializable(VelocityOverLifetimeModule)
], ParticleSystem.prototype, "velocityOverLifetime", void 0);
__decorate([
serializable(LimitVelocityOverLifetimeModule)
], ParticleSystem.prototype, "limitVelocityOverLifetime", void 0);
__decorate([
serializable(InheritVelocityModule)
], ParticleSystem.prototype, "inheritVelocity", void 0);
__decorate([
serializable(ColorBySpeedModule)
], ParticleSystem.prototype, "colorBySpeed", void 0);
__decorate([
serializable(TextureSheetAnimationModule)
], ParticleSystem.prototype, "textureSheetAnimation", void 0);
__decorate([
serializable(RotationOverLifetimeModule)
], ParticleSystem.prototype, "rotationOverLifetime", void 0);
__decorate([
serializable(RotationBySpeedModule)
], ParticleSystem.prototype, "rotationBySpeed", void 0);
__decorate([
serializable(SizeBySpeedModule)
], ParticleSystem.prototype, "sizeBySpeed", void 0);
/** @internal */
export class SubEmitterSystem {
particleSystem;
emitProbability = 1;
properties;
type;
_deserialize(_context, gameObject) {
const ps = this.particleSystem;
if (ps instanceof ParticleSystem)
return;
let guid = "";
if (ps && typeof ps["guid"] === "string") {
guid = ps["guid"];
// subemitter MUST be a child of the particle system
this.particleSystem = GameObject.findByGuid(guid, gameObject);
}
if (debug && !(this.particleSystem instanceof ParticleSystem)) {
console.warn("Could not find particle system for sub emitter", guid, gameObject, this);
}
}
}
function getLocalSimulationScale(system, vec) {
vec.set(1, 1, 1);
if (system.gameObject.parent && system.localspace) {
switch (system.main.scalingMode) {
case ParticleSystemScalingMode.Local:
vec = getWorldScale(system.gameObject.parent, vec);
vec.x = 1 / vec.x;
vec.y = 1 / vec.y;
vec.z = 1 / vec.z;
break;
default:
if (!system["unsupported_scaling_mode"]) {
system["unsupported_scaling_mode"] = true;
const msg = "ParticleSystem scale mode " + ParticleSystemScalingMode[system.main.scalingMode] + " is not supported";
if (isDevEnvironment())
showBalloonWarning(msg);
console.warn(msg, system.name, system);
}
vec = getWorldScale(system.gameObject, vec);
break;
}
}
return vec;
}
//# sourceMappingURL=ParticleSystem.js.map