UNPKG

@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,269 lines (1,081 loc) • 50.8 kB
import { AxesHelper, BackSide, Blending, BufferGeometry, FrontSide, LinearSRGBColorSpace, Material, Matrix4, Mesh, MeshBasicMaterial, MeshStandardMaterial, NormalBlending, Object3D, PlaneGeometry, Quaternion, SpriteMaterial, Texture, Vector3, Vector4 } from "three"; import type { BatchedRenderer, Behavior, BurstParameters, EmissionState, EmitterShape, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, GeneratorMemory, Particle, ParticleSystemParameters, RecordState, TrailSettings, ValueGenerator } from "three.quarks"; import { BatchedParticleRenderer, ConstantColor, ConstantValue, ParticleSystem as _ParticleSystem, RenderMode, TrailParticle, Vector3 as QVector3, 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 { Context } from "../../engine/engine_setup.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, type IParticleSystem, LimitVelocityOverLifetimeModule, MainModule, MinMaxCurve, MinMaxGradient, 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 type { Particle as QParticle, Behavior as QParticleBehaviour, TrailParticle as QTrailParticle } from "three.quarks" export enum SubEmitterType { Birth = 0, Collision = 1, Death = 2, Trigger = 3, Manual = 4, } /** @internal */ export class ParticleSystemRenderer extends Behaviour { @serializable() renderMode?: ParticleSystemRenderMode; @serializable(Material) particleMaterial?: SpriteMaterial | MeshBasicMaterial; @serializable(Material) trailMaterial?: SpriteMaterial | MeshBasicMaterial; // @serializable(Mesh) particleMesh?: Mesh | string; @serializable() maxParticleSize!: number; @serializable() minParticleSize!: number; @serializable() velocityScale?: number; @serializable() cameraVelocityScale?: number; @serializable() lengthScale?: number; 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(): boolean { const res = this.particleMaterial?.transparent ?? false; // console.log(res, this.particleMaterial); return res; } getMaterial(trailEnabled: boolean = 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?: ParticleSystemRenderMode) { let geo: BufferGeometry | null = 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; } } class MinMaxCurveFunction implements FunctionValueGenerator { private _curve: MinMaxCurve; private _factor: number; constructor(curve: MinMaxCurve, factor: number = 1) { this._curve = curve; this._factor = factor; } type: "function" = "function"; startGen(_memory: GeneratorMemory): void { // ... } genValue(_memory: GeneratorMemory, t: number): number { return this._curve.evaluate(t, Math.random()) * this._factor; } toJSON(): FunctionJSON { throw new Error("Method not implemented."); } clone(): FunctionValueGenerator { throw new Error("Method not implemented."); } } class MinMaxGradientFunction implements FunctionColorGenerator { private _curve: MinMaxGradient; constructor(curve: MinMaxGradient) { this._curve = curve; } type: "function" = "function"; startGen(_memory: GeneratorMemory): void { throw new Error("Method not implemented."); } genColor(_memory: GeneratorMemory, color: QVector4, t: number): QVector4 { 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(): FunctionJSON { throw new Error("Method not implemented."); } clone(): FunctionColorGenerator { throw new Error("Method not implemented."); } } abstract class BaseValueGenerator implements ValueGenerator { type: "value" = "value"; toJSON(): FunctionJSON { throw new Error("Method not implemented."); } clone(): ValueGenerator { throw new Error("Method not implemented."); } startGen(_memory: any): void { } abstract genValue(): number; readonly system: ParticleSystem; constructor(system: ParticleSystem) { this.system = system; } } class TextureSheetStartFrameGenerator extends BaseValueGenerator { genValue(): number { return this.system.textureSheetAnimation.getStartIndex(); } } class ParticleSystemEmissionOverTime extends BaseValueGenerator { private _lastPosition: Vector3 = new Vector3(); private _lastDistance: number = 0; update() { const currentPosition = getWorldPosition(this.system.gameObject); this._lastDistance = this._lastPosition.distanceTo(currentPosition) this._lastPosition.copy(currentPosition); } genValue(): number { 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(): number { 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 abstract class ParticleSystemBaseBehaviour implements Behavior { system!: ParticleSystem; get context() { return this.system.context; } constructor(ps?: ParticleSystem) { this.type = Object.getPrototypeOf(this).constructor.name || "ParticleSystemBaseBehaviour"; if (ps) this.system = ps; } type: string; initialize(_particle: Particle): void { } update(_particle: Particle, _delta: number): void { } frameUpdate(_delta: number): void { } toJSON() { throw new Error("Method not implemented."); } clone(): Behavior { throw new Error("Method not implemented."); } reset() { } } const $startFrame = Symbol("startFrame") class TextureSheetAnimationBehaviour extends ParticleSystemBaseBehaviour { type: string = "NeedleTextureSheet" // initialize(_particle: Particle): void { // _particle[$startFrame] = this.system.textureSheetAnimation.getStartIndex(); // } update(particle: Particle, _delta: number) { 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: string = "NeedleRotation" initialize(particle: Particle) { particle[$particleRotation] = Math.random(); } update(particle: Particle, delta: number) { 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: string = "NeedleSize"; private _minSize = 0; private _maxSize = 1; initialize(particle: Particle) { particle[$sizeLerpFactor] = Math.random(); this._minSize = this.system.renderer.minParticleSize; this._maxSize = this.system.renderer.maxParticleSize; } update(particle: Particle, _delta: number): void { 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: string = "NeedleTrail"; initialize(particle: 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: Particle) { if (this.system.trails?.enabled && particle instanceof TrailParticle) { const trailParticle = particle as TrailParticle; 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 as RecordState; 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: string = "NeedleVelocity"; private _gravityDirection = new Vector3(); initialize(particle: Particle): void { 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: Particle, delta: number): void { ////////////////////// // 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: string = "NeedleColor"; initialize(_particle: Particle): void { } private _init(particle: 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: Particle, _delta: number): void { 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 implements ParticleSystemParameters { private readonly system: ParticleSystem; private readonly emission: ParticleSystemEmissionOverTime; private get anim(): TextureSheetAnimationModule { return this.system.textureSheetAnimation; } constructor(system: ParticleSystem) { 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) as Material; } get layers() { return this.system.gameObject.layers; } update() { this.emission.update(); } autoDestroy?: boolean | undefined; get looping() { return this.system.main.loop; } get duration() { return this.system.duration; } get shape(): EmitterShape { return this.system.shape as unknown as EmitterShape; } 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?: ValueGenerator | FunctionValueGenerator | undefined; /** 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?: BurstParameters[] | undefined; onlyUsedByOther?: boolean | undefined; readonly behaviors: Behavior[] = []; 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: TrailSettings = { 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; } private flatWhiteTexture?: Texture; private clonedTexture: { original?: Texture, clone?: Texture } = { original: undefined, clone: undefined }; get texture(): Texture { const mat = this.material; if (mat && mat["map"]) { const original = mat["map"]! as Texture; // 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(): 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 implements EmissionState { burstParticleIndex: number = 0; burstParticleCount: number = 0; isBursting: boolean = false; travelDistance: number = 0; previousWorldPos?: QVector3 | undefined; burstIndex: number = 0; burstWaveIndex: number = 0; time: number = 0; waitEmiting: number = 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 implements IParticleSystem { play(includeChildren: boolean = 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: boolean = 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(); } } private _state?: ParticlesEmissionState; emit(count: number) { 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 as any); this._particleSystem["emitEnded"] = emitEndedState; } } get playOnAwake(): boolean { return this.main.playOnAwake; } set playOnAwake(val: boolean) { this.main.playOnAwake = val; } @serializable(ColorOverLifetimeModule) readonly colorOverLifetime!: ColorOverLifetimeModule; @serializable(MainModule) readonly main!: MainModule; @serializable(EmissionModule) readonly emission!: EmissionModule; @serializable(SizeOverLifetimeModule) readonly sizeOverLifetime!: SizeOverLifetimeModule; @serializable(ShapeModule) readonly shape!: ShapeModule; @serializable(NoiseModule) readonly noise!: NoiseModule; @serializable(TrailModule) readonly trails!: TrailModule; @serializable(VelocityOverLifetimeModule) readonly velocityOverLifetime!: VelocityOverLifetimeModule; @serializable(LimitVelocityOverLifetimeModule) readonly limitVelocityOverLifetime!: LimitVelocityOverLifetimeModule; @serializable(InheritVelocityModule) inheritVelocity!: InheritVelocityModule; @serializable(ColorBySpeedModule) readonly colorBySpeed!: ColorBySpeedModule; @serializable(TextureSheetAnimationModule) readonly textureSheetAnimation!: TextureSheetAnimationModule; @serializable(RotationOverLifetimeModule) readonly rotationOverLifetime!: RotationOverLifetimeModule; @serializable(RotationBySpeedModule) readonly rotationBySpeed!: RotationBySpeedModule; @serializable(SizeBySpeedModule) readonly sizeBySpeed!: SizeBySpeedModule; get renderer(): ParticleSystemRenderer { 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(): number { return this._cameraScale; } private _cameraScale: number = 1; get container(): Object3D { return this._container!; } get worldspace() { return this.main.simulationSpace === ParticleSystemSimulationSpace.World; } get localspace() { return this.main.simulationSpace === ParticleSystemSimulationSpace.Local; } private __worldQuaternion = new Quaternion(); get worldQuaternion(): Quaternion { return this.__worldQuaternion; } private _worldQuaternionInverted = new Quaternion(); get worldQuaternionInverted(): Quaternion { return this._worldQuaternionInverted; } private _worldScale = new Vector3(); get worldScale(): Vector3 { return this._worldScale; } private _worldPositionFrame: number = -1; private _worldPos: Vector3 = new Vector3(); get worldPos(): Vector3 { if (this._worldPositionFrame !== this.context.time.frame) { this._worldPositionFrame = this.context.time.frame; getWorldPosition(this.gameObject, this._worldPos); } return this._worldPos; } get matrixWorld(): Matrix4 { 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: Behavior | ParticleSystemBaseBehaviour): boolean { 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: Behavior | ParticleSystemBaseBehaviour): boolean { 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(): Behavior[] | null { 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(): _ParticleSystem | null { return this._particleSystem ?? null; } private _renderer!: ParticleSystemRenderer; private _batchSystem?: BatchedRenderer; private _particleSystem?: _ParticleSystem; private _interface!: ParticleSystemInterface; // private _system!: System; // private _emitter: Emitter; // private _size!: SizeBehaviour; private _container!: Object3D; private _time: number = 0; private _isPlaying: boolean = true; private _isUsedAsSubsystem: boolean = false; private _didPreWarm: boolean = false; /** called from deserialization */ private set bursts(arr: ParticleBurst[]) { 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; } private _bursts?: ParticleBurst[]; /** called from deserialization */ private set subEmitterSystems(arr: SubEmitterSystem[]) { 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; } private _subEmitterSystems?: SubEmitterSystem[]; /** @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(): void { this._worldPositionFrame = -1; this._renderer = this.gameObject.getComponent(ParticleSystemRenderer) as 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(): void { 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); } private 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); } } private _lastBatchesCount = -1; private onSimulate(dt: number) { 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; } private 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; private 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); } private 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); } } } } /** @internal */ export class SubEmitterSystem { particleSystem?: ParticleSystem; emitProbability: number = 1; properties?: number; type?: SubEmitterType; _deserialize(_context: Context, gameObject: 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) as ParticleSystem; } if (debug && !(this.particleSystem instanceof ParticleSystem)) { console.warn("Could not find particle system for sub emitter", guid, gameObject, this); } } } function getLocalSimulationScale(system: ParticleSystem, vec: Vector3) { vec.set(1, 1, 1); if (system.gameObject.parent && system.localspace) { switch (system.main.scalingMode) { case Particl