//// LODManager.cpp// interface/src/LODManager.h//// Created by Clement on 1/16/15.// Copyright 2015 High Fidelity, Inc.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "LODManager.h" #include <SettingHandle.h>#include <OctreeUtils.h>#include <Util.h> #include "Application.h"#include "ui/DialogsManager.h"#include "InterfaceLogging.h" const float LODManager::DEFAULT_DESKTOP_LOD_DOWN_FPS = LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_DESKTOP_FPS;const float LODManager::DEFAULT_HMD_LOD_DOWN_FPS = LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_HMD_FPS; Setting::Handle<float> desktopLODDecreaseFPS("desktopLODDecreaseFPS", LODManager::DEFAULT_DESKTOP_LOD_DOWN_FPS);Setting::Handle<float> hmdLODDecreaseFPS("hmdLODDecreaseFPS", LODManager::DEFAULT_HMD_LOD_DOWN_FPS); LODManager::LODManager() {} // We use a "time-weighted running average" of the maxRenderTime and compare it against min/max thresholds// to determine if we should adjust the level of detail (LOD).//// A time-weighted running average has a timescale which determines how fast the average tracks the measured// value in real-time. Given a step-function in the mesured value, and assuming measurements happen// faster than the runningAverage is computed, the error between the value and its runningAverage will be// reduced by 1/e every timescale of real-time that passes.const float LOD_ADJUST_RUNNING_AVG_TIMESCALE = 0.08f; // sec // batchTIme is always contained in presentTime.// We favor using batchTime instead of presentTime as a representative value for rendering duration (on present thread) // if batchTime + cushionTime < presentTime.// since we are shooting for fps around 60, 90Hz, the ideal frames are around 10ms// so we are picking a cushion time of 3ms const float LOD_BATCH_TO_PRESENT_CUSHION_TIME = 3.0f; // msec void LODManager::setRenderTimes(float presentTime, float engineRunTime, float batchTime, float gpuTime) { // Make sure the sampled time are positive values _presentTime = std::max(0.0f, presentTime); _engineRunTime = std::max(0.0f, engineRunTime); _batchTime = std::max(0.0f, batchTime); _gpuTime = std::max(0.0f, gpuTime);} void LODManager::autoAdjustLOD(float realTimeDelta) { // The "render time" is the worse of: // - engineRunTime: Time spent in the render thread in the engine producing the gpu::Frame N // - batchTime: Time spent in the present thread processing the batches of gpu::Frame N+1 // - presentTime: Time spent in the present thread between the last 2 swap buffers considered the total time to submit gpu::Frame N+1 // - gpuTime: Time spent in the GPU executing the gpu::Frame N + 2 // But Present time is in reality synched with the monitor/display refresh rate, it s always longer than batchTime. // So if batchTime is fast enough relative to presentTime we are using it, otherwise we are using presentTime. got it ? auto presentTime = (_presentTime > _batchTime + LOD_BATCH_TO_PRESENT_CUSHION_TIME ? _batchTime + LOD_BATCH_TO_PRESENT_CUSHION_TIME : _presentTime); float maxRenderTime = glm::max(glm::max(presentTime, _engineRunTime), _gpuTime); // maxRenderTime must be a realistic valid duration in order for the regulation to work correctly. // We make sure it s a non zero positive value (1.0ms) under 1 sec maxRenderTime = std::max(1.0f, std::min(maxRenderTime, (float)MSECS_PER_SECOND)); // realTimeDelta must be a realistic valid duration in order for the regulation to work correctly. // We make sure it a positive value under 1 sec // note that if real time delta is very small we will early exit to avoid division by zero realTimeDelta = std::max(0.0f, std::min(realTimeDelta, 1.0f)); // compute time-weighted running average render time (now and smooth) // We MUST clamp the blend between 0.0 and 1.0 for stability float nowBlend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE) ? realTimeDelta / LOD_ADJUST_RUNNING_AVG_TIMESCALE : 1.0f; float smoothBlend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE * _smoothScale) ? realTimeDelta / (LOD_ADJUST_RUNNING_AVG_TIMESCALE * _smoothScale) : 1.0f; //Evaluate the running averages for the render time // We must sanity check for the output average evaluated to be in a valid range to avoid issues _nowRenderTime = (1.0f - nowBlend) * _nowRenderTime + nowBlend * maxRenderTime; // msec _nowRenderTime = std::max(0.0f, std::min(_nowRenderTime, (float)MSECS_PER_SECOND)); _smoothRenderTime = (1.0f - smoothBlend) * _smoothRenderTime + smoothBlend * maxRenderTime; // msec _smoothRenderTime = std::max(0.0f, std::min(_smoothRenderTime, (float)MSECS_PER_SECOND)); // Early exit if not regulating or if the simulation or render times don't matter if (!_automaticLODAdjust || realTimeDelta <= 0.0f || _nowRenderTime <= 0.0f || _smoothRenderTime <= 0.0f) { return; } // Previous values for output float oldOctreeSizeScale = getOctreeSizeScale(); float oldLODAngle = getLODAngleDeg(); // Target fps is slightly overshooted by 5hz float targetFPS = getLODTargetFPS() + LOD_OFFSET_FPS; // Current fps based on latest measurments float currentNowFPS = (float)MSECS_PER_SECOND / _nowRenderTime; float currentSmoothFPS = (float)MSECS_PER_SECOND / _smoothRenderTime; // Compute the Variance of the FPS signal (FPS - smouthFPS)^2 // Also scale it by a percentage for fine tuning (default is 100%) float currentVarianceFPS = (currentSmoothFPS - currentNowFPS); currentVarianceFPS *= currentVarianceFPS; currentVarianceFPS *= _pidCoefs.w; // evaluate current error between the current smoothFPS and target FPS // and the sqaure of the error to compare against the Variance auto currentErrorFPS = (targetFPS - currentSmoothFPS); auto currentErrorFPSSquare = currentErrorFPS * currentErrorFPS; // Define a noiseCoef that is trying to adjust the error to the FPS target value based on its strength // relative to the current Variance of the FPS signal. // If the error is within the variance, just set to 0. // if its within 2x the variance scale the control // and full control if error is bigger than 2x variance auto noiseCoef = 1.0f; if (currentErrorFPSSquare < currentVarianceFPS) { noiseCoef = 0.0f; } else if (currentErrorFPSSquare < 2.0f * currentVarianceFPS) { noiseCoef = (currentErrorFPSSquare - currentVarianceFPS) / currentVarianceFPS; } // The final normalized error is the the error to the FPS target, weighted by the noiseCoef, then normailzed by the target FPS. // it s also clamped in the [-1, 1] range auto error = noiseCoef * currentErrorFPS / targetFPS; error = glm::clamp(error, -1.0f, 1.0f); // Now we are getting into the P.I.D. controler code // retreive the history of pid error and integral auto previous_error = _pidHistory.x; auto previous_integral = _pidHistory.y; // The dt used for temporal values of the controller is the current realTimedelta // clamped to a reasonable granularity to make sure we are not over reacting auto dt = std::min(realTimeDelta, LOD_ADJUST_RUNNING_AVG_TIMESCALE); // Compute the current integral and clamp to avoid accumulation auto integral = previous_integral + error * dt; glm::clamp(integral, -1.0f, 1.0f); // Compute derivative // dt is never zero because realTimeDelta would have early exit above, but if it ever was let's zero the derivative term auto derivative = (dt <= 0.0f ? 0.0f : (error - previous_error) / dt); // remember history _pidHistory.x = error; _pidHistory.y = integral; _pidHistory.z = derivative; // Compute the output of the PID and record intermediate results for tuning _pidOutputs.x = _pidCoefs.x * error; // Kp * error _pidOutputs.y = _pidCoefs.y * integral; // Ki * integral _pidOutputs.z = _pidCoefs.z * derivative; // Kd * derivative auto output = _pidOutputs.x + _pidOutputs.y + _pidOutputs.z; _pidOutputs.w = output; // And now add the output of the controller to the LODAngle where we will guarantee it is in the proper range setLODAngleDeg(oldLODAngle + output); if (oldOctreeSizeScale != _octreeSizeScale) { auto lodToolsDialog = DependencyManager::get<DialogsManager>()->getLodToolsDialog(); if (lodToolsDialog) { lodToolsDialog->reloadSliders(); } }} float LODManager::getLODAngleHalfTan() const { return getPerspectiveAccuracyAngleTan(_octreeSizeScale, _boundaryLevelAdjust);}float LODManager::getLODAngle() const { return 2.0f * atanf(getLODAngleHalfTan());}float LODManager::getLODAngleDeg() const { return glm::degrees(getLODAngle());} void LODManager::setLODAngleDeg(float lodAngle) { auto newSolidAngle = std::max(0.5f, std::min(lodAngle, 90.f)); auto halTan = glm::tan(glm::radians(newSolidAngle * 0.5f)); auto octreeSizeScale = TREE_SCALE * OCTREE_TO_MESH_RATIO / halTan; setOctreeSizeScale(octreeSizeScale);} void LODManager::setSmoothScale(float t) { _smoothScale = glm::max(1.0f, t);} float LODManager::getPidKp() const { return _pidCoefs.x;}float LODManager::getPidKi() const { return _pidCoefs.y;}float LODManager::getPidKd() const { return _pidCoefs.z;}float LODManager::getPidKv() const { return _pidCoefs.w;}void LODManager::setPidKp(float k) { _pidCoefs.x = k;}void LODManager::setPidKi(float k) { _pidCoefs.y = k;}void LODManager::setPidKd(float k) { _pidCoefs.z = k;}void LODManager::setPidKv(float t) { _pidCoefs.w = t;} float LODManager::getPidOp() const { return _pidOutputs.x;}float LODManager::getPidOi() const { return _pidOutputs.y;}float LODManager::getPidOd() const { return _pidOutputs.z;}float LODManager::getPidO() const { return _pidOutputs.w;} void LODManager::resetLODAdjust() {} void LODManager::setAutomaticLODAdjust(bool value) { _automaticLODAdjust = value; emit autoLODChanged();} bool LODManager::shouldRender(const RenderArgs* args, const AABox& bounds) { // To decide if the bound should be rendered or not at the specified Args->lodAngle, // we need to compute the apparent angle of the bound from the frustum origin, // and compare it against the lodAngle, if it is greater or equal we should render the content of that bound. // we abstract the bound as a sphere centered on the bound center and of radius half diagonal of the bound. // Instead of comparing angles, we are comparing the tangent of the half angle which are more efficient to compute: // we are comparing the square of the half tangent apparent angle for the bound against the LODAngle Half tangent square // if smaller, the bound is too small and we should NOT render it, return true otherwise. // Tangent Adjacent side is eye to bound center vector length auto pos = args->getViewFrustum().getPosition() - bounds.calcCenter(); auto halfTanAdjacentSq = glm::dot(pos, pos); // Tangent Opposite side is the half length of the dimensions vector of the bound auto dim = bounds.getDimensions(); auto halfTanOppositeSq = 0.25f * glm::dot(dim, dim); // The test is: // isVisible = halfTanSq >= lodHalfTanSq = (halfTanOppositeSq / halfTanAdjacentSq) >= lodHalfTanSq // which we express as below to avoid division // (halfTanOppositeSq) >= lodHalfTanSq * halfTanAdjacentSq return (halfTanOppositeSq >= args->_lodAngleHalfTanSq * halfTanAdjacentSq);}; void LODManager::setOctreeSizeScale(float sizeScale) { _octreeSizeScale = sizeScale;} void LODManager::setBoundaryLevelAdjust(int boundaryLevelAdjust) { _boundaryLevelAdjust = boundaryLevelAdjust;} QString LODManager::getLODFeedbackText() { // determine granularity feedback int boundaryLevelAdjust = getBoundaryLevelAdjust(); QString granularityFeedback; switch (boundaryLevelAdjust) { case 0: { granularityFeedback = QString("."); } break; case 1: { granularityFeedback = QString(" at half of standard granularity."); } break; case 2: { granularityFeedback = QString(" at a third of standard granularity."); } break; default: { granularityFeedback = QString(" at 1/%1th of standard granularity.").arg(boundaryLevelAdjust + 1); } break; } // distance feedback float octreeSizeScale = getOctreeSizeScale(); float relativeToDefault = octreeSizeScale / DEFAULT_OCTREE_SIZE_SCALE; int relativeToTwentyTwenty = 20 / relativeToDefault; QString result; if (relativeToDefault > 1.01f) { result = QString("20:%1 or %2 times further than average vision%3").arg(relativeToTwentyTwenty).arg(relativeToDefault, 0, 'f', 2).arg(granularityFeedback); } else if (relativeToDefault > 0.99f) { result = QString("20:20 or the default distance for average vision%1").arg(granularityFeedback); } else if (relativeToDefault > 0.01f) { result = QString("20:%1 or %2 of default distance for average vision%3").arg(relativeToTwentyTwenty).arg(relativeToDefault, 0, 'f', 3).arg(granularityFeedback); } else { result = QString("%2 of default distance for average vision%3").arg(relativeToDefault, 0, 'f', 3).arg(granularityFeedback); } return result;} void LODManager::loadSettings() { setDesktopLODTargetFPS(desktopLODDecreaseFPS.get()); setHMDLODTargetFPS(hmdLODDecreaseFPS.get());} void LODManager::saveSettings() { desktopLODDecreaseFPS.set(getDesktopLODTargetFPS()); hmdLODDecreaseFPS.set(getHMDLODTargetFPS());} const float MIN_DECREASE_FPS = 0.5f; void LODManager::setDesktopLODTargetFPS(float fps) { if (fps < MIN_DECREASE_FPS) { // avoid divide by zero fps = MIN_DECREASE_FPS; } _desktopTargetFPS = fps;} float LODManager::getDesktopLODTargetFPS() const { return _desktopTargetFPS;} void LODManager::setHMDLODTargetFPS(float fps) { if (fps < MIN_DECREASE_FPS) { // avoid divide by zero fps = MIN_DECREASE_FPS; } _hmdTargetFPS = fps;} float LODManager::getHMDLODTargetFPS() const { return _hmdTargetFPS;} float LODManager::getLODTargetFPS() const { if (qApp->isHMDMode()) { return getHMDLODTargetFPS(); } return getDesktopLODTargetFPS();} void LODManager::setWorldDetailQuality(float quality) { static const float MIN_FPS = 10; static const float LOW = 0.25f; bool isLowestValue = quality == LOW; bool isHMDMode = qApp->isHMDMode(); float maxFPS = isHMDMode ? LOD_MAX_LIKELY_HMD_FPS : LOD_MAX_LIKELY_DESKTOP_FPS; float desiredFPS = maxFPS; if (!isLowestValue) { float calculatedFPS = (maxFPS - (maxFPS * quality)); desiredFPS = calculatedFPS < MIN_FPS ? MIN_FPS : calculatedFPS; } if (isHMDMode) { setHMDLODTargetFPS(desiredFPS); } else { setDesktopLODTargetFPS(desiredFPS); } emit worldDetailQualityChanged();} float LODManager::getWorldDetailQuality() const { static const float LOW = 0.25f; static const float MEDIUM = 0.5f; static const float HIGH = 0.75f; bool inHMD = qApp->isHMDMode(); float targetFPS = 0.0f; if (inHMD) { targetFPS = getHMDLODTargetFPS(); } else { targetFPS = getDesktopLODTargetFPS(); } float maxFPS = inHMD ? LOD_MAX_LIKELY_HMD_FPS : LOD_MAX_LIKELY_DESKTOP_FPS; float percentage = 1.0f - targetFPS / maxFPS; if (percentage <= LOW) { return LOW; } else if (percentage <= MEDIUM) { return MEDIUM; } return HIGH;} void LODManager::setLODQualityLevel(float quality) { _lodQualityLevel = quality;} float LODManager::getLODQualityLevel() const { return _lodQualityLevel;}
//
// LODManager.cpp
// interface/src/LODManager.h
//
// Created by Clement on 1/16/15.
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "LODManager.h"
#include <SettingHandle.h>
#include <OctreeUtils.h>
#include <Util.h>
#include "Application.h"
#include "ui/DialogsManager.h"
#include "InterfaceLogging.h"
const float LODManager::DEFAULT_DESKTOP_LOD_DOWN_FPS = LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_DESKTOP_FPS;
const float LODManager::DEFAULT_HMD_LOD_DOWN_FPS = LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_HMD_FPS;
Setting::Handle<float> desktopLODDecreaseFPS("desktopLODDecreaseFPS", LODManager::DEFAULT_DESKTOP_LOD_DOWN_FPS);
Setting::Handle<float> hmdLODDecreaseFPS("hmdLODDecreaseFPS", LODManager::DEFAULT_HMD_LOD_DOWN_FPS);
LODManager::LODManager() {
}
// We use a "time-weighted running average" of the maxRenderTime and compare it against min/max thresholds
// to determine if we should adjust the level of detail (LOD).
//
// A time-weighted running average has a timescale which determines how fast the average tracks the measured
// value in real-time. Given a step-function in the mesured value, and assuming measurements happen
// faster than the runningAverage is computed, the error between the value and its runningAverage will be
// reduced by 1/e every timescale of real-time that passes.
const float LOD_ADJUST_RUNNING_AVG_TIMESCALE = 0.08f; // sec
// batchTIme is always contained in presentTime.
// We favor using batchTime instead of presentTime as a representative value for rendering duration (on present thread)
// if batchTime + cushionTime < presentTime.
// since we are shooting for fps around 60, 90Hz, the ideal frames are around 10ms
// so we are picking a cushion time of 3ms
const float LOD_BATCH_TO_PRESENT_CUSHION_TIME = 3.0f; // msec
void LODManager::setRenderTimes(float presentTime, float engineRunTime, float batchTime, float gpuTime) {
// Make sure the sampled time are positive values
_presentTime = std::max(0.0f, presentTime);
_engineRunTime = std::max(0.0f, engineRunTime);
_batchTime = std::max(0.0f, batchTime);
_gpuTime = std::max(0.0f, gpuTime);
}
void LODManager::autoAdjustLOD(float realTimeDelta) {
// The "render time" is the worse of:
// - engineRunTime: Time spent in the render thread in the engine producing the gpu::Frame N
// - batchTime: Time spent in the present thread processing the batches of gpu::Frame N+1
// - presentTime: Time spent in the present thread between the last 2 swap buffers considered the total time to submit gpu::Frame N+1
// - gpuTime: Time spent in the GPU executing the gpu::Frame N + 2
// But Present time is in reality synched with the monitor/display refresh rate, it s always longer than batchTime.
// So if batchTime is fast enough relative to presentTime we are using it, otherwise we are using presentTime. got it ?
auto presentTime = (_presentTime > _batchTime + LOD_BATCH_TO_PRESENT_CUSHION_TIME ? _batchTime + LOD_BATCH_TO_PRESENT_CUSHION_TIME : _presentTime);
float maxRenderTime = glm::max(glm::max(presentTime, _engineRunTime), _gpuTime);
// maxRenderTime must be a realistic valid duration in order for the regulation to work correctly.
// We make sure it s a non zero positive value (1.0ms) under 1 sec
maxRenderTime = std::max(1.0f, std::min(maxRenderTime, (float)MSECS_PER_SECOND));
// realTimeDelta must be a realistic valid duration in order for the regulation to work correctly.
// We make sure it a positive value under 1 sec
// note that if real time delta is very small we will early exit to avoid division by zero
realTimeDelta = std::max(0.0f, std::min(realTimeDelta, 1.0f));
// compute time-weighted running average render time (now and smooth)
// We MUST clamp the blend between 0.0 and 1.0 for stability
float nowBlend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE) ? realTimeDelta / LOD_ADJUST_RUNNING_AVG_TIMESCALE : 1.0f;
float smoothBlend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE * _smoothScale) ? realTimeDelta / (LOD_ADJUST_RUNNING_AVG_TIMESCALE * _smoothScale) : 1.0f;
//Evaluate the running averages for the render time
// We must sanity check for the output average evaluated to be in a valid range to avoid issues
_nowRenderTime = (1.0f - nowBlend) * _nowRenderTime + nowBlend * maxRenderTime; // msec
_nowRenderTime = std::max(0.0f, std::min(_nowRenderTime, (float)MSECS_PER_SECOND));
_smoothRenderTime = (1.0f - smoothBlend) * _smoothRenderTime + smoothBlend * maxRenderTime; // msec
_smoothRenderTime = std::max(0.0f, std::min(_smoothRenderTime, (float)MSECS_PER_SECOND));
// Early exit if not regulating or if the simulation or render times don't matter
if (!_automaticLODAdjust || realTimeDelta <= 0.0f || _nowRenderTime <= 0.0f || _smoothRenderTime <= 0.0f) {
return;
}
// Previous values for output
float oldOctreeSizeScale = getOctreeSizeScale();
float oldLODAngle = getLODAngleDeg();
// Target fps is slightly overshooted by 5hz
float targetFPS = getLODTargetFPS() + LOD_OFFSET_FPS;
// Current fps based on latest measurments
float currentNowFPS = (float)MSECS_PER_SECOND / _nowRenderTime;
float currentSmoothFPS = (float)MSECS_PER_SECOND / _smoothRenderTime;
// Compute the Variance of the FPS signal (FPS - smouthFPS)^2
// Also scale it by a percentage for fine tuning (default is 100%)
float currentVarianceFPS = (currentSmoothFPS - currentNowFPS);
currentVarianceFPS *= currentVarianceFPS;
currentVarianceFPS *= _pidCoefs.w;
// evaluate current error between the current smoothFPS and target FPS
// and the sqaure of the error to compare against the Variance
auto currentErrorFPS = (targetFPS - currentSmoothFPS);
auto currentErrorFPSSquare = currentErrorFPS * currentErrorFPS;
// Define a noiseCoef that is trying to adjust the error to the FPS target value based on its strength
// relative to the current Variance of the FPS signal.
// If the error is within the variance, just set to 0.
// if its within 2x the variance scale the control
// and full control if error is bigger than 2x variance
auto noiseCoef = 1.0f;
if (currentErrorFPSSquare < currentVarianceFPS) {
noiseCoef = 0.0f;
} else if (currentErrorFPSSquare < 2.0f * currentVarianceFPS) {
noiseCoef = (currentErrorFPSSquare - currentVarianceFPS) / currentVarianceFPS;
}
// The final normalized error is the the error to the FPS target, weighted by the noiseCoef, then normailzed by the target FPS.
// it s also clamped in the [-1, 1] range
auto error = noiseCoef * currentErrorFPS / targetFPS;
error = glm::clamp(error, -1.0f, 1.0f);
// Now we are getting into the P.I.D. controler code
// retreive the history of pid error and integral
auto previous_error = _pidHistory.x;
auto previous_integral = _pidHistory.y;
// The dt used for temporal values of the controller is the current realTimedelta
// clamped to a reasonable granularity to make sure we are not over reacting
auto dt = std::min(realTimeDelta, LOD_ADJUST_RUNNING_AVG_TIMESCALE);
// Compute the current integral and clamp to avoid accumulation
auto integral = previous_integral + error * dt;
glm::clamp(integral, -1.0f, 1.0f);
// Compute derivative
// dt is never zero because realTimeDelta would have early exit above, but if it ever was let's zero the derivative term
auto derivative = (dt <= 0.0f ? 0.0f : (error - previous_error) / dt);
// remember history
_pidHistory.x = error;
_pidHistory.y = integral;
_pidHistory.z = derivative;
// Compute the output of the PID and record intermediate results for tuning
_pidOutputs.x = _pidCoefs.x * error; // Kp * error
_pidOutputs.y = _pidCoefs.y * integral; // Ki * integral
_pidOutputs.z = _pidCoefs.z * derivative; // Kd * derivative
auto output = _pidOutputs.x + _pidOutputs.y + _pidOutputs.z;
_pidOutputs.w = output;
// And now add the output of the controller to the LODAngle where we will guarantee it is in the proper range
setLODAngleDeg(oldLODAngle + output);
if (oldOctreeSizeScale != _octreeSizeScale) {
auto lodToolsDialog = DependencyManager::get<DialogsManager>()->getLodToolsDialog();
if (lodToolsDialog) {
lodToolsDialog->reloadSliders();
}
}
}
float LODManager::getLODAngleHalfTan() const {
return getPerspectiveAccuracyAngleTan(_octreeSizeScale, _boundaryLevelAdjust);
}
float LODManager::getLODAngle() const {
return 2.0f * atanf(getLODAngleHalfTan());
}
float LODManager::getLODAngleDeg() const {
return glm::degrees(getLODAngle());
}
void LODManager::setLODAngleDeg(float lodAngle) {
auto newSolidAngle = std::max(0.5f, std::min(lodAngle, 90.f));
auto halTan = glm::tan(glm::radians(newSolidAngle * 0.5f));
auto octreeSizeScale = TREE_SCALE * OCTREE_TO_MESH_RATIO / halTan;
setOctreeSizeScale(octreeSizeScale);
}
void LODManager::setSmoothScale(float t) {
_smoothScale = glm::max(1.0f, t);
}
float LODManager::getPidKp() const {
return _pidCoefs.x;
}
float LODManager::getPidKi() const {
return _pidCoefs.y;
}
float LODManager::getPidKd() const {
return _pidCoefs.z;
}
float LODManager::getPidKv() const {
return _pidCoefs.w;
}
void LODManager::setPidKp(float k) {
_pidCoefs.x = k;
}
void LODManager::setPidKi(float k) {
_pidCoefs.y = k;
}
void LODManager::setPidKd(float k) {
_pidCoefs.z = k;
}
void LODManager::setPidKv(float t) {
_pidCoefs.w = t;
}
float LODManager::getPidOp() const {
return _pidOutputs.x;
}
float LODManager::getPidOi() const {
return _pidOutputs.y;
}
float LODManager::getPidOd() const {
return _pidOutputs.z;
}
float LODManager::getPidO() const {
return _pidOutputs.w;
}
void LODManager::resetLODAdjust() {
}
void LODManager::setAutomaticLODAdjust(bool value) {
_automaticLODAdjust = value;
emit autoLODChanged();
}
bool LODManager::shouldRender(const RenderArgs* args, const AABox& bounds) {
// To decide if the bound should be rendered or not at the specified Args->lodAngle,
// we need to compute the apparent angle of the bound from the frustum origin,
// and compare it against the lodAngle, if it is greater or equal we should render the content of that bound.
// we abstract the bound as a sphere centered on the bound center and of radius half diagonal of the bound.
// Instead of comparing angles, we are comparing the tangent of the half angle which are more efficient to compute:
// we are comparing the square of the half tangent apparent angle for the bound against the LODAngle Half tangent square
// if smaller, the bound is too small and we should NOT render it, return true otherwise.
// Tangent Adjacent side is eye to bound center vector length
auto pos = args->getViewFrustum().getPosition() - bounds.calcCenter();
auto halfTanAdjacentSq = glm::dot(pos, pos);
// Tangent Opposite side is the half length of the dimensions vector of the bound
auto dim = bounds.getDimensions();
auto halfTanOppositeSq = 0.25f * glm::dot(dim, dim);
// The test is:
// isVisible = halfTanSq >= lodHalfTanSq = (halfTanOppositeSq / halfTanAdjacentSq) >= lodHalfTanSq
// which we express as below to avoid division
// (halfTanOppositeSq) >= lodHalfTanSq * halfTanAdjacentSq
return (halfTanOppositeSq >= args->_lodAngleHalfTanSq * halfTanAdjacentSq);
};
void LODManager::setOctreeSizeScale(float sizeScale) {
_octreeSizeScale = sizeScale;
}
void LODManager::setBoundaryLevelAdjust(int boundaryLevelAdjust) {
_boundaryLevelAdjust = boundaryLevelAdjust;
}
QString LODManager::getLODFeedbackText() {
// determine granularity feedback
int boundaryLevelAdjust = getBoundaryLevelAdjust();
QString granularityFeedback;
switch (boundaryLevelAdjust) {
case 0: {
granularityFeedback = QString(".");
} break;
case 1: {
granularityFeedback = QString(" at half of standard granularity.");
} break;
case 2: {
granularityFeedback = QString(" at a third of standard granularity.");
} break;
default: {
granularityFeedback = QString(" at 1/%1th of standard granularity.").arg(boundaryLevelAdjust + 1);
} break;
}
// distance feedback
float octreeSizeScale = getOctreeSizeScale();
float relativeToDefault = octreeSizeScale / DEFAULT_OCTREE_SIZE_SCALE;
int relativeToTwentyTwenty = 20 / relativeToDefault;
QString result;
if (relativeToDefault > 1.01f) {
result = QString("20:%1 or %2 times further than average vision%3").arg(relativeToTwentyTwenty).arg(relativeToDefault, 0, 'f', 2).arg(granularityFeedback);
} else if (relativeToDefault > 0.99f) {
result = QString("20:20 or the default distance for average vision%1").arg(granularityFeedback);
} else if (relativeToDefault > 0.01f) {
result = QString("20:%1 or %2 of default distance for average vision%3").arg(relativeToTwentyTwenty).arg(relativeToDefault, 0, 'f', 3).arg(granularityFeedback);
} else {
result = QString("%2 of default distance for average vision%3").arg(relativeToDefault, 0, 'f', 3).arg(granularityFeedback);
}
return result;
}
void LODManager::loadSettings() {
setDesktopLODTargetFPS(desktopLODDecreaseFPS.get());
setHMDLODTargetFPS(hmdLODDecreaseFPS.get());
}
void LODManager::saveSettings() {
desktopLODDecreaseFPS.set(getDesktopLODTargetFPS());
hmdLODDecreaseFPS.set(getHMDLODTargetFPS());
}
const float MIN_DECREASE_FPS = 0.5f;
void LODManager::setDesktopLODTargetFPS(float fps) {
if (fps < MIN_DECREASE_FPS) {
// avoid divide by zero
fps = MIN_DECREASE_FPS;
}
_desktopTargetFPS = fps;
}
float LODManager::getDesktopLODTargetFPS() const {
return _desktopTargetFPS;
}
void LODManager::setHMDLODTargetFPS(float fps) {
if (fps < MIN_DECREASE_FPS) {
// avoid divide by zero
fps = MIN_DECREASE_FPS;
}
_hmdTargetFPS = fps;
}
float LODManager::getHMDLODTargetFPS() const {
return _hmdTargetFPS;
}
float LODManager::getLODTargetFPS() const {
if (qApp->isHMDMode()) {
return getHMDLODTargetFPS();
}
return getDesktopLODTargetFPS();
}
void LODManager::setWorldDetailQuality(float quality) {
static const float MIN_FPS = 10;
static const float LOW = 0.25f;
bool isLowestValue = quality == LOW;
bool isHMDMode = qApp->isHMDMode();
float maxFPS = isHMDMode ? LOD_MAX_LIKELY_HMD_FPS : LOD_MAX_LIKELY_DESKTOP_FPS;
float desiredFPS = maxFPS;
if (!isLowestValue) {
float calculatedFPS = (maxFPS - (maxFPS * quality));
desiredFPS = calculatedFPS < MIN_FPS ? MIN_FPS : calculatedFPS;
}
if (isHMDMode) {
setHMDLODTargetFPS(desiredFPS);
} else {
setDesktopLODTargetFPS(desiredFPS);
}
emit worldDetailQualityChanged();
}
float LODManager::getWorldDetailQuality() const {
static const float LOW = 0.25f;
static const float MEDIUM = 0.5f;
static const float HIGH = 0.75f;
bool inHMD = qApp->isHMDMode();
float targetFPS = 0.0f;
if (inHMD) {
targetFPS = getHMDLODTargetFPS();
} else {
targetFPS = getDesktopLODTargetFPS();
}
float maxFPS = inHMD ? LOD_MAX_LIKELY_HMD_FPS : LOD_MAX_LIKELY_DESKTOP_FPS;
float percentage = 1.0f - targetFPS / maxFPS;
if (percentage <= LOW) {
return LOW;
} else if (percentage <= MEDIUM) {
return MEDIUM;
}
return HIGH;
}
void LODManager::setLODQualityLevel(float quality) {
_lodQualityLevel = quality;
}
float LODManager::getLODQualityLevel() const {
return _lodQualityLevel;
}