From ede42285b1ba1c6d1d2045c0be42ab699e684ab9 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Sat, 9 May 2015 07:57:29 -0700 Subject: [PATCH 1/2] Improvements to particle entity. * Changed particle geometry to billboarded quads * Added texture support * Added ajt-test.js particle test script. * GeometryCache support for batched quads with texCoords. * Bug fix for infinite loop if _lifetime was very large. * Don't reset the simulation on animation loop. * stop emitting particles on animation stop, but keep simulating until there are no more living particles. * Removed some trailing whitespace --- examples/ajt-test.js | 83 ++++++++++ .../RenderableParticleEffectEntityItem.cpp | 148 ++++++++++++++---- .../src/RenderableParticleEffectEntityItem.h | 9 +- .../entities/src/ParticleEffectEntityItem.cpp | 86 +++++----- .../entities/src/ParticleEffectEntityItem.h | 39 ++--- libraries/render-utils/src/GeometryCache.cpp | 70 +++++++++ libraries/render-utils/src/GeometryCache.h | 1 + 7 files changed, 341 insertions(+), 95 deletions(-) create mode 100644 examples/ajt-test.js diff --git a/examples/ajt-test.js b/examples/ajt-test.js new file mode 100644 index 0000000000..a908978a02 --- /dev/null +++ b/examples/ajt-test.js @@ -0,0 +1,83 @@ + +(function () { + var spawnPoint = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); + + // constructor + function TestBox() { + this.entity = Entities.addEntity({ type: "Box", + position: spawnPoint, + dimentions: { x: 1, y: 1, z: 1 }, + color: { red: 100, green: 100, blue: 255 }, + gravity: { x: 0, y: 0, z: 0 }, + visible: true, + locked: false, + lifetime: 6000 }); + var self = this; + this.timer = Script.setInterval(function () { + var colorProp = { color: { red: Math.random() * 255, + green: Math.random() * 255, + blue: Math.random() * 255 } }; + Entities.editEntity(self.entity, colorProp); + }, 1000); + } + + TestBox.prototype.Destroy = function () { + Script.clearInterval(this.timer); + Entities.editEntity(this.entity, { locked: false }); + Entities.deleteEntity(this.entity); + } + + // constructor + function TestFx() { + var animationSettings = JSON.stringify({ fps: 30, + frameIndex: 0, + running: true, + firstFrame: 0, + lastFrame: 30, + loop: false }); + + this.entity = Entities.addEntity({ type: "ParticleEffect", + animationSettings: animationSettings, + position: spawnPoint, + textures: "http://www.hyperlogic.org/images/particle.png", + emitRate: 30, + emitStrength: 3, + color: { red: 128, green: 255, blue: 255 }, + visible: true, + locked: false }); + + var self = this; + this.timer = Script.setInterval(function () { + print("AJT: setting animation props"); + var animProp = { animationFrameIndex: 0, + animationIsPlaying: true }; + Entities.editEntity(self.entity, animProp); + }, 2000); + + } + + TestFx.prototype.Destroy = function () { + Script.clearInterval(this.timer); + Entities.editEntity(this.entity, { locked: false }); + Entities.deleteEntity(this.entity); + } + + var objs = []; + function Init() { + objs.push(new TestBox()); + objs.push(new TestFx()); + } + + function ShutDown() { + var i, len = entities.length; + for (i = 0; i < len; i++) { + objs[i].Destroy(); + } + objs = []; + } + + Init(); + Script.scriptEnding.connect(ShutDown); +})(); + + diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index b4dd5b4c64..32fd06c837 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -16,6 +16,7 @@ #include #include #include +#include "EntitiesRendererLogging.h" #include "RenderableParticleEffectEntityItem.h" @@ -29,62 +30,155 @@ RenderableParticleEffectEntityItem::RenderableParticleEffectEntityItem(const Ent } void RenderableParticleEffectEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderableParticleEffectEntityItem::render"); + assert(getType() == EntityTypes::ParticleEffect); + PerformanceTimer perfTimer("RenderableParticleEffectEntityItem::render"); + + if (_texturesChangedFlag) + { + if (_textures.isEmpty()) { + _texture.clear(); + } else { + // for now use the textures string directly. + // Eventually we'll want multiple textures in a map or array. + _texture = DependencyManager::get()->getTexture(_textures); + } + _texturesChangedFlag = false; + } + + if (!_texture) { + renderUntexturedQuads(args); + } else if (_texture && !_texture->isLoaded()) { + renderUntexturedQuads(args); + } else { + renderTexturedQuads(args); + } +}; + +void RenderableParticleEffectEntityItem::renderUntexturedQuads(RenderArgs* args) { + float pa_rad = getParticleRadius(); const float MAX_COLOR = 255.0f; glm::vec4 paColor(getColor()[RED_INDEX] / MAX_COLOR, getColor()[GREEN_INDEX] / MAX_COLOR, getColor()[BLUE_INDEX] / MAX_COLOR, getLocalRenderAlpha()); - // Right now we're just iterating over particles and rendering as a cross of four quads. - // This is pretty dumb, it was quick enough to code up. Really, there should be many - // rendering modes, including the all-important textured billboards. + glm::vec3 up_offset = args->_viewFrustum->getUp() * pa_rad; + glm::vec3 right_offset = args->_viewFrustum->getRight() * pa_rad; QVector* pointVec = new QVector(_paCount * VERTS_PER_PARTICLE); + quint32 paCount = 0; quint32 paIter = _paHead; - while (_paLife[paIter] > 0.0f) { + while (_paLife[paIter] > 0.0f && paCount < _maxParticles) { int j = paIter * XYZ_STRIDE; - pointVec->append(glm::vec3(_paPosition[j] - pa_rad, _paPosition[j + 1] + pa_rad, _paPosition[j + 2])); - pointVec->append(glm::vec3(_paPosition[j] + pa_rad, _paPosition[j + 1] + pa_rad, _paPosition[j + 2])); - pointVec->append(glm::vec3(_paPosition[j] + pa_rad, _paPosition[j + 1] - pa_rad, _paPosition[j + 2])); - pointVec->append(glm::vec3(_paPosition[j] - pa_rad, _paPosition[j + 1] - pa_rad, _paPosition[j + 2])); + glm::vec3 pos(_paPosition[j], _paPosition[j + 1], _paPosition[j + 2]); - pointVec->append(glm::vec3(_paPosition[j] + pa_rad, _paPosition[j + 1] + pa_rad, _paPosition[j + 2])); - pointVec->append(glm::vec3(_paPosition[j] - pa_rad, _paPosition[j + 1] + pa_rad, _paPosition[j + 2])); - pointVec->append(glm::vec3(_paPosition[j] - pa_rad, _paPosition[j + 1] - pa_rad, _paPosition[j + 2])); - pointVec->append(glm::vec3(_paPosition[j] + pa_rad, _paPosition[j + 1] - pa_rad, _paPosition[j + 2])); - - pointVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1] + pa_rad, _paPosition[j + 2] - pa_rad)); - pointVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1] + pa_rad, _paPosition[j + 2] + pa_rad)); - pointVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1] - pa_rad, _paPosition[j + 2] + pa_rad)); - pointVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1] - pa_rad, _paPosition[j + 2] - pa_rad)); - - pointVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1] + pa_rad, _paPosition[j + 2] + pa_rad)); - pointVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1] + pa_rad, _paPosition[j + 2] - pa_rad)); - pointVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1] - pa_rad, _paPosition[j + 2] - pa_rad)); - pointVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1] - pa_rad, _paPosition[j + 2] + pa_rad)); + pointVec->append(pos - right_offset + up_offset); + pointVec->append(pos + right_offset + up_offset); + pointVec->append(pos + right_offset - up_offset); + pointVec->append(pos - right_offset - up_offset); paIter = (paIter + 1) % _maxParticles; + paCount++; } DependencyManager::get()->updateVertices(_cacheID, *pointVec, paColor); - + glPushMatrix(); glm::vec3 position = getPosition(); glTranslatef(position.x, position.y, position.z); glm::quat rotation = getRotation(); glm::vec3 axis = glm::axis(rotation); glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); - + glPushMatrix(); glm::vec3 positionToCenter = getCenter() - position; glTranslatef(positionToCenter.x, positionToCenter.y, positionToCenter.z); - + DependencyManager::get()->renderVertices(gpu::QUADS, _cacheID); glPopMatrix(); glPopMatrix(); delete pointVec; -}; +} + +static glm::vec3 s_dir; +static bool zSort(const glm::vec3& rhs, const glm::vec3& lhs) { + return glm::dot(rhs, s_dir) > glm::dot(lhs, s_dir); +} + +void RenderableParticleEffectEntityItem::renderTexturedQuads(RenderArgs* args) { + + float pa_rad = getParticleRadius(); + + const float MAX_COLOR = 255.0f; + glm::vec4 paColor(getColor()[RED_INDEX] / MAX_COLOR, getColor()[GREEN_INDEX] / MAX_COLOR, + getColor()[BLUE_INDEX] / MAX_COLOR, getLocalRenderAlpha()); + + QVector* posVec = new QVector(_paCount); + quint32 paCount = 0; + quint32 paIter = _paHead; + while (_paLife[paIter] > 0.0f && paCount < _maxParticles) { + int j = paIter * XYZ_STRIDE; + posVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1], _paPosition[j + 2])); + + paIter = (paIter + 1) % _maxParticles; + paCount++; + } + + // sort particles back to front + qSort(posVec->begin(), posVec->end(), zSort); + + QVector* vertVec = new QVector(_paCount * VERTS_PER_PARTICLE); + QVector* texCoordVec = new QVector(_paCount * VERTS_PER_PARTICLE); + + s_dir = args->_viewFrustum->getDirection(); + glm::vec3 up_offset = args->_viewFrustum->getUp() * pa_rad; + glm::vec3 right_offset = args->_viewFrustum->getRight() * pa_rad; + + for (int i = 0; i < posVec->size(); i++) { + glm::vec3 pos = posVec->at(i); + + vertVec->append(pos - right_offset + up_offset); + vertVec->append(pos + right_offset + up_offset); + vertVec->append(pos + right_offset - up_offset); + vertVec->append(pos - right_offset - up_offset); + + texCoordVec->append(glm::vec2(0, 1)); + texCoordVec->append(glm::vec2(1, 1)); + texCoordVec->append(glm::vec2(1, 0)); + texCoordVec->append(glm::vec2(0, 0)); + } + + DependencyManager::get()->updateVertices(_cacheID, *vertVec, *texCoordVec, paColor); + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, _texture->getID()); + + glPushMatrix(); + + glm::vec3 position = getPosition(); + glTranslatef(position.x, position.y, position.z); + glm::quat rotation = getRotation(); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); + + glPushMatrix(); + + glm::vec3 positionToCenter = getCenter() - position; + glTranslatef(positionToCenter.x, positionToCenter.y, positionToCenter.z); + + DependencyManager::get()->renderVertices(gpu::QUADS, _cacheID); + + glPopMatrix(); + glPopMatrix(); + + glDisable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); + + delete posVec; + delete vertVec; + delete texCoordVec; +} + diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 74b29574c3..25bbe5c147 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -12,6 +12,7 @@ #define hifi_RenderableParticleEffectEntityItem_h #include +#include class RenderableParticleEffectEntityItem : public ParticleEffectEntityItem { public: @@ -19,9 +20,15 @@ public: RenderableParticleEffectEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties); virtual void render(RenderArgs* args); + void renderUntexturedQuads(RenderArgs* args); + void renderTexturedQuads(RenderArgs* args); + protected: + int _cacheID; - const int VERTS_PER_PARTICLE = 16; + const int VERTS_PER_PARTICLE = 4; + + NetworkTexturePointer _texture; }; diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 45312f465b..fc4927fdcd 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -18,12 +18,8 @@ // - Add controls for spread (which is currently hard-coded) and varying emission strength (not currently implemented). // - Add drag. // - Add some kind of support for collisions. -// - For simplicity, I'm currently just rendering each particle as a cross of four axis-aligned quads. Really, we'd -// want multiple render modes, including (the most important) textured billboards (always facing camera). Also, these -// should support animated textures. // - 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. -// - MORE? // // Created by Jason Rickwald on 3/2/15. // @@ -55,7 +51,7 @@ const glm::vec3 ParticleEffectEntityItem::DEFAULT_EMIT_DIRECTION(0.0f, 1.0f, 0.0 const float ParticleEffectEntityItem::DEFAULT_EMIT_STRENGTH = 25.0f; const float ParticleEffectEntityItem::DEFAULT_LOCAL_GRAVITY = -9.8f; const float ParticleEffectEntityItem::DEFAULT_PARTICLE_RADIUS = 0.025f; - +const QString ParticleEffectEntityItem::DEFAULT_TEXTURES = ""; EntityItem* ParticleEffectEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { return new ParticleEffectEntityItem(entityID, properties); @@ -72,6 +68,7 @@ ParticleEffectEntityItem::ParticleEffectEntityItem(const EntityItemID& entityIte _emitStrength = DEFAULT_EMIT_STRENGTH; _localGravity = DEFAULT_LOCAL_GRAVITY; _particleRadius = DEFAULT_PARTICLE_RADIUS; + _textures = DEFAULT_TEXTURES; setProperties(properties); // this is a pretty dumb thing to do, and it should probably be changed to use a more dynamic // data structure in the future. I'm just trying to get some code out the door for now (and it's @@ -96,7 +93,7 @@ ParticleEffectEntityItem::~ParticleEffectEntityItem() { EntityItemProperties ParticleEffectEntityItem::getProperties() const { EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class - + COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(animationIsPlaying, getAnimationIsPlaying); COPY_ENTITY_PROPERTY_TO_PROPERTIES(animationFrameIndex, getAnimationFrameIndex); @@ -111,6 +108,7 @@ EntityItemProperties ParticleEffectEntityItem::getProperties() const { COPY_ENTITY_PROPERTY_TO_PROPERTIES(emitStrength, getEmitStrength); COPY_ENTITY_PROPERTY_TO_PROPERTIES(localGravity, getLocalGravity); COPY_ENTITY_PROPERTY_TO_PROPERTIES(particleRadius, getParticleRadius); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(textures, getTextures); return properties; } @@ -132,6 +130,7 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert SET_ENTITY_PROPERTY_FROM_PROPERTIES(emitStrength, setEmitStrength); SET_ENTITY_PROPERTY_FROM_PROPERTIES(localGravity, setLocalGravity); SET_ENTITY_PROPERTY_FROM_PROPERTIES(particleRadius, setParticleRadius); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(textures, setTextures); if (somethingChanged) { bool wantDebug = false; @@ -186,6 +185,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch READ_ENTITY_PROPERTY(PROP_EMIT_STRENGTH, float, _emitStrength); READ_ENTITY_PROPERTY(PROP_LOCAL_GRAVITY, float, _localGravity); READ_ENTITY_PROPERTY(PROP_PARTICLE_RADIUS, float, _particleRadius); + READ_ENTITY_PROPERTY(PROP_TEXTURES, QString, _textures); return bytesRead; } @@ -194,7 +194,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch // 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_ANIMATION_FPS; requestedProperties += PROP_ANIMATION_FRAME_INDEX; @@ -208,6 +208,7 @@ EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstrea requestedProperties += PROP_EMIT_STRENGTH; requestedProperties += PROP_LOCAL_GRAVITY; requestedProperties += PROP_PARTICLE_RADIUS; + requestedProperties += PROP_TEXTURES; return requestedProperties; } @@ -234,11 +235,12 @@ void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, APPEND_ENTITY_PROPERTY(PROP_EMIT_STRENGTH, appendValue, getEmitStrength()); APPEND_ENTITY_PROPERTY(PROP_LOCAL_GRAVITY, appendValue, getLocalGravity()); APPEND_ENTITY_PROPERTY(PROP_PARTICLE_RADIUS, appendValue, getParticleRadius()); + APPEND_ENTITY_PROPERTY(PROP_TEXTURES, appendValue, getTextures()); } bool ParticleEffectEntityItem::isAnimatingSomething() const { - return getAnimationIsPlaying() && - getAnimationFPS() != 0.0f; + // keep animating if there are particles still alive. + return (getAnimationIsPlaying() || _paCount > 0) && getAnimationFPS() != 0.0f; } bool ParticleEffectEntityItem::needsToCallUpdate() const { @@ -246,33 +248,25 @@ bool ParticleEffectEntityItem::needsToCallUpdate() const { } 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()) { - float deltaTime = (float)(now - _lastAnimated) / (float)USECS_PER_SECOND; - _lastAnimated = now; - float lastFrame = _animationLoop.getFrameIndex(); _animationLoop.simulate(deltaTime); - float curFrame = _animationLoop.getFrameIndex(); - if (curFrame > lastFrame) { - stepSimulation(deltaTime); - } - else if (curFrame < lastFrame) { - // we looped around, so restart the sim and only sim up to the point - // since the beginning of the frame range. - resetSimulation(); - stepSimulation((curFrame - _animationLoop.getFirstFrame()) / _animationLoop.getFPS()); - } - } - else { - _lastAnimated = now; } - // update the dimensions - glm::vec3 dims; - dims.x = glm::max(glm::abs(_paXmin), glm::abs(_paXmax)) * 2.0f; - dims.y = glm::max(glm::abs(_paYmin), glm::abs(_paYmax)) * 2.0f; - dims.z = glm::max(glm::abs(_paZmin), glm::abs(_paZmax)) * 2.0f; - setDimensions(dims); + if (isAnimatingSomething()) { + stepSimulation(deltaTime); + + // update the dimensions + glm::vec3 dims; + dims.x = glm::max(glm::abs(_paXmin), glm::abs(_paXmax)) * 2.0f; + dims.y = glm::max(glm::abs(_paYmin), glm::abs(_paYmax)) * 2.0f; + dims.z = glm::max(glm::abs(_paZmin), glm::abs(_paZmax)) * 2.0f; + setDimensions(dims); + } EntityItem::update(now); // let our base class handle it's updates... } @@ -422,9 +416,12 @@ void ParticleEffectEntityItem::stepSimulation(float deltaTime) { _paXmin = _paYmin = _paZmin = -1.0f; _paXmax = _paYmax = _paZmax = 1.0f; + float oneHalfATSquared = 0.5f * _localGravity * deltaTime * deltaTime; + // update particles + quint32 updateCount = 0; quint32 updateIter = _paHead; - while (_paLife[updateIter] > 0.0f) { + while (_paLife[updateIter] > 0.0f && updateCount < _maxParticles) { _paLife[updateIter] -= deltaTime; if (_paLife[updateIter] <= 0.0f) { _paLife[updateIter] = -1.0f; @@ -432,10 +429,10 @@ void ParticleEffectEntityItem::stepSimulation(float deltaTime) { _paCount--; } else { - // DUMB FORWARD EULER just to get it done int j = updateIter * XYZ_STRIDE; + _paPosition[j] += _paVelocity[j] * deltaTime; - _paPosition[j+1] += _paVelocity[j+1] * deltaTime; + _paPosition[j+1] += _paVelocity[j+1] * deltaTime + oneHalfATSquared; _paPosition[j+2] += _paVelocity[j+2] * deltaTime; _paXmin = glm::min(_paXmin, _paPosition[j]); @@ -449,29 +446,37 @@ void ParticleEffectEntityItem::stepSimulation(float deltaTime) { _paVelocity[j + 1] += deltaTime * _localGravity; } updateIter = (updateIter + 1) % _maxParticles; + updateCount++; } - // emit new particles quint32 emitIdx = updateIter; - _partialEmit += ((float)_emitRate) * deltaTime; - quint32 birthed = (quint32)_partialEmit; - _partialEmit -= (float)birthed; + quint32 birthed = 0; glm::vec3 randOffset; + // emit new particles, but only if animaiton is playing + if (getAnimationIsPlaying()) { + _partialEmit += ((float)_emitRate) * deltaTime; + birthed = (quint32)_partialEmit; + _partialEmit -= (float)birthed; + } else { + _partialEmit = 0; + } + for (quint32 i = 0; i < birthed; i++) { if (_paLife[emitIdx] < 0.0f) { int j = emitIdx * XYZ_STRIDE; _paLife[emitIdx] = _lifespan; + randOffset.x = (((double)rand() / (double)RAND_MAX) - 0.5) * 0.25 * _emitStrength; randOffset.y = (((double)rand() / (double)RAND_MAX) - 0.5) * 0.25 * _emitStrength; randOffset.z = (((double)rand() / (double)RAND_MAX) - 0.5) * 0.25 * _emitStrength; + _paVelocity[j] = (_emitDirection.x * _emitStrength) + randOffset.x; _paVelocity[j + 1] = (_emitDirection.y * _emitStrength) + randOffset.y; _paVelocity[j + 2] = (_emitDirection.z * _emitStrength) + randOffset.z; - // DUMB FORWARD EULER just to get it done _paPosition[j] += _paVelocity[j] * deltaTime; - _paPosition[j + 1] += _paVelocity[j + 1] * deltaTime; + _paPosition[j + 1] += _paVelocity[j + 1] * deltaTime + oneHalfATSquared; _paPosition[j + 2] += _paVelocity[j + 2] * deltaTime; _paXmin = glm::min(_paXmin, _paPosition[j]); @@ -510,4 +515,3 @@ void ParticleEffectEntityItem::resetSimulation() { srand(_randSeed); } - diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index b00eb94685..3533ce84e7 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -2,29 +2,6 @@ // ParticleEffectEntityItem.h // 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 controls for spread (which is currently hard-coded) and varying emission strength (not currently implemented). -// - Add drag. -// - Add some kind of support for collisions. -// - For simplicity, I'm currently just rendering each particle as a cross of four axis-aligned quads. Really, we'd -// want multiple render modes, including (the most important) textured billboards (always facing camera). Also, these -// should support animated textures. -// - 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. -// - MORE? -// // Created by Jason Rickwald on 3/2/15. // // Distributed under the Apache License, Version 2.0. @@ -39,14 +16,14 @@ class ParticleEffectEntityItem : public EntityItem { public: - + static EntityItem* factory(const EntityItemID& entityID, const EntityItemProperties& properties); ParticleEffectEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties); virtual ~ParticleEffectEntityItem(); ALLOW_INSTANTIATION // This class can be instantiated - + // methods for getting/setting all properties of this entity virtual EntityItemProperties getProperties() const; virtual bool setProperties(const EntityItemProperties& properties); @@ -141,6 +118,15 @@ public: float getAnimationFPS() const { return _animationLoop.getFPS(); } QString getAnimationSettings() const; + static const QString DEFAULT_TEXTURES; + const QString& getTextures() const { return _textures; } + void setTextures(const QString& textures) { + if (_textures != textures) { + _textures = textures; + _texturesChangedFlag = true; + } + } + protected: bool isAnimatingSomething() const; @@ -159,6 +145,8 @@ protected: quint64 _lastAnimated; AnimationLoop _animationLoop; QString _animationSettings; + QString _textures; + bool _texturesChangedFlag; ShapeType _shapeType = SHAPE_TYPE_NONE; // all the internals of running the particle sim @@ -180,4 +168,3 @@ protected: }; #endif // hifi_ParticleEffectEntityItem_h - diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 61d92ccc17..770717edbe 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -721,6 +721,76 @@ void GeometryCache::updateVertices(int id, const QVector& points, con #endif } +void GeometryCache::updateVertices(int id, const QVector& points, const QVector& texCoords, const glm::vec4& color) { + BatchItemDetails& details = _registeredVertices[id]; + + if (details.isCreated) { + details.clear(); + #ifdef WANT_DEBUG + qCDebug(renderutils) << "updateVertices()... RELEASING REGISTERED"; + #endif // def WANT_DEBUG + } + + const int FLOATS_PER_VERTEX = 5; + details.isCreated = true; + details.vertices = points.size(); + details.vertexSize = FLOATS_PER_VERTEX; + + gpu::BufferPointer verticesBuffer(new gpu::Buffer()); + gpu::BufferPointer colorBuffer(new gpu::Buffer()); + gpu::Stream::FormatPointer streamFormat(new gpu::Stream::Format()); + gpu::BufferStreamPointer stream(new gpu::BufferStream()); + + details.verticesBuffer = verticesBuffer; + details.colorBuffer = colorBuffer; + details.streamFormat = streamFormat; + details.stream = stream; + + details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); + details.streamFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), 3 * sizeof(float)); + details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::UINT8, gpu::RGBA)); + + details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride); + details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride); + + assert(points.size() == texCoords.size()); + + details.vertices = points.size(); + details.vertexSize = FLOATS_PER_VERTEX; + + int compactColor = ((int(color.x * 255.0f) & 0xFF)) | + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); + + GLfloat* vertexData = new GLfloat[details.vertices * FLOATS_PER_VERTEX]; + GLfloat* vertex = vertexData; + + int* colorData = new int[details.vertices]; + int* colorDataAt = colorData; + + for (int i = 0; i < points.size(); i++) { + glm::vec3 point = points[i]; + glm::vec2 texCoord = texCoords[i]; + *(vertex++) = point.x; + *(vertex++) = point.y; + *(vertex++) = point.z; + *(vertex++) = texCoord.x; + *(vertex++) = texCoord.y; + + *(colorDataAt++) = compactColor; + } + + details.verticesBuffer->append(sizeof(GLfloat) * FLOATS_PER_VERTEX * details.vertices, (gpu::Byte*) vertexData); + details.colorBuffer->append(sizeof(int) * details.vertices, (gpu::Byte*) colorData); + delete[] vertexData; + delete[] colorData; + + #ifdef WANT_DEBUG + qCDebug(renderutils) << "new registered linestrip buffer made -- _registeredVertices.size():" << _registeredVertices.size(); + #endif +} + void GeometryCache::renderVertices(gpu::Primitive primitiveType, int id) { BatchItemDetails& details = _registeredVertices[id]; if (details.isCreated) { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 8ccbb65dbb..672e4ddb78 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -198,6 +198,7 @@ public: void updateVertices(int id, const QVector& points, const glm::vec4& color); void updateVertices(int id, const QVector& points, const glm::vec4& color); + void updateVertices(int id, const QVector& points, const QVector& texCoords, const glm::vec4& color); void renderVertices(gpu::Primitive primitiveType, int id); /// Loads geometry from the specified URL. From b3af5152240c20583f93855906b0beac66d50c35 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Mon, 11 May 2015 19:21:33 -0700 Subject: [PATCH 2/2] Particle entity improvements based on code review. * Updated variable naming to match coding standards. * Changed particle raw float arrays to vectors. * Bug fix: changing maxParticles will no longer lead to memory corruption. * Made particle ring buffer more explicit, added _particleTailIndex. * Added getLivingParticleCount() private method. * Moved integration and bounds code into private methods. * Bug fix: high emit rates now properly integrate particles forward for the remaing frame time, not the entire frame. * Bug fix: new particles were not initiaized to origin. * Added more particles to ajt-test.js. * Bug fix: ajt-test.js script was not shutting down properly. * Removed random seed, unless we have a psudo random number generator per particle system instance, it's unlikely that this will preserve sync. * Bumped packet version number. --- examples/ajt-test.js | 34 ++- .../RenderableParticleEffectEntityItem.cpp | 143 +++++------ .../entities/src/ParticleEffectEntityItem.cpp | 232 +++++++++--------- .../entities/src/ParticleEffectEntityItem.h | 51 ++-- libraries/networking/src/PacketHeaders.cpp | 2 +- libraries/networking/src/PacketHeaders.h | 1 + 6 files changed, 238 insertions(+), 225 deletions(-) diff --git a/examples/ajt-test.js b/examples/ajt-test.js index a908978a02..c26458a9af 100644 --- a/examples/ajt-test.js +++ b/examples/ajt-test.js @@ -28,32 +28,34 @@ } // constructor - function TestFx() { + function TestFx(color, emitDirection, emitRate, emitStrength, blinkRate) { var animationSettings = JSON.stringify({ fps: 30, frameIndex: 0, running: true, firstFrame: 0, lastFrame: 30, - loop: false }); + loop: true }); this.entity = Entities.addEntity({ type: "ParticleEffect", animationSettings: animationSettings, position: spawnPoint, textures: "http://www.hyperlogic.org/images/particle.png", - emitRate: 30, - emitStrength: 3, - color: { red: 128, green: 255, blue: 255 }, + emitRate: emitRate, + emitStrength: emitStrength, + emitDirection: emitDirection, + color: color, visible: true, locked: false }); + this.isPlaying = true; + var self = this; this.timer = Script.setInterval(function () { - print("AJT: setting animation props"); - var animProp = { animationFrameIndex: 0, - animationIsPlaying: true }; + // flip is playing state + self.isPlaying = !self.isPlaying; + var animProp = { animationIsPlaying: self.isPlaying }; Entities.editEntity(self.entity, animProp); - }, 2000); - + }, (1 / blinkRate) * 1000); } TestFx.prototype.Destroy = function () { @@ -65,11 +67,19 @@ var objs = []; function Init() { objs.push(new TestBox()); - objs.push(new TestFx()); + objs.push(new TestFx({ red: 255, blue: 0, green: 0 }, + { x: 0.5, y: 1.0, z: 0.0 }, + 100, 3, 1)); + objs.push(new TestFx({ red: 0, blue: 255, green: 0 }, + { x: 0, y: 1, z: 0 }, + 1000, 5, 0.5)); + objs.push(new TestFx({ red: 0, blue: 0, green: 255 }, + { x: -0.5, y: 1, z: 0 }, + 100, 3, 1)); } function ShutDown() { - var i, len = entities.length; + var i, len = objs.length; for (i = 0; i < len; i++) { objs[i].Destroy(); } diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 32fd06c837..009d26481a 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -34,8 +34,7 @@ void RenderableParticleEffectEntityItem::render(RenderArgs* args) { assert(getType() == EntityTypes::ParticleEffect); PerformanceTimer perfTimer("RenderableParticleEffectEntityItem::render"); - if (_texturesChangedFlag) - { + if (_texturesChangedFlag) { if (_textures.isEmpty()) { _texture.clear(); } else { @@ -57,35 +56,38 @@ void RenderableParticleEffectEntityItem::render(RenderArgs* args) { void RenderableParticleEffectEntityItem::renderUntexturedQuads(RenderArgs* args) { - float pa_rad = getParticleRadius(); + float particleRadius = getParticleRadius(); const float MAX_COLOR = 255.0f; - glm::vec4 paColor(getColor()[RED_INDEX] / MAX_COLOR, getColor()[GREEN_INDEX] / MAX_COLOR, - getColor()[BLUE_INDEX] / MAX_COLOR, getLocalRenderAlpha()); + glm::vec4 particleColor(getColor()[RED_INDEX] / MAX_COLOR, + getColor()[GREEN_INDEX] / MAX_COLOR, + getColor()[BLUE_INDEX] / MAX_COLOR, + getLocalRenderAlpha()); - glm::vec3 up_offset = args->_viewFrustum->getUp() * pa_rad; - glm::vec3 right_offset = args->_viewFrustum->getRight() * pa_rad; + glm::vec3 upOffset = args->_viewFrustum->getUp() * particleRadius; + glm::vec3 rightOffset = args->_viewFrustum->getRight() * particleRadius; - QVector* pointVec = new QVector(_paCount * VERTS_PER_PARTICLE); - quint32 paCount = 0; - quint32 paIter = _paHead; - while (_paLife[paIter] > 0.0f && paCount < _maxParticles) { - int j = paIter * XYZ_STRIDE; + QVector vertices(getLivingParticleCount() * VERTS_PER_PARTICLE); + quint32 count = 0; + for (quint32 i = _particleHeadIndex; i != _particleTailIndex; i = (i + 1) % _maxParticles) { + glm::vec3 pos = _particlePositions[i]; - glm::vec3 pos(_paPosition[j], _paPosition[j + 1], _paPosition[j + 2]); - - pointVec->append(pos - right_offset + up_offset); - pointVec->append(pos + right_offset + up_offset); - pointVec->append(pos + right_offset - up_offset); - pointVec->append(pos - right_offset - up_offset); - - paIter = (paIter + 1) % _maxParticles; - paCount++; + // generate corners of quad, aligned to face the camera + vertices.append(pos - rightOffset + upOffset); + vertices.append(pos + rightOffset + upOffset); + vertices.append(pos + rightOffset - upOffset); + vertices.append(pos - rightOffset - upOffset); + count++; } - DependencyManager::get()->updateVertices(_cacheID, *pointVec, paColor); + // just double checking, cause if this invarient is false, we might have memory corruption bugs. + assert(count == getLivingParticleCount()); + + // update geometry cache with all the verts in model coordinates. + DependencyManager::get()->updateVertices(_cacheID, vertices, particleColor); glPushMatrix(); + glm::vec3 position = getPosition(); glTranslatef(position.x, position.y, position.z); glm::quat rotation = getRotation(); @@ -93,92 +95,93 @@ void RenderableParticleEffectEntityItem::renderUntexturedQuads(RenderArgs* args) glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); glPushMatrix(); + glm::vec3 positionToCenter = getCenter() - position; glTranslatef(positionToCenter.x, positionToCenter.y, positionToCenter.z); DependencyManager::get()->renderVertices(gpu::QUADS, _cacheID); - glPopMatrix(); - glPopMatrix(); - delete pointVec; + glPopMatrix(); + + glPopMatrix(); } -static glm::vec3 s_dir; +static glm::vec3 zSortAxis; static bool zSort(const glm::vec3& rhs, const glm::vec3& lhs) { - return glm::dot(rhs, s_dir) > glm::dot(lhs, s_dir); + return glm::dot(rhs, ::zSortAxis) > glm::dot(lhs, ::zSortAxis); } void RenderableParticleEffectEntityItem::renderTexturedQuads(RenderArgs* args) { - float pa_rad = getParticleRadius(); + float particleRadius = getParticleRadius(); const float MAX_COLOR = 255.0f; - glm::vec4 paColor(getColor()[RED_INDEX] / MAX_COLOR, getColor()[GREEN_INDEX] / MAX_COLOR, - getColor()[BLUE_INDEX] / MAX_COLOR, getLocalRenderAlpha()); + glm::vec4 particleColor(getColor()[RED_INDEX] / MAX_COLOR, + getColor()[GREEN_INDEX] / MAX_COLOR, + getColor()[BLUE_INDEX] / MAX_COLOR, + getLocalRenderAlpha()); - QVector* posVec = new QVector(_paCount); - quint32 paCount = 0; - quint32 paIter = _paHead; - while (_paLife[paIter] > 0.0f && paCount < _maxParticles) { - int j = paIter * XYZ_STRIDE; - posVec->append(glm::vec3(_paPosition[j], _paPosition[j + 1], _paPosition[j + 2])); - - paIter = (paIter + 1) % _maxParticles; - paCount++; + QVector positions(getLivingParticleCount()); + quint32 count = 0; + for (quint32 i = _particleHeadIndex; i != _particleTailIndex; i = (i + 1) % _maxParticles) { + positions.append(_particlePositions[i]); + count++; } + // just double checking, cause if this invarient is false, we might have memory corruption bugs. + assert(count == getLivingParticleCount()); + // sort particles back to front - qSort(posVec->begin(), posVec->end(), zSort); + ::zSortAxis = args->_viewFrustum->getDirection(); + qSort(positions.begin(), positions.end(), zSort); - QVector* vertVec = new QVector(_paCount * VERTS_PER_PARTICLE); - QVector* texCoordVec = new QVector(_paCount * VERTS_PER_PARTICLE); + QVector vertices(getLivingParticleCount() * VERTS_PER_PARTICLE); + QVector textureCoords(getLivingParticleCount() * VERTS_PER_PARTICLE); - s_dir = args->_viewFrustum->getDirection(); - glm::vec3 up_offset = args->_viewFrustum->getUp() * pa_rad; - glm::vec3 right_offset = args->_viewFrustum->getRight() * pa_rad; + glm::vec3 upOffset = args->_viewFrustum->getUp() * particleRadius; + glm::vec3 rightOffset = args->_viewFrustum->getRight() * particleRadius; - for (int i = 0; i < posVec->size(); i++) { - glm::vec3 pos = posVec->at(i); + for (int i = 0; i < positions.size(); i++) { + glm::vec3 pos = positions[i]; - vertVec->append(pos - right_offset + up_offset); - vertVec->append(pos + right_offset + up_offset); - vertVec->append(pos + right_offset - up_offset); - vertVec->append(pos - right_offset - up_offset); + // generate corners of quad aligned to face the camera. + vertices.append(pos - rightOffset + upOffset); + vertices.append(pos + rightOffset + upOffset); + vertices.append(pos + rightOffset - upOffset); + vertices.append(pos - rightOffset - upOffset); - texCoordVec->append(glm::vec2(0, 1)); - texCoordVec->append(glm::vec2(1, 1)); - texCoordVec->append(glm::vec2(1, 0)); - texCoordVec->append(glm::vec2(0, 0)); + textureCoords.append(glm::vec2(0, 1)); + textureCoords.append(glm::vec2(1, 1)); + textureCoords.append(glm::vec2(1, 0)); + textureCoords.append(glm::vec2(0, 0)); } - DependencyManager::get()->updateVertices(_cacheID, *vertVec, *texCoordVec, paColor); + // update geometry cache with all the verts in model coordinates. + DependencyManager::get()->updateVertices(_cacheID, vertices, textureCoords, particleColor); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, _texture->getID()); glPushMatrix(); - glm::vec3 position = getPosition(); - glTranslatef(position.x, position.y, position.z); - glm::quat rotation = getRotation(); - glm::vec3 axis = glm::axis(rotation); - glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); + glm::vec3 position = getPosition(); + glTranslatef(position.x, position.y, position.z); + glm::quat rotation = getRotation(); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); - glPushMatrix(); + glPushMatrix(); - glm::vec3 positionToCenter = getCenter() - position; - glTranslatef(positionToCenter.x, positionToCenter.y, positionToCenter.z); + glm::vec3 positionToCenter = getCenter() - position; + glTranslatef(positionToCenter.x, positionToCenter.y, positionToCenter.z); - DependencyManager::get()->renderVertices(gpu::QUADS, _cacheID); + DependencyManager::get()->renderVertices(gpu::QUADS, _cacheID); + + glPopMatrix(); - glPopMatrix(); glPopMatrix(); glDisable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); - - delete posVec; - delete vertVec; - delete texCoordVec; } diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index fc4927fdcd..d5492cb485 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -41,6 +41,7 @@ #include "EntitiesLogging.h" #include "ParticleEffectEntityItem.h" +const xColor ParticleEffectEntityItem::DEFAULT_COLOR = { 255, 255, 255 }; 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; @@ -53,42 +54,42 @@ const float ParticleEffectEntityItem::DEFAULT_LOCAL_GRAVITY = -9.8f; const float ParticleEffectEntityItem::DEFAULT_PARTICLE_RADIUS = 0.025f; const QString ParticleEffectEntityItem::DEFAULT_TEXTURES = ""; + EntityItem* ParticleEffectEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { return new ParticleEffectEntityItem(entityID, properties); } // our non-pure virtual subclass for now... ParticleEffectEntityItem::ParticleEffectEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties) : - EntityItem(entityItemID, properties) { + EntityItem(entityItemID, properties), + _maxParticles(DEFAULT_MAX_PARTICLES), + _lifespan(DEFAULT_LIFESPAN), + _emitRate(DEFAULT_EMIT_RATE), + _emitDirection(DEFAULT_EMIT_DIRECTION), + _emitStrength(DEFAULT_EMIT_STRENGTH), + _localGravity(DEFAULT_LOCAL_GRAVITY), + _particleRadius(DEFAULT_PARTICLE_RADIUS), + _lastAnimated(usecTimestampNow()), + _animationLoop(), + _animationSettings(), + _textures(DEFAULT_TEXTURES), + _texturesChangedFlag(false), + _shapeType(SHAPE_TYPE_NONE), + _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)), + _timeUntilNextEmit(0.0f), + _particleHeadIndex(0), + _particleTailIndex(0), + _particleMaxBound(glm::vec3(1.0f, 1.0f, 1.0f)), + _particleMinBound(glm::vec3(-1.0f, -1.0f, -1.0f)) { + _type = EntityTypes::ParticleEffect; - _maxParticles = DEFAULT_MAX_PARTICLES; - _lifespan = DEFAULT_LIFESPAN; - _emitRate = DEFAULT_EMIT_RATE; - _emitDirection = DEFAULT_EMIT_DIRECTION; - _emitStrength = DEFAULT_EMIT_STRENGTH; - _localGravity = DEFAULT_LOCAL_GRAVITY; - _particleRadius = DEFAULT_PARTICLE_RADIUS; - _textures = DEFAULT_TEXTURES; + setColor(DEFAULT_COLOR); setProperties(properties); - // this is a pretty dumb thing to do, and it should probably be changed to use a more dynamic - // data structure in the future. I'm just trying to get some code out the door for now (and it's - // at least time efficient (though not space efficient). - // Also, this being a real-time application, it's doubtful we'll ever have millions of particles - // to keep track of, so this really isn't all that bad. - _paLife = new float[_maxParticles]; - _paPosition = new float[_maxParticles * XYZ_STRIDE]; // x,y,z - _paVelocity = new float[_maxParticles * XYZ_STRIDE]; // x,y,z - _paXmax = _paYmax = _paZmax = 1.0f; - _paXmin = _paYmin = _paZmin = -1.0f; - _randSeed = (unsigned int) glm::abs(_lifespan + _emitRate + _localGravity + getPosition().x + getPosition().y + getPosition().z); - resetSimulation(); - _lastAnimated = usecTimestampNow(); } ParticleEffectEntityItem::~ParticleEffectEntityItem() { - delete [] _paLife; - delete [] _paPosition; - delete [] _paVelocity; } EntityItemProperties ParticleEffectEntityItem::getProperties() const { @@ -146,8 +147,8 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert } int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, - ReadBitstreamToTreeParams& args, - EntityPropertyFlags& propertyFlags, bool overwriteLocalData) { + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData) { int bytesRead = 0; const unsigned char* dataAt = data; @@ -214,12 +215,12 @@ EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstrea } void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, - EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, - EntityPropertyFlags& requestedProperties, - EntityPropertyFlags& propertyFlags, - EntityPropertyFlags& propertiesDidntFit, - int& propertyCount, - OctreeElement::AppendState& appendState) const { + EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const { bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_COLOR, appendColor, getColor()); @@ -240,7 +241,7 @@ void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, bool ParticleEffectEntityItem::isAnimatingSomething() const { // keep animating if there are particles still alive. - return (getAnimationIsPlaying() || _paCount > 0) && getAnimationFPS() != 0.0f; + return (getAnimationIsPlaying() || getLivingParticleCount() > 0) && getAnimationFPS() != 0.0f; } bool ParticleEffectEntityItem::needsToCallUpdate() const { @@ -262,9 +263,9 @@ void ParticleEffectEntityItem::update(const quint64& now) { // update the dimensions glm::vec3 dims; - dims.x = glm::max(glm::abs(_paXmin), glm::abs(_paXmax)) * 2.0f; - dims.y = glm::max(glm::abs(_paYmin), glm::abs(_paYmax)) * 2.0f; - dims.z = glm::max(glm::abs(_paZmin), glm::abs(_paZmax)) * 2.0f; + dims.x = glm::max(glm::abs(_particleMinBound.x), glm::abs(_particleMaxBound.x)) * 2.0f; + dims.y = glm::max(glm::abs(_particleMinBound.y), glm::abs(_particleMaxBound.y)) * 2.0f; + dims.z = glm::max(glm::abs(_particleMinBound.z), glm::abs(_particleMaxBound.z)) * 2.0f; setDimensions(dims); } @@ -412,106 +413,103 @@ QString ParticleEffectEntityItem::getAnimationSettings() const { return jsonByteString; } +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 atSquared(0.0f, 0.5f * _localGravity * deltaTime * deltaTime, 0.0f); + glm::vec3 at(0.0f, _localGravity * deltaTime, 0.0f); + _particlePositions[index] += _particleVelocities[index] * deltaTime + atSquared; + _particleVelocities[index] += at; +} + void ParticleEffectEntityItem::stepSimulation(float deltaTime) { - _paXmin = _paYmin = _paZmin = -1.0f; - _paXmax = _paYmax = _paZmax = 1.0f; - float oneHalfATSquared = 0.5f * _localGravity * deltaTime * deltaTime; + _particleMinBound = glm::vec3(-1.0f, -1.0f, -1.0f); + _particleMaxBound = glm::vec3(1.0f, 1.0f, 1.0f); - // update particles - quint32 updateCount = 0; - quint32 updateIter = _paHead; - while (_paLife[updateIter] > 0.0f && updateCount < _maxParticles) { - _paLife[updateIter] -= deltaTime; - if (_paLife[updateIter] <= 0.0f) { - _paLife[updateIter] = -1.0f; - _paHead = (_paHead + 1) % _maxParticles; - _paCount--; + // 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 { - int j = updateIter * XYZ_STRIDE; - - _paPosition[j] += _paVelocity[j] * deltaTime; - _paPosition[j+1] += _paVelocity[j+1] * deltaTime + oneHalfATSquared; - _paPosition[j+2] += _paVelocity[j+2] * deltaTime; - - _paXmin = glm::min(_paXmin, _paPosition[j]); - _paYmin = glm::min(_paYmin, _paPosition[j+1]); - _paZmin = glm::min(_paZmin, _paPosition[j+2]); - _paXmax = glm::max(_paXmax, _paPosition[j]); - _paYmax = glm::max(_paYmax, _paPosition[j + 1]); - _paZmax = glm::max(_paZmax, _paPosition[j + 2]); - - // massless particles - _paVelocity[j + 1] += deltaTime * _localGravity; + integrateParticle(i, deltaTime); + extendBounds(_particlePositions[i]); } - updateIter = (updateIter + 1) % _maxParticles; - updateCount++; } - quint32 emitIdx = updateIter; - quint32 birthed = 0; - glm::vec3 randOffset; - // emit new particles, but only if animaiton is playing if (getAnimationIsPlaying()) { - _partialEmit += ((float)_emitRate) * deltaTime; - birthed = (quint32)_partialEmit; - _partialEmit -= (float)birthed; - } else { - _partialEmit = 0; - } - for (quint32 i = 0; i < birthed; i++) { - if (_paLife[emitIdx] < 0.0f) { - int j = emitIdx * XYZ_STRIDE; - _paLife[emitIdx] = _lifespan; + float timeLeftInFrame = deltaTime; + while (_timeUntilNextEmit < timeLeftInFrame) { - randOffset.x = (((double)rand() / (double)RAND_MAX) - 0.5) * 0.25 * _emitStrength; - randOffset.y = (((double)rand() / (double)RAND_MAX) - 0.5) * 0.25 * _emitStrength; - randOffset.z = (((double)rand() / (double)RAND_MAX) - 0.5) * 0.25 * _emitStrength; + timeLeftInFrame -= _timeUntilNextEmit; + _timeUntilNextEmit = 1.0f / _emitRate; - _paVelocity[j] = (_emitDirection.x * _emitStrength) + randOffset.x; - _paVelocity[j + 1] = (_emitDirection.y * _emitStrength) + randOffset.y; - _paVelocity[j + 2] = (_emitDirection.z * _emitStrength) + randOffset.z; + // emit a new particle at tail index. + quint32 i = _particleTailIndex; + _particleLifetimes[i] = _lifespan; - _paPosition[j] += _paVelocity[j] * deltaTime; - _paPosition[j + 1] += _paVelocity[j + 1] * deltaTime + oneHalfATSquared; - _paPosition[j + 2] += _paVelocity[j + 2] * deltaTime; + // jitter the _emitDirection by a random offset + glm::vec3 randOffset; + randOffset.x = (randFloat() - 0.5f) * 0.25f * _emitStrength; + randOffset.y = (randFloat() - 0.5f) * 0.25f * _emitStrength; + randOffset.z = (randFloat() - 0.5f) * 0.25f * _emitStrength; - _paXmin = glm::min(_paXmin, _paPosition[j]); - _paYmin = glm::min(_paYmin, _paPosition[j + 1]); - _paZmin = glm::min(_paZmin, _paPosition[j + 2]); - _paXmax = glm::max(_paXmax, _paPosition[j]); - _paYmax = glm::max(_paYmax, _paPosition[j + 1]); - _paZmax = glm::max(_paZmax, _paPosition[j + 2]); + // set initial conditions + _particlePositions[i] = glm::vec3(0.0f, 0.0f, 0.0f); + _particleVelocities[i] = _emitDirection * _emitStrength + randOffset; - // massless particles - // and simple gravity down - _paVelocity[j + 1] += deltaTime * _localGravity; + integrateParticle(i, timeLeftInFrame); + extendBounds(_particlePositions[i]); - emitIdx = (emitIdx + 1) % _maxParticles; - _paCount++; + _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; + } } - else - break; + + _timeUntilNextEmit -= timeLeftInFrame; } } -void ParticleEffectEntityItem::resetSimulation() { - for (quint32 i = 0; i < _maxParticles; i++) { - quint32 j = i * XYZ_STRIDE; - _paLife[i] = -1.0f; - _paPosition[j] = 0.0f; - _paPosition[j+1] = 0.0f; - _paPosition[j+2] = 0.0f; - _paVelocity[j] = 0.0f; - _paVelocity[j+1] = 0.0f; - _paVelocity[j+2] = 0.0f; - } - _paCount = 0; - _paHead = 0; - _partialEmit = 0.0f; +void ParticleEffectEntityItem::setMaxParticles(quint32 maxParticles) { + _maxParticles = maxParticles; - srand(_randSeed); + // 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); + + // effectivly 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; + } } diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 3533ce84e7..6d1ef601f6 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -31,16 +31,16 @@ public: virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, - EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, - EntityPropertyFlags& requestedProperties, - EntityPropertyFlags& propertyFlags, - EntityPropertyFlags& propertiesDidntFit, - int& propertyCount, - OctreeElement::AppendState& appendState) const; + EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const; virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, - ReadBitstreamToTreeParams& args, - EntityPropertyFlags& propertyFlags, bool overwriteLocalData); + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData); virtual void update(const quint64& now); virtual bool needsToCallUpdate() const; @@ -48,6 +48,7 @@ public: const rgbColor& getColor() const { return _color; } xColor getXColor() const { xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; } + static const xColor DEFAULT_COLOR; void setColor(const rgbColor& value) { memcpy(_color, value, sizeof(_color)); } void setColor(const xColor& value) { _color[RED_INDEX] = value.red; @@ -86,7 +87,7 @@ public: float getAnimationLastFrame() const { return _animationLoop.getLastFrame(); } static const quint32 DEFAULT_MAX_PARTICLES; - void setMaxParticles(quint32 maxParticles) { _maxParticles = maxParticles; } + void setMaxParticles(quint32 maxParticles); quint32 getMaxParticles() const { return _maxParticles; } static const float DEFAULT_LIFESPAN; @@ -98,7 +99,7 @@ public: float getEmitRate() const { return _emitRate; } static const glm::vec3 DEFAULT_EMIT_DIRECTION; - void setEmitDirection(glm::vec3 emitDirection) { _emitDirection = emitDirection; } + void setEmitDirection(glm::vec3 emitDirection) { _emitDirection = glm::normalize(emitDirection); } const glm::vec3& getEmitDirection() const { return _emitDirection; } static const float DEFAULT_EMIT_STRENGTH; @@ -131,7 +132,9 @@ protected: bool isAnimatingSomething() const; void stepSimulation(float deltaTime); - void resetSimulation(); + void extendBounds(const glm::vec3& point); + void integrateParticle(quint32 index, float deltaTime); + quint32 getLivingParticleCount() const; // the properties of this entity rgbColor _color; @@ -150,21 +153,19 @@ protected: ShapeType _shapeType = SHAPE_TYPE_NONE; // all the internals of running the particle sim - const quint32 XYZ_STRIDE = 3; - float* _paLife; - float* _paPosition; - float* _paVelocity; - float _partialEmit; - quint32 _paCount; - quint32 _paHead; - float _paXmin; - float _paXmax; - float _paYmin; - float _paYmax; - float _paZmin; - float _paZmax; - unsigned int _randSeed; + QVector _particleLifetimes; + QVector _particlePositions; + QVector _particleVelocities; + float _timeUntilNextEmit; + // particle arrays are a ring buffer, use these indicies + // to keep track of the living particles. + quint32 _particleHeadIndex; + quint32 _particleTailIndex; + + // bounding volume + glm::vec3 _particleMaxBound; + glm::vec3 _particleMinBound; }; #endif // hifi_ParticleEffectEntityItem_h diff --git a/libraries/networking/src/PacketHeaders.cpp b/libraries/networking/src/PacketHeaders.cpp index ec83e18c3d..87851ed80e 100644 --- a/libraries/networking/src/PacketHeaders.cpp +++ b/libraries/networking/src/PacketHeaders.cpp @@ -74,7 +74,7 @@ PacketVersion versionForPacketType(PacketType type) { return 1; case PacketTypeEntityAddOrEdit: case PacketTypeEntityData: - return VERSION_ENTITIES_ZONE_ENTITIES_HAVE_SKYBOX; + return VERSION_ENTITIES_PARTICLE_ENTITIES_HAVE_TEXTURES; case PacketTypeEntityErase: return 2; case PacketTypeAudioStreamStats: diff --git a/libraries/networking/src/PacketHeaders.h b/libraries/networking/src/PacketHeaders.h index 38aeed4993..82fc927166 100644 --- a/libraries/networking/src/PacketHeaders.h +++ b/libraries/networking/src/PacketHeaders.h @@ -142,5 +142,6 @@ const PacketVersion VERSION_ENTITIES_ZONE_ENTITIES_HAVE_DYNAMIC_SHAPE = 18; const PacketVersion VERSION_ENTITIES_HAVE_NAMES = 19; const PacketVersion VERSION_ENTITIES_ZONE_ENTITIES_HAVE_ATMOSPHERE = 20; const PacketVersion VERSION_ENTITIES_ZONE_ENTITIES_HAVE_SKYBOX = 21; +const PacketVersion VERSION_ENTITIES_PARTICLE_ENTITIES_HAVE_TEXTURES = 22; #endif // hifi_PacketHeaders_h