@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,413 lines (1,240 loc) • 60.3 kB
text/typescript
import { createNoise4D, type NoiseFunction4D } from 'simplex-noise';
import { BufferGeometry, Color, Euler, Matrix4, Mesh, Object3D, Quaternion, Triangle, Vector2, Vector3, Vector4 } from "three";
import type { EmitterShape, IParticleSystem as QParticleSystem, Particle, ShapeJSON, Vector3 as QVector3, Vector4 as QVector4 } from "three.quarks";
import { isDevEnvironment } from '../../engine/debug/index.js';
import { Gizmos } from "../../engine/engine_gizmos.js";
import { Mathf } from "../../engine/engine_math.js";
import { serializable } from "../../engine/engine_serialization.js";
import { Context } from "../../engine/engine_setup.js";
import { getTempVector, getWorldQuaternion } from '../../engine/engine_three_utils.js';
import type { Vec2, Vec3 } from "../../engine/engine_types.js";
import { getParam } from "../../engine/engine_utils.js";
import { RGBAColor } from "../../engine/js-extensions/index.js";
import { AnimationCurve } from "../AnimationCurve.js";
import { MeshRenderer } from '../Renderer.js';
const debug = getParam("debugparticles");
declare type Color4 = { r: number, g: number, b: number, a: number };
declare type ColorKey = { time: number, color: Color4 };
declare type AlphaKey = { time: number, alpha: number };
export interface IParticleSystem {
get currentParticles(): number;
get maxParticles(): number;
get time(): number;
get deltaTime(): number;
get duration(): number;
readonly main: MainModule;
get container(): Object3D;
get worldspace(): boolean;
get worldPos(): Vector3;
get worldQuaternion(): Quaternion;
get worldQuaternionInverted(): Quaternion;
get worldScale(): Vector3;
get matrixWorld(): Matrix4;
}
export enum ParticleSystemRenderMode {
Billboard = 0,
Stretch = 1,
HorizontalBillboard = 2,
VerticalBillboard = 3,
Mesh = 4,
// None = 5,
}
export class Gradient {
@serializable()
alphaKeys: Array<AlphaKey> = [];
@serializable()
colorKeys: Array<ColorKey> = [];
get duration(): number {
return 1;
}
evaluate(time: number, target: RGBAColor) {
// target.r = this.colorKeys[0].color.r;
// target.g = this.colorKeys[0].color.g;
// target.b = this.colorKeys[0].color.b;
// target.alpha = this.alphaKeys[0].alpha;
// return;
let closestAlpha: AlphaKey | undefined = undefined;
let closestAlphaIndex = 0;
let closestColor: ColorKey | null = null;
let closestColorIndex = 0;
for (let i = 0; i < this.alphaKeys.length; i++) {
const key = this.alphaKeys[i];
if (key.time < time || !closestAlpha) {
closestAlpha = key;
closestAlphaIndex = i;
}
}
for (let i = 0; i < this.colorKeys.length; i++) {
const key = this.colorKeys[i];
if (key.time < time || !closestColor) {
closestColor = key;
closestColorIndex = i;
}
}
if (closestColor) {
const hasNextColor = closestColorIndex + 1 < this.colorKeys.length;
if (hasNextColor) {
const nextColor = this.colorKeys[closestColorIndex + 1];
const t = Mathf.remap(time, closestColor.time, nextColor.time, 0, 1);
target.r = Mathf.lerp(closestColor.color.r, nextColor.color.r, t);
target.g = Mathf.lerp(closestColor.color.g, nextColor.color.g, t);
target.b = Mathf.lerp(closestColor.color.b, nextColor.color.b, t);
}
else {
target.r = closestColor.color.r;
target.g = closestColor.color.g;
target.b = closestColor.color.b;
}
}
if (closestAlpha) {
const hasNextAlpha = closestAlphaIndex + 1 < this.alphaKeys.length;
if (hasNextAlpha) {
const nextAlpha = this.alphaKeys[closestAlphaIndex + 1];
const t = Mathf.remap(time, closestAlpha.time, nextAlpha.time, 0, 1);
target.alpha = Mathf.lerp(closestAlpha.alpha, nextAlpha.alpha, t);
}
else {
target.alpha = closestAlpha.alpha;
}
}
return target;
}
}
export enum ParticleSystemCurveMode {
Constant = 0,
Curve = 1,
TwoCurves = 2,
TwoConstants = 3
}
declare type ParticleSystemCurveModeKeys = keyof typeof ParticleSystemCurveMode;
export enum ParticleSystemGradientMode {
Color = 0,
Gradient = 1,
TwoColors = 2,
TwoGradients = 3,
RandomColor = 4,
}
declare type ParticleSystemGradientModeKeys = keyof typeof ParticleSystemGradientMode;
export enum ParticleSystemSimulationSpace {
Local = 0,
World = 1,
Custom = 2
}
export enum ParticleSystemShapeType {
Sphere = 0,
SphereShell = 1,
Hemisphere = 2,
HemisphereShell = 3,
Cone = 4,
Box = 5,
Mesh = 6,
ConeShell = 7,
ConeVolume = 8,
ConeVolumeShell = 9,
Circle = 10,
CircleEdge = 11,
SingleSidedEdge = 12,
MeshRenderer = 13,
SkinnedMeshRenderer = 14,
BoxShell = 15,
BoxEdge = 16,
Donut = 17,
Rectangle = 18,
Sprite = 19,
SpriteRenderer = 20
}
export enum ParticleSystemShapeMultiModeValue {
Random = 0,
Loop = 1,
PingPong = 2,
BurstSpread = 3,
}
export class MinMaxCurve {
static constant(val: number) {
const obj = new MinMaxCurve();
obj.setConstant(val);
return obj;
}
static betweenTwoConstants(min: number, max: number) {
const obj = new MinMaxCurve();
obj.setMinMaxConstant(min, max);
return obj;
}
static curve(curve: AnimationCurve, multiplier: number = 1) {
const obj = new MinMaxCurve();
obj.setCurve(curve, multiplier);
return obj;
}
setConstant(val: number) {
this.mode = ParticleSystemCurveMode.Constant;
this.constant = val;
}
setMinMaxConstant(min: number, max: number) {
this.mode = ParticleSystemCurveMode.TwoConstants;
this.constantMin = min;
this.constantMax = max;
}
setCurve(curve: AnimationCurve, multiplier: number = 1) {
this.mode = ParticleSystemCurveMode.Curve;
this.curve = curve;
this.curveMultiplier = multiplier;
}
@serializable()
mode: ParticleSystemCurveMode | ParticleSystemCurveModeKeys = "Constant";
@serializable()
constant!: number;
@serializable()
constantMin!: number;
@serializable()
constantMax!: number;
@serializable(AnimationCurve)
curve?: AnimationCurve;
@serializable(AnimationCurve)
curveMin?: AnimationCurve;
@serializable(AnimationCurve)
curveMax?: AnimationCurve;
@serializable()
curveMultiplier?: number;
clone() {
const clone = new MinMaxCurve();
clone.mode = this.mode;
clone.constant = this.constant;
clone.constantMin = this.constantMin;
clone.constantMax = this.constantMax;
clone.curve = this.curve?.clone();
clone.curveMin = this.curveMin?.clone();
clone.curveMax = this.curveMax?.clone();
clone.curveMultiplier = this.curveMultiplier;
return clone;
}
evaluate(t01: number, lerpFactor?: number): number {
const t = lerpFactor === undefined ? Math.random() : lerpFactor;
switch (this.mode) {
case ParticleSystemCurveMode.Constant:
case "Constant":
return this.constant;
case ParticleSystemCurveMode.Curve:
case "Curve":
t01 = Mathf.clamp01(t01);
return this.curve!.evaluate(t01) * this.curveMultiplier!;
case ParticleSystemCurveMode.TwoCurves:
case "TwoCurves":
const t1 = t01 * this.curveMin!.duration;
const t2 = t01 * this.curveMax!.duration;
return Mathf.lerp(this.curveMin!.evaluate(t1), this.curveMax!.evaluate(t2), t % 1) * this.curveMultiplier!;
case ParticleSystemCurveMode.TwoConstants:
case "TwoConstants":
return Mathf.lerp(this.constantMin, this.constantMax, t % 1)
default:
this.curveMax!.evaluate(t01) * this.curveMultiplier!;
break;
}
return 0;
}
getMax(): number {
switch (this.mode) {
case ParticleSystemCurveMode.Constant:
case "Constant":
return this.constant;
case ParticleSystemCurveMode.Curve:
case "Curve":
return this.getMaxFromCurve(this.curve!) * this.curveMultiplier!;
case ParticleSystemCurveMode.TwoCurves:
case "TwoCurves":
return Math.max(this.getMaxFromCurve(this.curveMin), this.getMaxFromCurve(this.curveMax)) * this.curveMultiplier!;
case ParticleSystemCurveMode.TwoConstants:
case "TwoConstants":
return Math.max(this.constantMin, this.constantMax);
default:
return 0;
}
}
private getMaxFromCurve(curve?: AnimationCurve) {
if (!curve) return 0;
let maxNumber = Number.MIN_VALUE;
for (let i = 0; i < curve!.keys.length; i++) {
const key = curve!.keys[i];
if (key.value > maxNumber) {
maxNumber = key.value;
}
}
return maxNumber;
}
}
export class MinMaxGradient {
static constant(color: RGBAColor | Color) {
const obj = new MinMaxGradient();
obj.constant(color);
return obj;
}
static betweenTwoColors(color1: RGBAColor | Color, color2: RGBAColor | Color) {
const obj = new MinMaxGradient();
obj.betweenTwoColors(color1, color2);
return obj;
}
constant(color: RGBAColor | Color) {
this.mode = ParticleSystemGradientMode.Color;
this.color = color;
return this;
}
betweenTwoColors(color1: RGBAColor | Color, color2: RGBAColor | Color) {
this.mode = ParticleSystemGradientMode.TwoColors;
this.colorMin = color1;
this.colorMax = color2;
return this;
}
/**
* The mode of the gradient, which can be Color, Gradient, TwoColors or TwoGradients.
*/
@serializable()
mode: ParticleSystemGradientMode | ParticleSystemGradientModeKeys = ParticleSystemGradientMode.Color;
@serializable(RGBAColor)
color!: RGBAColor | Color;
@serializable(RGBAColor)
colorMin!: RGBAColor | Color;
@serializable(RGBAColor)
colorMax!: RGBAColor | Color;
@serializable(Gradient)
gradient!: Gradient;
@serializable(Gradient)
gradientMin!: Gradient;
@serializable(Gradient)
gradientMax!: Gradient;
private static _temp: RGBAColor = new RGBAColor(0, 0, 0, 1);
private static _temp2: RGBAColor = new RGBAColor(0, 0, 0, 1);
evaluate(t01: number, lerpFactor?: number): RGBAColor | Color {
const t = lerpFactor === undefined ? Math.random() : lerpFactor;
switch (this.mode) {
case ParticleSystemGradientMode.Color:
case "Color":
return this.color;
case ParticleSystemGradientMode.Gradient:
case "Gradient":
this.gradient.evaluate(t01, MinMaxGradient._temp);
return MinMaxGradient._temp
case ParticleSystemGradientMode.TwoColors:
case "TwoColors":
const col1 = MinMaxGradient._temp.lerpColors(this.colorMin, this.colorMax, t);
return col1;
case ParticleSystemGradientMode.TwoGradients:
case "TwoGradients":
this.gradientMin.evaluate(t01, MinMaxGradient._temp);
this.gradientMax.evaluate(t01, MinMaxGradient._temp2);
return MinMaxGradient._temp.lerp(MinMaxGradient._temp2, t);
case ParticleSystemGradientMode.RandomColor:
case "RandomColor":
const random_t = Math.random();
this.gradientMin.evaluate(t01, MinMaxGradient._temp);
this.gradientMax.evaluate(t01, MinMaxGradient._temp2);
return MinMaxGradient._temp.lerp(MinMaxGradient._temp2, random_t);
}
// console.warn("Not implemented", ParticleSystemGradientMode[this.mode]);
MinMaxGradient._temp.set(0xffffff)
MinMaxGradient._temp.alpha = 1;
return MinMaxGradient._temp;
}
}
export enum ParticleSystemScalingMode {
Hierarchy = 0,
Local = 1,
Shape = 2,
}
export class MainModule {
cullingMode!: number;
duration!: number;
emitterVelocityMode!: number;
flipRotation!: number;
@serializable(MinMaxCurve)
gravityModifier!: MinMaxCurve;
gravityModifierMultiplier!: number;
loop!: boolean;
maxParticles!: number;
playOnAwake!: boolean;
prewarm!: boolean;
ringBufferLoopRange!: { x: number, y: number };
ringBufferMode!: boolean;
scalingMode!: ParticleSystemScalingMode;
simulationSpace!: ParticleSystemSimulationSpace;
simulationSpeed!: number;
@serializable(MinMaxGradient)
startColor!: MinMaxGradient;
@serializable(MinMaxCurve)
startDelay!: MinMaxCurve;
startDelayMultiplier!: number;
@serializable(MinMaxCurve)
startLifetime!: MinMaxCurve;
startLifetimeMultiplier!: number;
@serializable(MinMaxCurve)
startRotation!: MinMaxCurve;
startRotationMultiplier!: number;
startRotation3D!: boolean;
@serializable(MinMaxCurve)
startRotationX!: MinMaxCurve;
startRotationXMultiplier!: number;
@serializable(MinMaxCurve)
startRotationY!: MinMaxCurve;
startRotationYMultiplier!: number;
@serializable(MinMaxCurve)
startRotationZ!: MinMaxCurve;
startRotationZMultiplier!: number;
@serializable(MinMaxCurve)
startSize!: MinMaxCurve;
startSize3D!: boolean;
startSizeMultiplier!: number;
@serializable(MinMaxCurve)
startSizeX!: MinMaxCurve;
startSizeXMultiplier!: number;
@serializable(MinMaxCurve)
startSizeY!: MinMaxCurve;
startSizeYMultiplier!: number;
@serializable(MinMaxCurve)
startSizeZ!: MinMaxCurve;
startSizeZMultiplier!: number;
@serializable(MinMaxCurve)
startSpeed!: MinMaxCurve;
startSpeedMultiplier!: number;
stopAction!: number;
useUnscaledTime!: boolean;
}
export class ParticleBurst {
cycleCount!: number;
maxCount!: number;
minCount!: number;
probability!: number;
repeatInterval!: number;
time!: number;
count!: {
constant: number;
constantMax: number;
constantMin: number;
curve?: AnimationCurve;
curveMax?: AnimationCurve;
curveMin?: AnimationCurve;
curveMultiplier?: number;
mode: ParticleSystemCurveMode;
}
private _performed: number = 0;
reset() {
this._performed = 0;
}
run(time: number): number {
if (time <= this.time) {
return 0;
}
let amount = 0;
if (this.cycleCount === 0 || this._performed < this.cycleCount) {
const nextTime = this.time + this.repeatInterval * this._performed;
if (time >= nextTime) {
this._performed += 1;
if (Math.random() < this.probability) {
switch (this.count.mode) {
case ParticleSystemCurveMode.Constant:
amount = this.count.constant;
break;
case ParticleSystemCurveMode.TwoConstants:
amount = Mathf.lerp(this.count.constantMin, this.count.constantMax, Math.random());
break;
case ParticleSystemCurveMode.Curve:
amount = this.count.curve!.evaluate(Math.random());
break;
case ParticleSystemCurveMode.TwoCurves:
const t = Math.random();
amount = Mathf.lerp(this.count.curveMin!.evaluate(t), this.count.curveMax!.evaluate(t), Math.random());
break;
}
}
}
}
return amount;
}
}
export class EmissionModule {
@serializable()
enabled!: boolean;
get burstCount() {
return this.bursts?.length ?? 0;
}
@serializable()
bursts!: ParticleBurst[];
@serializable(MinMaxCurve)
rateOverTime!: MinMaxCurve;
@serializable()
rateOverTimeMultiplier!: number;
@serializable(MinMaxCurve)
rateOverDistance!: MinMaxCurve;
@serializable()
rateOverDistanceMultiplier!: number;
/** set from system */
system!: IParticleSystem;
reset() {
this.bursts?.forEach(b => b.reset());
}
getBurst() {
let amount = 0;
if (this.burstCount > 0) {
for (let i = 0; i < this.burstCount; i++) {
const burst = this.bursts[i];
if (this.system.main.loop && burst.time >= this.system.time) {
burst.reset();
}
amount += Math.round(burst.run(this.system.time));
}
}
return amount;
}
}
export class ColorOverLifetimeModule {
enabled!: boolean;
@serializable(MinMaxGradient)
color!: MinMaxGradient;
}
export class SizeOverLifetimeModule {
enabled!: boolean;
separateAxes!: boolean;
@serializable(MinMaxCurve)
size!: MinMaxCurve;
sizeMultiplier!: number;
@serializable(MinMaxCurve)
x!: MinMaxCurve;
xMultiplier!: number;
@serializable(MinMaxCurve)
y!: MinMaxCurve;
yMultiplier!: number;
@serializable(MinMaxCurve)
z!: MinMaxCurve;
zMultiplier!: number;
private _time: number = 0;
private _temp = new Vector3();
evaluate(t01: number, target?: Vec3, lerpFactor?: number) {
if (!target) target = this._temp;
if (!this.enabled) {
target.x = target.y = target.z = 1;
return target;
}
if (!this.separateAxes) {
const scale = this.size.evaluate(t01, lerpFactor) * this.sizeMultiplier;
target.x = scale;
// target.y = scale;
// target.z = scale;
}
else {
target.x = this.x.evaluate(t01, lerpFactor) * this.xMultiplier;
target.y = this.y.evaluate(t01, lerpFactor) * this.yMultiplier;
target.z = this.z.evaluate(t01, lerpFactor) * this.zMultiplier;
}
return target;
}
}
export enum ParticleSystemMeshShapeType {
Vertex = 0,
Edge = 1,
Triangle = 2,
}
export class ShapeModule implements EmitterShape {
// Emittershape start
get type(): string {
return ParticleSystemShapeType[this.shapeType];
}
initialize(particle: Particle): void {
this.onInitialize(particle);
particle.position.x = this._vector.x;
particle.position.y = this._vector.y;
particle.position.z = this._vector.z;
}
toJSON(): ShapeJSON {
return this;
}
clone(): EmitterShape {
return new ShapeModule();
}
// EmitterShape end
@serializable()
shapeType: ParticleSystemShapeType = ParticleSystemShapeType.Box;
@serializable()
enabled: boolean = true;
@serializable()
alignToDirection: boolean = false;
@serializable()
angle: number = 0;
@serializable()
arc: number = 360;
@serializable()
arcSpread!: number;
@serializable()
arcSpeedMultiplier!: number;
@serializable()
arcMode!: ParticleSystemShapeMultiModeValue;
@serializable(Vector3)
boxThickness!: Vector3;
@serializable(Vector3)
position!: Vector3;
@serializable(Vector3)
rotation!: Vector3;
private _rotation: Euler = new Euler();
@serializable(Vector3)
scale!: Vector3;
@serializable()
radius!: number;
@serializable()
radiusThickness!: number;
@serializable()
sphericalDirectionAmount!: number;
@serializable()
randomDirectionAmount!: number;
@serializable()
randomPositionAmount!: number;
/** Controls if particles should spawn off vertices, faces or edges. `shapeType` must be set to `MeshRenderer` */
@serializable()
meshShapeType?: ParticleSystemMeshShapeType;
/** When assigned and `shapeType` is set to `MeshRenderer` particles will spawn using a mesh in the scene.
* Use the `meshShapeType` to choose if particles should be spawned from vertices, faces or edges
* To re-assign use the `setMesh` function to cache the mesh and geometry
* */
@serializable(MeshRenderer)
meshRenderer?: MeshRenderer;
private _meshObj?: Mesh;
private _meshGeometry?: BufferGeometry;
setMesh(mesh: MeshRenderer) {
this.meshRenderer = mesh;
if (mesh) {
this._meshObj = mesh.sharedMeshes[Math.floor(Math.random() * mesh.sharedMeshes.length)];
this._meshGeometry = this._meshObj.geometry;
}
else {
this._meshObj = undefined;
this._meshGeometry = undefined;
}
}
private system!: IParticleSystem;
private _space?: ParticleSystemSimulationSpace;
private readonly _worldSpaceMatrix: Matrix4 = new Matrix4();
private readonly _worldSpaceMatrixInverse: Matrix4 = new Matrix4();
constructor() {
if (debug)
console.log(this);
}
update(_system: QParticleSystem, _delta: number): void {
/* this is called by quarks */
}
onUpdate(system: IParticleSystem, _context: Context, simulationSpace: ParticleSystemSimulationSpace, obj: Object3D) {
this.system = system;
this._space = simulationSpace;
if (simulationSpace === ParticleSystemSimulationSpace.World) {
this._worldSpaceMatrix.copy(obj.matrixWorld);
// set scale to 1
this._worldSpaceMatrix.elements[0] = 1;
this._worldSpaceMatrix.elements[5] = 1;
this._worldSpaceMatrix.elements[10] = 1;
this._worldSpaceMatrixInverse.copy(this._worldSpaceMatrix).invert();
}
}
private applyRotation(vector: Vector3) {
const isRotated = this.rotation.x !== 0 || this.rotation.y !== 0 || this.rotation.z !== 0;
if (isRotated) {
// console.log(this._rotation);
// TODO: we need to convert this to threejs euler
this._rotation.x = Mathf.toRadians(this.rotation.x);
this._rotation.y = Mathf.toRadians(this.rotation.y);
this._rotation.z = Mathf.toRadians(this.rotation.z);
this._rotation.order = 'ZYX';
vector.applyEuler(this._rotation);
// this._quat.setFromEuler(this._rotation);
// // this._quat.invert();
// this._quat.x *= -1;
// // this._quat.y *= -1;
// // this._quat.z *= -1;
// this._quat.w *= -1;
// vector.applyQuaternion(this._quat);
}
return isRotated;
}
/** nebula implementations: */
/** initializer implementation */
private _vector: Vector3 = new Vector3(0, 0, 0);
private _temp: Vector3 = new Vector3(0, 0, 0);
private _triangle: Triangle = new Triangle();
onInitialize(particle: Particle): void {
this._vector.set(0, 0, 0);
// remove mesh from particle in case it got destroyed (we dont want to keep a reference to a destroyed mesh in the particle system)
particle["mesh"] = undefined;
particle["mesh_geometry"] = undefined;
const pos = this._temp.copy(this.position);
const isWorldSpace = this._space === ParticleSystemSimulationSpace.World;
if (isWorldSpace) {
pos.applyQuaternion(this.system.worldQuaternion);
}
let radius = this.radius;
if (isWorldSpace) radius *= this.system.worldScale.x;
if (this.enabled) {
switch (this.shapeType) {
case ParticleSystemShapeType.Box:
if (debug) Gizmos.DrawWireBox(this.position, this.scale, 0xdddddd, 1);
this._vector.x = Math.random() * this.scale.x - this.scale.x / 2;
this._vector.y = Math.random() * this.scale.y - this.scale.y / 2;
this._vector.z = Math.random() * this.scale.z - this.scale.z / 2;
this._vector.add(pos);
break;
case ParticleSystemShapeType.Cone:
this.randomConePoint(this.position, this.angle, radius, this.radiusThickness, this.arc, this.arcMode, this._vector);
break;
case ParticleSystemShapeType.Sphere:
this.randomSpherePoint(this.position, radius, this.radiusThickness, this.arc, this._vector);
break;
case ParticleSystemShapeType.Circle:
this.randomCirclePoint(this.position, radius, this.radiusThickness, this.arc, this._vector);
break;
case ParticleSystemShapeType.MeshRenderer:
const renderer = this.meshRenderer;
if (renderer?.destroyed == false) this.setMesh(renderer);
const mesh = particle["mesh"] = this._meshObj;
const geometry = particle["mesh_geometry"] = this._meshGeometry;
if (mesh && geometry) {
switch (this.meshShapeType) {
case ParticleSystemMeshShapeType.Vertex:
{
const vertices = geometry.getAttribute("position");
const index = Math.floor(Math.random() * vertices.count);
this._vector.fromBufferAttribute(vertices, index);
this._vector.applyMatrix4(mesh.matrixWorld);
particle["mesh_normal"] = index;
}
break;
case ParticleSystemMeshShapeType.Edge:
break;
case ParticleSystemMeshShapeType.Triangle:
{
const faces = geometry.index;
if (faces) {
let u = Math.random();
let v = Math.random();
if (u + v > 1) {
u = 1 - u;
v = 1 - v;
}
const faceIndex = Math.floor(Math.random() * (faces.count / 3));
let i0 = faceIndex * 3;
let i1 = faceIndex * 3 + 1;
let i2 = faceIndex * 3 + 2;
i0 = faces.getX(i0);
i1 = faces.getX(i1);
i2 = faces.getX(i2);
const positionAttribute = geometry.getAttribute("position");
this._triangle.a.fromBufferAttribute(positionAttribute, i0);
this._triangle.b.fromBufferAttribute(positionAttribute, i1);
this._triangle.c.fromBufferAttribute(positionAttribute, i2);
this._vector
.set(0, 0, 0)
.addScaledVector(this._triangle.a, u)
.addScaledVector(this._triangle.b, v)
.addScaledVector(this._triangle.c, 1 - (u + v));
this._vector.applyMatrix4(mesh.matrixWorld);
particle["mesh_normal"] = faceIndex;
}
}
break;
}
}
break;
default:
this._vector.set(0, 0, 0);
if (isDevEnvironment() && !globalThis["__particlesystem_shapetype_unsupported"]) {
console.warn("ParticleSystem ShapeType is not supported:", ParticleSystemShapeType[this.shapeType]);
globalThis["__particlesystem_shapetype_unsupported"] = true;
}
break;
// case ParticleSystemShapeType.Hemisphere:
// randomSpherePoint(this.position.x, this.position.y, this.position.z, this.radius, this.radiusThickness, 180, this._vector);
// break;
}
this.randomizePosition(this._vector, this.randomPositionAmount);
}
this.applyRotation(this._vector);
if (isWorldSpace) {
this._vector.applyQuaternion(this.system.worldQuaternion);
this._vector.add(this.system.worldPos);
}
if (debug) {
Gizmos.DrawSphere(this._vector, .03, 0xff0000, .5, true);
}
}
private _dir: Vector3 = new Vector3();
getDirection(particle: Particle, pos: Vec3): Vector3 {
if (!this.enabled) {
this._dir.set(0, 0, 1);
return this._dir;
}
switch (this.shapeType) {
case ParticleSystemShapeType.Box:
this._dir.set(0, 0, 1);
break;
case ParticleSystemShapeType.Cone:
this._dir.set(0, 0, 1);
// apply cone angle
// this._dir.applyAxisAngle(new Vector3(0, 1, 0), Mathf.toRadians(this.angle));
break;
case ParticleSystemShapeType.Circle:
case ParticleSystemShapeType.Sphere:
const rx = pos.x;
const ry = pos.y;
const rz = pos.z;
this._dir.set(rx, ry, rz)
if (this.system?.worldspace)
this._dir.sub(this.system.worldPos)
else
this._dir.sub(this.position)
break;
case ParticleSystemShapeType.MeshRenderer:
const mesh = particle["mesh"];
const geometry = particle["mesh_geometry"];
if (mesh && geometry) {
switch (this.meshShapeType) {
case ParticleSystemMeshShapeType.Vertex:
{
const normal = geometry.getAttribute("normal");
const index = particle["mesh_normal"];
this._dir.fromBufferAttribute(normal, index);
}
break;
case ParticleSystemMeshShapeType.Edge:
break;
case ParticleSystemMeshShapeType.Triangle:
{
const faces = geometry.index;
if (faces) {
const index = particle["mesh_normal"];
const i0 = faces.getX(index * 3);
const i1 = faces.getX(index * 3 + 1);
const i2 = faces.getX(index * 3 + 2);
const positionAttribute = geometry.getAttribute("position");
const a = getTempVector();
const b = getTempVector();
const c = getTempVector();
a.fromBufferAttribute(positionAttribute, i0);
b.fromBufferAttribute(positionAttribute, i1);
c.fromBufferAttribute(positionAttribute, i2);
a.sub(b);
c.sub(b);
a.cross(c);
this._dir.copy(a).multiplyScalar(-1);
const rot = getWorldQuaternion(mesh);
this._dir.applyQuaternion(rot)
}
}
break;
}
}
break;
default:
this._dir.set(0, 0, 1);
break;
}
if (this._space === ParticleSystemSimulationSpace.World) {
this._dir.applyQuaternion(this.system.worldQuaternion);
}
this.applyRotation(this._dir);
this._dir.normalize();
this.spherizeDirection(this._dir, this.sphericalDirectionAmount);
this.randomizeDirection(this._dir, this.randomDirectionAmount);
if (debug) {
Gizmos.DrawSphere(pos, .01, 0x883300, .5, true);
Gizmos.DrawDirection(pos, this._dir, 0x883300, .5, true);
}
return this._dir;
}
private static _randomQuat = new Quaternion();
private static _tempVec = new Vector3();
private randomizePosition(pos: Vector3, amount: number) {
if (amount <= 0) return;
const rp = ShapeModule._tempVec;
rp.set(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1);
rp.x *= amount * this.scale.x;
rp.y *= amount * this.scale.y;
rp.z *= amount * this.scale.z;
pos.add(rp);
}
private randomizeDirection(direction: Vector3, amount: number) {
if (amount === 0) return;
const randomQuat = ShapeModule._randomQuat;
const tempVec = ShapeModule._tempVec;
tempVec.set(Math.random() - .5, Math.random() - .5, Math.random() - .5).normalize();
randomQuat.setFromAxisAngle(tempVec, amount * Math.random() * Math.PI);
direction.applyQuaternion(randomQuat);
}
private spherizeDirection(dir: Vector3, amount: number) {
if (amount === 0) return;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(1 - Math.random() * 2);
const x = Math.sin(phi) * Math.cos(theta);
const y = Math.sin(phi) * Math.sin(theta);
const z = Math.cos(phi);
const v = new Vector3(x, y, z);
dir.lerp(v, amount);
}
private randomSpherePoint(pos: Vec3, radius: number, thickness: number, arc: number, vec: Vec3) {
const u = Math.random();
const v = Math.random();
const theta = 2 * Math.PI * u * (arc / 360);
const phi = Math.acos(2 * v - 1);
const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * (radius);
const x = pos.x + this.scale.x * (-r * Math.sin(phi) * Math.cos(theta));
const y = pos.y + this.scale.y * (r * Math.sin(phi) * Math.sin(theta));
const z = pos.z + this.scale.z * (r * Math.cos(phi));
vec.x = x;
vec.y = y;
vec.z = z;
}
private randomCirclePoint(pos: Vec3, radius: number, thickness: number, arg: number, vec: Vec3) {
const u = Math.random();
const theta = 2 * Math.PI * u * (arg / 360);
const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * (radius);
const x = pos.x + this.scale.x * r * Math.cos(theta);
const y = pos.y + this.scale.y * r * Math.sin(theta);
const z = pos.z;
vec.x = x;
vec.y = y;
vec.z = z;
}
private _loopTime: number = 0;
private _loopDirection: number = 1;
private randomConePoint(pos: Vec3, _angle: number, radius: number, thickness: number, arc: number, arcMode: ParticleSystemShapeMultiModeValue, vec: Vec3) {
let u = 0;
let v = 0;
switch (arcMode) {
case ParticleSystemShapeMultiModeValue.Random:
u = Math.random();
v = Math.random();
break;
case ParticleSystemShapeMultiModeValue.PingPong:
if (this._loopTime > 1) this._loopDirection = -1;
if (this._loopTime < 0) this._loopDirection = 1;
// continue with loop
case ParticleSystemShapeMultiModeValue.Loop:
u = .5;
v = Math.random()
this._loopTime += this.system.deltaTime * this._loopDirection;
break;
}
let theta = 2 * Math.PI * u * (arc / 360);
switch (arcMode) {
case ParticleSystemShapeMultiModeValue.PingPong:
case ParticleSystemShapeMultiModeValue.Loop:
theta += Math.PI + .5;
theta += this._loopTime * Math.PI * 2;
theta %= Mathf.toRadians(arc);
break;
}
const phi = Math.acos(2 * v - 1);
const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * radius;
const x = pos.x + (-r * Math.sin(phi) * Math.cos(theta));
const y = pos.y + (r * Math.sin(phi) * Math.sin(theta));
const z = pos.z;
vec.x = x * this.scale.x;
vec.y = y * this.scale.y;
vec.z = z * this.scale.z;
}
}
export class NoiseModule {
@serializable()
damping!: boolean;
@serializable()
enabled!: boolean;
@serializable()
frequency!: number;
@serializable()
octaveCount!: number;
@serializable()
octaveMultiplier!: number;
@serializable()
octaveScale!: number;
@serializable(MinMaxCurve)
positionAmount!: MinMaxCurve;
@serializable()
quality!: number;
@serializable(MinMaxCurve)
remap!: MinMaxCurve;
@serializable()
remapEnabled!: boolean;
@serializable()
remapMultiplier!: number;
@serializable(MinMaxCurve)
remapX!: MinMaxCurve;
@serializable()
remapXMultiplier!: number;
@serializable(MinMaxCurve)
remapY!: MinMaxCurve;
@serializable()
remapYMultiplier!: number;
@serializable(MinMaxCurve)
remapZ!: MinMaxCurve;
@serializable()
remapZMultiplier!: number;
@serializable()
scrollSpeedMultiplier!: number;
@serializable()
separateAxes!: boolean;
@serializable()
strengthMultiplier!: number;
@serializable(MinMaxCurve)
strengthX!: MinMaxCurve;
@serializable()
strengthXMultiplier!: number;
@serializable(MinMaxCurve)
strengthY!: MinMaxCurve;
@serializable()
strengthYMultiplier!: number;
@serializable(MinMaxCurve)
strengthZ!: MinMaxCurve;
@serializable()
strengthZMultiplier!: number;
private _noise?: NoiseFunction4D;
private _time: number = 0;
update(context: Context) {
this._time += context.time.deltaTime * this.scrollSpeedMultiplier;
}
/** nebula implementations: */
private _temp: Vector3 = new Vector3();
apply(_index: number, pos: Vec3, vel: Vec3, _deltaTime: number, age: number, life: number) {
if (!this.enabled) return;
if (!this._noise) {
this._noise = createNoise4D(() => 0);
}
const temp = this._temp.set(pos.x, pos.y, pos.z).multiplyScalar(this.frequency);
const nx = this._noise(temp.x, temp.y, temp.z, this._time);
const ny = this._noise(temp.x, temp.y, temp.z, this._time + 1000 * this.frequency);
const nz = this._noise(temp.x, temp.y, temp.z, this._time + 2000 * this.frequency);
this._temp.set(nx, ny, nz).normalize()
const t = age / life;
let strengthFactor = this.positionAmount.evaluate(t);
if (!this.separateAxes) {
if (this.strengthX) {
strengthFactor *= this.strengthX.evaluate(t) * 1.5;
}
// strengthFactor *= this.strengthMultiplier;
// strengthFactor *= deltaTime;
this._temp.multiplyScalar(strengthFactor);
}
else {
this._temp.x *= strengthFactor * this.strengthXMultiplier
this._temp.y *= strengthFactor * this.strengthYMultiplier;
this._temp.z *= strengthFactor * this.strengthZMultiplier;
}
// this._temp.setLength(strengthFactor * deltaTime);
vel.x += this._temp.x;
vel.y += this._temp.y;
vel.z += this._temp.z;
}
}
export enum ParticleSystemTrailMode {
PerParticle,
Ribbon,
}
export enum ParticleSystemTrailTextureMode {
Stretch = 0,
Tile = 1,
DistributePerSegment = 2,
RepeatPerSegment = 3,
}
export class TrailModule {
@serializable()
enabled!: boolean;
@serializable()
attachRibbonToTransform = false;
@serializable(MinMaxGradient)
colorOverLifetime!: MinMaxGradient;
@serializable(MinMaxGradient)
colorOverTrail!: MinMaxGradient;
@serializable()
dieWithParticles: boolean = true;
@serializable()
inheritParticleColor: boolean = true;
@serializable(MinMaxCurve)
lifetime!: MinMaxCurve;
@serializable()
lifetimeMultiplier!: number;
@serializable()
minVertexDistance: number = .2;
@serializable()
mode: ParticleSystemTrailMode = ParticleSystemTrailMode.PerParticle;
@serializable()
ratio: number = 1;
@serializable()
ribbonCount: number = 1;
@serializable()
shadowBias: number = 0;
@serializable()
sizeAffectsLifetime: boolean = false;
@serializable()
sizeAffectsWidth: boolean = false;
@serializable()
splitSubEmitterRibbons: boolean = false;
@serializable()
textureMode: ParticleSystemTrailTextureMode = ParticleSystemTrailTextureMode.Stretch;
@serializable(MinMaxCurve)
widthOverTrail!: MinMaxCurve;
@serializable()
widthOverTrailMultiplier!: number;
@serializable()
worldSpace: boolean = false;
getWidth(size: number, _life01: number, pos01: number, t: number) {
const res = this.widthOverTrail.evaluate(pos01, t);
size *= res;
return size;
}
getColor(color: Vector4 | QVector4, life01: number, pos01: number) {
const overTrail = this.colorOverTrail.evaluate(pos01);
const overLife = this.colorOverLifetime.evaluate(life01);
color.x *= overTrail.r * overLife.r;
color.y *= overTrail.g * overLife.g;
color.z *= overTrail.b * overLife.b;
if ("alpha" in overTrail && "alpha" in overLife)
color.w *= overTrail.alpha * overLife.alpha;
}
}
export class VelocityOverLifetimeModule {
@serializable()
enabled!: boolean;
@serializable()
space: ParticleSystemSimulationSpace = ParticleSystemSimulationSpace.Local;
@serializable(MinMaxCurve)
orbitalX!: MinMaxCurve;
@serializable(MinMaxCurve)
orbitalY!: MinMaxCurve;
@serializable(MinMaxCurve)
orbitalZ!: MinMaxCurve;
@serializable()
orbitalXMultiplier!: number;
@serializable()
orbitalYMultiplier!: number;
@serializable()
orbitalZMultiplier!: number;
@serializable()
orbitalOffsetX!: number;
@serializable()
orbitalOffsetY!: number;
@serializable()
orbitalOffsetZ!: number;
@serializable(MinMaxCurve)
speedModifier!: MinMaxCurve;
@serializable()
speedModifierMultiplier!: number;
@serializable(MinMaxCurve)
x!: MinMaxCurve;
@serializable()
xMultiplier!: number;
@serializable(MinMaxCurve)
y!: MinMaxCurve;
@serializable()
yMultiplier!: number;
@serializable(MinMaxCurve)
z!: MinMaxCurve;
@serializable()
zMultiplier!: number;
private _system?: IParticleSystem;
// private _worldRotation: Quaternion = new Quaternion();
update(system: IParticleSystem) {
this._system = system;
}
private _temp: Vector3 = new Vector3();
private _temp2: Vector3 = new Vector3();
private _temp3: Vector3 = new Vector3();
private _hasOrbital = false;
private _index = 0;
private _orbitalMatrix: Matrix4 = new Matrix4();
init(particle: object) {
if (this._index == 0) particle["debug"] = true;
this._index += 1;
particle["orbitx"] = this.orbitalX.evaluate(Math.random());
particle["orbity"] = this.orbitalY.evaluate(Math.random());
particle["orbitz"] = this.orbitalZ.evaluate(Math.random());
// console.log(particle["orbitx"], particle["orbity"], particle["orbitz"])
this._hasOrbital = particle["orbitx"] != 0 || particle["orbity"] != 0 || particle["orbitz"] != 0;
}
apply(_particle: object, _index: number, _pos: Vec3, vel: Vec3, _dt: number, age: number, life: number) {
if (!this.enabled) return;
const t = age / life;
const speed = this.speedModifier.evaluate(t) * this.speedModifierMultiplier;
const x = this.x.evaluate(t);
const y = this.y.evaluate(t);
const z = this.z.evaluate(t);
this._temp.set(-x, y, z);
if (this._system) {
// if (this.space === ParticleSystemSimulationSpace.World) {
// this._temp.applyQuaternion(this._system.worldQuaternionInverted);
// }
if (this._system.main.simulationSpace === ParticleSystemSimulationSpace.World) {
this._temp.applyQuaternion(this._system.worldQuaternion);
}
}
if (this._hasOrbital) {
const position = this._system?.worldPos;
if (position) {
// TODO: we absolutely need to fix this, this is a hack for a specific usecase and doesnt work yet correctly
// https://github.com/needle-tools/needle-tiny/issues/710
const pos = this._temp2.set(_pos.x, _pos.y, _pos.z);
const ox = this.orbitalXMultiplier;// particle["orbitx"];
const oy = this.orbitalYMultiplier;// particle["orbity"];
const oz = this.orbitalZMultiplier;// particle["orbitz"];
const angle = speed * Math.PI * 2 * 10; // < Oh god
const cosX = Math.cos(angle * ox);
const sinX = Math.sin(angle * ox);
const cosY = Math.cos(angle * oy);
const sinY = Math.sin(angle * oy);
const cosZ = Math.cos(angle * oz);
const sinZ = Math.sin(angle * oz);
const newX = pos.x * (cosY * cosZ) + pos.y * (cosY * sinZ) + pos.z * (-sinY);
const newY = pos.x * (sinX * sinY * cosZ - cosX * sinZ) + pos.y * (sinX * sinY * sinZ + cosX * cosZ) + pos.z * (sinX * cosY);
const newZ = pos.x * (cosX * sinY * cosZ + sinX * sinZ) + pos.y * (cosX * sinY * sinZ - sinX * cosZ) + pos.z * (cosX * cosY);
// pos.x += this.orbitalOffsetX;
// pos.y += this.orbitalOffsetY;
// pos.z += this.orbitalOffsetZ;
const v = this._temp3.set(pos.x - newX, pos.y - newY, pos.z - newZ);
v.normalize();
v.multiplyScalar(.2 / _dt * (Math.max(this.orbitalXMultiplier, this.orbitalYMultiplier, this.orbitalZMultiplier)));
vel.x += v.x;
vel.y += v.y;
vel.z += v.z;
}
}
vel.x += this._temp.x;
vel.y += this._temp.y;
vel.z += this._temp.z;
vel.x *= speed;
vel.y *= speed;
vel.z *= speed;
}
}
enum ParticleSystemAnimationTimeMode {
Lifetime,
Speed,
FPS,
}
enum ParticleSystemAnimationMode {
Grid,
Sprites,
}
enum ParticleSystemAnimationRowMode {
Custom,
Random,
MeshIndex,
}
enum ParticleSystemAnimationType {
WholeSheet,
SingleRow,
}
export class TextureSheetAnimationModule {
@serializable()
animation!: ParticleSystemAnimationType;
@serializable()
enabled!: boolean;
@serializable()
cycleCount!: number;
@serializable(MinMaxCurve)
frameOverTime!: MinMaxCurve;
@serializable()
frameOverTimeMultiplier!: number;
@serializable()
numTilesX!: number;
@serializable()
numTilesY!: number;
@serializable(MinMaxCurve)
startFrame!: MinMaxCurve;