// // ParticleEffectEntityItem.cpp // libraries/entities/src // // Some starter code for a particle simulation entity, which could ideally be used for a variety of effects. // This is some really early and rough stuff here. It was enough for now to just get it up and running in the interface. // // Todo's and other notes: // - The simulation should restart when the AnimationLoop's max frame is reached (or passed), but there doesn't seem // to be a good way to set that max frame to something reasonable right now. // - There seems to be a bug whereby entities on the edge of screen will just pop off or on. This is probably due // to my lack of understanding of how entities in the octree are picked for rendering. I am updating the entity // dimensions based on the bounds of the sim, but maybe I need to update a dirty flag or something. // - This should support some kind of pre-roll of the simulation. // - Just to get this out the door, I just did forward Euler integration. There are better ways. // - Gravity always points along the Y axis. Support an actual gravity vector. // - Add the ability to add arbitrary forces to the simulation. // - Add drag. // - Add some kind of support for collisions. // - There's no synchronization of the simulation across clients at all. In fact, it's using rand() under the hood, so // there's no gaurantee that different clients will see simulations that look anything like the other. // // Created by Jason Rickwald on 3/2/15. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include "EntityTree.h" #include "EntityTreeElement.h" #include "EntitiesLogging.h" #include "EntityScriptingInterface.h" #include "ParticleEffectEntityItem.h" const glm::vec3 X_AXIS = glm::vec3(1.0f, 0.0f, 0.0f); const glm::vec3 Z_AXIS = glm::vec3(0.0f, 0.0f, 1.0f); const xColor ParticleEffectEntityItem::DEFAULT_COLOR = { 255, 255, 255 }; const xColor ParticleEffectEntityItem::DEFAULT_COLOR_SPREAD = { 0, 0, 0 }; const float ParticleEffectEntityItem::DEFAULT_ALPHA = 1.0f; const float ParticleEffectEntityItem::DEFAULT_ALPHA_SPREAD = 0.0f; const float ParticleEffectEntityItem::DEFAULT_ALPHA_START = DEFAULT_ALPHA; const float ParticleEffectEntityItem::DEFAULT_ALPHA_FINISH = DEFAULT_ALPHA; const float ParticleEffectEntityItem::DEFAULT_ANIMATION_FRAME_INDEX = 0.0f; const bool ParticleEffectEntityItem::DEFAULT_ANIMATION_IS_PLAYING = false; const float ParticleEffectEntityItem::DEFAULT_ANIMATION_FPS = 30.0f; const quint32 ParticleEffectEntityItem::DEFAULT_MAX_PARTICLES = 1000; const float ParticleEffectEntityItem::DEFAULT_LIFESPAN = 3.0f; const float ParticleEffectEntityItem::DEFAULT_EMIT_RATE = 15.0f; const float ParticleEffectEntityItem::DEFAULT_EMIT_SPEED = 5.0f; const float ParticleEffectEntityItem::DEFAULT_SPEED_SPREAD = 1.0f; const glm::quat ParticleEffectEntityItem::DEFAULT_EMIT_ORIENTATION = glm::angleAxis(-PI_OVER_TWO, X_AXIS); // Vertical const glm::vec3 ParticleEffectEntityItem::DEFAULT_EMIT_DIMENSIONS = glm::vec3(0.0f, 0.0f, 0.0f); // Emit from point const float ParticleEffectEntityItem::DEFAULT_EMIT_RADIUS_START = 1.0f; // Emit from surface (when emitDimensions > 0) const float ParticleEffectEntityItem::DEFAULT_POLAR_START = 0.0f; // Emit along z-axis const float ParticleEffectEntityItem::DEFAULT_POLAR_FINISH = 0.0f; // "" const float ParticleEffectEntityItem::DEFAULT_AZIMUTH_START = -PI; // Emit full circumference (when polarFinish > 0) const float ParticleEffectEntityItem::DEFAULT_AZIMUTH_FINISH = PI; // "" const glm::vec3 ParticleEffectEntityItem::DEFAULT_EMIT_ACCELERATION(0.0f, -9.8f, 0.0f); const glm::vec3 ParticleEffectEntityItem::DEFAULT_ACCELERATION_SPREAD(0.0f, 0.0f, 0.0f); const float ParticleEffectEntityItem::DEFAULT_PARTICLE_RADIUS = 0.025f; const float ParticleEffectEntityItem::DEFAULT_RADIUS_SPREAD = 0.0f; const float ParticleEffectEntityItem::DEFAULT_RADIUS_START = DEFAULT_PARTICLE_RADIUS; const float ParticleEffectEntityItem::DEFAULT_RADIUS_FINISH = DEFAULT_PARTICLE_RADIUS; const QString ParticleEffectEntityItem::DEFAULT_TEXTURES = ""; EntityItemPointer ParticleEffectEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { return std::make_shared(entityID, properties); } // our non-pure virtual subclass for now... ParticleEffectEntityItem::ParticleEffectEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties) : EntityItem(entityItemID), _lastAnimated(usecTimestampNow()), _animationLoop(), _animationSettings(), _particleLifetimes(DEFAULT_MAX_PARTICLES, 0.0f), _particlePositions(DEFAULT_MAX_PARTICLES, glm::vec3(0.0f, 0.0f, 0.0f)), _particleVelocities(DEFAULT_MAX_PARTICLES, glm::vec3(0.0f, 0.0f, 0.0f)), _particleAccelerations(DEFAULT_MAX_PARTICLES, glm::vec3(0.0f, 0.0f, 0.0f)), _particleRadiuses(DEFAULT_MAX_PARTICLES, DEFAULT_PARTICLE_RADIUS), _radiusStarts(DEFAULT_MAX_PARTICLES, DEFAULT_PARTICLE_RADIUS), _radiusMiddles(DEFAULT_MAX_PARTICLES, DEFAULT_PARTICLE_RADIUS), _radiusFinishes(DEFAULT_MAX_PARTICLES, DEFAULT_PARTICLE_RADIUS), _particleColors(DEFAULT_MAX_PARTICLES, DEFAULT_COLOR), _colorStarts(DEFAULT_MAX_PARTICLES, DEFAULT_COLOR), _colorMiddles(DEFAULT_MAX_PARTICLES, DEFAULT_COLOR), _colorFinishes(DEFAULT_MAX_PARTICLES, DEFAULT_COLOR), _particleAlphas(DEFAULT_MAX_PARTICLES, DEFAULT_ALPHA), _alphaStarts(DEFAULT_MAX_PARTICLES, DEFAULT_ALPHA), _alphaMiddles(DEFAULT_MAX_PARTICLES, DEFAULT_ALPHA), _alphaFinishes(DEFAULT_MAX_PARTICLES, DEFAULT_ALPHA), _particleMaxBound(glm::vec3(1.0f, 1.0f, 1.0f)), _particleMinBound(glm::vec3(-1.0f, -1.0f, -1.0f)) { _type = EntityTypes::ParticleEffect; setColor(DEFAULT_COLOR); setProperties(properties); _animationProperties.associateWithAnimationLoop(&_animationLoop); _animationLoop.setResetOnRunning(false); } ParticleEffectEntityItem::~ParticleEffectEntityItem() { } void ParticleEffectEntityItem::setEmitSpeed(float emitSpeed) { _emitSpeed = emitSpeed; computeAndUpdateDimensions(); } void ParticleEffectEntityItem::setSpeedSpread(float speedSpread) { _speedSpread = speedSpread; computeAndUpdateDimensions(); } void ParticleEffectEntityItem::setEmitOrientation(const glm::quat& emitOrientation) { _emitOrientation = emitOrientation; computeAndUpdateDimensions(); } void ParticleEffectEntityItem::setEmitDimensions(const glm::vec3& emitDimensions) { _emitDimensions = emitDimensions; computeAndUpdateDimensions(); } void ParticleEffectEntityItem::setEmitAcceleration(const glm::vec3& emitAcceleration) { _emitAcceleration = emitAcceleration; computeAndUpdateDimensions(); } void ParticleEffectEntityItem::setAccelerationSpread(const glm::vec3& accelerationSpread){ _accelerationSpread = accelerationSpread; computeAndUpdateDimensions(); } void ParticleEffectEntityItem::computeAndUpdateDimensions() { const float time = _lifespan * 1.1f; // add 10% extra time to account for incremental timer accumulation error glm::vec3 velocity = _emitSpeed * (_emitOrientation * Z_AXIS); glm::vec3 velocitySpread = _speedSpread * (_emitOrientation * Z_AXIS); glm::vec3 maxVelocity = glm::abs(velocity) + velocitySpread; glm::vec3 maxAccleration = glm::abs(_acceleration) + _accelerationSpread; glm::vec3 maxDistance = 0.5f * _emitDimensions + time * maxVelocity + (0.5f * time * time) * maxAccleration; float maxDistanceValue = std::max(maxDistance.x, std::max(maxDistance.y, maxDistance.z)); //times 2 because dimensions are diameters not radii glm::vec3 dims(2.0f * maxDistanceValue); EntityItem::setDimensions(dims); } EntityItemProperties ParticleEffectEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha); COPY_ENTITY_PROPERTY_TO_PROPERTIES(glowLevel, getGlowLevel); COPY_ENTITY_PROPERTY_TO_PROPERTIES(shapeType, getShapeType); COPY_ENTITY_PROPERTY_TO_PROPERTIES(maxParticles, getMaxParticles); COPY_ENTITY_PROPERTY_TO_PROPERTIES(lifespan, getLifespan); COPY_ENTITY_PROPERTY_TO_PROPERTIES(emitRate, getEmitRate); COPY_ENTITY_PROPERTY_TO_PROPERTIES(emitSpeed, getEmitSpeed); COPY_ENTITY_PROPERTY_TO_PROPERTIES(speedSpread, getSpeedSpread); COPY_ENTITY_PROPERTY_TO_PROPERTIES(emitOrientation, getEmitOrientation); COPY_ENTITY_PROPERTY_TO_PROPERTIES(emitDimensions, getEmitDimensions); COPY_ENTITY_PROPERTY_TO_PROPERTIES(emitRadiusStart, getEmitRadiusStart); COPY_ENTITY_PROPERTY_TO_PROPERTIES(polarStart, getPolarStart); COPY_ENTITY_PROPERTY_TO_PROPERTIES(polarFinish, getPolarFinish); COPY_ENTITY_PROPERTY_TO_PROPERTIES(azimuthStart, getAzimuthStart); COPY_ENTITY_PROPERTY_TO_PROPERTIES(azimuthFinish, getAzimuthFinish); COPY_ENTITY_PROPERTY_TO_PROPERTIES(emitAcceleration, getEmitAcceleration); COPY_ENTITY_PROPERTY_TO_PROPERTIES(accelerationSpread, getAccelerationSpread); COPY_ENTITY_PROPERTY_TO_PROPERTIES(particleRadius, getParticleRadius); COPY_ENTITY_PROPERTY_TO_PROPERTIES(radiusSpread, getRadiusSpread); COPY_ENTITY_PROPERTY_TO_PROPERTIES(radiusStart, getRadiusStart); COPY_ENTITY_PROPERTY_TO_PROPERTIES(radiusFinish, getRadiusFinish); COPY_ENTITY_PROPERTY_TO_PROPERTIES(colorSpread, getColorSpread); COPY_ENTITY_PROPERTY_TO_PROPERTIES(colorStart, getColorStart); COPY_ENTITY_PROPERTY_TO_PROPERTIES(colorFinish, getColorFinish); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alphaSpread, getAlphaSpread); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alphaStart, getAlphaStart); COPY_ENTITY_PROPERTY_TO_PROPERTIES(alphaFinish, getAlphaFinish); COPY_ENTITY_PROPERTY_TO_PROPERTIES(textures, getTextures); _animationProperties.getProperties(properties); return properties; } bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); SET_ENTITY_PROPERTY_FROM_PROPERTIES(glowLevel, setGlowLevel); SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(maxParticles, setMaxParticles); SET_ENTITY_PROPERTY_FROM_PROPERTIES(lifespan, setLifespan); SET_ENTITY_PROPERTY_FROM_PROPERTIES(emitRate, setEmitRate); SET_ENTITY_PROPERTY_FROM_PROPERTIES(emitSpeed, setEmitSpeed); SET_ENTITY_PROPERTY_FROM_PROPERTIES(speedSpread, setSpeedSpread); SET_ENTITY_PROPERTY_FROM_PROPERTIES(emitOrientation, setEmitOrientation); SET_ENTITY_PROPERTY_FROM_PROPERTIES(emitDimensions, setEmitDimensions); SET_ENTITY_PROPERTY_FROM_PROPERTIES(emitRadiusStart, setEmitRadiusStart); SET_ENTITY_PROPERTY_FROM_PROPERTIES(polarStart, setPolarStart); SET_ENTITY_PROPERTY_FROM_PROPERTIES(polarFinish, setPolarFinish); SET_ENTITY_PROPERTY_FROM_PROPERTIES(azimuthStart, setAzimuthStart); SET_ENTITY_PROPERTY_FROM_PROPERTIES(azimuthFinish, setAzimuthFinish); SET_ENTITY_PROPERTY_FROM_PROPERTIES(emitAcceleration, setEmitAcceleration); SET_ENTITY_PROPERTY_FROM_PROPERTIES(accelerationSpread, setAccelerationSpread); SET_ENTITY_PROPERTY_FROM_PROPERTIES(particleRadius, setParticleRadius); SET_ENTITY_PROPERTY_FROM_PROPERTIES(radiusSpread, setRadiusSpread); SET_ENTITY_PROPERTY_FROM_PROPERTIES(radiusStart, setRadiusStart); SET_ENTITY_PROPERTY_FROM_PROPERTIES(radiusFinish, setRadiusFinish); SET_ENTITY_PROPERTY_FROM_PROPERTIES(colorSpread, setColorSpread); SET_ENTITY_PROPERTY_FROM_PROPERTIES(colorStart, setColorStart); SET_ENTITY_PROPERTY_FROM_PROPERTIES(colorFinish, setColorFinish); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alphaSpread, setAlphaSpread); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alphaStart, setAlphaStart); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alphaFinish, setAlphaFinish); SET_ENTITY_PROPERTY_FROM_PROPERTIES(textures, setTextures); bool somethingChangedInAnimations = _animationProperties.setProperties(properties); somethingChanged = somethingChanged || somethingChangedInAnimations; if (somethingChanged) { bool wantDebug = false; if (wantDebug) { uint64_t now = usecTimestampNow(); int elapsed = now - getLastEdited(); qCDebug(entities) << "ParticleEffectEntityItem::setProperties() AFTER update... edited AGO=" << elapsed << "now=" << now << " getLastEdited()=" << getLastEdited(); } setLastEdited(properties.getLastEdited()); } return somethingChanged; } int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, EntityPropertyFlags& propertyFlags, bool overwriteLocalData) { int bytesRead = 0; const unsigned char* dataAt = data; READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); // Because we're using AnimationLoop which will reset the frame index if you change it's running state // we want to read these values in the order they appear in the buffer, but call our setters in an // order that allows AnimationLoop to preserve the correct frame rate. float animationFPS = getAnimationFPS(); float animationFrameIndex = getAnimationFrameIndex(); bool animationIsPlaying = getAnimationIsPlaying(); if (args.bitstreamVersion < VERSION_ENTITIES_ANIMATION_PROPERTIES_GROUP) { READ_ENTITY_PROPERTY(PROP_ANIMATION_FPS, float, setAnimationFPS); READ_ENTITY_PROPERTY(PROP_ANIMATION_FRAME_INDEX, float, setAnimationFrameIndex); READ_ENTITY_PROPERTY(PROP_ANIMATION_PLAYING, bool, setAnimationIsPlaying); } if (propertyFlags.getHasProperty(PROP_ANIMATION_PLAYING)) { if (animationIsPlaying != getAnimationIsPlaying()) { setAnimationIsPlaying(animationIsPlaying); } } if (propertyFlags.getHasProperty(PROP_ANIMATION_FPS)) { setAnimationFPS(animationFPS); } if (propertyFlags.getHasProperty(PROP_ANIMATION_FRAME_INDEX)) { setAnimationFrameIndex(animationFrameIndex); } if (args.bitstreamVersion < VERSION_ENTITIES_ANIMATION_PROPERTIES_GROUP) { READ_ENTITY_PROPERTY(PROP_ANIMATION_SETTINGS, QString, setAnimationSettings); } else { int bytesFromAnimation = _animationProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, propertyFlags, overwriteLocalData); bytesRead += bytesFromAnimation; dataAt += bytesFromAnimation; // FIXME -- we need to actually get the property values out of the _animationProperties // and use them for our AnimationLoop } READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); READ_ENTITY_PROPERTY(PROP_MAX_PARTICLES, quint32, setMaxParticles); READ_ENTITY_PROPERTY(PROP_LIFESPAN, float, setLifespan); READ_ENTITY_PROPERTY(PROP_EMIT_RATE, float, setEmitRate); if (args.bitstreamVersion < VERSION_ENTITIES_PARTICLE_ELLIPSOID_EMITTER) { // OLD PROP_EMIT_VELOCITY FAKEOUT SKIP_ENTITY_PROPERTY(PROP_EMIT_SPEED, glm::vec3); } if (args.bitstreamVersion >= VERSION_ENTITIES_PARTICLE_MODIFICATIONS) { READ_ENTITY_PROPERTY(PROP_EMIT_ACCELERATION, glm::vec3, setEmitAcceleration); READ_ENTITY_PROPERTY(PROP_ACCELERATION_SPREAD, glm::vec3, setAccelerationSpread); READ_ENTITY_PROPERTY(PROP_PARTICLE_RADIUS, float, setParticleRadius); READ_ENTITY_PROPERTY(PROP_TEXTURES, QString, setTextures); if (args.bitstreamVersion < VERSION_ENTITIES_PARTICLE_ELLIPSOID_EMITTER) { // OLD PROP_VELOCITY_SPREAD FAKEOUT SKIP_ENTITY_PROPERTY(PROP_SPEED_SPREAD, glm::vec3); } } else { // OLD PROP_EMIT_ACCELERATION FAKEOUT SKIP_ENTITY_PROPERTY(PROP_PARTICLE_RADIUS, float); // OLD PROP_ACCELERATION_SPREAD FAKEOUT SKIP_ENTITY_PROPERTY(PROP_PARTICLE_RADIUS, float); READ_ENTITY_PROPERTY(PROP_PARTICLE_RADIUS, float, setParticleRadius); READ_ENTITY_PROPERTY(PROP_TEXTURES, QString, setTextures); } if (args.bitstreamVersion >= VERSION_ENTITIES_PARTICLE_RADIUS_PROPERTIES) { READ_ENTITY_PROPERTY(PROP_RADIUS_SPREAD, float, setRadiusSpread); READ_ENTITY_PROPERTY(PROP_RADIUS_START, float, setRadiusStart); READ_ENTITY_PROPERTY(PROP_RADIUS_FINISH, float, setRadiusFinish); } if (args.bitstreamVersion >= VERSION_ENTITIES_PARTICLE_COLOR_PROPERTIES) { READ_ENTITY_PROPERTY(PROP_COLOR_SPREAD, xColor, setColorSpread); READ_ENTITY_PROPERTY(PROP_COLOR_START, xColor, setColorStart); READ_ENTITY_PROPERTY(PROP_COLOR_FINISH, xColor, setColorFinish); READ_ENTITY_PROPERTY(PROP_ALPHA, float, setAlpha); READ_ENTITY_PROPERTY(PROP_ALPHA_SPREAD, float, setAlphaSpread); READ_ENTITY_PROPERTY(PROP_ALPHA_START, float, setAlphaStart); READ_ENTITY_PROPERTY(PROP_ALPHA_FINISH, float, setAlphaFinish); } if (args.bitstreamVersion >= VERSION_ENTITIES_PARTICLE_ELLIPSOID_EMITTER) { READ_ENTITY_PROPERTY(PROP_EMIT_SPEED, float, setEmitSpeed); READ_ENTITY_PROPERTY(PROP_SPEED_SPREAD, float, setSpeedSpread); READ_ENTITY_PROPERTY(PROP_EMIT_ORIENTATION, glm::quat, setEmitOrientation); READ_ENTITY_PROPERTY(PROP_EMIT_DIMENSIONS, glm::vec3, setEmitDimensions); READ_ENTITY_PROPERTY(PROP_EMIT_RADIUS_START, float, setEmitRadiusStart); READ_ENTITY_PROPERTY(PROP_POLAR_START, float, setPolarStart); READ_ENTITY_PROPERTY(PROP_POLAR_FINISH, float, setPolarFinish); READ_ENTITY_PROPERTY(PROP_AZIMUTH_START, float, setAzimuthStart); READ_ENTITY_PROPERTY(PROP_AZIMUTH_FINISH, float, setAzimuthFinish); } return bytesRead; } // TODO: eventually only include properties changed since the params.lastViewFrustumSent time EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_COLOR; requestedProperties += PROP_SHAPE_TYPE; requestedProperties += PROP_MAX_PARTICLES; requestedProperties += PROP_LIFESPAN; requestedProperties += PROP_EMIT_RATE; requestedProperties += PROP_EMIT_ACCELERATION; requestedProperties += PROP_ACCELERATION_SPREAD; requestedProperties += PROP_PARTICLE_RADIUS; requestedProperties += PROP_TEXTURES; requestedProperties += PROP_RADIUS_SPREAD; requestedProperties += PROP_RADIUS_START; requestedProperties += PROP_RADIUS_FINISH; requestedProperties += PROP_COLOR_SPREAD; requestedProperties += PROP_COLOR_START; requestedProperties += PROP_COLOR_FINISH; requestedProperties += PROP_ALPHA; requestedProperties += PROP_ALPHA_SPREAD; requestedProperties += PROP_ALPHA_START; requestedProperties += PROP_ALPHA_FINISH; requestedProperties += PROP_EMIT_SPEED; requestedProperties += PROP_SPEED_SPREAD; requestedProperties += PROP_EMIT_ORIENTATION; requestedProperties += PROP_EMIT_DIMENSIONS; requestedProperties += PROP_EMIT_RADIUS_START; requestedProperties += PROP_POLAR_START; requestedProperties += PROP_POLAR_FINISH; requestedProperties += PROP_AZIMUTH_START; requestedProperties += PROP_AZIMUTH_FINISH; requestedProperties += _animationProperties.getEntityProperties(params); return requestedProperties; } void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData, EntityPropertyFlags& requestedProperties, EntityPropertyFlags& propertyFlags, EntityPropertyFlags& propertiesDidntFit, int& propertyCount, OctreeElement::AppendState& appendState) const { bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); // FIXME - we probably need to make sure the _animationProperties has the latest data from the AnimationLoop _animationProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)getShapeType()); APPEND_ENTITY_PROPERTY(PROP_MAX_PARTICLES, getMaxParticles()); APPEND_ENTITY_PROPERTY(PROP_LIFESPAN, getLifespan()); APPEND_ENTITY_PROPERTY(PROP_EMIT_RATE, getEmitRate()); APPEND_ENTITY_PROPERTY(PROP_EMIT_ACCELERATION, getEmitAcceleration()); APPEND_ENTITY_PROPERTY(PROP_ACCELERATION_SPREAD, getAccelerationSpread()); APPEND_ENTITY_PROPERTY(PROP_PARTICLE_RADIUS, getParticleRadius()); APPEND_ENTITY_PROPERTY(PROP_TEXTURES, getTextures()); APPEND_ENTITY_PROPERTY(PROP_RADIUS_SPREAD, getRadiusSpread()); APPEND_ENTITY_PROPERTY(PROP_RADIUS_START, getRadiusStart()); APPEND_ENTITY_PROPERTY(PROP_RADIUS_FINISH, getRadiusFinish()); APPEND_ENTITY_PROPERTY(PROP_COLOR_SPREAD, getColorSpread()); APPEND_ENTITY_PROPERTY(PROP_COLOR_START, getColorStart()); APPEND_ENTITY_PROPERTY(PROP_COLOR_FINISH, getColorFinish()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); APPEND_ENTITY_PROPERTY(PROP_ALPHA_SPREAD, getAlphaSpread()); APPEND_ENTITY_PROPERTY(PROP_ALPHA_START, getAlphaStart()); APPEND_ENTITY_PROPERTY(PROP_ALPHA_FINISH, getAlphaFinish()); APPEND_ENTITY_PROPERTY(PROP_EMIT_SPEED, getEmitSpeed()); APPEND_ENTITY_PROPERTY(PROP_SPEED_SPREAD, getSpeedSpread()); APPEND_ENTITY_PROPERTY(PROP_EMIT_ORIENTATION, getEmitOrientation()); APPEND_ENTITY_PROPERTY(PROP_EMIT_DIMENSIONS, getEmitDimensions()); APPEND_ENTITY_PROPERTY(PROP_EMIT_RADIUS_START, getEmitRadiusStart()); APPEND_ENTITY_PROPERTY(PROP_POLAR_START, getPolarStart()); APPEND_ENTITY_PROPERTY(PROP_POLAR_FINISH, getPolarFinish()); APPEND_ENTITY_PROPERTY(PROP_AZIMUTH_START, getAzimuthStart()); APPEND_ENTITY_PROPERTY(PROP_AZIMUTH_FINISH, getAzimuthFinish()); } bool ParticleEffectEntityItem::isAnimatingSomething() const { // keep animating if there are particles still alive. return (getAnimationIsPlaying() || getLivingParticleCount() > 0) && getAnimationFPS() != 0.0f; } bool ParticleEffectEntityItem::needsToCallUpdate() const { return true; } void ParticleEffectEntityItem::update(const quint64& now) { float deltaTime = (float)(now - _lastAnimated) / (float)USECS_PER_SECOND; _lastAnimated = now; // only advance the frame index if we're playing if (getAnimationIsPlaying()) { _animationLoop.simulate(deltaTime); } if (isAnimatingSomething()) { stepSimulation(deltaTime); } EntityItem::update(now); // let our base class handle it's updates... } void ParticleEffectEntityItem::debugDump() const { quint64 now = usecTimestampNow(); qCDebug(entities) << "PA EFFECT EntityItem id:" << getEntityItemID() << "---------------------------------------------"; qCDebug(entities) << " color:" << _color[0] << "," << _color[1] << "," << _color[2]; qCDebug(entities) << " position:" << debugTreeVector(getPosition()); qCDebug(entities) << " dimensions:" << debugTreeVector(getDimensions()); qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } void ParticleEffectEntityItem::updateShapeType(ShapeType type) { if (type != _shapeType) { _shapeType = type; _dirtyFlags |= EntityItem::DIRTY_SHAPE | EntityItem::DIRTY_MASS; } } void ParticleEffectEntityItem::setAnimationFrameIndex(float value) { #ifdef WANT_DEBUG if (isAnimatingSomething()) { qCDebug(entities) << "ParticleEffectEntityItem::setAnimationFrameIndex()"; qCDebug(entities) << " value:" << value; qCDebug(entities) << " was:" << _animationLoop.getFrameIndex(); } #endif _animationLoop.setFrameIndex(value); } void ParticleEffectEntityItem::setAnimationSettings(const QString& value) { // the animations setting is a JSON string that may contain various animation settings. // if it includes fps, frameIndex, or running, those values will be parsed out and // will over ride the regular animation settings QJsonDocument settingsAsJson = QJsonDocument::fromJson(value.toUtf8()); QJsonObject settingsAsJsonObject = settingsAsJson.object(); QVariantMap settingsMap = settingsAsJsonObject.toVariantMap(); if (settingsMap.contains("fps")) { float fps = settingsMap["fps"].toFloat(); setAnimationFPS(fps); } if (settingsMap.contains("frameIndex")) { float frameIndex = settingsMap["frameIndex"].toFloat(); #ifdef WANT_DEBUG if (isAnimatingSomething()) { qCDebug(entities) << "ParticleEffectEntityItem::setAnimationSettings() calling setAnimationFrameIndex()..."; qCDebug(entities) << " settings:" << value; qCDebug(entities) << " settingsMap[frameIndex]:" << settingsMap["frameIndex"]; qCDebug(entities, " frameIndex: %20.5f", frameIndex); } #endif setAnimationFrameIndex(frameIndex); } if (settingsMap.contains("running")) { bool running = settingsMap["running"].toBool(); if (running != getAnimationIsPlaying()) { setAnimationIsPlaying(running); } } if (settingsMap.contains("firstFrame")) { float firstFrame = settingsMap["firstFrame"].toFloat(); setAnimationFirstFrame(firstFrame); } if (settingsMap.contains("lastFrame")) { float lastFrame = settingsMap["lastFrame"].toFloat(); setAnimationLastFrame(lastFrame); } if (settingsMap.contains("loop")) { bool loop = settingsMap["loop"].toBool(); setAnimationLoop(loop); } if (settingsMap.contains("hold")) { bool hold = settingsMap["hold"].toBool(); setAnimationHold(hold); } if (settingsMap.contains("startAutomatically")) { bool startAutomatically = settingsMap["startAutomatically"].toBool(); setAnimationStartAutomatically(startAutomatically); } _animationSettings = value; _dirtyFlags |= EntityItem::DIRTY_UPDATEABLE; } void ParticleEffectEntityItem::setAnimationIsPlaying(bool value) { _dirtyFlags |= EntityItem::DIRTY_UPDATEABLE; _animationLoop.setRunning(value); } void ParticleEffectEntityItem::setAnimationFPS(float value) { _dirtyFlags |= EntityItem::DIRTY_UPDATEABLE; _animationLoop.setFPS(value); } QString ParticleEffectEntityItem::getAnimationSettings() const { // the animations setting is a JSON string that may contain various animation settings. // if it includes fps, frameIndex, or running, those values will be parsed out and // will over ride the regular animation settings QString value = _animationSettings; QJsonDocument settingsAsJson = QJsonDocument::fromJson(value.toUtf8()); QJsonObject settingsAsJsonObject = settingsAsJson.object(); QVariantMap settingsMap = settingsAsJsonObject.toVariantMap(); QVariant fpsValue(getAnimationFPS()); settingsMap["fps"] = fpsValue; QVariant frameIndexValue(getAnimationFrameIndex()); settingsMap["frameIndex"] = frameIndexValue; QVariant runningValue(getAnimationIsPlaying()); settingsMap["running"] = runningValue; QVariant firstFrameValue(getAnimationFirstFrame()); settingsMap["firstFrame"] = firstFrameValue; QVariant lastFrameValue(getAnimationLastFrame()); settingsMap["lastFrame"] = lastFrameValue; QVariant loopValue(getAnimationLoop()); settingsMap["loop"] = loopValue; QVariant holdValue(getAnimationHold()); settingsMap["hold"] = holdValue; QVariant startAutomaticallyValue(getAnimationStartAutomatically()); settingsMap["startAutomatically"] = startAutomaticallyValue; settingsAsJsonObject = QJsonObject::fromVariantMap(settingsMap); QJsonDocument newDocument(settingsAsJsonObject); QByteArray jsonByteArray = newDocument.toJson(QJsonDocument::Compact); QString jsonByteString(jsonByteArray); return jsonByteString; } void ParticleEffectEntityItem::updateRadius(quint32 index, float age) { _particleRadiuses[index] = Interpolate::interpolate3Points(_radiusStarts[index], _radiusMiddles[index], _radiusFinishes[index], age); } void ParticleEffectEntityItem::updateColor(quint32 index, float age) { _particleColors[index].red = (int)Interpolate::interpolate3Points(_colorStarts[index].red, _colorMiddles[index].red, _colorFinishes[index].red, age); _particleColors[index].green = (int)Interpolate::interpolate3Points(_colorStarts[index].green, _colorMiddles[index].green, _colorFinishes[index].green, age); _particleColors[index].blue = (int)Interpolate::interpolate3Points(_colorStarts[index].blue, _colorMiddles[index].blue, _colorFinishes[index].blue, age); } void ParticleEffectEntityItem::updateAlpha(quint32 index, float age) { _particleAlphas[index] = Interpolate::interpolate3Points(_alphaStarts[index], _alphaMiddles[index], _alphaFinishes[index], age); } void ParticleEffectEntityItem::extendBounds(const glm::vec3& point) { _particleMinBound.x = glm::min(_particleMinBound.x, point.x); _particleMinBound.y = glm::min(_particleMinBound.y, point.y); _particleMinBound.z = glm::min(_particleMinBound.z, point.z); _particleMaxBound.x = glm::max(_particleMaxBound.x, point.x); _particleMaxBound.y = glm::max(_particleMaxBound.y, point.y); _particleMaxBound.z = glm::max(_particleMaxBound.z, point.z); } void ParticleEffectEntityItem::integrateParticle(quint32 index, float deltaTime) { glm::vec3 accel = _particleAccelerations[index]; glm::vec3 atSquared = (0.5f * deltaTime * deltaTime) * accel; glm::vec3 at = accel * deltaTime; _particlePositions[index] += _particleVelocities[index] * deltaTime + atSquared; _particleVelocities[index] += at; } void ParticleEffectEntityItem::stepSimulation(float deltaTime) { _particleMinBound = glm::vec3(-1.0f, -1.0f, -1.0f); _particleMaxBound = glm::vec3(1.0f, 1.0f, 1.0f); // update particles between head and tail for (quint32 i = _particleHeadIndex; i != _particleTailIndex; i = (i + 1) % _maxParticles) { _particleLifetimes[i] -= deltaTime; // if particle has died. if (_particleLifetimes[i] <= 0.0f) { // move head forward _particleHeadIndex = (_particleHeadIndex + 1) % _maxParticles; } else { float age = (1.0f - _particleLifetimes[i] / _lifespan); // 0.0 .. 1.0 updateRadius(i, age); updateColor(i, age); updateAlpha(i, age); integrateParticle(i, deltaTime); extendBounds(_particlePositions[i]); } } // emit new particles, but only if animation is playing if (getAnimationIsPlaying()) { float timeLeftInFrame = deltaTime; while (_timeUntilNextEmit < timeLeftInFrame) { timeLeftInFrame -= _timeUntilNextEmit; _timeUntilNextEmit = 1.0f / _emitRate; // emit a new particle at tail index. quint32 i = _particleTailIndex; _particleLifetimes[i] = _lifespan; // Radius if (_radiusSpread == 0.0f) { _radiusStarts[i] = getRadiusStart(); _radiusMiddles[i] =_particleRadius; _radiusFinishes[i] = getRadiusFinish(); } else { float spreadMultiplier = 1.0f + (2.0f * randFloat() - 1.0f) * _radiusSpread / _particleRadius; _radiusStarts[i] = spreadMultiplier * getRadiusStart(); _radiusMiddles[i] = spreadMultiplier * _particleRadius; _radiusFinishes[i] = spreadMultiplier * getRadiusFinish(); } updateRadius(i, 0.0f); // Position, velocity, and acceleration if (_polarStart == 0.0f && _polarFinish == 0.0f && _emitDimensions.z == 0.0f) { // Emit along z-axis from position _particlePositions[i] = getPosition(); _particleVelocities[i] = (_emitSpeed + (2.0f * randFloat() - 1.0f) * _speedSpread) * (_emitOrientation * Z_AXIS); _particleAccelerations[i] = _emitAcceleration + (2.0f * randFloat() - 1.0f) * _accelerationSpread; } else { // Emit around point or from ellipsoid // - Distribute directions evenly around point // - Distribute points relatively evenly over ellipsoid surface // - Distribute points relatively evenly within ellipsoid volume float elevationMinZ = sin(PI_OVER_TWO - _polarFinish); float elevationMaxZ = sin(PI_OVER_TWO - _polarStart); float elevation = asin(elevationMinZ + (elevationMaxZ - elevationMinZ) * randFloat()); float azimuth; if (_azimuthFinish >= _azimuthStart) { azimuth = _azimuthStart + (_azimuthFinish - _azimuthStart) * randFloat(); } else { azimuth = _azimuthStart + (2.0f * PI + _azimuthFinish - _azimuthStart) * randFloat(); } glm::vec3 emitDirection; if (_emitDimensions == glm::vec3()) { // Point emitDirection = glm::angleAxis(PI_OVER_TWO - elevation, X_AXIS) * Z_AXIS; emitDirection = glm::angleAxis(azimuth, Z_AXIS) * emitDirection; _particlePositions[i] = getPosition(); } else { // Ellipsoid float radiusScale = 1.0f; if (_emitRadiusStart < 1.0f) { float emitRadiusStart = glm::max(_emitRadiusStart, EPSILON); // Avoid math complications at center float randRadius = emitRadiusStart + (1.0f - emitRadiusStart) * randFloat(); radiusScale = 1.0f - std::pow(1.0f - randRadius, 3.0f); } glm::vec3 radiuses = radiusScale * 0.5f * _emitDimensions; float x = radiuses.x * glm::cos(elevation) * glm::cos(azimuth); float y = radiuses.y * glm::cos(elevation) * glm::sin(azimuth); float z = radiuses.z * glm::sin(elevation); glm::vec3 emitPosition = glm::vec3(x, y, z); emitDirection = glm::normalize(glm::vec3( radiuses.x > 0.0f ? x / (radiuses.x * radiuses.x) : 0.0f, radiuses.y > 0.0f ? y / (radiuses.y * radiuses.y) : 0.0f, radiuses.z > 0.0f ? z / (radiuses.z * radiuses.z) : 0.0f )); _particlePositions[i] = getPosition() + _emitOrientation * emitPosition; } _particleVelocities[i] = (_emitSpeed + (2.0f * randFloat() - 1.0f) * _speedSpread) * (_emitOrientation * emitDirection); _particleAccelerations[i] = _emitAcceleration + (2.0f * randFloat() - 1.0f) * _accelerationSpread; } integrateParticle(i, timeLeftInFrame); extendBounds(_particlePositions[i]); // Color if (_colorSpread == xColor{ 0, 0, 0 }) { _colorStarts[i] = getColorStart(); _colorMiddles[i] = getXColor(); _colorFinishes[i] = getColorFinish(); } else { xColor startColor = getColorStart(); xColor middleColor = getXColor(); xColor finishColor = getColorFinish(); float spread = 2.0f * randFloat() - 1.0f; float spreadMultiplierRed = 1.0f + spread * (float)_colorSpread.red / (float)middleColor.red; float spreadMultiplierGreen = 1.0f + spread * (float)_colorSpread.green / (float)middleColor.green; float spreadMultiplierBlue = 1.0f + spread * (float)_colorSpread.blue / (float)middleColor.blue; _colorStarts[i].red = (int)glm::clamp(spreadMultiplierRed * (float)startColor.red, 0.0f, 255.0f); _colorStarts[i].green = (int)glm::clamp(spreadMultiplierGreen * (float)startColor.green, 0.0f, 255.0f); _colorStarts[i].blue = (int)glm::clamp(spreadMultiplierBlue * (float)startColor.blue, 0.0f, 255.0f); _colorMiddles[i].red = (int)glm::clamp(spreadMultiplierRed * (float)middleColor.red, 0.0f, 255.0f); _colorMiddles[i].green = (int)glm::clamp(spreadMultiplierGreen * (float)middleColor.green, 0.0f, 255.0f); _colorMiddles[i].blue = (int)glm::clamp(spreadMultiplierBlue * (float)middleColor.blue, 0.0f, 255.0f); _colorFinishes[i].red = (int)glm::clamp(spreadMultiplierRed * (float)finishColor.red, 0.0f, 255.0f); _colorFinishes[i].green = (int)glm::clamp(spreadMultiplierGreen * (float)finishColor.green, 0.0f, 255.0f); _colorFinishes[i].blue = (int)glm::clamp(spreadMultiplierBlue * (float)finishColor.blue, 0.0f, 255.0f); } updateColor(i, 0.0f); // Alpha if (_alphaSpread == 0.0f) { _alphaStarts[i] = getAlphaStart(); _alphaMiddles[i] = _alpha; _alphaFinishes[i] = getAlphaFinish(); } else { float spreadMultiplier = 1.0f + (2.0f * randFloat() - 1) * _alphaSpread / _alpha; _alphaStarts[i] = spreadMultiplier * getAlphaStart(); _alphaMiddles[i] = spreadMultiplier * _alpha; _alphaFinishes[i] = spreadMultiplier * getAlphaFinish(); } updateAlpha(i, 0.0f); _particleTailIndex = (_particleTailIndex + 1) % _maxParticles; // overflow! move head forward by one. // because the case of head == tail indicates an empty array, not a full one. // This can drop an existing older particle, but this is by design, newer particles are a higher priority. if (_particleTailIndex == _particleHeadIndex) { _particleHeadIndex = (_particleHeadIndex + 1) % _maxParticles; } } _timeUntilNextEmit -= timeLeftInFrame; } } void ParticleEffectEntityItem::setMaxParticles(quint32 maxParticles) { if (_maxParticles != maxParticles) { _maxParticles = maxParticles; // TODO: try to do something smart here and preserve the state of existing particles. // resize vectors _particleLifetimes.resize(_maxParticles); _particlePositions.resize(_maxParticles); _particleVelocities.resize(_maxParticles); _particleRadiuses.resize(_maxParticles); _radiusStarts.resize(_maxParticles); _radiusMiddles.resize(_maxParticles); _radiusFinishes.resize(_maxParticles); _particleColors.resize(_maxParticles); _colorStarts.resize(_maxParticles); _colorMiddles.resize(_maxParticles); _colorFinishes.resize(_maxParticles); _particleAlphas.resize(_maxParticles); _alphaStarts.resize(_maxParticles); _alphaMiddles.resize(_maxParticles); _alphaFinishes.resize(_maxParticles); // effectively clear all particles and start emitting new ones from scratch. _particleHeadIndex = 0; _particleTailIndex = 0; _timeUntilNextEmit = 0.0f; } } // because particles are in a ring buffer, this isn't trivial quint32 ParticleEffectEntityItem::getLivingParticleCount() const { if (_particleTailIndex >= _particleHeadIndex) { return _particleTailIndex - _particleHeadIndex; } else { return (_maxParticles - _particleHeadIndex) + _particleTailIndex; } }