#include "stdafx.h" #include "SceneUpdater.h" #include "MegaCluster.h" #include "SmoothCluster.h" #include "FastCluster.h" #include "SuperCluster.h" #include "SpatialHashedScene.h" #include "LightGrid.h" #include "LightObject.h" #include "ParticleEmitter.h" #include "ExplosionEmitter.h" #include "SpatialGrid.h" #include "GfxBase/FrameRateManager.h" #include "humanoid/Humanoid.h" #include "v8DataModel/Accoutrement.h" #include "v8world/ContactManager.h" #include "v8datamodel/DataModel.h" #include "v8datamodel/Workspace.h" #include "v8datamodel/Light.h" #include "v8datamodel/Lighting.h" #include "v8datamodel/MegaCluster.h" #include "v8datamodel/ContentProvider.h" #include "v8datamodel/ForceField.h" #include "v8datamodel/Explosion.h" #include "v8datamodel/PartCookie.h" #include "v8datamodel/CustomParticleEmitter.h" #include "Voxel/Util.h" #include "VisualEngine.h" #include "SceneManager.h" #include "GlobalShaderData.h" #include "v8datamodel/CustomParticleEmitter.h" #include "CustomEmitter.h" #include "voxel2/Grid.h" #include "rbx/Profiler.h" LOGGROUP(GfxClusters) LOGGROUP(GfxClustersFull) LOGGROUP(RenderLightGrid) LOGGROUP(RenderLightGridAgeProportion) FASTFLAG(RenderNewExplosionEnable) FASTFLAG(NoRandomColorsWithoutOutlines) LOGGROUP(ViewRbxInit) LOGGROUP(TerrainCellListener) FASTFLAGVARIABLE(FixCameraTargetStudio, false) FASTFLAGVARIABLE(CustomEmitterRenderEnabled, false) FASTFLAG(SmoothTerrainRenderLOD) DYNAMIC_FASTFLAG(HumanoidCookieRecursive) DYNAMIC_FASTFLAG(UseR15Character) FASTINTVARIABLE(FastClusterUpdateWaitingBudgetMs, 4) namespace RBX { namespace Graphics { const double CLUSTER_INVALIDATE_FRAME_BUDGET_MS = 4.0; #if defined(RBX_PLATFORM_IOS) || defined(__ANDROID__) const int FAST_CLUSTER_PRIORITY_INVALIDATE_BUDGET = 2; const size_t MAX_INVALIDATIONS_PER_FRAME = 16; #else const int FAST_CLUSTER_PRIORITY_INVALIDATE_BUDGET = 4; const size_t MAX_INVALIDATIONS_PER_FRAME = 64; #endif SceneUpdater::SceneUpdater(shared_ptr dataModel, VisualEngine* ve) : dataModel(dataModel) , mSettings(ve->getSettings()) , mRenderCaps(ve->getRenderCaps()) , mRenderStats(ve->getRenderStats()) , currentFrameNum(0) , mVisualEngine(ve) , mLastLightingUpdates(0) , mLastOccupancyUpdates(0) , mLightingComputeAverage(15) , mAgeDirtyProportion(0) , mLightingActive(false) , computeLightingEnabled(true) { mSeenFastClusters.reserve(FAST_CLUSTER_PRIORITY_INVALIDATE_BUDGET); FASTLOG(FLog::ViewRbxInit, "SceneUpdater bind - start"); // Find stuff to render: dataModel->getWorkspace()->visitDescendants(boost::bind(&SceneUpdater::onWorkspaceDescendantAdded, this, _1)); workspaceDescendantAddedConnection = dataModel->getWorkspace()->onDemandWrite()->descendantAddedSignal.connect(boost::bind(&SceneUpdater::onWorkspaceDescendantAdded, this, _1)); FASTLOG1(FLog::GfxClusters, "After initial bind, added parts: %u", mAddedParts.size()); RBX::Vector3 cellExtents = RBX::Vector3(32 * 4, 16 * 4, 32 * 4); float largeCoeff = 1.5f; mFastGridSC.reset(new FastGridSC(cellExtents, largeCoeff)); FASTLOG(FLog::ViewRbxInit, "SceneUpdater bind - end"); if (FFlag::NoRandomColorsWithoutOutlines) { RBX::Lighting* lighting = ServiceProvider::find(dataModel.get()); if (lighting) propertyChangedSignal = lighting->propertyChangedSignal.connect(boost::bind(&SceneUpdater::onPropertyChanged, this, _1)); } } SceneUpdater::~SceneUpdater() { } void SceneUpdater::unbind() { FASTLOG(FLog::ViewRbxInit, "SceneUpdater unbind - start"); for(size_t i = 0; i < connections.size(); ++i) { connections[i].disconnect(); } connections.clear(); FASTLOG(FLog::ViewRbxInit, "SceneUpdater unbind - end"); for (GfxPartSet::iterator it = mAttachments.begin(); it != mAttachments.end(); ++it) delete *it; for (HumanoidClusterMap::const_iterator it = mHumanoidClusters.begin(); it != mHumanoidClusters.end(); ++it) delete it->second; GfxPartSet megaClusters = mMegaClusters; for (GfxPartSet::const_iterator it = megaClusters.begin(); it != megaClusters.end(); ++it) delete *it; RBXASSERT(mMegaClusters.empty()); mAttachments.clear(); mHumanoidClusters.clear(); mMegaClusters.clear(); mFastGridSC.reset(); } void SceneUpdater::onWorkspaceDescendantAdded(shared_ptr descendant) { // See if the new instance is a PartInstance RBX::PartInstance* pi = RBX::Instance::fastDynamicCast(descendant.get()); if (pi!=NULL) { queuePartToCreate(shared_from(pi)); return; } if ( dynamic_cast(descendant.get())) { queueAttachementToCreate(descendant); } } bool SceneUpdater::isPartStatic(RBX::PartInstance* part) { return part->getSleeping(); } void SceneUpdater::queuePartToCreate(const boost::shared_ptr& part) { RBX::mutex::scoped_lock scoped_lock(queue_mutex); if (part->getPartType() == MEGACLUSTER_PART) { FASTLOG2(FLog::GfxClustersFull, "Frame %u: Adding Megacluster to queue - %p", currentFrameNum, part.get()); mAddedMegaClusters[part.get()] = part; } else { FASTLOG2(FLog::GfxClustersFull, "Frame %u: Adding part to queue - %p", currentFrameNum, part.get()); mAddedParts[part.get()] = part; } } void SceneUpdater::queueAttachementToCreate(const boost::shared_ptr& instance) { RBX::mutex::scoped_lock scoped_lock(queue_mutex); mAddedAttachementInstances[instance.get()] = instance; } void SceneUpdater::processPendingAttachments() { RBXPROFILER_SCOPE("Render", "processPendingAttachments"); InstanceSet tmp; { RBX::mutex::scoped_lock scoped_lock(queue_mutex); std::swap(tmp, mAddedAttachementInstances); } { // in dynamic clumping, we just create clumps of size 1. for(InstanceSet::iterator it = tmp.begin(); it != tmp.end(); ++it) { shared_ptr p = it->second.lock(); if (p && GfxBinding::isInWorkspace(p.get())) // part could have dissapeared before render ever happened. { addAttachment(p); } } } } void SceneUpdater::queueChunkInvalidateMegaCluster(RBX::GfxPart* part, const SpatialRegion::Id& pos, bool isWaterChunk) { RBXASSERT(part->getPartInstance()->getPartType() == RBX::MEGACLUSTER_PART); RBX::mutex::scoped_lock scoped_lock(queue_mutex); FASTLOG3(FLog::MegaClusterDirty, "Chunk queued for update: %u x %u x %u", pos.value().x, pos.value().y, pos.value().z); const Camera* camera = dataModel->getWorkspace()->getConstCamera(); RBX::Vector3 testLocation(SpatialRegion::centerOfRegionInGlobalCoordStuds(pos).toVector3()); float distance = (camera->getCameraCoordinateFrame().translation - testLocation).squaredMagnitude(); if (distance < 256 * 256) { // radius = ~ 2 chunks mCloseChunkInvalidates.insert(mCloseChunkInvalidates.begin(), MegaClusterChunk(part, pos, isWaterChunk)); } else if (distance < 2048 * 2048) { // radius = ~ 16 chunks mMiddleChunkInvalidates.insert(mMiddleChunkInvalidates.begin(), MegaClusterChunk(part, pos, isWaterChunk)); } else { mFarChunkInvalidates.insert(mFarChunkInvalidates.begin(), MegaClusterChunk(part, pos, isWaterChunk)); } } void SceneUpdater::queueFullInvalidateMegaCluster(RBX::GfxPart* part) { RBXASSERT(part->getPartInstance()->getPartType() == RBX::MEGACLUSTER_PART); FASTLOG1(FLog::MegaClusterInit, "Full cluster queued for update - %p", part); RBX::mutex::scoped_lock scoped_lock(queue_mutex); mFullInvalidatedClusters.insert(part); } void SceneUpdater::queueInvalidatePart(RBX::GfxPart* part) { FASTLOG2(FLog::GfxClustersFull, "Frame %u: Queue invalidate part: %p", currentFrameNum, part); RBX::mutex::scoped_lock scoped_lock(queue_mutex); mInvalidatedParts.insert(part); } void SceneUpdater::queueInvalidateFastCluster(RBX::GfxPart* cluster) { FASTLOG2(FLog::GfxClustersFull, "Frame %u: Queue invalidate fast cluster: %p", currentFrameNum, cluster); RBX::mutex::scoped_lock scoped_lock(queue_mutex); mInvalidatedFastClusters.insert(cluster); } void SceneUpdater::queuePriorityInvalidateFastCluster(RBX::GfxPart* cluster) { FASTLOG2(FLog::GfxClustersFull, "Frame %u: Queue priority invalidate fast cluster: %p", currentFrameNum, cluster); mPriorityInvalidateFastClusters.insert(cluster); } void SceneUpdater::notifyWaitingForAssets(RBX::GfxPart* part, const std::vector& ids) { std::vector uids = ids; std::sort(uids.begin(), uids.end()); uids.erase(std::unique(uids.begin(), uids.end()), uids.end()); FASTLOG3(FLog::GfxClustersFull, "Cluster %p: notify waiting for %d assets (%d unique)", part, ids.size(), uids.size()); RBX::mutex::scoped_lock scoped_lock(queue_mutex); mWaitingParts.erase(part); for (size_t i = 0; i < uids.size(); ++i) mWaitingParts.insert(std::make_pair(part, uids[i])); } void SceneUpdater::updateAllInvalidParts(bool bulkExecution) { RBXPROFILER_SCOPE("Render", "updateInvalidParts"); GfxPartSet tmp; { RBX::mutex::scoped_lock scoped_lock(queue_mutex); if(mInvalidatedParts.size() > 0) FASTLOG2(FLog::GfxClusters, "Total invalidated parts: %u, bulkExecution: %u", mInvalidatedParts.size(), bulkExecution); if(mInvalidatedParts.size() < MAX_INVALIDATIONS_PER_FRAME || bulkExecution) { std::swap(tmp, mInvalidatedParts); } else { GfxPartSet::iterator it = mInvalidatedParts.begin(); while(tmp.size() < MAX_INVALIDATIONS_PER_FRAME) { tmp.insert(*it); mInvalidatedParts.erase(it++); } } } if(!tmp.empty()) FASTLOG1(FLog::GfxClusters, "Invalidating entities: %u", tmp.size()); for(GfxPartSet::iterator it = tmp.begin(); it != tmp.end(); ++it) { // remove this part from the waiting list if it managed to find it's way in the invalidation queue (it could get deleted!) mWaitingParts.erase(*it); FASTLOG1(FLog::GfxClustersFull, "Updating entity on part cluster: %p", *it); (*it)->updateEntity(); } } struct WaitingPart { GfxPart* part; std::multimap::iterator assets; float distance; unsigned int assetCount; bool operator<(const WaitingPart& other) const { return distance < other.distance; } bool isNewContentAvailable(ContentProvider* contentProvider) const { auto it = assets; for (unsigned int i = 0; i < assetCount; ++i) { const ContentId& id = it->second; if (contentProvider->hasContent(id) || contentProvider->isUrlBad(id)) return true; ++it; } return false; } }; void SceneUpdater::updateWaitingParts(bool bulkExecution) { RBXPROFILER_SCOPE("Render", "updateWaitingParts"); if(!mWaitingParts.empty()) FASTLOG1(FLog::GfxClusters, "Waiting parts, pass begin: %u", mWaitingParts.size()); ContentProvider* contentProvider = ServiceProvider::create(dataModel.get()); // Gather all clusters that are waiting for assets std::vector waitingParts; for (AssetPartMap::iterator it = mWaitingParts.begin(); it != mWaitingParts.end(); ) { float distance = (it->first->getCenter() - pointOfInterest).squaredLength(); WaitingPart part = { it->first, it, distance, 0 }; // Scan through assets with the same part (map is sorted by part pointer) while (it != mWaitingParts.end() && it->first == part.part) { ++it; part.assetCount++; } waitingParts.push_back(part); } // Sort all clusters so that we process front-to-back std::sort(waitingParts.begin(), waitingParts.end()); // Process all clusters with a timeout - closest clusters get processed first Timer timer; for (auto& part: waitingParts) { // part.assets iterator must still point to a valid element since map iterators are only invalidated by erases of their keys RBXASSERT(part.assets->first == part.part); if (part.isNewContentAvailable(contentProvider)) { // Remove all entries for this part from the list; they will reappear if the part still needs them and they are not available mWaitingParts.erase(part.part); part.part->updateEntity(/* assetsUpdated= */ true); if (!bulkExecution && timer.delta().msec() > FInt::FastClusterUpdateWaitingBudgetMs) { // come again next time! break; } } } } static void limitCopy(unsigned int maxSize, SceneUpdater::MegaClusterChunkList& source, SceneUpdater::MegaClusterChunkList& dest) { while (dest.size() < maxSize && !source.empty()) { dest.push_back(source.back()); source.pop_back(); } } void SceneUpdater::updateMegaClusters(bool bulkExecution) { RBXPROFILER_SCOPE("Render", "updateClusters"); GfxPartSet tmpFull; { RBX::mutex::scoped_lock scoped_lock(queue_mutex); std::swap(tmpFull, mFullInvalidatedClusters); } if(tmpFull.size() > 0) FASTLOG1(FLog::GfxClusters, "Updating %u Full clusters", tmpFull.size()); for(GfxPartSet::iterator it = tmpFull.begin(); it != tmpFull.end(); ++it) { (*it)->updateEntity(); } MegaClusterChunkList tmpChunk; { RBX::mutex::scoped_lock scoped_lock(queue_mutex); if (bulkExecution) { limitCopy(UINT_MAX, mCloseChunkInvalidates, tmpChunk); limitCopy(UINT_MAX, mMiddleChunkInvalidates, tmpChunk); limitCopy(UINT_MAX, mFarChunkInvalidates, tmpChunk); } else { limitCopy(5, mCloseChunkInvalidates, tmpChunk); limitCopy(7, mMiddleChunkInvalidates, tmpChunk); limitCopy(8, mFarChunkInvalidates, tmpChunk); limitCopy(8, mCloseChunkInvalidates, tmpChunk); limitCopy(8, mMiddleChunkInvalidates, tmpChunk); limitCopy(8, mFarChunkInvalidates, tmpChunk); } } if(tmpChunk.size() > 0) FASTLOG1(FLog::GfxClusters, "Updating %u cluster chunks", tmpChunk.size()); mRenderStats->lastFrameMegaClusterChunks = 0; for(MegaClusterChunkList::iterator itChunk = tmpChunk.begin(); itChunk != tmpChunk.end(); ++itChunk) { itChunk->cluster->updateChunk(itChunk->chunkPos, itChunk->isWaterChunk); mRenderStats->lastFrameMegaClusterChunks++; } } void SceneUpdater::updateInvalidatedFastClusters(bool bulkExecution /* = false */) { RBXPROFILER_SCOPE("Render", "updateInvalidatedFastClusters"); mRenderStats->lastFrameFast.clusters = 0; mRenderStats->lastFrameFast.parts = 0; RBX::Timer timer; if(FLog::GfxClusters) { RBX::mutex::scoped_lock scoped_lock(queue_mutex); if(!mPriorityInvalidateFastClusters.empty() || !mInvalidatedFastClusters.empty()) { FASTLOG2(FLog::GfxClusters, "Invalidating Fast Clusters, PriorityInvalidatedClusters: %u, InvalidatedClusters: %u", mPriorityInvalidateFastClusters.size(), mInvalidatedFastClusters.size()); } } do { GfxPart* cluster = NULL; if(mPriorityInvalidateFastClusters.empty()) { RBX::mutex::scoped_lock scoped_lock(queue_mutex); if (mInvalidatedFastClusters.empty()) break; cluster = *mInvalidatedFastClusters.begin(); mInvalidatedFastClusters.erase(mInvalidatedFastClusters.begin()); FASTLOG3(FLog::GfxClusters, "Updating invalidated cluster %p (remaining: %u, bulkExecution: %u)", cluster, mPriorityInvalidateFastClusters.size(), bulkExecution); } else { cluster = *mPriorityInvalidateFastClusters.begin(); mPriorityInvalidateFastClusters.erase(mPriorityInvalidateFastClusters.begin()); } RBXASSERT(cluster); mRenderStats->lastFrameFast.clusters++; mRenderStats->lastFrameFast.parts += cluster->getPartCount(); // update cluster cluster->updateEntity(); } while (bulkExecution || !mPriorityInvalidateFastClusters.empty() || timer.delta().msec() <= CLUSTER_INVALIDATE_FRAME_BUDGET_MS); } bool SceneUpdater::arePartsWaitingForAssets() { return !mWaitingParts.empty(); } size_t SceneUpdater::getUpdateQueueSize() const { return mInvalidatedFastClusters.size() + mPriorityInvalidateFastClusters.size(); } void SceneUpdater::notifyAwake(RBX::GfxPart* part) { RBX::mutex::scoped_lock scoped_lock(queue_mutex); FASTLOG1(FLog::GfxClustersFull, "notifyAwake, adding cluster %p to dynamic nodes", part); mDynamicNodes.insert(part); } void SceneUpdater::notifySleeping(RBX::GfxPart* part) { RBX::mutex::scoped_lock scoped_lock(queue_mutex); GfxPartSet::iterator it = mDynamicNodes.find(part); if(it != mDynamicNodes.end()) { FASTLOG1(FLog::GfxClustersFull, "NotifySleeping for cluster %p, erasing from dynamic nodes", part); mDynamicNodes.erase(it); } } struct IsChunkFromCluster { GfxPart* part; IsChunkFromCluster(GfxPart* part): part(part) { } bool operator()(const SceneUpdater::MegaClusterChunk& chunk) const { return chunk.cluster == part; } }; void SceneUpdater::notifyDestroyed(RBX::GfxPart* part) { // clear deleted parts from invalid/waiting lists. RBX::mutex::scoped_lock scoped_lock(queue_mutex); mInvalidatedParts.erase(part); mWaitingParts.erase(part); mDynamicNodes.erase(part); mFastClustersToCheck.erase(part); mFastClustersToCheckFW.erase(part); mPriorityInvalidateFastClusters.erase(part); mInvalidatedFastClusters.erase(part); if (mMegaClusters.count(part)) { mMegaClusters.erase(part); mFullInvalidatedClusters.erase(part); mCloseChunkInvalidates.erase(std::remove_if(mCloseChunkInvalidates.begin(), mCloseChunkInvalidates.end(), IsChunkFromCluster(part)), mCloseChunkInvalidates.end()); mMiddleChunkInvalidates.erase(std::remove_if(mMiddleChunkInvalidates.begin(), mMiddleChunkInvalidates.end(), IsChunkFromCluster(part)), mMiddleChunkInvalidates.end()); mFarChunkInvalidates.erase(std::remove_if(mFarChunkInvalidates.begin(), mFarChunkInvalidates.end(), IsChunkFromCluster(part)), mFarChunkInvalidates.end()); if (MegaClusterInstance* terrain = Instance::fastDynamicCast(part->getPartInstance())) { if (terrain->isSmooth() == (dynamic_cast(part) != NULL)) { if (terrain->isSmooth()) terrain->getSmoothGrid()->disconnectListener(this); else terrain->getVoxelGrid()->disconnectListener(this); } else { // terrain is smooth but the cluster is not smooth // this can happen when we do convert-to-smooth // however, in this case we don't really need to disconnect from old grid since it's dead at this point } } } } void SceneUpdater::queueFastClusterCheck(RBX::GfxPart* cluster, bool isFW) { // FIXME: Do we need to take queue_mutex here? Right now it seems to be protected by physics DM Write access FASTLOG2(FLog::GfxClustersFull, "Fast Cluster queued to check - %p, isFW - %u", cluster, isFW); if(isFW) mFastClustersToCheckFW.insert(cluster); else mFastClustersToCheck.insert(cluster); } void SceneUpdater::updateDynamicParts() { RBXPROFILER_SCOPE("Render", "updateDynamicParts"); RBX::Profiling::Mark mark(*mRenderStats->updateDynamicParts, true, true); std::vector tmp; tmp.reserve(mDynamicNodes.size()); { RBX::mutex::scoped_lock scoped_lock(queue_mutex); std::copy(mDynamicNodes.begin(), mDynamicNodes.end(), std::back_inserter(tmp)); } if(!mDynamicNodes.empty()) FASTLOG1(FLog::GfxClusters, "Dynamic parts to update: %u", mDynamicNodes.size()); //update the cframes of all the dynamic objects for(std::vector::iterator childit = tmp.begin(); childit != tmp.end(); ++childit ) { GfxPart* aggNode = *childit; aggNode->updateCoordinateFrame(); } } void SceneUpdater::processPendingMegaClusters() { RBXPROFILER_SCOPE("Render", "processPendingClusters"); if(mAddedMegaClusters.size() > 0) FASTLOG1(FLog::GfxClusters, "Processing %u new MegaClusters", mAddedMegaClusters.size()); PartInstanceSet localCopy; { RBX::mutex::scoped_lock scoped_lock(queue_mutex); std::swap(localCopy, mAddedMegaClusters); } for(PartInstanceSet::iterator it = localCopy.begin(); it != localCopy.end(); ++it) { shared_ptr part = it->second.lock(); if(part) addMegaCluster(part); } } void SceneUpdater::processPendingParts(bool priorityParts) { RBXPROFILER_SCOPE("Render", "processPendingParts"); PartInstanceSet localCopy; { RBX::mutex::scoped_lock scoped_lock(queue_mutex); localCopy.swap(mAddedParts); } if(!localCopy.empty()) FASTLOG2(FLog::GfxClusters, "Added parts to process: %u, priority: %u", localCopy.size(), priorityParts); for(PartInstanceSet::iterator it = localCopy.begin(); it != localCopy.end(); ++it) { shared_ptr p = it->second.lock(); if(p && GfxBinding::isInWorkspace(p.get())) { p->setCookie(PartCookie::compute(p.get())); addFastPart(p, /* isFW= */ isPartStatic(p.get()), priorityParts); } else { FASTLOG2(FLog::GfxClustersFull, "Part %p died or removed from workspace before we could process it (died: %u)", it->first, p.get() == NULL); } } } void SceneUpdater::updatePrepare(unsigned long currentFrameNum, const RBX::Frustum& updateFrustum) { RBXPROFILER_SCOPE("Render", "UpdatePrepare"); this->currentFrameNum = currentFrameNum; this->updateFrustum = updateFrustum; FASTLOG2(FLog::GfxClusters, "Scene updater %p update cycle, current frame num: %u", this, currentFrameNum); RBX::Profiling::Mark mark(*mRenderStats->updateSceneGraph, true, true); processPendingParts(false); processPendingMegaClusters(); processPendingAttachments(); updateMegaClusters(mSettings->getEagerBulkExecution()); updateAllInvalidParts(mSettings->getEagerBulkExecution()); updateWaitingParts(mSettings->getEagerBulkExecution()); checkFastClusters(); processPendingParts(true); updateInvalidatedFastClusters(mSettings->getEagerBulkExecution()); updateDynamicParts(); computeLightingPrepare(); FASTLOG(FLog::GfxClusters, "Scene updater finish"); } void SceneUpdater::updatePerform() { computeLightingPerform(); } void SceneUpdater::terrainCellChanged(const Voxel::CellChangeInfo& info) { // info.position is in terrain voxel coordinates; convert to studs before using G3D::Vector3 terrainCoordInStuds = Voxel::cellToWorld_center(info.position); if (LightGrid* lgrid = mVisualEngine->getLightGrid()) { if (mLightingActive) { lgrid->invalidatePoint(terrainCoordInStuds, LightGridChunk::Dirty_OccupancyAndDependents | LightGridChunk::Dirty_HighPriority); } } } void SceneUpdater::onTerrainRegionChanged(const Voxel2::Region& region) { if (LightGrid* lgrid = mVisualEngine->getLightGrid()) { if (mLightingActive) { Vector3 begin = Voxel::cellSpaceToWorldSpace(region.begin().toVector3()); Vector3 end = Voxel::cellSpaceToWorldSpace(region.end().toVector3()); lgrid->invalidateExtents(Extents(begin, end), LightGridChunk::Dirty_OccupancyAndDependents); lgrid->invalidatePoint((begin + end) / 2, LightGridChunk::Dirty_HighPriority); } } } void SceneUpdater::lightingInvalidateOccupancy(const RBX::Extents& extents, const RBX::Vector3& highPriorityPoint, bool isFixed) { if (LightGrid* lgrid = mVisualEngine->getLightGrid()) { if (mLightingActive && (isFixed || lgrid->getNonFixedPartsEnabled())) { lgrid->invalidateExtents(extents, LightGridChunk::Dirty_OccupancyAndDependents); lgrid->invalidatePoint(highPriorityPoint, LightGridChunk::Dirty_HighPriority); } } } void SceneUpdater::lightingInvalidateLocal(const RBX::Extents& extents) { if (LightGrid* lgrid = mVisualEngine->getLightGrid()) { checkAndActivateLighting(); lgrid->invalidateExtents(extents, LightGridChunk::Dirty_LightingLocal); } } void SceneUpdater::checkAndActivateLighting() { if (!mLightingActive) { mLightingActive = true; // inactive lighting muted all occupancy updates; re-issue them if (LightGrid* lgrid = mVisualEngine->getLightGrid()) { lgrid->invalidateAll(LightGridChunk::Dirty_Occupancy); } } } void SceneUpdater::setComputeLightingEnabled(bool value) { computeLightingEnabled = value; } void SceneUpdater::onPropertyChanged(const RBX::Reflection::PropertyDescriptor* descriptor) { if (*descriptor==Lighting::desc_Outlines) { invalidateAllFastClusters(); } } void SceneUpdater::computeLightingPrepare() { RBXPROFILER_SCOPE("Render", "computeLightingPrepare"); bool bulkExecution = mSettings->getEagerBulkExecution(); GlobalShaderData& globalShaderData = mVisualEngine->getSceneManager()->writeGlobalShaderData(); // Dummy setup so that light grid sampling still works without the grid globalShaderData.LightConfig0 = Vector4(); globalShaderData.LightConfig1 = Vector4(); globalShaderData.LightConfig2 = Vector4(); globalShaderData.LightConfig3 = Vector4(-1, -1, -1, 0); globalShaderData.LightBorder = Vector4(0, 0, 0, 1); if (!computeLightingEnabled) return; mLastOccupancyUpdates = 0; if (!computeLightingEnabled) return; if (LightGrid* lgrid = mVisualEngine->getLightGrid()) { ContactManager* contactManager = dataModel->getWorkspace()->getWorld()->getContactManager(); MegaClusterInstance* terrain = boost::polymorphic_downcast(dataModel->getWorkspace()->getTerrain()); RBX::Camera* camera = dataModel->getWorkspace()->getCamera(); RBX::Lighting* lighting = ServiceProvider::find(dataModel.get()); // Make sure lighting is active if global shadows are on if (lighting->getGlobalShadows()) { checkAndActivateLighting(); } // Compute focus point; note that the computation is different in play mode and studio mode if (camera->getCameraSubject()) { mFocusPoint = camera->getCameraFocus().translation; } else { RBX::Vector3 hit = RBX::Vector3::zero(); if (FFlag::FixCameraTargetStudio) { RBX::Vector3 origin = mVisualEngine->getSceneManager()->getMinumumSqDistanceCenter(); RBX::Vector3 direction = camera->getCameraCoordinateFrame().vectorToWorldSpace(RBX::Vector3(0.0f,0.0f,-1.0f)); hit = origin + direction; } else { // TODO: getMinumumSqPartDistance will get data from previous frame, fix this when we move compute lighting to perform float minSqDistance = mVisualEngine->getSceneManager()->getMinumumSqPartDistance(); float distance = (minSqDistance == FLT_MAX ? 0 : sqrt(minSqDistance)); // Has to be consistent with minSqDistance, so take it from SceneManager, not from camera RBX::Vector3 origin = mVisualEngine->getSceneManager()->getMinumumSqDistanceCenter(); // WARN: still taking direction from camera, might mean one frame delay RBX::Vector3 direction = camera->getCameraCoordinateFrame().vectorToWorldSpace(RBX::Vector3(0.0f,0.0f,-1.0f)); hit = origin + direction*distance; direction.y = 0; hit += direction * 16*4.0f; } mFocusPoint = hit; } unsigned chunkBudget = getChunkBudget(); // Relocate the grid if focus point moved far enough // Note: we can skip updating chunk contents (filling with dummy color and uploading) if lighting is not active mLightgridMoved = lgrid->updateGridCenter(mFocusPoint, /* skipChunkUpdates= */ !mLightingActive); mOccupancyPartCache.clear(); mLgridchunksToUpdate.clear(); if ((!mLightgridMoved || bulkExecution) && mLightingActive) { lgrid->updateAgePriorityForChunks(mFocusPoint); // Update global light attributes for the light grid if (1) { lgrid->setLightShadows(lighting->getGlobalShadows()); if (lighting->getGlobalShadows()) { lgrid->setSkyAmbient(Color3uint8(lighting->getSkyAmbient())); } else { lgrid->setSkyAmbient(Color3uint8()); } } if (lighting->getGlobalShadows()) { lgrid->setLightDirection(-lighting->getSkyParameters().lightDirection.unit()); } for (unsigned i = 0; i < chunkBudget; ++i) { LightGridChunk* chunk = NULL; if (bulkExecution) chunk = lgrid->findFirstDirtyChunk(); else if (mAgeDirtyProportion == FLog::RenderLightGridAgeProportion) { chunk = lgrid->findOldestChunk(); mAgeDirtyProportion = 0; } else { chunk = lgrid->findDirtyChunk(); mAgeDirtyProportion++; } if (!chunk) break; RBXASSERT(chunk->dirty); if (chunk->dirty & LightGridChunk::Dirty_Occupancy) { lgrid->occupancyUpdateChunkPrepare(*chunk, terrain, contactManager, mOccupancyPartCache); mLastOccupancyUpdates++; } mLgridchunksToUpdate.push_back( std::make_pair( chunk, (unsigned)chunk->dirty ) ); // reset chunk dirty flag so that findDirtyChunk does not pick it up again chunk->dirty = 0; chunk->age = 0; } // restore chunk dirty flags so that we get correct state in perform for (auto& chunk: mLgridchunksToUpdate) chunk.first->dirty = chunk.second; } } } void SceneUpdater::computeLightingPerform() { RBXPROFILER_SCOPE("Render", "computeLightingPerform"); bool bulkExecution = mSettings->getEagerBulkExecution(); GlobalShaderData& globalShaderData = mVisualEngine->getSceneManager()->writeGlobalShaderData(); if (LightGrid* lgrid = mVisualEngine->getLightGrid()) { RBX::Timer timer; mLastLightingUpdates = 0; SpatialHashedScene* spatialHashedScene = mVisualEngine->getSceneManager()->getSpatialHashedScene(); // Don't update lighting in the same frame where we move the grid to reduce relocation stall // Don't update lighting unless it is active // Do update lighting if bulkExecution is true so that one pass is enough if ((!mLightgridMoved || bulkExecution) && mLightingActive) { RBX::Timer timer; lgrid->updateBorderColor(mFocusPoint, updateFrustum); lgrid->occupancyUpdateChunkPerform(mOccupancyPartCache); unsigned chunkBudget = getChunkBudget(); if ((!mLightgridMoved || bulkExecution) && mLightingActive) { for (unsigned i = 0; i < chunkBudget; ++i) { // now the tricky part... LightGridChunk* chunk = NULL; if( i < mLgridchunksToUpdate.size() ) // first, try the chunk cache from prepare { chunk = mLgridchunksToUpdate[i].first; } else // otherwise, we have some budget left, try other dirty chunks { if (bulkExecution) chunk = lgrid->findFirstDirtyChunk(); else chunk = lgrid->findDirtyChunk(); } if(!chunk) break; // nothing to update if (chunk->dirty & (LightGridChunk::Dirty_LightingLocal | LightGridChunk::Dirty_LightingLocalShadowed)) lgrid->lightingUpdateChunkLocal(*chunk, spatialHashedScene); if (chunk->dirty & LightGridChunk::Dirty_LightingGlobal) lgrid->lightingUpdateChunkGlobal(*chunk); if (chunk->dirty & LightGridChunk::Dirty_LightingSkylight) lgrid->lightingUpdateChunkSkylight(*chunk); if (chunk->dirty & (LightGridChunk::Dirty_LightingGlobal | LightGridChunk::Dirty_LightingSkylight)) lgrid->lightingUpdateChunkAverage(*chunk); mLastLightingUpdates++; if (!bulkExecution) lgrid->lightingUploadChunk(*chunk); chunk->dirty = 0; chunk->age = 0; } if (bulkExecution && mLastLightingUpdates != 0) lgrid->lightingUploadAll(); lgrid->lightingUploadCommit(); if (mLastOccupancyUpdates > 0 || mLastLightingUpdates > 0) { FASTLOG3(FLog::RenderLightGrid, "LightGrid: Updated %d chunks in %d usec (occupancy: %d chunks)", mLastLightingUpdates, (int)(timer.delta().msec() * 1000), mLastOccupancyUpdates); } } } // Pass grid offset to shader via global data RBX::Vector3 gridOffset = lgrid->getGridCornerOffset(); RBX::Vector3 gridSize = lgrid->getGridSize(); RBX::Color4uint8 borderColor = lgrid->getBorderColor(); float frmRadius = mVisualEngine->getFrameRateManager()->getLightGridRadius(); RBX::Vector3 gridCenter = gridOffset + gridSize / 2.f; RBX::Vector3 gridRadius = RBX::Vector3(frmRadius, gridSize.y / 2.f, frmRadius).min(gridSize / 2.f); // world space -> texture space: v * scale + offset RBX::Vector3 gridTextureScale = RBX::Vector3(1.f / gridSize.x, 1.f / gridSize.y, 1.f / gridSize.z); // note: technically we can make the offset zero - the texture is wrapped and shifted so that // the world space to texture space mapping is constant. // however, to improve precision we'd like the transformed coordinates to be small, so we need to offset. RBX::Vector3 gridTextureOffset = lgrid->getWrapSafeOffset() * gridTextureScale; // for 2D texture sampling we have to offset the texture space by half of the texel in Y so that nearest sampling on LUT works if (lgrid->hasTexture() && lgrid->getTexture()->getType() == Texture::Type_2D) { float gridDepth = lgrid->getChunkCount().y * kLightGridChunkSizeY; gridTextureOffset.y -= 0.5f / gridDepth; // iPad4 has a precision issue with perspective interpolation: a constant output from VS is // interpolated with precision issues. For voxel-aligned surfaces the sampling coordinate is transformed // to an integer in texture space - which is a boundary condition for frac() and nearest filtering. // Add a small offset so that at least voxel-aligned surfaces are perfect. gridTextureOffset.y += 0.01f / gridDepth; } // half-size of the grid in Y direction is 32 pixels, so need at least 0.5/32=0.015625 wide border // if an extra half-texel offset is applied, the border shifts by the full texel => 1/32=0.03125 RBX::Vector3 gridRadiusEffective = gridRadius * 0.95f; RBX::Vector3 gridTextureSpaceCenter = gridCenter * gridTextureScale + gridTextureOffset; RBX::Vector3 gridTextureSpaceRadius = gridRadiusEffective * gridTextureScale; // Note: the parameters have swizzle .yxz pre-applied so that we can avoid doing it in PS (helps ps_2_0 and GLSLES) globalShaderData.LightConfig0 = Vector4(gridTextureScale.y, gridTextureScale.x, gridTextureScale.z, 0); globalShaderData.LightConfig1 = Vector4(gridTextureOffset.y, gridTextureOffset.x, gridTextureOffset.z, 0); globalShaderData.LightConfig2 = Vector4(gridTextureSpaceCenter.y, gridTextureSpaceCenter.x, gridTextureSpaceCenter.z, 0); globalShaderData.LightConfig3 = Vector4(gridTextureSpaceRadius.y, gridTextureSpaceRadius.x, gridTextureSpaceRadius.z, 0); globalShaderData.LightBorder = Vector4(borderColor.r / 255.f, borderColor.g / 255.f, borderColor.b / 255.f, borderColor.a / 255.f); // Update grid settings from FRM // Make sure it's at the end so that FastCluster updates and lighting updates see the same value during one frame lgrid->setNonFixedPartsEnabled(mVisualEngine->getFrameRateManager()->getLightingNonFixedEnabled()); mLightingComputeAverage.sample(timer.delta().msec()); } } void SceneUpdater::checkFastClusters() { RBXPROFILER_SCOPE("Render", "checkFastClusters"); mSeenFastClusters.resize(0); if(!mFastClustersToCheck.empty() || !mFastClustersToCheckFW.empty()) FASTLOG2(FLog::GfxClusters, "Fast clusters to check for break: %u, fw: %u", mFastClustersToCheck.size(), mFastClustersToCheckFW.size()); while (!mFastClustersToCheck.empty() || !mFastClustersToCheckFW.empty()) { FastCluster* gfxcluster = NULL; if(!mFastClustersToCheckFW.empty()) { gfxcluster = boost::polymorphic_downcast(*mFastClustersToCheckFW.begin()); RBXASSERT(gfxcluster->isFW()); } else { RBXASSERT(!mFastClustersToCheck.empty()); gfxcluster = boost::polymorphic_downcast(*mFastClustersToCheck.begin()); } if(mSeenFastClusters.size() >= FAST_CLUSTER_PRIORITY_INVALIDATE_BUDGET) break; // Doesn't make sense to continue if just one slot left in the budget and current chunk is not there yet if(mSeenFastClusters.size() >= FAST_CLUSTER_PRIORITY_INVALIDATE_BUDGET-1 && !seenIndexBefore(gfxcluster->getSpatialIndex())) break; // erase has to happen before checkCluster because it can queue itself if over budget if(gfxcluster->isFW()) { RBXASSERT(!mFastClustersToCheckFW.empty()); mFastClustersToCheckFW.erase(mFastClustersToCheckFW.begin()); } else { RBXASSERT(!mFastClustersToCheck.empty()); mFastClustersToCheck.erase(mFastClustersToCheck.begin()); } gfxcluster->checkCluster(); } RBXASSERT(mSeenFastClusters.size() <= FAST_CLUSTER_PRIORITY_INVALIDATE_BUDGET); } bool SceneUpdater::seenIndexBefore(const SpatialGridIndex& index) { return std::find(mSeenFastClusters.begin(), mSeenFastClusters.end(), index) != mSeenFastClusters.end();; } bool SceneUpdater::checkAddSeenFastClusters(const SpatialGridIndex& index) { if(!seenIndexBefore(index)) { if(mSeenFastClusters.size() < FAST_CLUSTER_PRIORITY_INVALIDATE_BUDGET) mSeenFastClusters.push_back(index); else return false; } return true; } void SceneUpdater::addMegaCluster(const shared_ptr& part) { shared_ptr terrain = shared_polymorphic_downcast(part); GfxPart* cluster; if (terrain->isSmooth()) { terrain->getSmoothGrid()->connectListener(this); if (FFlag::SmoothTerrainRenderLOD) cluster = new SmoothClusterLOD(mVisualEngine, part); else cluster = new SmoothClusterChunked(mVisualEngine, part); } else { terrain->getVoxelGrid()->connectListener(this); cluster = new MegaCluster(mVisualEngine, part); } mMegaClusters.insert(cluster); queueFullInvalidateMegaCluster(cluster); } RBX::Humanoid* SceneUpdater::getHumanoid(RBX::PartInstance* part) { if (DFFlag::HumanoidCookieRecursive) { if (part->getCookie() & PartCookie::IS_HUMANOID_PART) return part->findAncestorModelWithHumanoid(); else return NULL; } else { RBX::Instance* parent = part->getParent(); // Regular humanoid part if (part->getCookie() & PartCookie::IS_HUMANOID_PART) return RBX::Humanoid::modelIsCharacter(parent); // For the purposes of flex clustering, we treat accoutrements as humanoid parts since we can composit their textures if (RBX::Instance::isA(parent) || RBX::Instance::isA(parent)) return RBX::Humanoid::modelIsCharacter(parent->getParent()); return NULL; } } RBX::WindowAverage::Stats SceneUpdater::getLightingTimeStats() { return mLightingComputeAverage.getStats(); } unsigned SceneUpdater::getLightOldestAge() { if (LightGrid* lgrid = mVisualEngine->getLightGrid()) { LightGridChunk* chunk = lgrid->findOldestChunk(); if(chunk) return chunk->age; } return 0; } void SceneUpdater::addFastPart(const shared_ptr& part, bool isFW, bool priorityPart) { if (RBX::Humanoid* humanoid = getHumanoid(part.get())) { FastCluster*& cluster = mHumanoidClusters[humanoid]; if (!cluster) cluster = new FastCluster(mVisualEngine, humanoid, NULL, false); cluster->addPart(part); cluster->invalidateEntity(); } else { SpatialGridIndex index = mFastGridSC->getIndexUnsafe(part.get(), isFW ? SpatialGridIndex::fFW : 0); FastGridSC::Cell* cell = mFastGridSC->requestCell(index); SuperCluster*& cluster = cell->cluster; if (!cluster) cluster = new SuperCluster(mVisualEngine, mFastGridSC.get(), index, isFW); FastCluster* fc = cluster->addPart(part); if(priorityPart) fc->priorityInvalidateEntity(); else fc->invalidateEntity(); } } void SceneUpdater::invalidateAllFastClusters() { std::vector clusters = mFastGridSC->getClusters(); for (unsigned i = 0; i < clusters.size(); ++i) clusters[i]->invalidateAllFastClusters(); } void SceneUpdater::destroyAttachment(GfxPart* object) { size_t count = mAttachments.erase(object); RBXASSERT(count == 1); delete object; } void SceneUpdater::destroyFastCluster(FastCluster* cluster) { if (void* humanoid = cluster->getHumanoidKey()) { size_t count = mHumanoidClusters.erase(humanoid); RBXASSERT(count == 1); delete cluster; } else { RBXASSERT( !"Owned FastClusters must be deleted via their owners (see SuperCluster.h)" ); } } void SceneUpdater::destroySuperCluster( SuperCluster* cluster ) { size_t count = mFastGridSC->removeCell(cluster->getSpatialIndex()); RBXASSERT( count == 1 ); delete cluster; } void SceneUpdater::addAttachment(const shared_ptr& instance) { PartInstance* part = 0; if(RBX::Instance::fastDynamicCast(instance.get())) // Special case for forcefield - only put on character's Torso if (DFFlag::UseR15Character) { Humanoid *humanoid = RBX::Instance::fastDynamicCast(instance->getParent()->findFirstChildByName2("Humanoid", false).get());; if (humanoid) part = humanoid->getVisibleTorsoSlow(); else part = RBX::Instance::fastDynamicCast(instance->getParent()->findFirstChildByName2("Torso", false).get()); } else { part = RBX::Instance::fastDynamicCast(instance->getParent()->findFirstChildByName2("Torso", false).get()); } else part = RBX::Instance::fastDynamicCast(instance->getParent()); if (dynamic_cast(instance.get())) { if (shared_ptr light = Instance::fastSharedDynamicCast(instance)) { LightObject* lightObject = new LightObject(mVisualEngine); lightObject->bind(shared_from(part), light); mAttachments.insert(lightObject); } else if (FFlag::RenderNewExplosionEnable && instance->isA()) { ExplosionEmitter* emitter = new ExplosionEmitter(mVisualEngine); emitter->bind(shared_from(part), instance); mAttachments.insert(emitter); } else if (FFlag::CustomEmitterRenderEnabled && instance->isA()) { CustomEmitter* emitter = new CustomEmitter(mVisualEngine); emitter->bind(shared_from(part), instance); mAttachments.insert(emitter); } else { ParticleEmitter* emitter = new ParticleEmitter(mVisualEngine); emitter->bind(shared_from(part), instance); mAttachments.insert(emitter); } } } unsigned SceneUpdater::getChunkBudget() { unsigned chunkBudget = mVisualEngine->getFrameRateManager()->getLightingChunkBudget(); if (mSettings->getEagerBulkExecution()) { RBX::Vector3int32 chunkCount = mVisualEngine->getLightGrid()->getChunkCount(); chunkBudget = chunkCount.x * chunkCount.y * chunkCount.z * kLightGridChunkSizeY; } return chunkBudget; } } }