mirror of
https://github.com/overte-org/overte.git
synced 2025-04-18 00:26:33 +02:00
Merge pull request #6453 from howard-stearns/avatar-render-controller2
Survive Flash Mobs Without Making Everyone Sick
This commit is contained in:
commit
bc20b72645
12 changed files with 303 additions and 5 deletions
|
@ -14,16 +14,17 @@
|
|||
// An assignment client script that animates one avatar at random location within 'spread' meters of 'origin'.
|
||||
// In Domain Server Settings, go to scripts and give the url of this script. Press '+', and then 'Save and restart'.
|
||||
|
||||
var origin = {x: 500, y: 502, z: 500};
|
||||
var spread = 10; // meters
|
||||
var origin = {x: 500, y: 500, z: 500};
|
||||
var spread = 20; // meters
|
||||
var animationData = {url: "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", lastFrame: 35};
|
||||
Avatar.skeletonModelURL = "https://hifi-public.s3.amazonaws.com/marketplace/contents/dd03b8e3-52fb-4ab3-9ac9-3b17e00cd85d/98baa90b3b66803c5d7bd4537fca6993.fst"; //lovejoy
|
||||
Avatar.displayName = "'Bot";
|
||||
var millisecondsToWaitBeforeStarting = 10 * 1000; // To give the various servers a chance to start.
|
||||
|
||||
Agent.isAvatar = true;
|
||||
function coord() { return (Math.random() * spread) - (spread / 2); } // randomly distribute a coordinate zero += spread/2.
|
||||
Script.setTimeout(function () {
|
||||
Avatar.position = Vec3.sum(origin, {x: Math.random() * spread, y: 0, z: Math.random() * spread});
|
||||
Avatar.position = Vec3.sum(origin, {x: coord(), y: 0, z: coord()});
|
||||
print("Starting at", JSON.stringify(Avatar.position));
|
||||
Avatar.startAnimation(animationData.url, animationData.fps || 30, 1, true, false, animationData.firstFrame || 0, animationData.lastFrame);
|
||||
}, millisecondsToWaitBeforeStarting);
|
||||
|
|
|
@ -256,6 +256,12 @@ Item {
|
|||
visible: root.expanded
|
||||
text: "LOD: " + root.lodStatus;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
visible: root.expanded
|
||||
text: "Renderable avatars: " + root.avatarRenderableCount + " w/in " + root.avatarRenderDistance + "m";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1077,8 +1077,10 @@ void Application::paintGL() {
|
|||
uint64_t now = usecTimestampNow();
|
||||
static uint64_t lastPaintBegin{ now };
|
||||
uint64_t diff = now - lastPaintBegin;
|
||||
float instantaneousFps = 0.0f;
|
||||
if (diff != 0) {
|
||||
_framesPerSecond.updateAverage((float)USECS_PER_SECOND / (float)diff);
|
||||
instantaneousFps = (float)USECS_PER_SECOND / (float)diff;
|
||||
_framesPerSecond.updateAverage(_lastInstantaneousFps);
|
||||
}
|
||||
|
||||
lastPaintBegin = now;
|
||||
|
@ -1109,6 +1111,29 @@ void Application::paintGL() {
|
|||
_inPaint = true;
|
||||
Finally clearFlagLambda([this] { _inPaint = false; });
|
||||
|
||||
// Some LOD-like controls need to know a smoothly varying "potential" frame rate that doesn't
|
||||
// include time waiting for vsync, and which can report a number above target if we've got the headroom.
|
||||
// For example, if we're shooting for 75fps and paintWait is 3.3333ms (= 75% * 13.33ms), our deducedNonVSyncFps
|
||||
// would be 100fps. In principle, a paintWait of zero would have deducedNonVSyncFps=75.
|
||||
// Here we make a guess for deducedNonVSyncFps = 1 / deducedNonVSyncPeriod.
|
||||
//
|
||||
// Time between previous paintGL call and this one, which can vary not only with vSync misses, but also with QT timing.
|
||||
// We're using this as a proxy for the time between vsync and displayEnd, below. (Not exact, but tends to be the same over time.)
|
||||
// This is not the same as update(deltaTime), because the latter attempts to throttle to 60hz and also clamps to 1/4 second.
|
||||
const float actualPeriod = diff / (float)USECS_PER_SECOND; // same as 1/instantaneousFps but easier for compiler to optimize
|
||||
// Note that _lastPaintWait (stored at end of last call) is for the same paint cycle.
|
||||
float deducedNonVSyncPeriod = actualPeriod - _lastPaintWait + _marginForDeducedFramePeriod; // plus a some non-zero time for machinery we can't measure
|
||||
// We don't know how much time to allow for that, but if we went over the target period, we know it's at least the portion
|
||||
// of paintWait up to the next vSync. This gives us enough of a penalty so that when actualPeriod crosses two cycles,
|
||||
// the key part (and not an exagerated part) of _lastPaintWait is accounted for.
|
||||
const float targetPeriod = getTargetFramePeriod();
|
||||
if (_lastPaintWait > EPSILON && actualPeriod > targetPeriod) {
|
||||
// Don't use C++ remainder(). It's authors are mathematically insane.
|
||||
deducedNonVSyncPeriod += fmod(actualPeriod, _lastPaintWait);
|
||||
}
|
||||
_lastDeducedNonVSyncFps = 1.0f / deducedNonVSyncPeriod;
|
||||
_lastInstantaneousFps = instantaneousFps;
|
||||
|
||||
auto displayPlugin = getActiveDisplayPlugin();
|
||||
displayPlugin->preRender();
|
||||
_offscreenContext->makeCurrent();
|
||||
|
@ -1355,6 +1380,7 @@ void Application::paintGL() {
|
|||
// Ensure all operations from the previous context are complete before we try to read the fbo
|
||||
glWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync(sync);
|
||||
uint64_t displayStart = usecTimestampNow();
|
||||
|
||||
{
|
||||
PROFILE_RANGE(__FUNCTION__ "/pluginDisplay");
|
||||
|
@ -1367,6 +1393,10 @@ void Application::paintGL() {
|
|||
PerformanceTimer perfTimer("bufferSwap");
|
||||
displayPlugin->finishFrame();
|
||||
}
|
||||
uint64_t displayEnd = usecTimestampNow();
|
||||
const float displayPeriodUsec = (float)(displayEnd - displayStart); // usecs
|
||||
_lastPaintWait = displayPeriodUsec / (float)USECS_PER_SECOND;
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
|
|
|
@ -159,6 +159,14 @@ public:
|
|||
bool isForeground() const { return _isForeground; }
|
||||
|
||||
float getFps() const { return _fps; }
|
||||
float const HMD_TARGET_FRAME_RATE = 75.0f;
|
||||
float const DESKTOP_TARGET_FRAME_RATE = 60.0f;
|
||||
float getTargetFrameRate() { return isHMDMode() ? HMD_TARGET_FRAME_RATE : DESKTOP_TARGET_FRAME_RATE; }
|
||||
float getTargetFramePeriod() { return isHMDMode() ? 1.0f / HMD_TARGET_FRAME_RATE : 1.0f / DESKTOP_TARGET_FRAME_RATE; } // same as 1/getTargetFrameRate, but w/compile-time division
|
||||
float getLastInstanteousFps() const { return _lastInstantaneousFps; }
|
||||
float getLastPaintWait() const { return _lastPaintWait; };
|
||||
float getLastDeducedNonVSyncFps() const { return _lastDeducedNonVSyncFps; }
|
||||
void setMarginForDeducedFramePeriod(float newValue) { _marginForDeducedFramePeriod = newValue; }
|
||||
|
||||
float getFieldOfView() { return _fieldOfView.get(); }
|
||||
void setFieldOfView(float fov);
|
||||
|
@ -429,6 +437,10 @@ private:
|
|||
float _fps;
|
||||
QElapsedTimer _timerStart;
|
||||
QElapsedTimer _lastTimeUpdated;
|
||||
float _lastInstantaneousFps { 0.0f };
|
||||
float _lastPaintWait { 0.0f };
|
||||
float _lastDeducedNonVSyncFps { 0.0f };
|
||||
float _marginForDeducedFramePeriod{ 0.002f }; // 2ms, adjustable
|
||||
|
||||
ShapeManager _shapeManager;
|
||||
PhysicalEntitySimulation _entitySimulation;
|
||||
|
|
|
@ -96,6 +96,7 @@ Avatar::Avatar(RigPointer rig) :
|
|||
_moving(false),
|
||||
_initialized(false),
|
||||
_shouldRenderBillboard(true),
|
||||
_shouldSkipRender(true),
|
||||
_voiceSphereID(GeometryCache::UNKNOWN_ID)
|
||||
{
|
||||
// we may have been created in the network thread, but we live in the main thread
|
||||
|
@ -183,9 +184,29 @@ void Avatar::simulate(float deltaTime) {
|
|||
if (_shouldRenderBillboard) {
|
||||
if (getLODDistance() < BILLBOARD_LOD_DISTANCE * (1.0f - BILLBOARD_HYSTERESIS_PROPORTION)) {
|
||||
_shouldRenderBillboard = false;
|
||||
qCDebug(interfaceapp) << "Unbillboarding" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for LOD" << getLODDistance();
|
||||
}
|
||||
} else if (getLODDistance() > BILLBOARD_LOD_DISTANCE * (1.0f + BILLBOARD_HYSTERESIS_PROPORTION)) {
|
||||
_shouldRenderBillboard = true;
|
||||
qCDebug(interfaceapp) << "Billboarding" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for LOD" << getLODDistance();
|
||||
}
|
||||
|
||||
const bool isControllerLogging = DependencyManager::get<AvatarManager>()->getRenderDistanceControllerIsLogging();
|
||||
float renderDistance = DependencyManager::get<AvatarManager>()->getRenderDistance();
|
||||
const float SKIP_HYSTERESIS_PROPORTION = isControllerLogging ? 0.0f : BILLBOARD_HYSTERESIS_PROPORTION;
|
||||
float distance = glm::distance(qApp->getCamera()->getPosition(), _position);
|
||||
if (_shouldSkipRender) {
|
||||
if (distance < renderDistance * (1.0f - SKIP_HYSTERESIS_PROPORTION)) {
|
||||
_shouldSkipRender = false;
|
||||
if (!isControllerLogging) { // Test for isMyAvatar is prophylactic. Never occurs in current code.
|
||||
qCDebug(interfaceapp) << "Rerendering" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for distance" << renderDistance;
|
||||
}
|
||||
}
|
||||
} else if (distance > renderDistance * (1.0f + SKIP_HYSTERESIS_PROPORTION)) {
|
||||
_shouldSkipRender = true;
|
||||
if (!isControllerLogging) {
|
||||
qCDebug(interfaceapp) << "Unrendering" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for distance" << renderDistance;
|
||||
}
|
||||
}
|
||||
|
||||
// simple frustum check
|
||||
|
@ -198,7 +219,7 @@ void Avatar::simulate(float deltaTime) {
|
|||
getHand()->simulate(deltaTime, false);
|
||||
}
|
||||
|
||||
if (!_shouldRenderBillboard && inViewFrustum) {
|
||||
if (!_shouldRenderBillboard && !_shouldSkipRender && inViewFrustum) {
|
||||
{
|
||||
PerformanceTimer perfTimer("skeleton");
|
||||
for (int i = 0; i < _jointData.size(); i++) {
|
||||
|
@ -588,6 +609,10 @@ void Avatar::fixupModelsInScene() {
|
|||
// check to see if when we added our models to the scene they were ready, if they were not ready, then
|
||||
// fix them up in the scene
|
||||
render::ScenePointer scene = qApp->getMain3DScene();
|
||||
_skeletonModel.setVisibleInScene(!_shouldSkipRender, scene);
|
||||
if (_shouldSkipRender) {
|
||||
return;
|
||||
}
|
||||
render::PendingChanges pendingChanges;
|
||||
if (_skeletonModel.isRenderable() && _skeletonModel.needsFixupInScene()) {
|
||||
_skeletonModel.removeFromScene(scene, pendingChanges);
|
||||
|
|
|
@ -140,6 +140,8 @@ public:
|
|||
Q_INVOKABLE glm::vec3 getAngularVelocity() const { return _angularVelocity; }
|
||||
Q_INVOKABLE glm::vec3 getAngularAcceleration() const { return _angularAcceleration; }
|
||||
|
||||
Q_INVOKABLE bool getShouldRender() const { return !_shouldSkipRender; }
|
||||
|
||||
/// Scales a world space position vector relative to the avatar position and scale
|
||||
/// \param vector position to be scaled. Will store the result
|
||||
void scaleVectorRelativeToPosition(glm::vec3 &positionToScale) const;
|
||||
|
@ -226,6 +228,7 @@ private:
|
|||
bool _initialized;
|
||||
NetworkTexturePointer _billboardTexture;
|
||||
bool _shouldRenderBillboard;
|
||||
bool _shouldSkipRender { false };
|
||||
bool _isLookAtTarget;
|
||||
|
||||
void renderBillboard(RenderArgs* renderArgs);
|
||||
|
|
|
@ -90,6 +90,21 @@ void AvatarManager::init() {
|
|||
_myAvatar->addToScene(_myAvatar, scene, pendingChanges);
|
||||
}
|
||||
scene->enqueuePendingChanges(pendingChanges);
|
||||
|
||||
const float target_fps = qApp->getTargetFrameRate();
|
||||
_renderDistanceController.setMeasuredValueSetpoint(target_fps);
|
||||
const float SMALLEST_REASONABLE_HORIZON = 5.0f; // meters
|
||||
_renderDistanceController.setControlledValueHighLimit(1.0f / SMALLEST_REASONABLE_HORIZON);
|
||||
_renderDistanceController.setControlledValueLowLimit(1.0f / (float) TREE_SCALE);
|
||||
// Advice for tuning parameters:
|
||||
// See PIDController.h. There's a section on tuning in the reference.
|
||||
// Turn on logging with the following (or from js with AvatarList.setRenderDistanceControllerHistory("avatar render", 300))
|
||||
//_renderDistanceController.setHistorySize("avatar render", target_fps * 4);
|
||||
// Note that extra logging/hysteresis is turned off in Avatar.cpp when the above logging is on.
|
||||
_renderDistanceController.setKP(0.0008f); // Usually about 0.6 of largest that doesn't oscillate when other parameters 0.
|
||||
_renderDistanceController.setKI(0.0006f); // Big enough to bring us to target with the above KP.
|
||||
_renderDistanceController.setKD(0.000001f); // A touch of kd increases the speed by which we get there.
|
||||
|
||||
}
|
||||
|
||||
void AvatarManager::updateMyAvatar(float deltaTime) {
|
||||
|
@ -123,6 +138,17 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
PerformanceWarning warn(showWarnings, "Application::updateAvatars()");
|
||||
|
||||
PerformanceTimer perfTimer("otherAvatars");
|
||||
|
||||
_renderDistanceController.setMeasuredValueSetpoint(qApp->getTargetFrameRate()); // No problem updating in flight.
|
||||
// The PID controller raises the controlled value when the measured value goes up.
|
||||
// The measured value is frame rate. When the controlled value (1 / render cutoff distance)
|
||||
// goes up, the render cutoff distance gets closer, the number of rendered avatars is less, and frame rate
|
||||
// goes up.
|
||||
const float deduced = qApp->getLastDeducedNonVSyncFps();
|
||||
const float distance = 1.0f / _renderDistanceController.update(deduced, deltaTime);
|
||||
_renderDistanceAverage.updateAverage(distance);
|
||||
_renderDistance = _renderDistanceAverage.getAverage();
|
||||
int renderableCount = 0;
|
||||
|
||||
// simulate avatars
|
||||
auto hashCopy = getHashCopy();
|
||||
|
@ -141,10 +167,14 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
} else {
|
||||
avatar->startUpdate();
|
||||
avatar->simulate(deltaTime);
|
||||
if (avatar->getShouldRender()) {
|
||||
renderableCount++;
|
||||
}
|
||||
avatar->endUpdate();
|
||||
++avatarIterator;
|
||||
}
|
||||
}
|
||||
_renderedAvatarCount = renderableCount;
|
||||
|
||||
// simulate avatar fades
|
||||
simulateAvatarFades(deltaTime);
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
#include <AvatarHashMap.h>
|
||||
#include <PhysicsEngine.h>
|
||||
#include <PIDController.h>
|
||||
#include <SimpleMovingAverage.h>
|
||||
|
||||
#include "Avatar.h"
|
||||
#include "AvatarMotionState.h"
|
||||
|
@ -43,6 +45,7 @@ public:
|
|||
void clearOtherAvatars();
|
||||
|
||||
bool shouldShowReceiveStats() const { return _shouldShowReceiveStats; }
|
||||
PIDController& getRenderDistanceController() { return _renderDistanceController; }
|
||||
|
||||
class LocalLight {
|
||||
public:
|
||||
|
@ -64,6 +67,17 @@ public:
|
|||
void handleCollisionEvents(const CollisionEvents& collisionEvents);
|
||||
|
||||
void updateAvatarPhysicsShape(Avatar* avatar);
|
||||
|
||||
// Expose results and parameter-tuning operations to other systems, such as stats and javascript.
|
||||
Q_INVOKABLE float getRenderDistance() { return _renderDistance; }
|
||||
Q_INVOKABLE int getNumberInRenderRange() { return _renderedAvatarCount; }
|
||||
Q_INVOKABLE bool getRenderDistanceControllerIsLogging() { return _renderDistanceController.getIsLogging(); }
|
||||
Q_INVOKABLE void setRenderDistanceControllerHistory(QString label, int size) { return _renderDistanceController.setHistorySize(label, size); }
|
||||
Q_INVOKABLE void setRenderDistanceKP(float newValue) { _renderDistanceController.setKP(newValue); }
|
||||
Q_INVOKABLE void setRenderDistanceKI(float newValue) { _renderDistanceController.setKI(newValue); }
|
||||
Q_INVOKABLE void setRenderDistanceKD(float newValue) { _renderDistanceController.setKD(newValue); }
|
||||
Q_INVOKABLE void setRenderDistanceLowLimit(float newValue) { _renderDistanceController.setControlledValueLowLimit(newValue); }
|
||||
Q_INVOKABLE void setRenderDistanceHighLimit(float newValue) { _renderDistanceController.setControlledValueHighLimit(newValue); }
|
||||
|
||||
public slots:
|
||||
void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; }
|
||||
|
@ -90,6 +104,10 @@ private:
|
|||
QVector<AvatarManager::LocalLight> _localLights;
|
||||
|
||||
bool _shouldShowReceiveStats = false;
|
||||
float _renderDistance { (float) TREE_SCALE };
|
||||
int _renderedAvatarCount { 0 };
|
||||
PIDController _renderDistanceController { };
|
||||
SimpleMovingAverage _renderDistanceAverage { 10 };
|
||||
|
||||
SetOfAvatarMotionStates _avatarMotionStates;
|
||||
SetOfMotionStates _motionStatesToAdd;
|
||||
|
|
|
@ -115,6 +115,8 @@ void Stats::updateStats(bool force) {
|
|||
auto avatarManager = DependencyManager::get<AvatarManager>();
|
||||
// we need to take one avatar out so we don't include ourselves
|
||||
STAT_UPDATE(avatarCount, avatarManager->size() - 1);
|
||||
STAT_UPDATE(avatarRenderableCount, avatarManager->getNumberInRenderRange());
|
||||
STAT_UPDATE(avatarRenderDistance, (int) round(avatarManager->getRenderDistance())); // deliberately truncating
|
||||
STAT_UPDATE(serverCount, nodeList->size());
|
||||
STAT_UPDATE(framerate, (int)qApp->getFps());
|
||||
STAT_UPDATE(simrate, (int)qApp->getAverageSimsPerSecond());
|
||||
|
|
|
@ -36,6 +36,8 @@ class Stats : public QQuickItem {
|
|||
STATS_PROPERTY(int, simrate, 0)
|
||||
STATS_PROPERTY(int, avatarSimrate, 0)
|
||||
STATS_PROPERTY(int, avatarCount, 0)
|
||||
STATS_PROPERTY(int, avatarRenderableCount, 0)
|
||||
STATS_PROPERTY(int, avatarRenderDistance, 0)
|
||||
STATS_PROPERTY(int, packetInCount, 0)
|
||||
STATS_PROPERTY(int, packetOutCount, 0)
|
||||
STATS_PROPERTY(float, mbpsIn, 0)
|
||||
|
@ -117,6 +119,8 @@ signals:
|
|||
void simrateChanged();
|
||||
void avatarSimrateChanged();
|
||||
void avatarCountChanged();
|
||||
void avatarRenderableCountChanged();
|
||||
void avatarRenderDistanceChanged();
|
||||
void packetInCountChanged();
|
||||
void packetOutCountChanged();
|
||||
void mbpsInChanged();
|
||||
|
|
78
libraries/shared/src/PIDController.cpp
Normal file
78
libraries/shared/src/PIDController.cpp
Normal file
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// PIDController.cpp
|
||||
// libraries/shared/src
|
||||
//
|
||||
// Created by Howard Stearns 11/13/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 <glm/glm.hpp>
|
||||
#include <QDebug>
|
||||
#include "SharedLogging.h"
|
||||
#include "PIDController.h"
|
||||
|
||||
float PIDController::update(float measuredValue, float dt, bool resetAccumulator) {
|
||||
const float error = getMeasuredValueSetpoint() - measuredValue; // Sign is the direction we want measuredValue to go. Positive means go higher.
|
||||
|
||||
const float p = getKP() * error; // term is Proportional to error
|
||||
|
||||
const float accumulatedError = glm::clamp(error * dt + (resetAccumulator ? 0 : _lastAccumulation), // integrate error
|
||||
getAccumulatedValueLowLimit(), // but clamp by anti-windup limits
|
||||
getAccumulatedValueHighLimit());
|
||||
const float i = getKI() * accumulatedError; // term is Integral of error
|
||||
|
||||
const float changeInError = (error - _lastError) / dt; // positive value denotes increasing deficit
|
||||
const float d = getKD() * changeInError; // term is Derivative of Error
|
||||
|
||||
const float computedValue = glm::clamp(p + i + d,
|
||||
getControlledValueLowLimit(),
|
||||
getControlledValueHighLimit());
|
||||
|
||||
if (getIsLogging()) { // if logging/reporting
|
||||
updateHistory(measuredValue, dt, error, accumulatedError, changeInError, p, i, d, computedValue);
|
||||
}
|
||||
Q_ASSERT(!isnan(computedValue));
|
||||
|
||||
// update state for next time
|
||||
_lastError = error;
|
||||
_lastAccumulation = accumulatedError;
|
||||
return computedValue;
|
||||
}
|
||||
|
||||
// Just for logging/reporting. Used when picking/verifying the operational parameters.
|
||||
void PIDController::updateHistory(float measuredValue, float dt, float error, float accumulatedError, float changeInError, float p, float i, float d, float computedValue) {
|
||||
// Don't report each update(), as the I/O messes with the results a lot.
|
||||
// Instead, add to history, and then dump out at once when full.
|
||||
// Typically, the first few values reported in each batch should be ignored.
|
||||
const int n = _history.size();
|
||||
_history.resize(n + 1);
|
||||
Row& next = _history[n];
|
||||
next.measured = measuredValue;
|
||||
next.dt = dt;
|
||||
next.error = error;
|
||||
next.accumulated = accumulatedError;
|
||||
next.changed = changeInError;
|
||||
next.p = p;
|
||||
next.i = i;
|
||||
next.d = d;
|
||||
next.computed = computedValue;
|
||||
if (_history.size() == _history.capacity()) { // report when buffer is full
|
||||
reportHistory();
|
||||
_history.resize(0);
|
||||
}
|
||||
}
|
||||
void PIDController::reportHistory() {
|
||||
qCDebug(shared) << _label << "measured dt FIXME || error accumulated changed || p i d controlled";
|
||||
for (int i = 0; i < _history.size(); i++) {
|
||||
Row& row = _history[i];
|
||||
qCDebug(shared) << row.measured << row.dt <<
|
||||
"||" << row.error << row.accumulated << row.changed <<
|
||||
"||" << row.p << row.i << row.d << row.computed << 1.0f/row.computed;
|
||||
}
|
||||
qCDebug(shared) << "Limits: setpoint" << getMeasuredValueSetpoint() << "accumulate" << getAccumulatedValueLowLimit() << getAccumulatedValueHighLimit() <<
|
||||
"controlled" << getControlledValueLowLimit() << getControlledValueHighLimit() <<
|
||||
"kp/ki/kd" << getKP() << getKI() << getKD();
|
||||
}
|
89
libraries/shared/src/PIDController.h
Normal file
89
libraries/shared/src/PIDController.h
Normal file
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// PIDController.h
|
||||
// libraries/shared/src
|
||||
//
|
||||
// Given a measure of system performance (such as frame rate, where bigger denotes more system work),
|
||||
// compute a value that the system can take as input to control the amount of work done (such as an 1/LOD-distance,
|
||||
// where bigger tends to give a higher measured system performance value). The controller's job is to compute a
|
||||
// controlled value such that the measured value stays near the specified setpoint, even as system load changes.
|
||||
// See http://www.wetmachine.com/inventing-the-future/mostly-reliable-performance-of-software-processes-by-dynamic-control-of-quality-parameters/
|
||||
//
|
||||
// Created by Howard Stearns 11/13/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
|
||||
//
|
||||
|
||||
#ifndef hifi_PIDController_h
|
||||
#define hifi_PIDController_h
|
||||
|
||||
#include <limits>
|
||||
#include <QVector>
|
||||
|
||||
// Although our coding standard shuns abbreviations, the control systems literature uniformly uses p, i, d, and dt rather than
|
||||
// proportionalTerm, integralTerm, derivativeTerm, and deltaTime. Here we will be consistent with the literature.
|
||||
class PIDController {
|
||||
|
||||
public:
|
||||
// These are the main interfaces:
|
||||
void setMeasuredValueSetpoint(float newValue) { _measuredValueSetpoint = newValue; }
|
||||
float update(float measuredValue, float dt, bool resetAccumulator = false); // returns the new computedValue
|
||||
void setHistorySize(QString label = QString(""), int size = 0) { _history.reserve(size); _history.resize(0); _label = label; } // non-empty does logging
|
||||
|
||||
bool getIsLogging() { return _history.capacity(); }
|
||||
float getMeasuredValueSetpoint() const { return _measuredValueSetpoint; }
|
||||
// In normal operation (where we can easily reach setpoint), controlledValue is typcially pinned at max.
|
||||
// Defaults to [0, max float], but for 1/LODdistance, it might be, say, [0, 0.2 or 0.1]
|
||||
float getControlledValueLowLimit() const { return _controlledValueLowLimit; }
|
||||
float getControlledValueHighLimit() const { return _controlledValueHighLimit; }
|
||||
float getAntiWindupFactor() const { return _antiWindupFactor; } // default 10
|
||||
float getKP() const { return _kp; } // proportional to error. See comment above class.
|
||||
float getKI() const { return _ki; } // to time integral of error
|
||||
float getKD() const { return _kd; } // to time derivative of error
|
||||
float getAccumulatedValueHighLimit() const { return getAntiWindupFactor() * getMeasuredValueSetpoint(); }
|
||||
float getAccumulatedValueLowLimit() const { return -getAntiWindupFactor() * getMeasuredValueSetpoint(); }
|
||||
|
||||
// There are several values that rarely change and might be thought of as "constants", but which do change during tuning, debugging, or other
|
||||
// special-but-expected circumstances. Thus the instance vars are not const.
|
||||
void setControlledValueLowLimit(float newValue) { _controlledValueLowLimit = newValue; }
|
||||
void setControlledValueHighLimit(float newValue) { _controlledValueHighLimit = newValue; }
|
||||
void setAntiWindupFactor(float newValue) { _antiWindupFactor = newValue; }
|
||||
void setKP(float newValue) { _kp = newValue; }
|
||||
void setKI(float newValue) { _ki = newValue; }
|
||||
void setKD(float newValue) { _kd = newValue; }
|
||||
|
||||
class Row { // one row of accumulated history, used only for logging (if at all)
|
||||
public:
|
||||
float measured;
|
||||
float dt;
|
||||
float error;
|
||||
float accumulated;
|
||||
float changed;
|
||||
float p;
|
||||
float i;
|
||||
float d;
|
||||
float computed;
|
||||
};
|
||||
protected:
|
||||
void reportHistory();
|
||||
void updateHistory(float measured, float dt, float error, float accumulatedError, float changeInErro, float p, float i, float d, float computedValue);
|
||||
float _measuredValueSetpoint { 0.0f };
|
||||
float _controlledValueLowLimit { 0.0f };
|
||||
float _controlledValueHighLimit { std::numeric_limits<float>::max() };
|
||||
float _antiWindupFactor { 10.0f };
|
||||
float _kp { 0.0f };
|
||||
float _ki { 0.0f };
|
||||
float _kd { 0.0f };
|
||||
|
||||
// Controller operating state
|
||||
float _lastError{ 0.0f };
|
||||
float _lastAccumulation{ 0.0f };
|
||||
|
||||
// reporting
|
||||
QVector<Row> _history{};
|
||||
QString _label{ "" };
|
||||
|
||||
};
|
||||
|
||||
#endif // hifi_PIDController_h
|
Loading…
Reference in a new issue