diff --git a/examples/ajt-test.js b/examples/ajt-test.js new file mode 100644 index 0000000000..c26458a9af --- /dev/null +++ b/examples/ajt-test.js @@ -0,0 +1,93 @@ + +(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(color, emitDirection, emitRate, emitStrength, blinkRate) { + var animationSettings = JSON.stringify({ fps: 30, + frameIndex: 0, + running: true, + firstFrame: 0, + lastFrame: 30, + loop: true }); + + this.entity = Entities.addEntity({ type: "ParticleEffect", + animationSettings: animationSettings, + position: spawnPoint, + textures: "http://www.hyperlogic.org/images/particle.png", + emitRate: emitRate, + emitStrength: emitStrength, + emitDirection: emitDirection, + color: color, + visible: true, + locked: false }); + + this.isPlaying = true; + + var self = this; + this.timer = Script.setInterval(function () { + // flip is playing state + self.isPlaying = !self.isPlaying; + var animProp = { animationIsPlaying: self.isPlaying }; + Entities.editEntity(self.entity, animProp); + }, (1 / blinkRate) * 1000); + } + + 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({ 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 = objs.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..009d26481a 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,158 @@ RenderableParticleEffectEntityItem::RenderableParticleEffectEntityItem(const Ent } void RenderableParticleEffectEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderableParticleEffectEntityItem::render"); + assert(getType() == EntityTypes::ParticleEffect); - float pa_rad = getParticleRadius(); + PerformanceTimer perfTimer("RenderableParticleEffectEntityItem::render"); - 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. - - QVector* pointVec = new QVector(_paCount * VERTS_PER_PARTICLE); - quint32 paIter = _paHead; - while (_paLife[paIter] > 0.0f) { - 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])); - - 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)); - - paIter = (paIter + 1) % _maxParticles; + 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; } - DependencyManager::get()->updateVertices(_cacheID, *pointVec, paColor); - + if (!_texture) { + renderUntexturedQuads(args); + } else if (_texture && !_texture->isLoaded()) { + renderUntexturedQuads(args); + } else { + renderTexturedQuads(args); + } +}; + +void RenderableParticleEffectEntityItem::renderUntexturedQuads(RenderArgs* args) { + + float particleRadius = getParticleRadius(); + + const float MAX_COLOR = 255.0f; + glm::vec4 particleColor(getColor()[RED_INDEX] / MAX_COLOR, + getColor()[GREEN_INDEX] / MAX_COLOR, + getColor()[BLUE_INDEX] / MAX_COLOR, + getLocalRenderAlpha()); + + glm::vec3 upOffset = args->_viewFrustum->getUp() * particleRadius; + glm::vec3 rightOffset = args->_viewFrustum->getRight() * particleRadius; + + QVector vertices(getLivingParticleCount() * VERTS_PER_PARTICLE); + quint32 count = 0; + for (quint32 i = _particleHeadIndex; i != _particleTailIndex; i = (i + 1) % _maxParticles) { + glm::vec3 pos = _particlePositions[i]; + + // 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++; + } + + // 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(); 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(); +} + +static glm::vec3 zSortAxis; +static bool zSort(const glm::vec3& rhs, const glm::vec3& lhs) { + return glm::dot(rhs, ::zSortAxis) > glm::dot(lhs, ::zSortAxis); +} + +void RenderableParticleEffectEntityItem::renderTexturedQuads(RenderArgs* args) { + + float particleRadius = getParticleRadius(); + + const float MAX_COLOR = 255.0f; + glm::vec4 particleColor(getColor()[RED_INDEX] / MAX_COLOR, + getColor()[GREEN_INDEX] / MAX_COLOR, + getColor()[BLUE_INDEX] / MAX_COLOR, + getLocalRenderAlpha()); + + 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 + ::zSortAxis = args->_viewFrustum->getDirection(); + qSort(positions.begin(), positions.end(), zSort); + + QVector vertices(getLivingParticleCount() * VERTS_PER_PARTICLE); + QVector textureCoords(getLivingParticleCount() * VERTS_PER_PARTICLE); + + glm::vec3 upOffset = args->_viewFrustum->getUp() * particleRadius; + glm::vec3 rightOffset = args->_viewFrustum->getRight() * particleRadius; + + for (int i = 0; i < positions.size(); i++) { + glm::vec3 pos = positions[i]; + + // 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); + + textureCoords.append(glm::vec2(0, 1)); + textureCoords.append(glm::vec2(1, 1)); + textureCoords.append(glm::vec2(1, 0)); + textureCoords.append(glm::vec2(0, 0)); + } + + // 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); + + glPushMatrix(); + + glm::vec3 positionToCenter = getCenter() - position; + glTranslatef(positionToCenter.x, positionToCenter.y, positionToCenter.z); + + DependencyManager::get()->renderVertices(gpu::QUADS, _cacheID); + + glPopMatrix(); + glPopMatrix(); - delete pointVec; -}; + glDisable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); +} + 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..d5492cb485 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. // @@ -45,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; @@ -55,6 +52,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) { @@ -63,40 +61,40 @@ EntityItem* ParticleEffectEntityItem::factory(const EntityItemID& entityID, cons // 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; + 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 { 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 +109,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 +131,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; @@ -147,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; @@ -186,6 +186,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 +195,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,17 +209,18 @@ EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstrea requestedProperties += PROP_EMIT_STRENGTH; requestedProperties += PROP_LOCAL_GRAVITY; requestedProperties += PROP_PARTICLE_RADIUS; + requestedProperties += PROP_TEXTURES; return requestedProperties; } 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()); @@ -234,11 +236,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() || getLivingParticleCount() > 0) && getAnimationFPS() != 0.0f; } bool ParticleEffectEntityItem::needsToCallUpdate() const { @@ -246,33 +249,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(_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); + } EntityItem::update(now); // let our base class handle it's updates... } @@ -418,96 +413,103 @@ QString ParticleEffectEntityItem::getAnimationSettings() const { return jsonByteString; } -void ParticleEffectEntityItem::stepSimulation(float deltaTime) { - _paXmin = _paYmin = _paZmin = -1.0f; - _paXmax = _paYmax = _paZmax = 1.0f; +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); +} - // update particles - quint32 updateIter = _paHead; - while (_paLife[updateIter] > 0.0f) { - _paLife[updateIter] -= deltaTime; - if (_paLife[updateIter] <= 0.0f) { - _paLife[updateIter] = -1.0f; - _paHead = (_paHead + 1) % _maxParticles; - _paCount--; +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) { + + _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 { - // 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+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; } - // emit new particles - quint32 emitIdx = updateIter; - _partialEmit += ((float)_emitRate) * deltaTime; - quint32 birthed = (quint32)_partialEmit; - _partialEmit -= (float)birthed; - glm::vec3 randOffset; + // emit new particles, but only if animaiton is playing + if (getAnimationIsPlaying()) { - 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; + float timeLeftInFrame = deltaTime; + while (_timeUntilNextEmit < timeLeftInFrame) { - // DUMB FORWARD EULER just to get it done - _paPosition[j] += _paVelocity[j] * deltaTime; - _paPosition[j + 1] += _paVelocity[j + 1] * deltaTime; - _paPosition[j + 2] += _paVelocity[j + 2] * deltaTime; + timeLeftInFrame -= _timeUntilNextEmit; + _timeUntilNextEmit = 1.0f / _emitRate; - _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]); + // emit a new particle at tail index. + quint32 i = _particleTailIndex; + _particleLifetimes[i] = _lifespan; - // massless particles - // and simple gravity down - _paVelocity[j + 1] += deltaTime * _localGravity; + // 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; - emitIdx = (emitIdx + 1) % _maxParticles; - _paCount++; + // set initial conditions + _particlePositions[i] = glm::vec3(0.0f, 0.0f, 0.0f); + _particleVelocities[i] = _emitDirection * _emitStrength + randOffset; + + integrateParticle(i, timeLeftInFrame); + extendBounds(_particlePositions[i]); + + _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 b00eb94685..6d1ef601f6 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); @@ -54,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; @@ -71,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; @@ -109,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; @@ -121,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; @@ -141,11 +119,22 @@ 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; 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; @@ -159,25 +148,24 @@ protected: quint64 _lastAnimated; AnimationLoop _animationLoop; QString _animationSettings; + QString _textures; + bool _texturesChangedFlag; 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 c2b65dbd55..5f9c5c4fd0 100644 --- a/libraries/networking/src/PacketHeaders.cpp +++ b/libraries/networking/src/PacketHeaders.cpp @@ -72,7 +72,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return 1; case PacketTypeEntityAddOrEdit: case PacketTypeEntityData: - return VERSION_ENTITIES_ZONE_ENTITIES_STAGE_HAS_AUTOMATIC_HOURDAY; + 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 978ba190bb..6f71655edf 100644 --- a/libraries/networking/src/PacketHeaders.h +++ b/libraries/networking/src/PacketHeaders.h @@ -173,5 +173,6 @@ 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_ZONE_ENTITIES_STAGE_HAS_AUTOMATIC_HOURDAY = 22; +const PacketVersion VERSION_ENTITIES_PARTICLE_ENTITIES_HAVE_TEXTURES = 23; #endif // hifi_PacketHeaders_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.