/** * @file llviewerpartsim.cpp * @brief LLViewerPart class implementation * * $LicenseInfo:firstyear=2003&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "llviewerpartsim.h" #include "llviewercontrol.h" #include "llagent.h" #include "llviewercamera.h" #include "llviewerobjectlist.h" #include "llviewerpartsource.h" #include "llviewerregion.h" #include "llvopartgroup.h" #include "llworld.h" #include "pipeline.h" #include "llspatialpartition.h" #include "llvoavatarself.h" #include "llvovolume.h" const F32 PART_SIM_BOX_SIDE = 16.f; //static S32 LLViewerPartSim::sMaxParticleCount = 0; S32 LLViewerPartSim::sParticleCount = 0; S32 LLViewerPartSim::sParticleCount2 = 0; // This controls how greedy individual particle burst sources are allowed to be, and adapts according to how near the particle-count limit we are. F32 LLViewerPartSim::sParticleAdaptiveRate = 0.0625f; F32 LLViewerPartSim::sParticleBurstRate = 0.5f; //static const S32 LLViewerPartSim::MAX_PART_COUNT = 8192; const F32 LLViewerPartSim::PART_THROTTLE_THRESHOLD = 0.9f; const F32 LLViewerPartSim::PART_ADAPT_RATE_MULT = 2.0f; //static const F32 LLViewerPartSim::PART_THROTTLE_RESCALE = PART_THROTTLE_THRESHOLD / (1.0f-PART_THROTTLE_THRESHOLD); const F32 LLViewerPartSim::PART_ADAPT_RATE_MULT_RECIP = 1.0f/PART_ADAPT_RATE_MULT; U32 LLViewerPart::sNextPartID = 1; F32 calc_desired_size(LLViewerCamera* camera, LLVector3 pos, LLVector2 scale) { F32 desired_size = (pos - camera->getOrigin()).magVec(); desired_size /= 4; return llclamp(desired_size, scale.magVec()*0.5f, PART_SIM_BOX_SIDE*2); } LLViewerPart::LLViewerPart() : mPartID(0), mLastUpdateTime(0.f), mSkipOffset(0.f), mVPCallback(NULL), mImagep(NULL) { mPartSourcep = NULL; mParent = NULL; mChild = NULL; ++LLViewerPartSim::sParticleCount2 ; } LLViewerPart::~LLViewerPart() { if (mPartSourcep.notNull() && mPartSourcep->mLastPart == this) { mPartSourcep->mLastPart = NULL; } //patch up holes in the ribbon if (mParent) { llassert(mParent->mChild == this); mParent->mChild = mChild; } if (mChild) { llassert (mChild->mParent == this); mChild->mParent = mParent; } mPartSourcep = NULL; --LLViewerPartSim::sParticleCount2 ; } void LLViewerPart::init(LLPointer sourcep, LLViewerTexture *imagep, LLVPCallback cb) { mPartID = LLViewerPart::sNextPartID; LLViewerPart::sNextPartID++; mFlags = 0x00f; mLastUpdateTime = 0.f; mMaxAge = 10.f; mSkipOffset = 0.0f; mVPCallback = cb; mPartSourcep = sourcep; mImagep = imagep; } ///////////////////////////// // // LLViewerPartGroup implementation // // LLViewerPartGroup::LLViewerPartGroup(const LLVector3 ¢er_agent, const F32 box_side, bool hud) : mHud(hud) { mVOPartGroupp = NULL; mUniformParticles = TRUE; mRegionp = LLWorld::getInstance()->getRegionFromPosAgent(center_agent); llassert_always(center_agent.isFinite()); if (!mRegionp) { //LL_WARNS() << "No region at position, using agent region!" << LL_ENDL; mRegionp = gAgent.getRegion(); } mCenterAgent = center_agent; mBoxRadius = F_SQRT3*box_side*0.5f; if (mHud) { mVOPartGroupp = (LLVOPartGroup *)gObjectList.createObjectViewer(LLViewerObject::LL_VO_HUD_PART_GROUP, getRegion()); } else { mVOPartGroupp = (LLVOPartGroup *)gObjectList.createObjectViewer(LLViewerObject::LL_VO_PART_GROUP, getRegion()); } mVOPartGroupp->setViewerPartGroup(this); mVOPartGroupp->setPositionAgent(getCenterAgent()); mBoxSide = box_side; F32 scale = box_side * 0.5f; mVOPartGroupp->setScale(LLVector3(scale,scale,scale)); //gPipeline.addObject(mVOPartGroupp); gPipeline.createObject(mVOPartGroupp); LLSpatialGroup* group = mVOPartGroupp->mDrawable->getSpatialGroup(); if (group != NULL) { LLVector3 center(group->getOctreeNode()->getCenter().getF32ptr()); LLVector3 size(group->getOctreeNode()->getSize().getF32ptr()); size += LLVector3(0.01f, 0.01f, 0.01f); mMinObjPos = center - size; mMaxObjPos = center + size; } else { // Not sure what else to set the obj bounds to when the drawable has no spatial group. LLVector3 extents(mBoxRadius, mBoxRadius, mBoxRadius); mMinObjPos = center_agent - extents; mMaxObjPos = center_agent + extents; } mSkippedTime = 0.f; static U32 id_seed = 0; mID = ++id_seed; } LLViewerPartGroup::~LLViewerPartGroup() { cleanup(); S32 count = (S32) mParticles.size(); for(S32 i = 0 ; i < count ; i++) { delete mParticles[i] ; } mParticles.clear(); LLViewerPartSim::decPartCount(count); } void LLViewerPartGroup::cleanup() { if (mVOPartGroupp) { if (!mVOPartGroupp->isDead()) { gObjectList.killObject(mVOPartGroupp); } mVOPartGroupp = NULL; } } BOOL LLViewerPartGroup::posInGroup(const LLVector3 &pos, const F32 desired_size) { if ((pos.mV[VX] < mMinObjPos.mV[VX]) || (pos.mV[VY] < mMinObjPos.mV[VY]) || (pos.mV[VZ] < mMinObjPos.mV[VZ])) { return FALSE; } if ((pos.mV[VX] > mMaxObjPos.mV[VX]) || (pos.mV[VY] > mMaxObjPos.mV[VY]) || (pos.mV[VZ] > mMaxObjPos.mV[VZ])) { return FALSE; } if (desired_size > 0 && (desired_size < mBoxRadius*0.5f || desired_size > mBoxRadius*2.f)) { return FALSE; } return TRUE; } BOOL LLViewerPartGroup::addPart(LLViewerPart* part, F32 desired_size) { if (part->mFlags & LLPartData::LL_PART_HUD && !mHud) { return FALSE; } BOOL uniform_part = part->mScale.mV[0] == part->mScale.mV[1] && !(part->mFlags & LLPartData::LL_PART_FOLLOW_VELOCITY_MASK); if (!posInGroup(part->mPosAgent, desired_size) || (mUniformParticles && !uniform_part) || (!mUniformParticles && uniform_part)) { return FALSE; } gPipeline.markRebuild(mVOPartGroupp->mDrawable, LLDrawable::REBUILD_ALL, TRUE); mParticles.push_back(part); part->mSkipOffset=mSkippedTime; LLViewerPartSim::incPartCount(1); return TRUE; } void LLViewerPartGroup::updateParticles(const F32 lastdt) { F32 dt; LLVector3 gravity(0.f, 0.f, GRAVITY); LLViewerPartSim::checkParticleCount(mParticles.size()); LLViewerCamera* camera = LLViewerCamera::getInstance(); LLViewerRegion *regionp = getRegion(); S32 end = (S32) mParticles.size(); for (S32 i = 0 ; i < (S32)mParticles.size();) { LLVector3 a(0.f, 0.f, 0.f); LLViewerPart* part = mParticles[i] ; dt = lastdt + mSkippedTime - part->mSkipOffset; part->mSkipOffset = 0.f; // Update current time const F32 cur_time = part->mLastUpdateTime + dt; const F32 frac = cur_time / part->mMaxAge; // "Drift" the object based on the source object if (part->mFlags & LLPartData::LL_PART_FOLLOW_SRC_MASK) { part->mPosAgent = part->mPartSourcep->mPosAgent; part->mPosAgent += part->mPosOffset; } // Do a custom callback if we have one... if (part->mVPCallback) { (*part->mVPCallback)(*part, dt); } if (part->mFlags & LLPartData::LL_PART_WIND_MASK) { part->mVelocity *= 1.f - 0.1f*dt; part->mVelocity += 0.1f*dt*regionp->mWind.getVelocity(regionp->getPosRegionFromAgent(part->mPosAgent)); } // Now do interpolation towards a target if (part->mFlags & LLPartData::LL_PART_TARGET_POS_MASK) { F32 remaining = part->mMaxAge - part->mLastUpdateTime; F32 step = dt / remaining; step = llclamp(step, 0.f, 0.1f); step *= 5.f; // we want a velocity that will result in reaching the target in the // Interpolate towards the target. LLVector3 delta_pos = part->mPartSourcep->mTargetPosAgent - part->mPosAgent; delta_pos /= remaining; part->mVelocity *= (1.f - step); part->mVelocity += step*delta_pos; } if (part->mFlags & LLPartData::LL_PART_TARGET_LINEAR_MASK) { LLVector3 delta_pos = part->mPartSourcep->mTargetPosAgent - part->mPartSourcep->mPosAgent; part->mPosAgent = part->mPartSourcep->mPosAgent; part->mPosAgent += frac*delta_pos; part->mVelocity = delta_pos; } else { // Do velocity interpolation part->mPosAgent += dt*part->mVelocity; part->mPosAgent += 0.5f*dt*dt*part->mAccel; part->mVelocity += part->mAccel*dt; } // Do a bounce test if (part->mFlags & LLPartData::LL_PART_BOUNCE_MASK) { // Need to do point vs. plane check... // For now, just check relative to object height... F32 dz = part->mPosAgent.mV[VZ] - part->mPartSourcep->mPosAgent.mV[VZ]; if (dz < 0) { part->mPosAgent.mV[VZ] += -2.f*dz; part->mVelocity.mV[VZ] *= -0.75f; } } // Reset the offset from the source position if (part->mFlags & LLPartData::LL_PART_FOLLOW_SRC_MASK) { part->mPosOffset = part->mPosAgent; part->mPosOffset -= part->mPartSourcep->mPosAgent; } // Do color interpolation if (part->mFlags & LLPartData::LL_PART_INTERP_COLOR_MASK) { part->mColor.setVec(part->mStartColor); // note: LLColor4's v%k means multiply-alpha-only, // LLColor4's v*k means multiply-rgb-only part->mColor *= 1.f - frac; // rgb*k part->mColor %= 1.f - frac; // alpha*k part->mColor += frac%(frac*part->mEndColor); // rgb,alpha } // Do scale interpolation if (part->mFlags & LLPartData::LL_PART_INTERP_SCALE_MASK) { part->mScale.setVec(part->mStartScale); part->mScale *= 1.f - frac; part->mScale += frac*part->mEndScale; } // Do glow interpolation part->mGlow.mV[3] = (U8) ll_round(lerp(part->mStartGlow, part->mEndGlow, frac)*255.f); // Set the last update time to now. part->mLastUpdateTime = cur_time; // Kill dead particles (either flagged dead, or too old) if ((part->mLastUpdateTime > part->mMaxAge) || (LLViewerPart::LL_PART_DEAD_MASK == part->mFlags)) { mParticles[i] = mParticles.back() ; mParticles.pop_back() ; delete part ; } else { F32 desired_size = calc_desired_size(camera, part->mPosAgent, part->mScale); if (!posInGroup(part->mPosAgent, desired_size)) { // Transfer particles between groups LLViewerPartSim::getInstance()->put(part) ; mParticles[i] = mParticles.back() ; mParticles.pop_back() ; } else { i++ ; } } } S32 removed = end - (S32)mParticles.size(); if (removed > 0) { // we removed one or more particles, so flag this group for update if (mVOPartGroupp.notNull()) { gPipeline.markRebuild(mVOPartGroupp->mDrawable, LLDrawable::REBUILD_ALL, TRUE); } LLViewerPartSim::decPartCount(removed); } // Kill the viewer object if this particle group is empty if (mParticles.empty()) { gObjectList.killObject(mVOPartGroupp); mVOPartGroupp = NULL; } LLViewerPartSim::checkParticleCount() ; } void LLViewerPartGroup::shift(const LLVector3 &offset) { mCenterAgent += offset; mMinObjPos += offset; mMaxObjPos += offset; for (S32 i = 0 ; i < (S32)mParticles.size(); i++) { mParticles[i]->mPosAgent += offset; } } void LLViewerPartGroup::removeParticlesByID(const U32 source_id) { for (S32 i = 0; i < (S32)mParticles.size(); i++) { if(mParticles[i]->mPartSourcep->getID() == source_id) { mParticles[i]->mFlags = LLViewerPart::LL_PART_DEAD_MASK; } } } ////////////////////////////////// // // LLViewerPartSim implementation // // //static void LLViewerPartSim::checkParticleCount(U32 size) { if(LLViewerPartSim::sParticleCount2 != LLViewerPartSim::sParticleCount) { LL_ERRS() << "sParticleCount: " << LLViewerPartSim::sParticleCount << " ; sParticleCount2: " << LLViewerPartSim::sParticleCount2 << LL_ENDL ; } if(size > (U32)LLViewerPartSim::sParticleCount2) { LL_ERRS() << "curren particle size: " << LLViewerPartSim::sParticleCount2 << " array size: " << size << LL_ENDL ; } } LLViewerPartSim::LLViewerPartSim() { sMaxParticleCount = llmin(gSavedSettings.getS32("RenderMaxPartCount"), LL_MAX_PARTICLE_COUNT); static U32 id_seed = 0; mID = ++id_seed; } //enable/disable particle system void LLViewerPartSim::enable(bool enabled) { if(!enabled && sMaxParticleCount > 0) { sMaxParticleCount = 0; //disable } else if(enabled && sMaxParticleCount < 1) { sMaxParticleCount = llmin(gSavedSettings.getS32("RenderMaxPartCount"), LL_MAX_PARTICLE_COUNT); } return; } void LLViewerPartSim::destroyClass() { S32 i; S32 count; // Kill all of the groups (and particles) count = (S32) mViewerPartGroups.size(); for (i = 0; i < count; i++) { delete mViewerPartGroups[i]; } mViewerPartGroups.clear(); // Kill all of the sources mViewerPartSources.clear(); } //static BOOL LLViewerPartSim::shouldAddPart() { if (sParticleCount >= MAX_PART_COUNT) { return FALSE; } if (sParticleCount > PART_THROTTLE_THRESHOLD*sMaxParticleCount) { F32 frac = (F32)sParticleCount/(F32)sMaxParticleCount; frac -= PART_THROTTLE_THRESHOLD; frac *= PART_THROTTLE_RESCALE; if (ll_frand() < frac) { // Skip... return FALSE; } } // Check frame rate, and don't add more if the viewer is really slow const F32 MIN_FRAME_RATE_FOR_NEW_PARTICLES = 4.f; if (gFPSClamped < MIN_FRAME_RATE_FOR_NEW_PARTICLES) { return FALSE; } return TRUE; } void LLViewerPartSim::addPart(LLViewerPart* part) { if (sParticleCount < MAX_PART_COUNT) { put(part); } else { //delete the particle if can not add it in delete part ; part = NULL ; } } LLViewerPartGroup *LLViewerPartSim::put(LLViewerPart* part) { const F32 MAX_MAG = 1000000.f*1000000.f; // 1 million LLViewerPartGroup *return_group = NULL ; if (part->mPosAgent.magVecSquared() > MAX_MAG || !part->mPosAgent.isFinite()) { #if 0 && !LL_RELEASE_FOR_DOWNLOAD LL_WARNS() << "LLViewerPartSim::put Part out of range!" << LL_ENDL; LL_WARNS() << part->mPosAgent << LL_ENDL; #endif } else { LLViewerCamera* camera = LLViewerCamera::getInstance(); F32 desired_size = calc_desired_size(camera, part->mPosAgent, part->mScale); S32 count = (S32) mViewerPartGroups.size(); for (S32 i = 0; i < count; i++) { if (mViewerPartGroups[i]->addPart(part, desired_size)) { // We found a spatial group that we fit into, add us and exit return_group = mViewerPartGroups[i]; break ; } } // Hmm, we didn't fit in any of the existing spatial groups // Create a new one... if(!return_group) { llassert_always(part->mPosAgent.isFinite()); LLViewerPartGroup *groupp = createViewerPartGroup(part->mPosAgent, desired_size, part->mFlags & LLPartData::LL_PART_HUD); groupp->mUniformParticles = (part->mScale.mV[0] == part->mScale.mV[1] && !(part->mFlags & LLPartData::LL_PART_FOLLOW_VELOCITY_MASK)); if (!groupp->addPart(part)) { LL_WARNS() << "LLViewerPartSim::put - Particle didn't go into its box!" << LL_ENDL; LL_INFOS() << groupp->getCenterAgent() << LL_ENDL; LL_INFOS() << part->mPosAgent << LL_ENDL; mViewerPartGroups.pop_back() ; delete groupp; groupp = NULL ; } return_group = groupp; } } if(!return_group) //failed to insert the particle { delete part ; part = NULL ; } return return_group ; } LLViewerPartGroup *LLViewerPartSim::createViewerPartGroup(const LLVector3 &pos_agent, const F32 desired_size, bool hud) { //find a box that has a center position divisible by PART_SIM_BOX_SIDE that encompasses //pos_agent LLViewerPartGroup *groupp = new LLViewerPartGroup(pos_agent, desired_size, hud); mViewerPartGroups.push_back(groupp); return groupp; } void LLViewerPartSim::shift(const LLVector3 &offset) { S32 i; S32 count; count = (S32) mViewerPartSources.size(); for (i = 0; i < count; i++) { mViewerPartSources[i]->mPosAgent += offset; mViewerPartSources[i]->mTargetPosAgent += offset; mViewerPartSources[i]->mLastUpdatePosAgent += offset; } count = (S32) mViewerPartGroups.size(); for (i = 0; i < count; i++) { mViewerPartGroups[i]->shift(offset); } } static LLTrace::BlockTimerStatHandle FTM_SIMULATE_PARTICLES("Simulate Particles"); void LLViewerPartSim::updateSimulation() { static LLFrameTimer update_timer; //reset VBO cursor LLVOPartGroup::sVBSlotCursor = 0; const F32 dt = llmin(update_timer.getElapsedTimeAndResetF32(), 0.1f); if (!(gPipeline.hasRenderType(LLPipeline::RENDER_TYPE_PARTICLES))) { return; } LL_RECORD_BLOCK_TIME(FTM_SIMULATE_PARTICLES); // Start at a random particle system so the same // particle system doesn't always get first pick at the // particles. Theoretically we'd want to do this in distance // order or something, but sorting particle sources will be a big // pain. S32 i; S32 count = (S32) mViewerPartSources.size(); S32 start = (S32)ll_frand((F32)count); S32 dir = 1; S32 deldir = 0; if (ll_frand() > 0.5f) { dir = -1; deldir = -1; } S32 num_updates = 0; for (i = start; num_updates < count;) { if (i >= count) { i = 0; } if (i < 0) { i = count - 1; } if (!mViewerPartSources[i]->isDead()) { BOOL upd = TRUE; LLViewerObject* vobj = mViewerPartSources[i]->mSourceObjectp; if (vobj && (vobj->getPCode() == LL_PCODE_VOLUME)) { if(vobj->getAvatar() && vobj->getAvatar()->isTooComplex()) { upd = FALSE; } LLVOVolume* vvo = (LLVOVolume *)vobj; if (!LLPipeline::sRenderAttachedParticles && vvo && vvo->isAttachment()) { upd = FALSE; } } if (upd) { mViewerPartSources[i]->update(dt); } } if (mViewerPartSources[i]->isDead()) { mViewerPartSources.erase(mViewerPartSources.begin() + i); count--; i+=deldir; } else { i += dir; } num_updates++; } count = (S32) mViewerPartGroups.size(); for (i = 0; i < count; i++) { LLViewerObject* vobj = mViewerPartGroups[i]->mVOPartGroupp; S32 visirate = 1; if (vobj) { LLSpatialGroup* group = vobj->mDrawable->getSpatialGroup(); if (group && !group->isVisible()) // && !group->isState(LLSpatialGroup::OBJECT_DIRTY)) { visirate = 8; } } if ((LLDrawable::getCurrentFrame()+mViewerPartGroups[i]->mID)%visirate == 0) { if (vobj) { gPipeline.markRebuild(vobj->mDrawable, LLDrawable::REBUILD_ALL, TRUE); } mViewerPartGroups[i]->updateParticles(dt * visirate); mViewerPartGroups[i]->mSkippedTime=0.0f; if (!mViewerPartGroups[i]->getCount()) { delete mViewerPartGroups[i]; mViewerPartGroups.erase(mViewerPartGroups.begin() + i); i--; count--; } } else { mViewerPartGroups[i]->mSkippedTime+=dt; } } if (LLDrawable::getCurrentFrame()%16==0) { if (sParticleCount > sMaxParticleCount * 0.875f && sParticleAdaptiveRate < 2.0f) { sParticleAdaptiveRate *= PART_ADAPT_RATE_MULT; } else { if (sParticleCount < sMaxParticleCount * 0.5f && sParticleAdaptiveRate > 0.03125f) { sParticleAdaptiveRate *= PART_ADAPT_RATE_MULT_RECIP; } } } updatePartBurstRate() ; //LL_INFOS() << "Particles: " << sParticleCount << " Adaptive Rate: " << sParticleAdaptiveRate << LL_ENDL; } void LLViewerPartSim::updatePartBurstRate() { if (!(LLDrawable::getCurrentFrame() & 0xf)) { if (sParticleCount >= MAX_PART_COUNT) //set rate to zero { sParticleBurstRate = 0.0f ; } else if(sParticleCount > 0) { if(sParticleBurstRate > 0.0000001f) { F32 total_particles = sParticleCount / sParticleBurstRate ; //estimated F32 new_rate = llclamp(0.9f * sMaxParticleCount / total_particles, 0.0f, 1.0f) ; F32 delta_rate_threshold = llmin(0.1f * llmax(new_rate, sParticleBurstRate), 0.1f) ; F32 delta_rate = llclamp(new_rate - sParticleBurstRate, -1.0f * delta_rate_threshold, delta_rate_threshold) ; sParticleBurstRate = llclamp(sParticleBurstRate + 0.5f * delta_rate, 0.0f, 1.0f) ; } else { sParticleBurstRate += 0.0000001f ; } } else { sParticleBurstRate += 0.00125f ; } } } void LLViewerPartSim::addPartSource(LLPointer sourcep) { if (!sourcep) { LL_WARNS() << "Null part source!" << LL_ENDL; return; } sourcep->setStart() ; mViewerPartSources.push_back(sourcep); } void LLViewerPartSim::removeLastCreatedSource() { mViewerPartSources.pop_back(); } void LLViewerPartSim::cleanupRegion(LLViewerRegion *regionp) { for (group_list_t::iterator i = mViewerPartGroups.begin(); i != mViewerPartGroups.end(); ) { group_list_t::iterator iter = i++; if ((*iter)->getRegion() == regionp) { delete *iter; i = mViewerPartGroups.erase(iter); } } } void LLViewerPartSim::clearParticlesByID(const U32 system_id) { for (group_list_t::iterator g = mViewerPartGroups.begin(); g != mViewerPartGroups.end(); ++g) { (*g)->removeParticlesByID(system_id); } for (source_list_t::iterator i = mViewerPartSources.begin(); i != mViewerPartSources.end(); ++i) { if ((*i)->getID() == system_id) { (*i)->setDead(); break; } } } void LLViewerPartSim::clearParticlesByOwnerID(const LLUUID& task_id) { for (source_list_t::iterator iter = mViewerPartSources.begin(); iter != mViewerPartSources.end(); ++iter) { if ((*iter)->getOwnerUUID() == task_id) { clearParticlesByID((*iter)->getID()); } } }