Merge pull request #613 from OpenSpace/touch-user-study

Touch user study code
This commit is contained in:
Gene Payne
2018-07-05 15:35:23 -06:00
committed by GitHub
2 changed files with 190 additions and 39 deletions

View File

@@ -44,6 +44,22 @@ namespace openspace {
class Camera;
class SceneGraphNode;
//Class used for keeping track of the recent average frame time
class FrameTimeAverage {
public:
//Update the circular buffer with the most recent frame time
void updateWithNewFrame(double sample);
//Get the value of the most recent average frame time (seconds)
double averageFrameTime() const;
private:
static const int TotalSamples = 10;
int _nSamples = 0;
double _samples[TotalSamples];
double _runningTotal = 0.0;
int _index = 0;
};
class TouchInteraction : public properties::PropertyOwner {
public:
using Point = std::pair<int, TUIO::TuioPoint>;
@@ -51,7 +67,7 @@ public:
TouchInteraction();
// for interpretInteraction()
enum Type { ROT = 0, PINCH, PAN, ROLL, PICK };
enum Type { ROT = 0, PINCH, PAN, ROLL, PICK, ZOOM_OUT };
// Stores the velocity in all 6DOF
struct VelocityStates {
@@ -148,6 +164,17 @@ private:
void computeVelocities(const std::vector<TUIO::TuioCursor>& list,
const std::vector<Point>& lastProcessed);
//Compute velocity based on double-tap for zooming
double computeTapZoomDistance(double zoomGain);
//Compute coefficient for velocity decay to be applied in decceleration
double computeConstTimeDecayCoefficient(double velocity);
//Compute coefficient of decay based on current frametime; if frametime has been
// longer than usual then multiple decay steps may be applied to keep the decay
// relative to user time
double computeDecayCoeffFromFrametime(double coeff, int times);
/* Decelerate the velocities. Function is called in step() but is dereferenced from
* frame time to assure same behaviour on all systems
*/
@@ -172,7 +199,8 @@ private:
properties::FloatProperty _rollAngleThreshold;
properties::FloatProperty _orbitSpeedThreshold;
properties::FloatProperty _spinSensitivity;
properties::FloatProperty _zoomSensitivity;
properties::FloatProperty _zoomSensitivityExponential;
properties::FloatProperty _zoomSensitivityProportionalDist;
properties::FloatProperty _zoomSensitivityDistanceThreshold;
properties::FloatProperty _zoomBoundarySphereMultiplier;
properties::FloatProperty _inputStillThreshold;
@@ -184,6 +212,7 @@ private:
properties::Vec4Property _friction;
properties::FloatProperty _pickingRadiusMinimum;
properties::BoolProperty _ignoreGui;
properties::FloatProperty _constTimeDecay_secs;
#ifdef TOUCH_DEBUG_PROPERTIES
struct DebugProperties : PropertyOwner {
@@ -215,6 +244,7 @@ private:
bool _directTouchMode;
bool _tap;
bool _doubleTap;
bool _zoomOutTap;
bool _lmSuccess;
bool _guiON;
std::vector<SelectedBody> _selected;
@@ -222,6 +252,16 @@ private:
LMstat _lmstat;
glm::dquat _toSlerp;
glm::dvec3 _centroid;
FrameTimeAverage _frameTimeAvg;
struct ConstantTimeDecayCoefficients {
double zoom = 0.0;
double orbit = 0.0;
double roll = 0.0;
double pan = 0.0;
};
ConstantTimeDecayCoefficients _constTimeDecayCoeff;
};
} // openspace namespace

View File

@@ -141,12 +141,18 @@ namespace {
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo ZoomSensitivityInfo = {
"ZoomSensitivity",
constexpr openspace::properties::Property::PropertyInfo ZoomSensitivityExpInfo = {
"ZoomSensitivityExp",
"Sensitivity of exponential zooming in relation to distance from focus node",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo ZoomSensitivityPropInfo = {
"ZoomSensitivityProp",
"Sensitivity of zooming proportional to distance from focus node",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo
ZoomSensitivityDistanceThresholdInfo = {
"ZoomSensitivityDistanceThreshold",
@@ -163,6 +169,12 @@ namespace {
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo ConstantTimeDecaySecsInfo = {
"ConstantTimeDecaySecs",
"Time duration that a pitch/roll/zoom/pan should take to decay to zero (seconds)",
""
};
constexpr openspace::properties::Property::PropertyInfo InputSensitivityInfo = {
"InputSensitivity",
"Threshold for interpreting input as still",
@@ -230,7 +242,8 @@ TouchInteraction::TouchInteraction()
, _rollAngleThreshold(RollThresholdInfo, 0.025f, 0.f, 0.05f)
, _orbitSpeedThreshold(OrbitSpinningThreshold, 0.005f, 0.f, 0.01f)
, _spinSensitivity(SpinningSensitivityInfo, 1.f, 0.f, 2.f)
, _zoomSensitivity(ZoomSensitivityInfo, 1.025f, 1.0f, 1.1f)
, _zoomSensitivityExponential(ZoomSensitivityExpInfo, 1.03f, 1.0f, 1.1f)
, _zoomSensitivityProportionalDist(ZoomSensitivityPropInfo, 11.f, 5.f, 50.f)
, _zoomSensitivityDistanceThreshold(
ZoomSensitivityDistanceThresholdInfo,
0.05f,
@@ -243,7 +256,7 @@ TouchInteraction::TouchInteraction()
, _centroidStillThreshold(StationaryCentroidInfo, 0.0018f, 0.f, 0.01f)
, _panEnabled(PanModeInfo, false)
, _interpretPan(PanDeltaDistanceInfo, 0.015f, 0.f, 0.1f)
, _slerpTime(SlerpTimeInfo, 3.f, 0.f, 5.f)
, _slerpTime(SlerpTimeInfo, 3.f, 0.1f, 5.f)
, _guiButton(
GuiButtonSizeInfo,
glm::ivec2(32, 64),
@@ -267,7 +280,8 @@ TouchInteraction::TouchInteraction()
false
)
, _vel{ glm::dvec2(0.0), 0.0, 0.0, glm::dvec2(0.0) }
, _sensitivity{ glm::dvec2(0.08, 0.045), 4.0, 2.75, glm::dvec2(0.08, 0.045) }
, _sensitivity{ glm::dvec2(0.08, 0.045), 12.0 /*4.0*/, 2.75, glm::dvec2(0.08, 0.045) }
, _constTimeDecay_secs(ConstantTimeDecaySecsInfo, 1.75f, 0.1f, 4.0f)
// calculated with two vectors with known diff in length, then
// projDiffLength/diffLength.
, _projectionScaleFactor(1.000004)
@@ -278,6 +292,7 @@ TouchInteraction::TouchInteraction()
, _directTouchMode(false)
, _tap(false)
, _doubleTap(false)
, _zoomOutTap(false)
, _lmSuccess(true)
, _guiON(false)
#ifdef TOUCH_DEBUG_PROPERTIES
@@ -296,9 +311,11 @@ TouchInteraction::TouchInteraction()
addProperty(_rollAngleThreshold);
addProperty(_orbitSpeedThreshold);
addProperty(_spinSensitivity);
addProperty(_zoomSensitivity);
addProperty(_zoomSensitivityExponential);
addProperty(_zoomSensitivityProportionalDist);
addProperty(_zoomSensitivityDistanceThreshold);
addProperty(_zoomBoundarySphereMultiplier);
addProperty(_constTimeDecay_secs);
addProperty(_inputStillThreshold);
addProperty(_centroidStillThreshold);
addProperty(_panEnabled);
@@ -345,6 +362,26 @@ void TouchInteraction::updateStateFromInput(const std::vector<TuioCursor>& list,
_time.initSession();
}
//Code for lower-right corner double-tap to zoom-out
WindowWrapper& wrapper = OsEng.windowWrapper();
glm::ivec2 res = wrapper.currentWindowSize();
glm::dvec2 pos = glm::vec2(
list.at(0).getScreenX(res.x),
list.at(0).getScreenY(res.y)
);
const float bottomCornerSizeForZoomTap_fraction = 0.08f;
int zoomTapThresholdX = static_cast<int>(res.x * (1.0f - bottomCornerSizeForZoomTap_fraction));
int zoomTapThresholdY = static_cast<int>(res.y * (1.0f - bottomCornerSizeForZoomTap_fraction));
bool isTapInLowerCorner = std::abs(pos.x) > zoomTapThresholdX &&
std::abs(pos.y) > zoomTapThresholdY;
if (_doubleTap && isTapInLowerCorner) {
_zoomOutTap = true;
_tap = false;
_doubleTap = false;
}
if (!guiMode(list)) {
if (_directTouchMode && _selected.size() > 0 && list.size() == _selected.size()) {
#ifdef TOUCH_DEBUG_PROPERTIES
@@ -400,6 +437,7 @@ bool TouchInteraction::guiMode(const std::vector<TuioCursor>& list) {
else if (_guiON) {
module.touchInput = { _guiON, pos, 1 }; // emulate touch input as a mouse
}
return _guiON;
}
@@ -919,10 +957,13 @@ int TouchInteraction::interpretInteraction(const std::vector<TuioCursor>& list,
_debugProperties.minDiff = minDiff;
#endif
if (_doubleTap) {
if (_zoomOutTap) {
return ZOOM_OUT;
}
else if (_doubleTap) {
return PICK;
}
else if (list.size() == 1) {
else if (list.size() == 1) {
return ROT;
}
else {
@@ -984,6 +1025,9 @@ void TouchInteraction::computeVelocities(const std::vector<TuioCursor>& list,
_vel.orbit += glm::dvec2(cursor.getXSpeed() *
_sensitivity.orbit.x, cursor.getYSpeed() *
_sensitivity.orbit.y);
double orbitVelocityAvg = glm::distance(_vel.orbit.x, _vel.orbit.y);
_constTimeDecayCoeff.orbit
= computeConstTimeDecayCoefficient(orbitVelocityAvg);
break;
}
case PINCH: {
@@ -1023,15 +1067,21 @@ void TouchInteraction::computeVelocities(const std::vector<TuioCursor>& list,
pinchConsecCt++;
pinchConsecZoomFactor += zoomFactor;
#endif
if ((length(currDistanceToFocusNode) / distanceFromFocusSurface) >
_zoomSensitivityDistanceThreshold)
{
zoomFactor *= pow(
distanceFromFocusSurface,
static_cast<float>(_zoomSensitivity)
);
_constTimeDecayCoeff.zoom = computeConstTimeDecayCoefficient(_vel.zoom);
if (distanceFromFocusSurface > 0.1) {
double ratioOfDistanceToNodeVsSurface =
length(currDistanceToFocusNode) / distanceFromFocusSurface;
if (ratioOfDistanceToNodeVsSurface > _zoomSensitivityDistanceThreshold) {
zoomFactor *= pow(
std::abs(distanceFromFocusSurface),
static_cast<float>(_zoomSensitivityExponential)
);
}
} else {
zoomFactor = 1.0;
}
_vel.zoom += zoomFactor * _sensitivity.zoom *
_vel.zoom = zoomFactor * _zoomSensitivityProportionalDist *
std::max(_touchScreenSize.value() * 0.1, 1.0);
break;
}
@@ -1071,12 +1121,15 @@ void TouchInteraction::computeVelocities(const std::vector<TuioCursor>& list,
) / list.size();
_vel.roll += -rollFactor * _sensitivity.roll;
_constTimeDecayCoeff.roll = computeConstTimeDecayCoefficient(_vel.roll);
break;
}
case PAN: {
// add local rotation velocity
_vel.pan += glm::dvec2(cursor.getXSpeed() *
_sensitivity.pan.x, cursor.getYSpeed() * _sensitivity.pan.y);
double panVelocityAvg = glm::distance(_vel.pan.x, _vel.pan.y);
_constTimeDecayCoeff.pan = computeConstTimeDecayCoefficient(panVelocityAvg);
break;
}
case PICK: {
@@ -1102,19 +1155,42 @@ void TouchInteraction::computeVelocities(const std::vector<TuioCursor>& list,
else {
// zooms in to current if PICK interpret happened but only space was
// selected
double dist = glm::distance(
_camera->positionVec3(), _camera->focusPositionVec3()
) - _focusNode->boundingSphere();
_vel.zoom = _sensitivity.zoom *
std::max(_touchScreenSize.value() * 0.1, 1.0) *
_tapZoomFactor * dist;
_vel.zoom = computeTapZoomDistance(0.3);
_constTimeDecayCoeff.zoom = computeConstTimeDecayCoefficient(_vel.zoom);
}
break;
}
case ZOOM_OUT: {
// zooms out from current if triple tap occurred
_vel.zoom = computeTapZoomDistance(-1.0);
_constTimeDecayCoeff.zoom = computeConstTimeDecayCoefficient(_vel.zoom);
}
}
}
double TouchInteraction::computeConstTimeDecayCoefficient(double velocity) {
const double postDecayVelocityTarget = 1e-6;
double stepsToDecay = _constTimeDecay_secs / _frameTimeAvg.averageFrameTime();
if (stepsToDecay > 0.0 && std::abs(velocity) > postDecayVelocityTarget) {
return std::pow(postDecayVelocityTarget / std::abs(velocity), 1.0 / stepsToDecay);
}
else {
return 1.0;
}
}
double TouchInteraction::computeTapZoomDistance(double zoomGain) {
double dist = glm::distance(_camera->positionVec3(), _camera->focusPositionVec3());
dist -= _focusNode->boundingSphere();
double newVelocity = dist * _tapZoomFactor;
newVelocity *= std::max(_touchScreenSize.value() * 0.1, 1.0);
newVelocity *= _zoomSensitivityProportionalDist * zoomGain;
return newVelocity;
}
// Main update call, calculates the new orientation and position for the camera depending
// on _vel and dt. Called every frame
void TouchInteraction::step(double dt) {
@@ -1194,6 +1270,8 @@ void TouchInteraction::step(double dt) {
double planetBoundaryRadius = length(centerToBoundingSphere);
planetBoundaryRadius *= _zoomBoundarySphereMultiplier;
double distToSurface = length(centerToCamera - planetBoundaryRadius);
//Apply the velocity to update camera position
if (length(_vel.zoom*dt) < distToSurface &&
length(centerToCamera + directionToCenter*_vel.zoom*dt)
> planetBoundaryRadius)
@@ -1227,6 +1305,7 @@ void TouchInteraction::step(double dt) {
_tap = false;
_doubleTap = false;
_zoomOutTap = false;
if (_reset) {
resetToDefault();
}
@@ -1278,25 +1357,36 @@ void TouchInteraction::unitTest() {
// Assume: frequency = 0.01, dt = 0.05 (200 fps), _timeSlack = 0.0001
// times = floor((0.05 + 0.0001) / 0.01) = 5
// _timeSlack = 0.0501 % 0.01 = 0.01
void TouchInteraction::decelerate(double dt) {
double frequency = 1.0 / _deceleratesPerSecond;
_frameTimeAvg.updateWithNewFrame(dt);
double expectedFrameTime = _frameTimeAvg.averageFrameTime();
// Number of times velocities should decelerate, depending on chosen frequency and
// time slack over from last frame
int times = static_cast<int>((dt + _timeSlack) / frequency);
int times = static_cast<int>((dt + _timeSlack) / expectedFrameTime);
// Save the new time slack for the next frame
_timeSlack = fmod((dt + _timeSlack), frequency) * frequency;
_timeSlack = fmod((dt + _timeSlack), expectedFrameTime) * expectedFrameTime;
// Decelerate zoom velocity quicker if we're close enough to use direct-manipulation
if (!_directTouchMode && _currentRadius > _nodeRadiusThreshold &&
_vel.zoom > _focusNode->boundingSphere())
{
_vel.zoom *= std::pow(1 - 2 * _friction.value().y, times);
//Ensure the number of times to apply the decay coefficient is valid
times = std::min(times, 1);
_vel.orbit *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.orbit, times);
_vel.roll *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.roll, times);
_vel.pan *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.pan, times);
_vel.zoom *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.zoom, times);
glm::dvec3 camPos = _camera->positionVec3();
glm::dvec3 centerPos = _focusNode->worldPosition();
glm::dvec3 centerToCamera = camPos - centerPos;
}
double TouchInteraction::computeDecayCoeffFromFrametime(double coeff, int times) {
if (coeff > 0.00001) {
return std::pow(coeff, times);
}
else {
return 0.0;
}
_vel.orbit *= std::pow(1 - _friction.value().x, times);
_vel.zoom *= std::pow(1 - _friction.value().y, times);
_vel.roll *= std::pow(1 - _friction.value().z, times);
_vel.pan *= std::pow(1 - _friction.value().w, times);
}
// Called if all fingers are off the screen
@@ -1352,7 +1442,7 @@ void TouchInteraction::resetToDefault() {
_rollAngleThreshold.set(0.025f);
_orbitSpeedThreshold.set(0.005f);
_spinSensitivity.set(1.0f);
_zoomSensitivity.set(1.025f);
_zoomSensitivityExponential.set(1.025f);
_inputStillThreshold.set(0.0005f);
_centroidStillThreshold.set(0.0018f);
_interpretPan.set(0.015f);
@@ -1384,6 +1474,27 @@ void TouchInteraction::setFocusNode(SceneGraphNode* focusNode) {
_focusNode = focusNode;
}
void FrameTimeAverage::updateWithNewFrame(double sample) {
if (sample > 0.0005) {
_samples[_index++] = sample;
if (_index >= TotalSamples) {
_index = 0;
}
if (_nSamples < TotalSamples) {
_nSamples++;
}
}
}
double FrameTimeAverage::averageFrameTime() const {
double ft;
if (_nSamples == 0)
ft = 1.0 / 60.0; //Just guess at 60fps if no data is available yet
else
ft = std::accumulate(_samples, _samples + _nSamples, 0.0) / (double)(_nSamples);
return ft;
}
#ifdef TOUCH_DEBUG_PROPERTIES
TouchInteraction::DebugProperties::DebugProperties()
: properties::PropertyOwner({ "TouchDebugProperties" })