/** * @file llviewerpartsim.cpp * @brief LLViewerPart class implementation * * $LicenseInfo:firstyear=2003&license=viewergpl$ * * Copyright (c) 2003-2007, Linden Research, Inc. * * Second Life Viewer Source Code * The source code in this file ("Source Code") is provided by Linden Lab * to you under the terms of the GNU General Public License, version 2.0 * ("GPL"), unless you have obtained a separate licensing agreement * ("Other License"), formally executed by you and Linden Lab. Terms of * the GPL can be found in doc/GPL-license.txt in this distribution, or * online at http://secondlife.com/developers/opensource/gplv2 * * There are special exceptions to the terms and conditions of the GPL as * it is applied to this Source Code. View the full text of the exception * in the file doc/FLOSS-exception.txt in this software distribution, or * online at http://secondlife.com/developers/opensource/flossexception * * By copying, modifying or distributing this software, you acknowledge * that you have read and understood your obligations described above, * and agree to abide by those obligations. * * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, * COMPLETENESS OR PERFORMANCE. * $/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" const F32 PART_SIM_BOX_SIDE = 16.f; const F32 PART_SIM_BOX_OFFSET = 0.5f*PART_SIM_BOX_SIDE; const F32 PART_SIM_BOX_RAD = 0.5f*F_SQRT3*PART_SIM_BOX_SIDE; //static S32 LLViewerPartSim::sMaxParticleCount = 0; S32 LLViewerPartSim::sParticleCount = 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(LLVector3 pos, LLVector2 scale) { F32 desired_size = (pos-LLViewerCamera::getInstance()->getOrigin()).magVec(); desired_size /= 4; return llclamp(desired_size, scale.magVec()*0.5f, PART_SIM_BOX_SIDE*2); } LLViewerPart::LLViewerPart() { LLMemType mt(LLMemType::MTYPE_PARTICLES); mPartSourcep = NULL; } LLViewerPart::~LLViewerPart() { LLMemType mt(LLMemType::MTYPE_PARTICLES); mPartSourcep = NULL; } void LLViewerPart::init(LLPointer sourcep, LLViewerImage *imagep, LLVPCallback cb) { LLMemType mt(LLMemType::MTYPE_PARTICLES); 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) { LLMemType mt(LLMemType::MTYPE_PARTICLES); mVOPartGroupp = NULL; mUniformParticles = TRUE; mRegionp = LLWorld::getInstance()->getRegionFromPosAgent(center_agent); llassert_always(center_agent.isFinite()); if (!mRegionp) { //llwarns << "No region at position, using agent region!" << llendl; mRegionp = gAgent.getRegion(); } mCenterAgent = center_agent; mBoxRadius = F_SQRT3*box_side*0.5f; mVOPartGroupp = (LLVOPartGroup *)gObjectList.createObjectViewer(LLViewerObject::LL_VO_PART_GROUP, getRegion()); mVOPartGroupp->setViewerPartGroup(this); mVOPartGroupp->setPositionAgent(getCenterAgent()); F32 scale = box_side * 0.5f; mVOPartGroupp->setScale(LLVector3(scale,scale,scale)); gPipeline.addObject(mVOPartGroupp); LLSpatialGroup* group = mVOPartGroupp->mDrawable->getSpatialGroup(); if (group != NULL) { LLVector3 center(group->mOctreeNode->getCenter()); LLVector3 size(group->mOctreeNode->getSize()); 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() { LLMemType mt(LLMemType::MTYPE_PARTICLES); cleanup(); S32 count = (S32) mParticles.size(); mParticles.clear(); LLViewerPartSim::decPartCount(count); } void LLViewerPartGroup::cleanup() { LLMemType mt(LLMemType::MTYPE_PARTICLES); if (mVOPartGroupp) { if (!mVOPartGroupp->isDead()) { gObjectList.killObject(mVOPartGroupp); } mVOPartGroupp = NULL; } } BOOL LLViewerPartGroup::posInGroup(const LLVector3 &pos, const F32 desired_size) { LLMemType mt(LLMemType::MTYPE_PARTICLES); 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) { LLMemType mt(LLMemType::MTYPE_PARTICLES); 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) { LLMemType mt(LLMemType::MTYPE_PARTICLES); S32 i; F32 dt; LLVector3 gravity(0.f, 0.f, -9.8f); LLViewerRegion *regionp = getRegion(); S32 end = (S32) mParticles.size(); for (i = 0; i < end; i++) { LLVector3 a(0.f, 0.f, 0.f); LLViewerPart& part = *((LLViewerPart*) 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) { LLVector3 tempVel(part.mVelocity); 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; } // 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)) { end--; LLPointer::swap(mParticles[i], mParticles[end]); // be sure to process the particle we just swapped-in i--; } else { F32 desired_size = calc_desired_size(part.mPosAgent, part.mScale); if (!posInGroup(part.mPosAgent, desired_size)) { // Transfer particles between groups LLViewerPartSim::getInstance()->put(&part); end--; LLPointer::swap(mParticles[i], mParticles[end]); // be sure to process the particle we just swapped-in i--; } } } S32 removed = (S32)mParticles.size() - end; if (removed > 0) { // we removed one or more particles, so flag this group for update mParticles.erase(mParticles.begin() + end, mParticles.end()); 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; } } void LLViewerPartGroup::shift(const LLVector3 &offset) { LLMemType mt(LLMemType::MTYPE_PARTICLES); mCenterAgent += offset; mMinObjPos += offset; mMaxObjPos += offset; S32 count = (S32) mParticles.size(); S32 i; for (i = 0; i < count; i++) { mParticles[i]->mPosAgent += offset; } } void LLViewerPartGroup::removeParticlesByID(const U32 source_id) { LLMemType mt(LLMemType::MTYPE_PARTICLES); S32 end = (S32) mParticles.size(); for (int i = 0; i < end; i++) { if(mParticles[i]->mPartSourcep->getID() == source_id) { mParticles[i]->mFlags = LLViewerPart::LL_PART_DEAD_MASK; } } } ////////////////////////////////// // // LLViewerPartSim implementation // // LLViewerPartSim::LLViewerPartSim() { LLMemType mt(LLMemType::MTYPE_PARTICLES); sMaxParticleCount = gSavedSettings.getS32("RenderMaxPartCount"); static U32 id_seed = 0; mID = ++id_seed; } void LLViewerPartSim::destroyClass() { LLMemType mt(LLMemType::MTYPE_PARTICLES); 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(); } BOOL LLViewerPartSim::shouldAddPart() { LLMemType mt(LLMemType::MTYPE_PARTICLES); 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; } } if (sParticleCount >= MAX_PART_COUNT) { return FALSE; } return TRUE; } void LLViewerPartSim::addPart(LLViewerPart* part) { LLMemType mt(LLMemType::MTYPE_PARTICLES); if (sParticleCount < MAX_PART_COUNT) { put(part); } } LLViewerPartGroup *LLViewerPartSim::put(LLViewerPart* part) { LLMemType mt(LLMemType::MTYPE_PARTICLES); const F32 MAX_MAG = 1000000.f*1000000.f; // 1 million if (part->mPosAgent.magVecSquared() > MAX_MAG || !part->mPosAgent.isFinite()) { #if 0 && !LL_RELEASE_FOR_DOWNLOAD llwarns << "LLViewerPartSim::put Part out of range!" << llendl; llwarns << part->mPosAgent << llendl; #endif return NULL; } F32 desired_size = calc_desired_size(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 mViewerPartGroups[i]; } } // Hmm, we didn't fit in any of the existing spatial groups // Create a new one... llassert_always(part->mPosAgent.isFinite()); LLViewerPartGroup *groupp = createViewerPartGroup(part->mPosAgent, desired_size); groupp->mUniformParticles = (part->mScale.mV[0] == part->mScale.mV[1] && !(part->mFlags & LLPartData::LL_PART_FOLLOW_VELOCITY_MASK)); if (!groupp->addPart(part)) { llwarns << "LLViewerPartSim::put - Particle didn't go into its box!" << llendl; llinfos << groupp->getCenterAgent() << llendl; llinfos << part->mPosAgent << llendl; delete groupp; return NULL; } return groupp; } LLViewerPartGroup *LLViewerPartSim::createViewerPartGroup(const LLVector3 &pos_agent, const F32 desired_size) { LLMemType mt(LLMemType::MTYPE_PARTICLES); //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); 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); } } void LLViewerPartSim::updateSimulation() { LLMemType mt(LLMemType::MTYPE_PARTICLES); static LLFrameTimer update_timer; const F32 dt = llmin(update_timer.getElapsedTimeAndResetF32(), 0.1f); if (!(gPipeline.hasRenderType(LLPipeline::RENDER_TYPE_PARTICLES))) { return; } LLFastTimer ftm(LLFastTimer::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()) { 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() ; //llinfos << "Particles: " << sParticleCount << " Adaptive Rate: " << sParticleAdaptiveRate << llendl; } 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) { LLMemType mt(LLMemType::MTYPE_PARTICLES); if (!sourcep) { llwarns << "Null part source!" << llendl; return; } sourcep->setStart() ; mViewerPartSources.push_back(sourcep); } void LLViewerPartSim::removeLastCreatedSource() { mViewerPartSources.pop_back(); } void LLViewerPartSim::cleanupRegion(LLViewerRegion *regionp) { LLMemType mt(LLMemType::MTYPE_PARTICLES); 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) { LLMemType mt(LLMemType::MTYPE_PARTICLES); 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) { LLMemType mt(LLMemType::MTYPE_PARTICLES); for (source_list_t::iterator iter = mViewerPartSources.begin(); iter != mViewerPartSources.end(); ++iter) { if ((*iter)->getOwnerUUID() == task_id) { clearParticlesByID((*iter)->getID()); } } }