diff --git a/examples/trailDemo.js b/examples/trailDemo.js index 3cddb7b..c9d99e8 100644 --- a/examples/trailDemo.js +++ b/examples/trailDemo.js @@ -29,6 +29,7 @@ import { Gradient, } from './js/three.quarks.esm.js'; import {Demo} from './demo.js'; + export class TrailDemo extends Demo { name = 'Trail Renderer and Physics'; diff --git a/src/BatchedRenderer.ts b/src/BatchedRenderer.ts index 44eb624..f5b1707 100644 --- a/src/BatchedRenderer.ts +++ b/src/BatchedRenderer.ts @@ -1,8 +1,37 @@ import {VFXBatch, RenderMode, StoredBatchSettings} from './VFXBatch'; -import {ParticleSystem, VFXBatchSettings} from './ParticleSystem'; -import {Object3D} from 'three'; +import {BufferGeometry, Layers, Material, Object3D} from 'three'; import {SpriteBatch} from './SpriteBatch'; import {TrailBatch} from './TrailBatch'; +import {ParticleEmitter} from './ParticleEmitter'; +import {Particle} from './Particle'; + +export interface VFXBatchSettings { + // 5 component x,y,z,u,v + instancingGeometry: BufferGeometry; + material: Material; + uTileCount: number; + vTileCount: number; + renderMode: RenderMode; + renderOrder: number; + layers: Layers; +} +export interface SerializationOptions { + useUrlForImage?: boolean; +} +export interface IParticleSystem { + speedFactor: number; + worldSpace: boolean; + particleNum: number; + particles: Array; + emitter: ParticleEmitter; + _renderer?: BatchedRenderer; + + getRendererSettings(): VFXBatchSettings; + + clone(): IParticleSystem; + + toJSON(metaData: any, options: SerializationOptions): any; +} /** * the class represents the batch renderer. a three.js scene should only have one batchedRenderer @@ -11,7 +40,7 @@ import {TrailBatch} from './TrailBatch'; */ export class BatchedRenderer extends Object3D { batches: Array = []; - systemToBatchIndex: Map = new Map(); + systemToBatchIndex: Map = new Map(); type = 'BatchedRenderer'; constructor() { @@ -35,7 +64,7 @@ export class BatchedRenderer extends Object3D { ); } - addSystem(system: ParticleSystem) { + addSystem(system: IParticleSystem) { system._renderer = this; const settings = system.getRendererSettings(); for (let i = 0; i < this.batches.length; i++) { @@ -64,7 +93,7 @@ export class BatchedRenderer extends Object3D { this.add(batch); } - deleteSystem(system: ParticleSystem) { + deleteSystem(system: IParticleSystem) { const batchIndex = this.systemToBatchIndex.get(system); if (batchIndex != undefined) { this.batches[batchIndex].removeSystem(system); @@ -79,7 +108,7 @@ export class BatchedRenderer extends Object3D { }*/ } - updateSystem(system: ParticleSystem) { + updateSystem(system: IParticleSystem) { this.deleteSystem(system); this.addSystem(system); } diff --git a/src/ParticleEmitter.ts b/src/ParticleEmitter.ts index ade5b08..7409a25 100644 --- a/src/ParticleEmitter.ts +++ b/src/ParticleEmitter.ts @@ -1,8 +1,5 @@ - -import {ParticleSystem, SerializationOptions} from './ParticleSystem'; -import { - Object3D, BaseEvent -} from 'three'; +import {Object3D, BaseEvent} from 'three'; +import {IParticleSystem, SerializationOptions} from './BatchedRenderer'; export interface MetaData { geometries: {[key: string]: any}; @@ -16,12 +13,11 @@ export interface MetaData { } export class ParticleEmitter extends Object3D { - - type = "ParticleEmitter"; - system: ParticleSystem; + type = 'ParticleEmitter'; + system: IParticleSystem; //interleavedBuffer: InterleavedBuffer; - constructor(system: ParticleSystem) { + constructor(system: IParticleSystem) { super(); this.system = system; // this.visible = false; @@ -34,34 +30,29 @@ export class ParticleEmitter extends Object3D { return system.emitter as any; } - dispose() { - } - + dispose() {} // extract data from the cache hash // remove metadata on each item // and return as array - extractFromCache( cache: any ) { + extractFromCache(cache: any) { const values = []; - for ( const key in cache ) { - - const data = cache[ key ]; + for (const key in cache) { + const data = cache[key]; delete data.metadata; - values.push( data ); - + values.push(data); } return values; } toJSON(meta?: MetaData, options: SerializationOptions = {}): any { - // meta is a string when called from JSON.stringify - const isRootObject = ( meta === undefined || typeof meta === 'string' ); + // meta is a string when called from JSON.stringify + const isRootObject = meta === undefined || typeof meta === 'string'; const output: any = {}; - // meta is a hash used to collect geometries, materials. - // not providing it implies that this is the root object - // being serialized. - if ( isRootObject ) { - + // meta is a hash used to collect geometries, materials. + // not providing it implies that this is the root object + // being serialized. + if (isRootObject) { // initialize meta obj meta = { geometries: {}, @@ -71,67 +62,66 @@ export class ParticleEmitter extends Object3D { shapes: {}, skeletons: {}, animations: {}, - nodes: {} + nodes: {}, }; output.metadata = { version: 4.5, type: 'Object', - generator: 'Object3D.toJSON' + generator: 'Object3D.toJSON', }; - } - // standard Object3D serialization - const object: any = {}; + // standard Object3D serialization + const object: any = {}; object.uuid = this.uuid; object.type = this.type; - if ( this.name !== '' ) object.name = this.name; - if ( this.castShadow === true ) object.castShadow = true; - if ( this.receiveShadow === true ) object.receiveShadow = true; - if ( this.visible === false ) object.visible = false; - if ( this.frustumCulled === false ) object.frustumCulled = false; - if ( this.renderOrder !== 0 ) object.renderOrder = this.renderOrder; - if ( JSON.stringify( this.userData ) !== '{}' ) object.userData = this.userData; + if (this.name !== '') object.name = this.name; + if (this.castShadow === true) object.castShadow = true; + if (this.receiveShadow === true) object.receiveShadow = true; + if (this.visible === false) object.visible = false; + if (this.frustumCulled === false) object.frustumCulled = false; + if (this.renderOrder !== 0) object.renderOrder = this.renderOrder; + if (JSON.stringify(this.userData) !== '{}') object.userData = this.userData; - object.layers = this.layers.mask; - object.matrix = this.matrix.toArray(); + object.layers = this.layers.mask; + object.matrix = this.matrix.toArray(); - if ( this.matrixAutoUpdate === false ) object.matrixAutoUpdate = false; + if (this.matrixAutoUpdate === false) object.matrixAutoUpdate = false; - // object specific properties + // object specific properties // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if ( this.system !== null ) object.ps = this.system.toJSON(meta!, options); + if (this.system !== null) object.ps = this.system.toJSON(meta!, options); - if ( this.children.length > 0 ) { - object.children = []; - for ( let i = 0; i < this.children.length; i ++ ) { - if (this.children[i].type !== "ParticleSystemPreview") { + if (this.children.length > 0) { + object.children = []; + for (let i = 0; i < this.children.length; i++) { + if (this.children[i].type !== 'ParticleSystemPreview') { object.children.push(this.children[i].toJSON(meta).object); } } - } + } - if ( isRootObject ) { + if (isRootObject) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const geometries = this.extractFromCache( meta!.geometries ); + const geometries = this.extractFromCache(meta!.geometries); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const materials = this.extractFromCache( meta!.materials ); + const materials = this.extractFromCache(meta!.materials); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const textures = this.extractFromCache( meta!.textures ); + const textures = this.extractFromCache(meta!.textures); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const images = this.extractFromCache( meta!.images ); + const images = this.extractFromCache(meta!.images); - if ( geometries.length > 0 ) output.geometries = geometries; - if ( materials.length > 0 ) output.materials = materials; - if ( textures.length > 0 ) output.textures = textures; - if ( images.length > 0 ) output.images = images; - } + if (geometries.length > 0) output.geometries = geometries; + if (materials.length > 0) output.materials = materials; + if (textures.length > 0) output.textures = textures; + if (images.length > 0) output.images = images; + } - output.object = object; - return output; + output.object = object; + return output; } } diff --git a/src/ParticleSystem.ts b/src/ParticleSystem.ts index 62f06ca..bb968ac 100644 --- a/src/ParticleSystem.ts +++ b/src/ParticleSystem.ts @@ -1,8 +1,8 @@ -import {FunctionValueGenerator, ValueGenerator, ValueGeneratorFromJSON} from './functions/ValueGenerator'; -import {Behavior, BehaviorFromJSON} from './behaviors/Behavior'; +import {FunctionValueGenerator, ValueGenerator, ValueGeneratorFromJSON} from './functions'; +import {Behavior, BehaviorFromJSON} from './behaviors'; import {Particle, SpriteParticle, TrailParticle} from './Particle'; import {MetaData, ParticleEmitter} from './ParticleEmitter'; -import {EmitterFromJSON, EmitterShape, ShapeJSON} from './shape/EmitterShape'; +import {EmitterFromJSON, EmitterShape, ShapeJSON} from './shape'; import { AdditiveBlending, BaseEvent, @@ -34,19 +34,8 @@ import { MemorizedFunctionColorGenerator, } from './functions'; import {RenderMode} from './VFXBatch'; -import {BatchedRenderer} from './BatchedRenderer'; +import {BatchedRenderer, SerializationOptions, VFXBatchSettings} from './BatchedRenderer'; import {RotationGenerator} from './functions/RotationGenerator'; - -export interface VFXBatchSettings { - // 5 component x,y,z,u,v - instancingGeometry: BufferGeometry; - material: Material; - uTileCount: number; - vTileCount: number; - renderMode: RenderMode; - renderOrder: number; - layers: Layers; -} export interface BurstParameters { time: number; count: number; @@ -167,10 +156,6 @@ export interface EmissionState { waitEmiting: number; } -export interface SerializationOptions { - useUrlForImage?: boolean; -} - /** * ParticleSystem represents a system that generates and controls particles with similar attributes. * diff --git a/src/QuarksLoader.ts b/src/QuarksLoader.ts index 8b43af8..89921b8 100644 --- a/src/QuarksLoader.ts +++ b/src/QuarksLoader.ts @@ -23,11 +23,18 @@ import { SkinnedMesh, Mesh, InstancedMesh, - InstancedBufferAttribute, LOD, Line, LineSegments, LineLoop, Points, SpriteMaterial, Bone -} from "three"; -import {ParticleSystem} from "./ParticleSystem"; -import {Behavior, EmitSubParticleSystem} from "./behaviors"; -import {ParticleEmitter} from "./ParticleEmitter"; + InstancedBufferAttribute, + LOD, + Line, + LineSegments, + LineLoop, + Points, + SpriteMaterial, + Bone, +} from 'three'; +import {ParticleSystem} from './ParticleSystem'; +import {Behavior, EmitSubParticleSystem} from './behaviors'; +import {ParticleEmitter} from './ParticleEmitter'; export class QuarksLoader extends ObjectLoader { /*manager: LoadingManager; @@ -43,24 +50,26 @@ export class QuarksLoader extends ObjectLoader { } linkReference(object: Object3D) { - const objectsMap: { [uuid: string]: Object3D } = {}; - object.traverse( function ( child ) { + const objectsMap: {[uuid: string]: Object3D} = {}; + object.traverse(function (child) { objectsMap[child.uuid] = child; - } ); - object.traverse( function ( child ) { - if ( child.type === "ParticleEmitter") { - const system = (child as ParticleEmitter).system; + }); + object.traverse(function (child) { + if (child.type === 'ParticleEmitter') { + const system = (child as ParticleEmitter).system as ParticleSystem; const shape = system.emitterShape; /*if (shape instanceof MeshSurfaceEmitter) { shape.geometry = objectsMap[shape.geometry as any] as Mesh; }*/ - for (let i = 0; i < system.behaviors.length; i ++) { + for (let i = 0; i < system.behaviors.length; i++) { if (system.behaviors[i] instanceof EmitSubParticleSystem) { - (system.behaviors[i] as EmitSubParticleSystem).subParticleSystem = objectsMap[(system.behaviors[i] as EmitSubParticleSystem).subParticleSystem as any] as ParticleEmitter; + (system.behaviors[i] as EmitSubParticleSystem).subParticleSystem = objectsMap[ + (system.behaviors[i] as EmitSubParticleSystem).subParticleSystem as any + ] as ParticleEmitter; } } } - } ); + }); } parse(json: any, onLoad?: (object: Object3D) => void): T { @@ -70,17 +79,20 @@ export class QuarksLoader extends ObjectLoader { } // @ts-ignore - parseObject>(data: any, geometries: any, materials: Material[], textures: Texture[], animations: AnimationClip[]): T { - + parseObject>( + data: any, + geometries: any, + materials: Material[], + textures: Texture[], + animations: AnimationClip[] + ): T { let object; function getGeometry(name: any) { if (geometries[name] === undefined) { console.warn('THREE.ObjectLoader: Undefined geometry', name); - } return geometries[name]; - } function getMaterial(name: any) { @@ -96,7 +108,6 @@ export class QuarksLoader extends ObjectLoader { } return array; - } if (materials[name] === undefined) { @@ -104,7 +115,6 @@ export class QuarksLoader extends ObjectLoader { } return materials[name]; - } function getTexture(uuid: number) { @@ -112,7 +122,6 @@ export class QuarksLoader extends ObjectLoader { console.warn('THREE.ObjectLoader: Undefined texture', uuid); } return textures[uuid]; - } let geometry, material; @@ -125,13 +134,11 @@ export class QuarksLoader extends ObjectLoader { const dependencies: {[uuid: string]: Behavior} = {}; switch (data.type) { - case 'ParticleEmitter': object = ParticleSystem.fromJSON(data.ps, meta as any, dependencies).emitter; break; case 'Scene': - object = new Scene(); if (data.background !== undefined) { if (Number.isInteger(data.background)) { @@ -146,13 +153,11 @@ export class QuarksLoader extends ObjectLoader { } if (data.fog !== undefined) { - if (data.fog.type === 'Fog') { object.fog = new Fog(data.fog.color, data.fog.near, data.fog.far); } else if (data.fog.type === 'FogExp2') { object.fog = new FogExp2(data.fog.color, data.fog.density); } - } if (data.backgroundBlurriness !== undefined) object.backgroundBlurriness = data.backgroundBlurriness; @@ -160,7 +165,6 @@ export class QuarksLoader extends ObjectLoader { break; case 'PerspectiveCamera': - object = new PerspectiveCamera(data.fov, data.aspect, data.near, data.far); if (data.focus !== undefined) object.focus = data.focus; @@ -172,7 +176,6 @@ export class QuarksLoader extends ObjectLoader { break; case 'OrthographicCamera': - object = new OrthographicCamera(data.left, data.right, data.top, data.bottom, data.near, data.far); if (data.zoom !== undefined) object.zoom = data.zoom; @@ -181,49 +184,48 @@ export class QuarksLoader extends ObjectLoader { break; case 'AmbientLight': - object = new AmbientLight(data.color, data.intensity); break; case 'DirectionalLight': - object = new DirectionalLight(data.color, data.intensity); break; case 'PointLight': - object = new PointLight(data.color, data.intensity, data.distance, data.decay); break; case 'RectAreaLight': - object = new RectAreaLight(data.color, data.intensity, data.width, data.height); break; case 'SpotLight': - - object = new SpotLight(data.color, data.intensity, data.distance, data.angle, data.penumbra, data.decay); + object = new SpotLight( + data.color, + data.intensity, + data.distance, + data.angle, + data.penumbra, + data.decay + ); break; case 'HemisphereLight': - object = new HemisphereLight(data.color, data.groundColor, data.intensity); break; case 'LightProbe': - object = new LightProbe().fromJSON(data); break; case 'SkinnedMesh': - geometry = getGeometry(data.geometry); material = getMaterial(data.material); @@ -236,7 +238,6 @@ export class QuarksLoader extends ObjectLoader { break; case 'Mesh': - geometry = getGeometry(data.geometry); material = getMaterial(data.material); @@ -253,64 +254,58 @@ export class QuarksLoader extends ObjectLoader { object = new InstancedMesh(geometry, material, count); object.instanceMatrix = new InstancedBufferAttribute(new Float32Array(instanceMatrix.array), 16); - if (instanceColor !== undefined) object.instanceColor = new InstancedBufferAttribute(new Float32Array(instanceColor.array), instanceColor.itemSize); + if (instanceColor !== undefined) + object.instanceColor = new InstancedBufferAttribute( + new Float32Array(instanceColor.array), + instanceColor.itemSize + ); break; } case 'LOD': - object = new LOD(); break; case 'Line': - object = new Line(getGeometry(data.geometry), getMaterial(data.material)); break; case 'LineLoop': - object = new LineLoop(getGeometry(data.geometry), getMaterial(data.material)); break; case 'LineSegments': - object = new LineSegments(getGeometry(data.geometry), getMaterial(data.material)); break; case 'PointCloud': case 'Points': - object = new Points(getGeometry(data.geometry), getMaterial(data.material)); break; case 'Sprite': - object = new Sprite(getMaterial(data.material) as SpriteMaterial); break; case 'Group': - object = new Group(); break; case 'Bone': - object = new Bone(); break; default: - object = new Object3D(); - } object.uuid = data.uuid; @@ -336,7 +331,8 @@ export class QuarksLoader extends ObjectLoader { if (data.shadow.normalBias !== undefined) (object as any).normalBias = data.shadow.normalBias; if (data.shadow.radius !== undefined) (object as any).radius = data.shadow.radius; if (data.shadow.mapSize !== undefined) (object as any).mapSize.fromArray(data.shadow.mapSize); - if (data.shadow.camera !== undefined) { // @ts-ignore + if (data.shadow.camera !== undefined) { + // @ts-ignore (object as any).camera = this.parseObject(data.shadow.camera); } } @@ -348,51 +344,37 @@ export class QuarksLoader extends ObjectLoader { if (data.layers !== undefined) object.layers.mask = data.layers; if (data.children !== undefined) { - const children = data.children; for (let i = 0; i < children.length; i++) { - object.add(this.parseObject(children[i], geometries, materials, textures, animations)); - } - } if (data.animations !== undefined) { - const objectAnimations = data.animations; for (let i = 0; i < objectAnimations.length; i++) { - const uuid = objectAnimations[i]; object.animations.push(animations[uuid]); - } - } if (data.type === 'LOD') { - if (data.autoUpdate !== undefined) (object as any).autoUpdate = data.autoUpdate; const levels = data.levels; for (let l = 0; l < levels.length; l++) { - const level = levels[l]; const child = object.getObjectByProperty('uuid', level.object); if (child !== undefined) { - // @ts-ignore object.addLevel(child, level.distance); - } - } - } // @ts-ignore diff --git a/src/SpriteBatch.ts b/src/SpriteBatch.ts index f364529..bba2539 100644 --- a/src/SpriteBatch.ts +++ b/src/SpriteBatch.ts @@ -23,7 +23,7 @@ import local_particle_physics_vert from './shaders/local_particle_physics_vert.g import stretched_bb_particle_vert from './shaders/stretched_bb_particle_vert.glsl'; import {VFXBatch, RenderMode} from './VFXBatch'; import {getMaterialUVChannelName} from './util/ThreeUtil'; -import {VFXBatchSettings} from './ParticleSystem'; +import {VFXBatchSettings} from './BatchedRenderer'; const UP = new Vector3(0, 0, 1); diff --git a/src/TrailBatch.ts b/src/TrailBatch.ts index 97822bf..ff40ea5 100644 --- a/src/TrailBatch.ts +++ b/src/TrailBatch.ts @@ -6,7 +6,6 @@ import { Uniform, Vector2, DynamicDrawUsage, - DoubleSide, BufferGeometry, Vector3, BufferAttribute, @@ -17,7 +16,7 @@ import trail_frag from './shaders/trail_frag.glsl'; import trail_vert from './shaders/trail_vert.glsl'; import {VFXBatch, RenderMode} from './VFXBatch'; import {getMaterialUVChannelName} from './util/ThreeUtil'; -import {VFXBatchSettings} from './ParticleSystem'; +import {VFXBatchSettings} from './BatchedRenderer'; const UP = new Vector3(0, 0, 1); diff --git a/src/VFXBatch.ts b/src/VFXBatch.ts index c7c7cce..4c4141a 100644 --- a/src/VFXBatch.ts +++ b/src/VFXBatch.ts @@ -1,5 +1,5 @@ -import {ParticleSystem, VFXBatchSettings} from './ParticleSystem'; -import {Mesh, ShaderMaterial, BufferGeometry, Material, Layers, Blending, Side, MeshBasicMaterial} from 'three'; +import {Mesh, ShaderMaterial, BufferGeometry, Material, Layers} from 'three'; +import {IParticleSystem, VFXBatchSettings} from './BatchedRenderer'; export interface StoredBatchSettings { // 5 component x,y,z,u,v @@ -23,7 +23,7 @@ export enum RenderMode { export abstract class VFXBatch extends Mesh { type = 'VFXBatch'; - systems: Set; + systems: Set; declare material: ShaderMaterial; settings: StoredBatchSettings; @@ -32,7 +32,7 @@ export abstract class VFXBatch extends Mesh { protected constructor(settings: VFXBatchSettings) { super(); this.maxParticles = 1000; - this.systems = new Set(); + this.systems = new Set(); const layers = new Layers(); layers.mask = settings.layers.mask; this.settings = { @@ -48,11 +48,11 @@ export abstract class VFXBatch extends Mesh { this.renderOrder = this.settings.renderOrder; } - addSystem(system: ParticleSystem) { + addSystem(system: IParticleSystem) { this.systems.add(system); } - removeSystem(system: ParticleSystem) { + removeSystem(system: IParticleSystem) { this.systems.delete(system); } diff --git a/src/behaviors/EmitSubParticleSystem.ts b/src/behaviors/EmitSubParticleSystem.ts index 2fb9523..f1c29f8 100644 --- a/src/behaviors/EmitSubParticleSystem.ts +++ b/src/behaviors/EmitSubParticleSystem.ts @@ -1,49 +1,67 @@ -import {Behavior} from "./Behavior"; -import {Particle} from "../Particle"; -import {EmissionState, ParticleSystem} from "../ParticleSystem"; -import {BaseEvent, Matrix4, Quaternion, Vector3} from "three"; -import {ParticleEmitter} from "../ParticleEmitter"; +import {Behavior} from './Behavior'; +import {Particle} from '../Particle'; +import {EmissionState, ParticleSystem} from '../ParticleSystem'; +import {BaseEvent, Matrix4, Quaternion, Vector3} from 'three'; +import {ParticleEmitter} from '../ParticleEmitter'; const VECTOR_ONE = new Vector3(1, 1, 1); const VECTOR_Z = new Vector3(0, 0, 1); export class EmitSubParticleSystem implements Behavior { - - type = "EmitSubParticleSystem"; + type = 'EmitSubParticleSystem'; //private matrix_ = new Matrix4(); private q_ = new Quaternion(); private v_ = new Vector3(); private v2_ = new Vector3(); - constructor(private particleSystem: ParticleSystem, public useVelocityAsBasis: boolean, public subParticleSystem?: ParticleEmitter) { + constructor( + private particleSystem: ParticleSystem, + public useVelocityAsBasis: boolean, + public subParticleSystem?: ParticleEmitter + ) { if (this.subParticleSystem && this.subParticleSystem.system) { - this.subParticleSystem.system.onlyUsedByOther = true; + (this.subParticleSystem.system as ParticleSystem).onlyUsedByOther = true; } } initialize(particle: Particle): void { particle.emissionState = { burstIndex: 0, - burstWaveIndex:0, - time:0, + burstWaveIndex: 0, + time: 0, waitEmiting: 0, matrix: new Matrix4(), } as EmissionState; } update(particle: Particle, delta: number): void { - if (!this.subParticleSystem || !particle.emissionState) - return; + if (!this.subParticleSystem || !particle.emissionState) return; const m = (particle.emissionState as any).matrix; let rotation; if (particle.rotation === undefined || this.useVelocityAsBasis) { - if (particle.velocity.x === 0 && particle.velocity.y === 0 && (particle.velocity.z === 1 || particle.velocity.z === 0)) { + if ( + particle.velocity.x === 0 && + particle.velocity.y === 0 && + (particle.velocity.z === 1 || particle.velocity.z === 0) + ) { m.set( - 1, 0, 0, particle.position.x, - 0, 1, 0, particle.position.y, - 0, 0, 1, particle.position.z, - 0, 0, 0, 1 + 1, + 0, + 0, + particle.position.x, + 0, + 1, + 0, + particle.position.y, + 0, + 0, + 1, + particle.position.z, + 0, + 0, + 0, + 1 ); } else { this.v_.copy(VECTOR_Z).cross(particle.velocity); @@ -51,10 +69,22 @@ export class EmitSubParticleSystem implements Behavior { const len = this.v_.length(); const len2 = this.v2_.length(); m.set( - this.v_.x / len, this.v2_.x / len2, particle.velocity.x, particle.position.x, - this.v_.y / len, this.v2_.y / len2, particle.velocity.y, particle.position.y, - this.v_.z / len, this.v2_.z / len2, particle.velocity.z, particle.position.z, - 0, 0, 0, 1 + this.v_.x / len, + this.v2_.x / len2, + particle.velocity.x, + particle.position.x, + this.v_.y / len, + this.v2_.y / len2, + particle.velocity.y, + particle.position.y, + this.v_.z / len, + this.v2_.z / len2, + particle.velocity.z, + particle.position.z, + 0, + 0, + 0, + 1 ); } } else { @@ -69,18 +99,16 @@ export class EmitSubParticleSystem implements Behavior { if (!this.particleSystem.worldSpace) { m.multiplyMatrices(this.particleSystem.emitter.matrixWorld, m); } - this.subParticleSystem.system.emit(delta, particle.emissionState, m); - } - - frameUpdate(delta: number): void { + (this.subParticleSystem.system as ParticleSystem).emit(delta, particle.emissionState, m); } + frameUpdate(delta: number): void {} toJSON(): any { return { type: this.type, - subParticleSystem: this.subParticleSystem ? this.subParticleSystem.uuid : "", - useVelocityAsBasis: this.useVelocityAsBasis + subParticleSystem: this.subParticleSystem ? this.subParticleSystem.uuid : '', + useVelocityAsBasis: this.useVelocityAsBasis, }; } @@ -91,6 +119,5 @@ export class EmitSubParticleSystem implements Behavior { clone(): Behavior { return new EmitSubParticleSystem(this.particleSystem, this.useVelocityAsBasis, this.subParticleSystem); } - reset(): void { - } + reset(): void {} } diff --git a/src/behaviors/RotationBySpeed.ts b/src/behaviors/RotationBySpeed.ts index f79770e..c640deb 100644 --- a/src/behaviors/RotationBySpeed.ts +++ b/src/behaviors/RotationBySpeed.ts @@ -1,5 +1,5 @@ import {Behavior} from './Behavior'; -import {Particle, SpriteParticle} from '../Particle'; +import {Particle} from '../Particle'; import {FunctionValueGenerator, ValueGenerator, ValueGeneratorFromJSON} from '../functions/ValueGenerator'; import {Quaternion} from 'three'; import {IntervalValue} from '../functions'; diff --git a/src/functions/ContinuousLinearFunction.ts b/src/functions/ContinuousLinearFunction.ts index 27be317..e00d290 100644 --- a/src/functions/ContinuousLinearFunction.ts +++ b/src/functions/ContinuousLinearFunction.ts @@ -1,9 +1,5 @@ -import {PiecewiseFunction} from './PiecewiseFunction'; -import {FunctionColorGenerator} from './ColorGenerator'; -import {Vector4} from 'three'; -import {ColorRange} from './ColorRange'; import {FunctionJSON} from './FunctionJSON'; -import {ColorToJSON, JSONToColor, JSONToValue, ValueToJSON} from '../util/JSONUtil'; +import {JSONToValue, ValueToJSON} from '../util/JSONUtil'; interface ObjectValueType { copy(value: T): ObjectValueType; diff --git a/src/functions/Gradient.ts b/src/functions/Gradient.ts index 3ba735d..42c538e 100644 --- a/src/functions/Gradient.ts +++ b/src/functions/Gradient.ts @@ -1,9 +1,7 @@ -import {PiecewiseFunction} from './PiecewiseFunction'; import {FunctionColorGenerator} from './ColorGenerator'; import {Vector3, Vector4} from 'three'; import {ColorRange} from './ColorRange'; import {FunctionJSON} from './FunctionJSON'; -import {ColorToJSON, JSONToColor} from '../util/JSONUtil'; import {ContinuousLinearFunction} from './ContinuousLinearFunction'; const tempVec3 = new Vector3(); @@ -43,7 +41,7 @@ export class Gradient implements FunctionColorGenerator { static fromJSON(json: FunctionJSON): Gradient { // compatibility if (json.functions) { - let keys = json.functions.map((func: any) => [ColorRange.fromJSON(func.function).a, func.start]); + const keys = json.functions.map((func: any) => [ColorRange.fromJSON(func.function).a, func.start]); if (json.functions.length > 0) { keys.push([ColorRange.fromJSON(json.functions[json.functions.length - 1].function).b, 1]); } @@ -52,7 +50,7 @@ export class Gradient implements FunctionColorGenerator { keys.map((key: any) => [key[0].w, key[1]]) ); } else { - let gradient = new Gradient(); + const gradient = new Gradient(); gradient.alpha = ContinuousLinearFunction.fromJSON(json.alpha); gradient.color = ContinuousLinearFunction.fromJSON(json.color); return gradient; @@ -60,7 +58,7 @@ export class Gradient implements FunctionColorGenerator { } clone(): FunctionColorGenerator { - let gradient = new Gradient(); + const gradient = new Gradient(); gradient.alpha = this.alpha.clone(); gradient.color = this.color.clone(); return gradient; diff --git a/src/functions/RandomColorBetweenGradient.ts b/src/functions/RandomColorBetweenGradient.ts index 8930337..c40f485 100644 --- a/src/functions/RandomColorBetweenGradient.ts +++ b/src/functions/RandomColorBetweenGradient.ts @@ -1,4 +1,4 @@ -import {FunctionColorGenerator, MemorizedFunctionColorGenerator} from './ColorGenerator'; +import {MemorizedFunctionColorGenerator} from './ColorGenerator'; import {Vector4} from 'three'; import {FunctionJSON} from './FunctionJSON'; import {Gradient} from './Gradient'; diff --git a/src/nodes/Interpreter.ts b/src/nodes/Interpreter.ts index a2f88cf..b9254ab 100644 --- a/src/nodes/Interpreter.ts +++ b/src/nodes/Interpreter.ts @@ -78,7 +78,9 @@ export class Interpreter { this.visited.clear(); for (let i = 0; i < graph.outputNodes.length; i++) { const node = graph.outputNodes[i]; - this.traverse(node); + if (node.inputs[0] !== undefined) { + this.traverse(node); + } } graph.compiled = true; } diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index 166c080..aa6fd9b 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -9,7 +9,7 @@ export class Node { outputs: Wire[][] = []; type: NodeType; signatureIndex: number = -1; - data: NodeData = {}; + data: NodeData; // display position: Vector2 = new Vector2(); @@ -17,30 +17,33 @@ export class Node { // execution outputValues: any[] = []; - constructor(type: NodeType) { + constructor(type: NodeType, signatureIndex: number = -1, data: NodeData = {}) { this.id = '' + Math.round(Math.random() * 100000); //TODO use real random this.type = type; - for (let i = 0; i < type.nodeTypeSignatures[0].inputTypes.length; i++) { + this.signatureIndex = signatureIndex; + this.data = data; + const realIndex = signatureIndex === -1 ? 0 : signatureIndex; + for (let i = 0; i < type.nodeTypeSignatures[realIndex].inputTypes.length; i++) { this.inputs.push(undefined); } - for (let i = 0; i < type.nodeTypeSignatures[0].outputTypes.length; i++) { + for (let i = 0; i < type.nodeTypeSignatures[realIndex].outputTypes.length; i++) { this.outputs.push([]); - this.outputValues.push(genDefaultForNodeValueType(type.nodeTypeSignatures[0].outputTypes[i])); + this.outputValues.push(genDefaultForNodeValueType(type.nodeTypeSignatures[realIndex].outputTypes[i])); } } get inputTypes(): NodeValueType[] { - let signatureIndex = this.signatureIndex === -1 ? 0 : this.signatureIndex; + const signatureIndex = this.signatureIndex === -1 ? 0 : this.signatureIndex; return this.type.nodeTypeSignatures[signatureIndex].inputTypes; } get outputTypes(): NodeValueType[] { - let signatureIndex = this.signatureIndex === -1 ? 0 : this.signatureIndex; + const signatureIndex = this.signatureIndex === -1 ? 0 : this.signatureIndex; return this.type.nodeTypeSignatures[signatureIndex].outputTypes; } func(context: ExecutionContext, inputs: any[], outputs: any[]) { - let signatureIndex = this.signatureIndex === -1 ? 0 : this.signatureIndex; + const signatureIndex = this.signatureIndex === -1 ? 0 : this.signatureIndex; this.type.nodeTypeSignatures[signatureIndex].func(context, this.data, inputs, outputs); } } diff --git a/src/nodes/NodeDefs.ts b/src/nodes/NodeDefs.ts index 8ca1fac..cc6de4c 100644 --- a/src/nodes/NodeDefs.ts +++ b/src/nodes/NodeDefs.ts @@ -5,7 +5,7 @@ import {NodeType} from './NodeType'; export const NodeTypes: {[key: string]: NodeType} = {}; // Math -let addNode = new NodeType('add'); +const addNode = new NodeType('add'); addNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], @@ -36,7 +36,7 @@ addNode.addSignature( ); NodeTypes['add'] = addNode; -let subNode = new NodeType('sub'); +const subNode = new NodeType('sub'); subNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], @@ -67,7 +67,7 @@ subNode.addSignature( ); NodeTypes['sub'] = subNode; -let mulNode = new NodeType('mul'); +const mulNode = new NodeType('mul'); mulNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], @@ -98,7 +98,7 @@ mulNode.addSignature( ); NodeTypes['mul'] = mulNode; -let divNode = new NodeType('div'); +const divNode = new NodeType('div'); divNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], @@ -129,31 +129,31 @@ divNode.addSignature( ); NodeTypes['div'] = divNode; -let sinNode = new NodeType('sin'); +const sinNode = new NodeType('sin'); sinNode.addSignature([NodeValueType.Number], [NodeValueType.Number], (context, data, inputs, outputs) => { outputs[0] = Math.sin(inputs[0] as number); }); NodeTypes['sin'] = sinNode; -let cosNode = new NodeType('cos'); +const cosNode = new NodeType('cos'); cosNode.addSignature([NodeValueType.Number], [NodeValueType.Number], (context, data, inputs, outputs) => { outputs[0] = Math.cos(inputs[0] as number); }); NodeTypes['cos'] = cosNode; -let tanNode = new NodeType('tan'); +const tanNode = new NodeType('tan'); tanNode.addSignature([NodeValueType.Number], [NodeValueType.Number], (context, data, inputs, outputs) => { outputs[0] = Math.tan(inputs[0] as number); }); NodeTypes['tan'] = tanNode; -let absNode = new NodeType('abs'); +const absNode = new NodeType('abs'); absNode.addSignature([NodeValueType.Number], [NodeValueType.Number], (context, data, inputs, outputs) => { outputs[0] = Math.abs(inputs[0] as number); }); NodeTypes['abs'] = absNode; -let minNode = new NodeType('min'); +const minNode = new NodeType('min'); minNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], @@ -163,7 +163,7 @@ minNode.addSignature( ); NodeTypes['min'] = minNode; -let maxNode = new NodeType('max'); +const maxNode = new NodeType('max'); maxNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], @@ -173,7 +173,7 @@ maxNode.addSignature( ); NodeTypes['max'] = maxNode; -let dot = new NodeType('dot'); +const dot = new NodeType('dot'); dot.addSignature([NodeValueType.Vec2, NodeValueType.Vec2], [NodeValueType.Number], (context, data, inputs, outputs) => { outputs[0] = (inputs[0] as Vector2).dot(inputs[1] as Vector2); }); @@ -185,13 +185,13 @@ dot.addSignature([NodeValueType.Vec4, NodeValueType.Vec4], [NodeValueType.Number }); NodeTypes['dot'] = dot; -let cross = new NodeType('cross'); +const cross = new NodeType('cross'); cross.addSignature([NodeValueType.Vec3, NodeValueType.Vec3], [NodeValueType.Vec3], (context, data, inputs, outputs) => { (outputs[0] as Vector3).crossVectors(inputs[0] as Vector3, inputs[1] as Vector3); }); NodeTypes['cross'] = cross; -let length = new NodeType('length'); +const length = new NodeType('length'); length.addSignature([NodeValueType.Vec2], [NodeValueType.Number], (context, data, inputs, outputs) => { outputs[0] = (inputs[0] as Vector2).length(); }); @@ -203,7 +203,7 @@ length.addSignature([NodeValueType.Vec4], [NodeValueType.Number], (context, data }); NodeTypes['length'] = length; -let lengthSq = new NodeType('lengthSq'); +const lengthSq = new NodeType('lengthSq'); lengthSq.addSignature([NodeValueType.Vec2], [NodeValueType.Number], (context, data, inputs, outputs) => { outputs[0] = (inputs[0] as Vector2).lengthSq(); }); @@ -215,7 +215,7 @@ lengthSq.addSignature([NodeValueType.Vec4], [NodeValueType.Number], (context, da }); NodeTypes['lengthSq'] = lengthSq; -let normalize = new NodeType('normalize'); +const normalize = new NodeType('normalize'); normalize.addSignature([NodeValueType.Vec2], [NodeValueType.Vec2], (context, data, inputs, outputs) => { (outputs[0] as Vector2).copy(inputs[0] as Vector2).normalize(); }); @@ -227,7 +227,7 @@ normalize.addSignature([NodeValueType.Vec4], [NodeValueType.Vec4], (context, dat }); NodeTypes['normalize'] = normalize; -let distance = new NodeType('distance'); +const distance = new NodeType('distance'); distance.addSignature( [NodeValueType.Vec2, NodeValueType.Vec2], [NodeValueType.Number], @@ -245,7 +245,7 @@ distance.addSignature( NodeTypes['distance'] = distance; // Logic -let andNode = new NodeType('and'); +const andNode = new NodeType('and'); andNode.addSignature( [NodeValueType.Boolean, NodeValueType.Boolean], [NodeValueType.Boolean], @@ -255,7 +255,7 @@ andNode.addSignature( ); NodeTypes['and'] = andNode; -let orNode = new NodeType('or'); +const orNode = new NodeType('or'); orNode.addSignature( [NodeValueType.Boolean, NodeValueType.Boolean], [NodeValueType.Boolean], @@ -265,13 +265,13 @@ orNode.addSignature( ); NodeTypes['or'] = orNode; -let notNode = new NodeType('not'); +const notNode = new NodeType('not'); notNode.addSignature([NodeValueType.Boolean], [NodeValueType.Boolean], (context, data, inputs, outputs) => { outputs[0] = !(inputs[0] as boolean); }); NodeTypes['not'] = notNode; -let equalNode = new NodeType('equal'); +const equalNode = new NodeType('equal'); equalNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Boolean], @@ -302,7 +302,7 @@ equalNode.addSignature( ); NodeTypes['equal'] = equalNode; -let lessThanNode = new NodeType('lessThan'); +const lessThanNode = new NodeType('lessThan'); lessThanNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Boolean], @@ -312,7 +312,7 @@ lessThanNode.addSignature( ); NodeTypes['lessThan'] = lessThanNode; -let greaterThanNode = new NodeType('greaterThan'); +const greaterThanNode = new NodeType('greaterThan'); greaterThanNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Boolean], @@ -322,7 +322,7 @@ greaterThanNode.addSignature( ); NodeTypes['greaterThan'] = greaterThanNode; -let lessThanOrEqualNode = new NodeType('lessThanOrEqual'); +const lessThanOrEqualNode = new NodeType('lessThanOrEqual'); lessThanOrEqualNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Boolean], @@ -332,7 +332,7 @@ lessThanOrEqualNode.addSignature( ); NodeTypes['lessThanOrEqual'] = lessThanOrEqualNode; -let greaterThanOrEqualNode = new NodeType('greaterThanOrEqual'); +const greaterThanOrEqualNode = new NodeType('greaterThanOrEqual'); greaterThanOrEqualNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Boolean], @@ -342,7 +342,7 @@ greaterThanOrEqualNode.addSignature( ); NodeTypes['greaterThanOrEqual'] = greaterThanOrEqualNode; -let ifNode = new NodeType('if'); +const ifNode = new NodeType('if'); ifNode.addSignature( [NodeValueType.Boolean, NodeValueType.AnyType, NodeValueType.AnyType], [NodeValueType.AnyType], @@ -353,60 +353,121 @@ ifNode.addSignature( NodeTypes['if'] = ifNode; // Constants -let numberNode = new NodeType('number'); +const numberNode = new NodeType('number'); numberNode.addSignature([], [NodeValueType.Number], (context, data, inputs, outputs) => { outputs[0] = data.value; }); NodeTypes['number'] = numberNode; -let vec2Node = new NodeType('vec2'); +const vec2Node = new NodeType('vec2'); vec2Node.addSignature([], [NodeValueType.Vec2], (context, data, inputs, outputs) => { outputs[0] = data.value; }); NodeTypes['vec2'] = vec2Node; -let vec3Node = new NodeType('vec3'); +const vec3Node = new NodeType('vec3'); vec3Node.addSignature([], [NodeValueType.Vec3], (context, data, inputs, outputs) => { outputs[0] = data.value; }); NodeTypes['vec3'] = vec3Node; -let vec4Node = new NodeType('vec4'); +const vec4Node = new NodeType('vec4'); vec4Node.addSignature([], [NodeValueType.Vec4], (context, data, inputs, outputs) => { - outputs[0] = data.value; + (outputs[0] as Vector4).copy(data.value); }); NodeTypes['vec4'] = vec4Node; -let boolNode = new NodeType('bool'); +const boolNode = new NodeType('bool'); boolNode.addSignature([], [NodeValueType.Boolean], (context, data, inputs, outputs) => { outputs[0] = data.value; }); NodeTypes['bool'] = boolNode; // Particles -let particlePositionNode = new NodeType('particlePosition'); -particlePositionNode.addSignature([], [NodeValueType.Vec3], (context, data, inputs, outputs) => { - outputs[0] = (context.particle as any)[data.property]; -}); -NodeTypes['particlePosition'] = particlePositionNode; - -let particleVelocityNode = new NodeType('particleVelocity'); -particleVelocityNode.addSignature([], [NodeValueType.Vec3], (context, data, inputs, outputs) => { - outputs[0] = (context.particle as any)[data.property]; -}); -NodeTypes['particleVelocity'] = particleVelocityNode; - -let particleRotationNode = new NodeType('particleRotation'); -particleRotationNode.addSignature([], [NodeValueType.Number], (context, data, inputs, outputs) => { - outputs[0] = (context.particle as any)[data.property]; -}); -NodeTypes['particleRotation'] = particleRotationNode; - -let particleAgeNode = new NodeType('particleAge'); -particleAgeNode.addSignature([], [NodeValueType.Number], (context, data, inputs, outputs) => { - outputs[0] = (context.particle as any)[data.property]; +const particlePropertyNode = new NodeType('particleProperty'); +particlePropertyNode.addSignature( + [NodeValueType.NullableAnyType], + [NodeValueType.NullableAnyType], + (context, data, inputs, outputs) => { + if (inputs[0] !== undefined) { + if (typeof inputs[0] === 'object') { + (context.particle as any)[data.property].copy(inputs[0]); + } else { + (context.particle as any)[data.property] = inputs[0]; + } + } + if (outputs[0] !== undefined) { + if (typeof inputs[0] === 'object') { + (outputs[0] as any).copy((context.particle as any)[data.property]); + } else { + outputs[0] = (context.particle as any)[data.property]; + } + } + } +); +NodeTypes['particleProperty'] = particlePropertyNode; + +// emit +const emitNode = new NodeType('emit'); +emitNode.addSignature([NodeValueType.EventStream], [], (context, data, inputs, outputs) => { + const arr = inputs[0] as Array; + for (let i = 0; i < arr.length; i++) { + context.signal(i, arr[i]); + } +}); +NodeTypes['emit'] = emitNode; + +const graphPropertyNode = new NodeType('graphProperty'); +graphPropertyNode.addSignature( + [NodeValueType.NullableAnyType], + [NodeValueType.NullableAnyType], + (context, data, inputs, outputs) => { + if (inputs[0] !== undefined) { + if (typeof inputs[0] === 'object') { + (context.graph as any)[data.property].copy(inputs[0]); + } else { + (context.graph as any)[data.property] = inputs[0]; + } + } + if (outputs[0] !== undefined) { + if (typeof inputs[0] === 'object') { + (outputs[0] as any).copy((context.graph as any)[data.property]); + } else { + outputs[0] = (context.graph as any)[data.property]; + } + } + } +); +NodeTypes['graphProperty'] = graphPropertyNode; + +const startEventNode = new NodeType('startEvent'); +startEventNode.addSignature([], [NodeValueType.EventStream], (context, data, inputs, outputs) => { + outputs[0] = [{type: 'start'}]; +}); +NodeTypes['startEvent'] = startEventNode; + +const repeaterNode = new NodeType('repeater'); +repeaterNode.addSignature( + [NodeValueType.EventStream, NodeValueType.Number], + [NodeValueType.EventStream], + (context, data, inputs, outputs) => { + const arr = inputs[0] as Array; + const count = inputs[1] as number; + const result = []; + for (let j = 0; j < arr.length; j++) { + for (let i = 0; i < count; i++) { + result.push(arr[j]); + } + } + outputs[0] = result; + } +); +NodeTypes['repeater'] = repeaterNode; + +const timeNode = new NodeType('time'); +timeNode.addSignature([], [NodeValueType.Number], (context, data, inputs, outputs) => { + outputs[0] = context.emissionState.time; }); -NodeTypes['particleAge'] = particleAgeNode; // input output let outputNode = new NodeType('output'); @@ -431,7 +492,7 @@ NodeTypes['output'] = outputNode; const normalD = (x: number) => { return (1 / Math.sqrt(2 * Math.PI)) * Math.exp(x * x * -0.5); }; -let normalDistributionNode = new NodeType('normDistrib'); +const normalDistributionNode = new NodeType('normDistrib'); normalDistributionNode.addSignature( [NodeValueType.Number], [NodeValueType.Number], @@ -441,7 +502,7 @@ normalDistributionNode.addSignature( ); NodeTypes['normDistrib'] = normalDistributionNode; -let normcdfNode = new NodeType('normcdf'); +const normcdfNode = new NodeType('normcdf'); normcdfNode.addSignature([NodeValueType.Number], [NodeValueType.Number], (context, data, inputs, outputs) => { // constants let x = inputs[0] as number; @@ -465,7 +526,7 @@ normcdfNode.addSignature([NodeValueType.Number], [NodeValueType.Number], (contex }); NodeTypes['normcdf'] = normcdfNode; -let normcdfInvNode = new NodeType('normcdfInv'); +const normcdfInvNode = new NodeType('normcdfInv'); const rationalApproximation = (t: number) => { // Abramowitz and Stegun formula 26.2.23. @@ -491,7 +552,7 @@ normcdfInvNode.addSignature([NodeValueType.Number], [NodeValueType.Number], (con }); NodeTypes['normcdfInv'] = normcdfInvNode; -let clampNode = new NodeType('clamp'); +const clampNode = new NodeType('clamp'); clampNode.addSignature( [NodeValueType.Number, NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], @@ -501,18 +562,18 @@ clampNode.addSignature( ); NodeTypes['clamp'] = clampNode; -let smoothstepNode = new NodeType('smoothstep'); +const smoothstepNode = new NodeType('smoothstep'); smoothstepNode.addSignature( [NodeValueType.Number, NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], (context, data, inputs, outputs) => { - let x = Math.max(Math.min(inputs[0] as number, inputs[2] as number), inputs[1] as number); + const x = Math.max(Math.min(inputs[0] as number, inputs[2] as number), inputs[1] as number); outputs[0] = x * x * (3 - 2 * x); } ); NodeTypes['smoothstep'] = smoothstepNode; -let randomNode = new NodeType('random'); +const randomNode = new NodeType('random'); randomNode.addSignature( [NodeValueType.Number, NodeValueType.Number], [NodeValueType.Number], @@ -546,7 +607,7 @@ randomNode.addSignature( ); NodeTypes['random'] = randomNode; -let vrandNode = new NodeType('vrand'); +const vrandNode = new NodeType('vrand'); vrandNode.addSignature([], [NodeValueType.Vec2], (context, data, inputs, outputs) => { let x = normcdfInv(Math.random()); let y = normcdfInv(Math.random()); @@ -569,3 +630,5 @@ vrandNode.addSignature([], [NodeValueType.Vec4], (context, data, inputs, outputs (outputs[0] as Vector4).set(x / mag, y / mag, z / mag, w / mag); }); NodeTypes['vrand'] = vrandNode; + +export const OutputNodeTypeNames = new Set(['output', 'particleProperty', 'graphProperty', 'emit']); diff --git a/src/nodes/NodeGraph.ts b/src/nodes/NodeGraph.ts index 5ee5607..be36d9d 100644 --- a/src/nodes/NodeGraph.ts +++ b/src/nodes/NodeGraph.ts @@ -1,5 +1,5 @@ import {Node, Wire} from './Node'; -import {NodeTypes} from './NodeDefs'; +import {NodeTypes, OutputNodeTypeNames} from './NodeDefs'; export class NodeGraph { id: string; @@ -31,7 +31,7 @@ export class NodeGraph { this.allNodes.set(node.id, node); if (node.type === NodeTypes['input']) { this.inputNodes.push(node); - } else if (node.type === NodeTypes['output']) { + } else if (OutputNodeTypeNames.has(node.type.name)) { this.outputNodes.push(node); } this.revision++; diff --git a/src/nodes/NodeType.ts b/src/nodes/NodeType.ts index 29dc90e..76c7ba6 100644 --- a/src/nodes/NodeType.ts +++ b/src/nodes/NodeType.ts @@ -6,13 +6,13 @@ import {Vector2, Vector3, Vector4} from 'three'; import {Node, NodeData} from './Node'; export interface ExecutionContext { - inputs: NodeValue[]; - outputs: NodeValue[]; + inputs?: NodeValue[]; + outputs?: NodeValue[]; particle?: Particle; - delta?: number; + [key: string]: any; } -type NodeValue = number | boolean | Vector2 | Vector3 | Vector4; +type NodeValue = number | boolean | Vector2 | Vector3 | Vector4 | Array; type NodeExecFunction = (context: ExecutionContext, data: NodeData, inputs: NodeValue[], outputs: NodeValue[]) => void; diff --git a/src/nodes/NodeVFX.ts b/src/nodes/NodeVFX.ts new file mode 100644 index 0000000..2b7db24 --- /dev/null +++ b/src/nodes/NodeVFX.ts @@ -0,0 +1,479 @@ +import {Particle, SpriteParticle, TrailParticle} from '../Particle'; +import {ParticleEmitter} from '../ParticleEmitter'; +import { + BaseEvent, + BufferGeometry, + Layers, + Material, + Matrix3, + Matrix4, + Object3D, + PlaneGeometry, + Quaternion, + Texture, + Vector3, +} from 'three'; +import {RenderMode} from '../VFXBatch'; +import {BatchedRenderer, IParticleSystem, SerializationOptions, VFXBatchSettings} from '../BatchedRenderer'; +import {NodeGraph} from './NodeGraph'; +import {Interpreter} from './Interpreter'; +import {ExecutionContext} from './NodeType'; +import {BillBoardSettings, MeshSettings, TrailSettings} from '../ParticleSystem'; + +const UP = new Vector3(0, 0, 1); +const tempQ = new Quaternion(); +const tempV = new Vector3(); +const tempV2 = new Vector3(); +const PREWARM_FPS = 60; + +export interface VFXParameters { + // parameters + autoDestroy?: boolean; + looping?: boolean; + prewarm?: boolean; + duration?: number; + + emissionGraph: NodeGraph; + updateGraph: NodeGraph; + + instancingGeometry?: BufferGeometry; + renderMode?: RenderMode; + rendererEmitterSettings?: TrailSettings | MeshSettings | BillBoardSettings; + speedFactor?: number; + material: Material; + layers?: Layers; + renderOrder?: number; + worldSpace?: boolean; +} + +const DEFAULT_GEOMETRY = new PlaneGeometry(1, 1, 1, 1); + +interface EmissionState { + time: number; +} + +/** + * NodeVFX represents a node graph based visual effect + * + * @class + */ +export class NodeVFX implements IParticleSystem { + emissionGraph: NodeGraph; + updateGraph: NodeGraph; + interpreter: Interpreter; + + /** + * Determines whether the ParticleSystem should be automatically disposed when it finishes emitting particles. + * + * @type {boolean} + */ + autoDestroy: boolean; + + /** + * Determines whether a looping ParticleSystem should prewarm, i.e., the Particle System looks like it has already simulated for one loop when first becoming visible. + * + * @type {boolean} + */ + prewarm: boolean; + + /** + * Determines whether the ParticleSystem should loop, i.e., restart emitting particles after the duration of the particle system is expired. + * + * @type {boolean} + */ + looping: boolean; + + /** + * The duration of the ParticleSystem in seconds. + * + * @type {number} + */ + duration: number; + + /** + * The number of particles in the ParticleSystem. + * + * @type {number} + */ + particleNum: number; + + /** + * Determines whether the ParticleSystem is paused. + * + * @type {boolean} + */ + paused: boolean; + + /** + * All the particles in the ParticleSystem. + * + * @type {Array} + */ + particles: Array; + + /** + * the emitter object that should be added in the scene. + * + * @type {ParticleEmitter} + */ + emitter: ParticleEmitter; + + /** + * the VFX renderer settings for the batch renderer + * + * @type {VFXBatchSettings} + */ + rendererSettings: VFXBatchSettings; + + /** + * whether needs to update renderer settings for the batch renderer + * + * @type {boolean} + */ + neededToUpdateRender: boolean; + + rendererEmitterSettings: {}; + worldSpace: boolean; + + private prewarmed: boolean; + private emissionState: EmissionState; + private emitEnded: boolean; + private markForDestroy: boolean; + private previousWorldPos?: Vector3; + private temp: Vector3 = new Vector3(); + private travelDistance = 0; + + private normalMatrix: Matrix3 = new Matrix3(); + /** @internal **/ + _renderer?: BatchedRenderer; + + set time(time: number) { + this.emissionState.time = time; + } + + get time(): number { + return this.emissionState.time; + } + + // currently if you change the layers setting, you need manually set this.neededToUpdateRender = true; + get layers() { + return this.rendererSettings.layers; + } + + get texture() { + return (this.rendererSettings.material as any).map; + } + + set texture(texture: Texture) { + (this.rendererSettings.material as any).map = texture; + this.neededToUpdateRender = true; + //this.emitter.material.uniforms.map.value = texture; + } + + get material() { + return this.rendererSettings.material; + } + + set material(material: Material) { + this.rendererSettings.material = material; + this.neededToUpdateRender = true; + } + + get instancingGeometry(): BufferGeometry { + return this.rendererSettings.instancingGeometry; + } + + set instancingGeometry(geometry: BufferGeometry) { + this.restart(); + this.particles.length = 0; + this.rendererSettings.instancingGeometry = geometry; + this.neededToUpdateRender = true; + } + + get renderMode(): RenderMode { + return this.rendererSettings.renderMode; + } + + set renderMode(renderMode: RenderMode) { + if ( + (this.rendererSettings.renderMode != RenderMode.Trail && renderMode === RenderMode.Trail) || + (this.rendererSettings.renderMode == RenderMode.Trail && renderMode !== RenderMode.Trail) + ) { + this.restart(); + this.particles.length = 0; + } + if (this.rendererSettings.renderMode !== renderMode) { + switch (renderMode) { + case RenderMode.Trail: + this.rendererEmitterSettings = { + startLength: 30, + followLocalOrigin: false, + }; + break; + case RenderMode.Mesh: + this.rendererEmitterSettings = { + geometry: new PlaneGeometry(1, 1), + }; + break; + case RenderMode.BillBoard: + case RenderMode.VerticalBillBoard: + case RenderMode.HorizontalBillBoard: + case RenderMode.StretchedBillBoard: + this.rendererEmitterSettings = {}; + break; + } + } + this.rendererSettings.renderMode = renderMode; + this.neededToUpdateRender = true; + //this.emitter.rebuildMaterial(); + } + + get renderOrder(): number { + return this.rendererSettings.renderOrder; + } + + set renderOrder(renderOrder: number) { + this.rendererSettings.renderOrder = renderOrder; + this.neededToUpdateRender = true; + //this.emitter.rebuildMaterial(); + } + + get blending() { + return this.rendererSettings.material.blending; + } + + set blending(blending) { + this.rendererSettings.material.blending = blending; + this.neededToUpdateRender = true; + } + + constructor(parameters: VFXParameters) { + this.autoDestroy = parameters.autoDestroy === undefined ? false : parameters.autoDestroy; + this.duration = parameters.duration ?? 1; + this.looping = parameters.looping === undefined ? true : parameters.looping; + this.prewarm = parameters.prewarm === undefined ? false : parameters.prewarm; + this.worldSpace = parameters.worldSpace ?? false; + this.rendererEmitterSettings = parameters.rendererEmitterSettings ?? {}; + this.emissionGraph = parameters.emissionGraph; + this.updateGraph = parameters.updateGraph; + this.interpreter = new Interpreter(); + + this.rendererSettings = { + instancingGeometry: parameters.instancingGeometry ?? DEFAULT_GEOMETRY, + renderMode: parameters.renderMode ?? RenderMode.BillBoard, + renderOrder: parameters.renderOrder ?? 0, + material: parameters.material, + layers: parameters.layers ?? new Layers(), + uTileCount: 1, + vTileCount: 1, + }; + this.neededToUpdateRender = true; + + this.particles = new Array(); + this.emitter = new ParticleEmitter(this); + + this.paused = false; + this.particleNum = 0; + this.emissionState = { + time: 0, + }; + + this.emitEnded = false; + this.markForDestroy = false; + this.prewarmed = false; + } + + pause() { + this.paused = true; + } + + play() { + this.paused = false; + } + + private spawn(emissionState: EmissionState, matrix: Matrix4) { + tempQ.setFromRotationMatrix(matrix); + const translation = tempV; + const quaternion = tempQ; + const scale = tempV2; + matrix.decompose(translation, quaternion, scale); + + this.particleNum++; + while (this.particles.length < this.particleNum) { + if (this.rendererSettings.renderMode === RenderMode.Trail) { + this.particles.push(new TrailParticle()); + } else { + this.particles.push(new SpriteParticle()); + } + } + const particle = this.particles[this.particleNum - 1]; + this.interpreter.run(this.emissionGraph, {particle: particle}); + + if ( + this.rendererSettings.renderMode === RenderMode.Trail && + (this.rendererEmitterSettings as TrailSettings).followLocalOrigin + ) { + const trail = particle as TrailParticle; + trail.localPosition = new Vector3().copy(trail.position); + } + if (this.worldSpace) { + particle.position.applyMatrix4(matrix); + particle.startSize = (particle.startSize * (Math.abs(scale.x) + Math.abs(scale.y) + Math.abs(scale.z))) / 3; + particle.size = particle.startSize; + particle.velocity.multiply(scale).applyMatrix3(this.normalMatrix); + if (particle.rotation && particle.rotation instanceof Quaternion) { + particle.rotation.multiplyQuaternions(tempQ, particle.rotation); + } + } else { + } + } + + endEmit() { + this.emitEnded = true; + if (this.autoDestroy) { + this.markForDestroy = true; + } + } + + dispose() { + if (this._renderer) this._renderer.deleteSystem(this); + this.emitter.dispose(); + if (this.emitter.parent) this.emitter.parent.remove(this.emitter); + } + + restart() { + this.paused = false; + this.particleNum = 0; + this.emissionState.time = 0; + this.emitEnded = false; + this.markForDestroy = false; + this.prewarmed = false; + } + + //firstTimeUpdate = true; + + private update(delta: number) { + /*if (this.firstTimeUpdate) { + this.renderer.addSystem(this); + this.firstTimeUpdate = false; + }*/ + + if (this.paused) return; + + let currentParent: Object3D = this.emitter; + while (currentParent.parent) { + currentParent = currentParent.parent; + } + if (currentParent.type !== 'Scene') { + this.dispose(); + return; + } + + if (this.emitEnded && this.particleNum === 0) { + if (this.markForDestroy && this.emitter.parent) this.dispose(); + return; + } + + if (this.looping && this.prewarm && !this.prewarmed) { + this.prewarmed = true; + for (let i = 0; i < this.duration * PREWARM_FPS; i++) { + // stack overflow? + this.update(1.0 / PREWARM_FPS); + } + } + + if (delta > 0.1) { + delta = 0.1; + } + + if (this.neededToUpdateRender) { + if (this._renderer) this._renderer.updateSystem(this); + this.neededToUpdateRender = false; + } + + this.emit(delta, this.emissionState, this.emitter.matrixWorld); + + for (let i = 0; i < this.particleNum; i++) { + if ( + (this.rendererEmitterSettings as TrailSettings).followLocalOrigin && + (this.particles[i] as TrailParticle).localPosition + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.particles[i].position.copy((this.particles[i] as TrailParticle).localPosition!); + if (this.particles[i].parentMatrix) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.particles[i].position.applyMatrix4(this.particles[i].parentMatrix!); + } else { + this.particles[i].position.applyMatrix4(this.emitter.matrixWorld); + } + } else { + this.particles[i].position.addScaledVector(this.particles[i].velocity, delta); + } + this.particles[i].age += delta; + } + + if (this.rendererSettings.renderMode === RenderMode.Trail) { + for (let i = 0; i < this.particleNum; i++) { + const particle = this.particles[i] as TrailParticle; + particle.update(); + } + } + + // particle die + for (let i = 0; i < this.particleNum; i++) { + const particle = this.particles[i]; + if (particle.died && (!(particle instanceof TrailParticle) || particle.previous.length === 0)) { + this.particles[i] = this.particles[this.particleNum - 1]; + this.particles[this.particleNum - 1] = particle; + this.particleNum--; + i--; + } + } + } + + public emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4) { + if (emissionState.time > this.duration) { + if (this.looping) { + emissionState.time -= this.duration; + } else { + if (!this.emitEnded) { + this.endEmit(); + } + } + } + + this.normalMatrix.getNormalMatrix(emitterMatrix); + + // spawn + + // spawn burst + const context: ExecutionContext = { + signal: () => { + this.spawn(emissionState, emitterMatrix); + }, + emissionState, + delta, + }; + if (!this.emitEnded) { + this.interpreter.run(this.emissionGraph, context); + //emissionState.waitEmiting += delta * this.interpreter.run(this.emissionGraph, context); + } + if (this.previousWorldPos === undefined) this.previousWorldPos = new Vector3(); + this.emitter.getWorldPosition(this.previousWorldPos); + emissionState.time += delta; + } + + toJSON(meta: any, options: SerializationOptions = {}): any { + return {}; + } + + getRendererSettings() { + return this.rendererSettings; + } + + clone(): IParticleSystem { + return this; + } + + speedFactor = 0; +} diff --git a/src/nodes/NodeValueType.ts b/src/nodes/NodeValueType.ts index 96499d2..43dfcb0 100644 --- a/src/nodes/NodeValueType.ts +++ b/src/nodes/NodeValueType.ts @@ -1,4 +1,4 @@ -import {Vector2, Vector3, Vector4} from "three"; +import {Vector2, Vector3, Vector4} from 'three'; export enum NodeValueType { Number = 0, @@ -7,6 +7,8 @@ export enum NodeValueType { Vec4 = 3, Boolean = 4, AnyType = 5, + NullableAnyType = 6, + EventStream = 7, } export const genDefaultForNodeValueType = (type: NodeValueType): any => { @@ -23,5 +25,7 @@ export const genDefaultForNodeValueType = (type: NodeValueType): any => { return new Vector4(); case NodeValueType.AnyType: return 0; + case NodeValueType.NullableAnyType: + return undefined; } -} +}; diff --git a/src/nodes/WASMCompiler.ts b/src/nodes/WASMCompiler.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/nodes/WebGPUCompiler.ts b/src/nodes/WebGPUCompiler.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/nodes/index.ts b/src/nodes/index.ts index 866eb1e..dca30b7 100644 --- a/src/nodes/index.ts +++ b/src/nodes/index.ts @@ -1,6 +1,7 @@ -export * from "./Interpreter"; -export * from "./Node"; -export * from "./NodeDefs"; -export * from "./NodeGraph"; -export * from "./NodeType"; -export * from "./NodeValueType"; +export * from './Interpreter'; +export * from './Node'; +export * from './NodeDefs'; +export * from './NodeGraph'; +export * from './NodeType'; +export * from './NodeValueType'; +export * from './NodeVFX'; diff --git a/test/ParticleEmitter.test.ts b/test/ParticleEmitter.test.ts index 32bd573..3d23daa 100644 --- a/test/ParticleEmitter.test.ts +++ b/test/ParticleEmitter.test.ts @@ -116,11 +116,12 @@ describe('ParticleEmitter', () => { // console.log(json); const loader = new QuarksLoader(); const emitter = loader.parse(json, () => {}) as ParticleEmitter; - expect(emitter.system.rendererSettings.layers.mask).toBe(3); - expect(emitter.system.startTileIndex.type).toBe('value'); - expect(emitter.system.rendererSettings.instancingGeometry).toBeDefined(); + const system = emitter.system as ParticleSystem; + expect(system.rendererSettings.layers.mask).toBe(3); + expect(system.startTileIndex.type).toBe('value'); + expect(system.rendererSettings.instancingGeometry).toBeDefined(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect((emitter.system.rendererEmitterSettings as TrailSettings).startLength!.type).toBe('value'); - expect(emitter.system.behaviors.length).toBe(2); + expect((system.rendererEmitterSettings as TrailSettings).startLength!.type).toBe('value'); + expect(system.behaviors.length).toBe(2); }); }); diff --git a/test/QuarksLoader.test.ts b/test/QuarksLoader.test.ts index 8a56cb9..b015753 100644 --- a/test/QuarksLoader.test.ts +++ b/test/QuarksLoader.test.ts @@ -1,7 +1,7 @@ /** * @jest-environment jsdom */ -import {QuarksLoader} from '../src'; +import {ParticleSystem, QuarksLoader} from '../src'; import {MeshSurfaceEmitter, ParticleEmitter} from '../src'; import {EmitSubParticleSystem} from '../src'; import JSON1 from './subPS.json'; @@ -12,14 +12,10 @@ describe('QuarksLoader', () => { const loader = new QuarksLoader(); const object = loader.parse(JSON1, () => {}); expect(object.children.length).toBe(2); - expect((object.children[0] as ParticleEmitter).system.behaviors.length).toBe(1); - expect(((object.children[0] as ParticleEmitter).system.behaviors[0] as any).particleSystem).toBe( - (object.children[0] as ParticleEmitter).system - ); - expect( - ((object.children[0] as ParticleEmitter).system.behaviors[0] as EmitSubParticleSystem) - .subParticleSystem - ).toBe(object.children[1]); + const system = (object.children[0] as ParticleEmitter).system as ParticleSystem; + expect(system.behaviors.length).toBe(1); + expect((system.behaviors[0] as any).particleSystem).toBe(system); + expect((system.behaviors[0] as EmitSubParticleSystem).subParticleSystem).toBe(object.children[1]); }); test('#loadMeshSurfaceEmitter', () => { @@ -29,8 +25,10 @@ describe('QuarksLoader', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect( Object.keys( - ((object.children[1] as ParticleEmitter).system.emitterShape as MeshSurfaceEmitter).geometry! - .attributes + ( + ((object.children[1] as ParticleEmitter).system as ParticleSystem) + .emitterShape as MeshSurfaceEmitter + ).geometry!.attributes ).length ).toBe(3); }); diff --git a/test/functions/Gradient.test.ts b/test/functions/Gradient.test.ts index b895afa..9554d51 100644 --- a/test/functions/Gradient.test.ts +++ b/test/functions/Gradient.test.ts @@ -1,4 +1,4 @@ -import {Gradient, ColorRange} from '../../src'; +import {Gradient} from '../../src'; import {Vector3, Vector4} from 'three'; describe('Gradient', () => { diff --git a/test/nodes/NodeVFX.test.ts b/test/nodes/NodeVFX.test.ts new file mode 100644 index 0000000..377c4ec --- /dev/null +++ b/test/nodes/NodeVFX.test.ts @@ -0,0 +1,73 @@ +import {NodeGraph, Node, Wire, NodeTypes, Interpreter, Particle} from '../../src'; +import {Vector3} from 'three'; + +// Node Graph 2.0 +describe('NodeVFX', () => { + test('Interpreter emission graph', () => { + const graph = new NodeGraph('test'); + const start = new Node(NodeTypes['startEvent'], 0); + + const repeater = new Node(NodeTypes['repeater'], 0); + repeater.inputs[1] = {getValue: () => 10}; + + const emit = new Node(NodeTypes['emit'], 0); + + graph.addNode(start); + graph.addNode(repeater); + graph.addNode(emit); + graph.addWire(new Wire(start, 0, repeater, 0)); + graph.addWire(new Wire(repeater, 0, emit, 0)); + + const interpreter = new Interpreter(); + const count = {value: 0}; + const context = { + signal: (i: number) => { + count.value += i; + }, + }; + interpreter.run(graph, context); + expect(count.value).toBe(45); + + interpreter.run(graph, context); + expect(count.value).toBe(90); + }); + + test('Interpreter particle properties', () => { + const graph = new NodeGraph('test'); + const pos = new Node(NodeTypes['vec3'], 0, {value: new Vector3(4, 5, 6)}); + const pos2 = new Node(NodeTypes['vec3'], 0, {value: new Vector3(1, 2, 3)}); + + const add = new Node(NodeTypes['add'], 2); + const ppos = new Node(NodeTypes['particleProperty'], 0, {property: 'position'}); + const pvel = new Node(NodeTypes['particleProperty'], 0, {property: 'velocity'}); + + graph.addNode(pos); + graph.addNode(pos2); + graph.addNode(add); + graph.addNode(ppos); + graph.addNode(pvel); + graph.addWire(new Wire(pos, 0, add, 0)); + graph.addWire(new Wire(pos2, 0, add, 1)); + graph.addWire(new Wire(add, 0, ppos, 0)); + graph.addWire(new Wire(pos2, 0, pvel, 0)); + + const interpreter = new Interpreter(); + const particle = {position: new Vector3(), velocity: new Vector3()} as Particle; + const context = {particle: particle}; + interpreter.run(graph, context); + expect(context.particle.velocity.x).toBe(1); + expect(context.particle.velocity.y).toBe(2); + expect(context.particle.velocity.z).toBe(3); + expect(context.particle.position.x).toBe(5); + expect(context.particle.position.y).toBe(7); + expect(context.particle.position.z).toBe(9); + + interpreter.run(graph, context); + expect(context.particle.velocity.x).toBe(1); + expect(context.particle.velocity.y).toBe(2); + expect(context.particle.velocity.z).toBe(3); + expect(context.particle.position.x).toBe(5); + expect(context.particle.position.y).toBe(7); + expect(context.particle.position.z).toBe(9); + }); +});