mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2026-02-28 07:59:37 -06:00
Merge pull request #613 from OpenSpace/touch-user-study
Touch user study code
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" })
|
||||
|
||||
Reference in New Issue
Block a user