/* Copyright (c) 2017,2018 Regents of the University of Minnesota. All Rights Reserved. See corresponding header file for details. */ #include "unicam.h" #include "gfxmath.h" namespace mingfx { UniCam::UniCam() : state_(UniCamState::START), defaultDepth_(4.0), boundingSphereRad_(1.0), dollyFactor_(1.0), dollyInitialized_(false), elapsedTime_(0.0), hitGeometry_(false), rotAngularVel_(0.0), rotInitialized_(false), rotLastTime_(0.0), showIcon_(false) { } UniCam::UniCam(const Matrix4 &initialViewMatrix) : state_(UniCamState::START), defaultDepth_(4.0), V_(initialViewMatrix), boundingSphereRad_(1.0), dollyFactor_(1.0), dollyInitialized_(false), elapsedTime_(0.0), hitGeometry_(false), rotAngularVel_(0.0), rotInitialized_(false), rotLastTime_(0.0), showIcon_(false) { } UniCam::~UniCam() { } void UniCam::recalc_angular_vel() { // update angular velocity float cutoff = (float)elapsedTime_ - 0.2f; // look just at the last 0.2 secs while ((rotAngularVelBuffer_.size()) && (rotAngularVelBuffer_[0].first < cutoff)) { rotAngularVelBuffer_.erase(rotAngularVelBuffer_.begin()); } rotAngularVel_ = 0.0; if (rotAngularVelBuffer_.size()) { for (int i=0; i panMovementThreshold) { // already lots of horizontal movement, we can go right to pan state_ = UniCamState::PAN; showIcon_ = false; } else if (fabs(mousePos[1] - initialClickPos_[1]) > dollyMovementThreshold) { // already lots of vertical movement, we can go right to dolly state_ = UniCamState::DOLLY; showIcon_ = false; } else if (elapsedTime_ > 1.0) { // timeout, this was not a quick click to set a center of rotation, // so there is no intent to rotate. instead we will be doing either // pan or dolly. state_ = UniCamState::PAN_DOLLY_DECISION; showIcon_ = false; } } else if (state_ == UniCamState::PAN_DOLLY_DECISION) { const double panMovementThreshold = 0.01; const double dollyMovementThreshold = 0.01; if (fabs(mousePos[0] - initialClickPos_[0]) > panMovementThreshold) { // lots of horizontal movement, go to pan state_ = UniCamState::PAN; } else if (fabs(mousePos[1] - initialClickPos_[1]) > dollyMovementThreshold) { // lots of vertical movement, go to dolly state_ = UniCamState::DOLLY; } } else if (state_ == UniCamState::PAN) { Matrix4 camMat = V_.Inverse(); Point3 eye = camMat.ColumnToPoint3(3); Vector3 look = -camMat.ColumnToVector3(2); float depth = (hitPoint_ - eye).Dot(look); Point3 pWorld1 = GfxMath::ScreenToDepthPlane(V_, Pdraw_, mouseLast_, depth); Point3 pWorld2 = GfxMath::ScreenToDepthPlane(V_, Pdraw_, mousePos, depth); V_ = V_ * Matrix4::Translation(pWorld2 - pWorld1); } else if (state_ == UniCamState::DOLLY) { if (!dollyInitialized_) { // Setup dollyFactor so that if you move the mouse to the bottom of the screen, the point // you clicked on will be right on top of the camera. Matrix4 camMat = V_.Inverse(); Point3 eye = camMat.ColumnToPoint3(3); Vector3 look = -camMat.ColumnToVector3(2); float depth = (hitPoint_ - eye).Dot(look); float deltaYToBottom = initialClickPos_[1] + 1; dollyFactor_ = depth / deltaYToBottom; dollyInitialized_ = true; } Vector3 d(0, 0, -dollyFactor_ * (mousePos[1] - mouseLast_[1])); V_ = Matrix4::Translation(d) * V_ ; } else if (state_ == UniCamState::ROT) { if (!rotInitialized_) { float depth = 0.0; if (hitGeometry_) { // if we hit some geometry, then make that the center of rotation boundingSphereCtr_ = hitPoint_; Matrix4 camMat = V_.Inverse(); Point3 eye = camMat.ColumnToPoint3(3); Vector3 look = -camMat.ColumnToVector3(2); depth = (hitPoint_ - eye).Dot(look); } else { // if we did not hit any geometry, then center the bounding sphere in front of // the camera at a distance that can be configured by the user. boundingSphereCtr_ = GfxMath::ScreenToDepthPlane(V_, Pdraw_, Point2(0,0), defaultDepth_); depth = defaultDepth_; } // determine the size of the bounding sphere by projecting a screen-space // distance of 0.75 units to the depth of the sphere center Point3 pWorld1 = GfxMath::ScreenToDepthPlane(V_, Pdraw_, Point2(0,0), depth); Point3 pWorld2 = GfxMath::ScreenToDepthPlane(V_, Pdraw_, Point2(0.75,0), depth); boundingSphereRad_ = (pWorld2-pWorld1).Length(); rotLastTime_ = elapsedTime_; rotAngularVelBuffer_.clear(); rotInitialized_ = true; } else { // Do a trackball rotation based on the mouse movement and the bounding sphere // setup earlier. Matrix4 camMat = V_.Inverse(); Point3 eye = camMat.ColumnToPoint3(3); // last mouse pos bool hit1 = false; Point3 mouse3D1 = GfxMath::ScreenToNearPlane(V_, Pdraw_, mouseLast_); Ray ray1(eye, mouse3D1 - eye); float t1; Point3 iPoint1; if (ray1.IntersectSphere(boundingSphereCtr_, boundingSphereRad_, &t1, &iPoint1)) { hit1 = true; } // current mouse pos bool hit2 = false; Point3 mouse3D2 = GfxMath::ScreenToNearPlane(V_, Pdraw_, mousePos); Ray ray2(eye, mouse3D2 - eye); float t2; Point3 iPoint2; if (ray2.IntersectSphere(boundingSphereCtr_, boundingSphereRad_, &t2, &iPoint2)) { hit2 = true; } rotLastIPoint_ = iPoint2; if (hit1 && hit2) { Vector3 v1 = (iPoint1 - boundingSphereCtr_).ToUnit(); Vector3 v2 = (iPoint2 - boundingSphereCtr_).ToUnit(); rotAxis_ = v1.Cross(v2).ToUnit(); float angle = std::acos(v1.Dot(v2)); if (std::isfinite(angle)) { Matrix4 R = Matrix4::Rotation(boundingSphereCtr_, rotAxis_, angle); R = R.Orthonormal(); V_ = V_ * R; //V_ = V_.orthonormal(); // add a sample to the angular vel vector double dt = elapsedTime_ - rotLastTime_; double avel = angle / dt; if (std::isfinite(avel)) { rotAngularVelBuffer_.push_back(std::make_pair(elapsedTime_, avel)); } rotLastTime_ = elapsedTime_; } } recalc_angular_vel(); } } else if (state_ == UniCamState::START) { // picked up a little mouse movement after "catching" a spinning model // nothing to do, just wait for the button up. } else { std::cerr << "UniCam::OnDrag() unexpected state." << std::endl; } mouseLast_ = mousePos; } void UniCam::OnButtonUp(const Point2 &mousePos) { if (state_ == UniCamState::PAN_DOLLY_ROT_DECISION) { // here, we got a quick click of the mouse to indicate a center of rotation // so we now go into a mode of waiting for a second click to start rotating // around that point. state_ = UniCamState::ROT_WAIT_FOR_SECOND_CLICK; } else if (state_ == UniCamState::ROT) { showIcon_ = false; // if we are leaving the rotation state and the angular velocity is // greater than some thresold, then the user has "thrown" the model // keep rotating the same way by entering the spinning state. recalc_angular_vel(); //std::cout << "check for spin: " << n-start << " " << rotAngularVel_ << " " << avel2 << std::endl; const float threshold = 0.2f; if (std::fabs(rotAngularVel_) > threshold) { state_ = UniCamState::SPINNING; } else { state_ = UniCamState::START; } } else { showIcon_ = false; // all other cases go back to the start state state_ = UniCamState::START; } } void UniCam::AdvanceAnimation(double dt) { elapsedTime_ += dt; if (state_ == UniCamState::SPINNING) { double deltaT = elapsedTime_ - rotLastTime_; rotLastTime_ = elapsedTime_; double angle = (double)rotAngularVel_ * deltaT; Matrix4 R = Matrix4::Rotation(boundingSphereCtr_, rotAxis_, (float)angle); //R = R.orthonormal(); V_ = V_ * R; } } void UniCam::Draw(const Matrix4 &projectionMatrix) { Pdraw_ = projectionMatrix; if (showIcon_) { Matrix4 camMat = V_.Inverse(); Point3 eye = camMat.ColumnToPoint3(3); Vector3 look = -camMat.ColumnToVector3(2); float depth = (hitPoint_ - eye).Dot(look); Point3 pWorld1 = GfxMath::ScreenToDepthPlane(V_, Pdraw_, Point2(0.f,0.f), depth); Point3 pWorld2 = GfxMath::ScreenToDepthPlane(V_, Pdraw_, Point2(0.015f,0.f), depth); float rad = (pWorld2 - pWorld1).Length(); Matrix4 M = Matrix4::Translation(hitPoint_ - Point3::Origin()) * Matrix4::Scale(Vector3(rad, rad, rad)); quickShapes_.DrawSphere(M, V_, Pdraw_, Color(0,0,0)); } } Matrix4 UniCam::view_matrix() { return V_; } void UniCam::set_view_matrix(Matrix4 viewMatrix) { V_ = viewMatrix; } void UniCam::set_default_depth(float d) { defaultDepth_ = d; } Point3 UniCam::eye() { Matrix4 camMat = V_.Inverse(); return camMat.ColumnToPoint3(3); } Vector3 UniCam::look() { Matrix4 camMat = V_.Inverse(); return -camMat.ColumnToVector3(2); } } // end namespace