summaryrefslogblamecommitdiffstats
path: root/dev/MinGfx/src/unicam.cc
blob: 0acb9c9d9bb122a0fcc7feff07c65b68c5c4caa1 (plain) (tree)



























































































































































































































































































































                                                                                                                
/*
 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<rotAngularVelBuffer_.size(); i++) {
            rotAngularVel_ += rotAngularVelBuffer_[i].second;
        }
        rotAngularVel_ /= rotAngularVelBuffer_.size();
    }
    //std::cout << rotAngularVelBuffer_.size() << " " << rotAngularVel_ << std::endl;
}


void UniCam::OnButtonDown(const Point2 &mousePos, float mouseZ) {
    if (state_ == UniCamState::START) {
        initialClickPos_ = mousePos;
        mouseLast_ = mousePos;
        elapsedTime_ = 0.0;
        rotInitialized_ = false;
        dollyInitialized_ = false;
        
        hitGeometry_ = (mouseZ < 1.0);
        if (hitGeometry_) {
            hitPoint_ = GfxMath::ScreenToWorld(V_, Pdraw_, mousePos, mouseZ);
        }
        else {
            hitPoint_ = GfxMath::ScreenToDepthPlane(V_, Pdraw_, Point2(0,0), defaultDepth_);
        }
        showIcon_ = true;
        state_ = UniCamState::PAN_DOLLY_ROT_DECISION;
    }
    else if (state_ == UniCamState::ROT_WAIT_FOR_SECOND_CLICK) {
        // we have the second click now, and we will start the trackball rotate interaction
        state_ = UniCamState::ROT;
    }
    else if (state_ == UniCamState::SPINNING) {
        // this click is to "catch" the model, stopping it from spinning.
        state_ = UniCamState::START;
    }
    else {
        std::cerr << "UniCam::OnButtonDown() unexpected state." << std::endl;
    }
}

void UniCam::OnDrag(const Point2 &mousePos) {
    if (state_ == UniCamState::PAN_DOLLY_ROT_DECISION) {
        const double panMovementThreshold  = 0.01;
        const double dollyMovementThreshold = 0.01;
        if (fabs(mousePos[0] - initialClickPos_[0]) > 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