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,413 lines (1,240 loc) • 60.3 kB
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;