/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2020 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include namespace { // Used in the LM algorithm struct FunctionData { std::vector selectedPoints; std::vector screenPoints; int nDOF; const openspace::Camera* camera; openspace::SceneGraphNode* node; LMstat stats; }; } // namespace namespace openspace { DirectInputSolver::DirectInputSolver() { levmarq_init(&_lmstat); } // project back a 3D point in model view to clip space [-1,1] coordinates on the view // plane glm::dvec2 castToNDC(const glm::dvec3& vec, Camera& camera, SceneGraphNode* node) { glm::dvec3 posInCamSpace = glm::inverse(camera.rotationQuaternion()) * (node->worldRotationMatrix() * vec + (node->worldPosition() - camera.positionVec3())); glm::dvec4 clipspace = camera.projectionMatrix() * glm::dvec4(posInCamSpace, 1.0); return (glm::dvec2(clipspace) / clipspace.w); } // Returns the screen point s(xi,par) dependent the transform M(par) and object point xi double distToMinimize(double* par, int x, void* fdata, LMstat* lmstat) { FunctionData* ptr = reinterpret_cast(fdata); // Apply transform to camera and find the new screen point of the updated camera state // { vec2 globalRot, zoom, roll, vec2 localRot } double q[6] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; for (int i = 0; i < ptr->nDOF; ++i) { q[i] = par[i]; } using namespace glm; // Create variables from current state dvec3 camPos = ptr->camera->positionVec3(); dvec3 centerPos = ptr->node->worldPosition(); dvec3 directionToCenter = normalize(centerPos - camPos); dvec3 lookUp = ptr->camera->lookUpVectorWorldSpace(); dvec3 camDirection = ptr->camera->viewDirectionWorldSpace(); // Make a representation of the rotation quaternion with local and global // rotations dmat4 lookAtMat = lookAt( dvec3(0, 0, 0), directionToCenter, // To avoid problem with lookup in up direction normalize(camDirection + lookUp) ); dquat globalCamRot = normalize(quat_cast(inverse(lookAtMat))); dquat localCamRot = inverse(globalCamRot) * ptr->camera->rotationQuaternion(); { // Roll dquat rollRot = angleAxis(q[3], dvec3(0.0, 0.0, 1.0)); localCamRot = localCamRot * rollRot; } { // Panning (local rotation) dvec3 eulerAngles(q[5], q[4], 0); dquat panRot = dquat(eulerAngles); localCamRot = localCamRot * panRot; } { // Orbit (global rotation) dvec3 eulerAngles(q[1], q[0], 0); dquat rotationDiffCamSpace = dquat(eulerAngles); dvec3 centerToCamera = camPos - centerPos; dquat rotationDiffWorldSpace = globalCamRot * rotationDiffCamSpace * inverse(globalCamRot); dvec3 rotationDiffVec3 = centerToCamera * rotationDiffWorldSpace - centerToCamera; camPos += rotationDiffVec3; centerToCamera = camPos - centerPos; directionToCenter = normalize(-centerToCamera); dvec3 lookUpWhenFacingCenter = globalCamRot * dvec3(ptr->camera->lookUpVectorCameraSpace()); lookAtMat = lookAt( dvec3(0, 0, 0), directionToCenter, lookUpWhenFacingCenter ); globalCamRot = normalize(quat_cast(inverse(lookAtMat))); } { // Zooming camPos += directionToCenter * q[2]; } // Update the camera state Camera cam = *(ptr->camera); cam.setPositionVec3(camPos); cam.setRotation(globalCamRot * localCamRot); // we now have a new position and orientation of camera, project surfacePoint to // the new screen to get distance to minimize glm::dvec2 newScreenPoint = castToNDC( ptr->selectedPoints.at(x), cam, ptr->node ); lmstat->pos.push_back(newScreenPoint); return glm::length(ptr->screenPoints.at(x) - newScreenPoint); } // Gradient of distToMinimize w.r.t par (using forward difference) void gradient(double* g, double* par, int x, void* fdata, LMstat* lmstat) { FunctionData* ptr = reinterpret_cast(fdata); double f0 = distToMinimize(par, x, fdata, lmstat); // scale value to find minimum step size h, dependant on planet size double scale = log10(ptr->node->boundingSphere()); std::vector dPar(ptr->nDOF, 0.0); dPar.assign(par, par + ptr->nDOF); for (int i = 0; i < ptr->nDOF; ++i) { // Initial values double h = 1e-8; double lastG = 1; dPar.at(i) += h; double f1 = distToMinimize(dPar.data(), x, fdata, lmstat); dPar.at(i) = par[i]; // Iterative process to find the minimum step h that gives a good gradient for (int j = 0; j < 100; ++j) { if ((f1 - f0) != 0 && lastG == 0) { // found minimum step size h // scale up to get a good initial guess value h *= scale * scale * scale; // clamp min step size to a fraction of the incoming parameter if (i == 2) { double epsilon = 1e-3; // make sure incoming parameter is larger than 0 h = std::max(std::max(std::abs(dPar.at(i)), epsilon) * 0.001, h); } else if (ptr->nDOF == 2) { h = std::max(std::abs(dPar.at(i)) * 0.001, h); } // calculate f1 with good h for finite difference dPar[i] += h; f1 = distToMinimize(dPar.data(), x, fdata, lmstat); dPar[i] = par[i]; break; } else if ((f1 - f0) != 0 && lastG != 0) { // h too big h /= scale; } else if ((f1 - f0) == 0) { // h too small h *= scale; } lastG = f1 - f0; dPar.at(i) += h; f1 = distToMinimize(dPar.data(), x, fdata, lmstat); dPar.at(i) = par[i]; } g[i] = (f1 - f0) / h; } if (ptr->nDOF == 2) { // normalize on 1 finger case to allow for horizontal/vertical movement for (int i = 0; i < 2; ++i) { g[i] = g[i] / std::abs(g[i]); } } else if (ptr->nDOF == 6) { for (int i = 0; i < ptr->nDOF; ++i) { // lock to only pan and zoom on 3 finger case, no roll/orbit g[i] = (i == 2) ? g[i] : g[i] / std::abs(g[i]); } } } bool DirectInputSolver::solve(const std::vector& list, const std::vector& selectedBodies, std::vector* parameters, const Camera& camera) { int nFingers = std::min(static_cast(list.size()), 3); _nDof = std::min(nFingers * 2, 6); // Parse input data to be used in the LM algorithm std::vector selectedPoints; std::vector screenPoints; for (int i = 0; i < nFingers; ++i) { const SelectedBody& sb = selectedBodies.at(i); selectedPoints.push_back(sb.coordinates); screenPoints.emplace_back( 2.0 * (list[i].latestInput().x - 0.5), -2.0 * (list[i].latestInput().y - 0.5) ); } FunctionData fData = { selectedPoints, screenPoints, _nDof, &camera, selectedBodies.at(0).node, _lmstat }; void* dataPtr = reinterpret_cast(&fData); bool result = levmarq( _nDof, parameters->data(), static_cast(screenPoints.size()), nullptr, distToMinimize, gradient, dataPtr, &_lmstat ); return result; } int DirectInputSolver::nDof() const { return _nDof; } const LMstat& DirectInputSolver::levMarqStat() { return _lmstat; } void DirectInputSolver::setLevMarqVerbosity(bool verbose) { _lmstat.verbose = verbose; } } // openspace namespace