欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ArcBall+glm 实现射线拾取Ray Picking

程序员文章站 2024-03-16 17:45:16
...

在做一个基于qt和opengl的网格变形实验,用户交互方面需要实现射线拾取,框架本身是使用ArcBall实现鼠标拖拽旋转的。探索了几天,使用glm实现了射线拾取

1 射线拾取原理

射线拾取的原理很多博客也讲过了,主要就是:以当前眼睛位置(摄像机位置)为起点,以眼睛位置(摄像机位置)指向鼠标点选位置所对应的世界坐标 的方向为方向,生成一条射线,再判断射线与网格中的各个三角形是否相交。
个人认为重点是得到眼睛位置(摄像机位置)鼠标点选位置所对应的世界坐标

2 实现步骤

2.1 生成射线

  1. 眼睛位置
  vec3 eyepos = eye_distance_*eye_direction_;;            //视点坐标与观察点坐标
    //ptr_arcball_是轨迹球,GetInvertedBallMatrix()是得到旋转四元数构成的矩阵的逆矩阵
    glm::mat4 invertM = glm::make_mat4(ptr_arcball_->GetInvertedBallMatrix());
    glm::mat3 invertM_3 = glm::mat3(invertM); //取前三行&前三列
    glm::vec3 eyepos_glm = glm::vec3(eyepos[0],eyepos[1],eyepos[2]);
    glm::vec3 cameraPos = invertM_3 * eyepos_glm;
  1. 鼠标点选位置对应的世界坐标
    // Get the matrix matView & matProj
    glm::mat4 matView = glm::make_mat4(ptr_arcball_->GetBallMatrix());//ptr_arcball_是轨迹球,GetBallMatrix()是得到旋转四元数构成的矩阵
    glm::mat4 matProj = glm::perspective(3.141592f / 2.0f, GLfloat(WIDTH / HEIGHT), 1.0f, 100.0f);

    vec3 mouse_coord = convert(x,y,width(),height());

    // x y z w  4D裁剪坐标 Z取1,至最远处,即z离相机100,即为(0,0-50)
    glm::vec4 ray_clip = glm::vec4(mouse_coord[0], mouse_coord[1], 1.0f, 1.0f);
    glm::vec4 ray_eye = glm::inverse(matProj) * ray_clip;   // 转为视觉坐标
    glm::vec4 ray_world = glm::inverse(matView) * ray_eye;  // 转为世界坐标

    ray_world.x /= ray_world.w;
    ray_world.y /= ray_world.w;
    ray_world.z /= ray_world.w;

    glm::vec3 ray_wordXYZ = glm::vec3(ray_world);

3 CastRay总体函数

void RenderWidget::CastRay(int x, int y, Ray &pickingRay)
{
    vec3 eyepos = eye_distance_*eye_direction_;;            //视点坐标与观察点坐标
    //ptr_arcball_是轨迹球,GetInvertedBallMatrix()是得到旋转四元数构成的矩阵的逆矩阵
    glm::mat4 invertM = glm::make_mat4(ptr_arcball_->GetInvertedBallMatrix());
    glm::mat3 invertM_3 = glm::mat3(invertM); //取前三行&前三列
    glm::vec3 eyepos_glm = glm::vec3(eyepos[0],eyepos[1],eyepos[2]);
    glm::vec3 cameraPos = invertM_3 * eyepos_glm;


    /*glm::vec3 cameraTarg = glm::vec3(0.0f, 0.0f, 0.0f);
    glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
    glm::mat4 matView = glm::lookAt(cameraPos, cameraTarg, cameraUp);*/

    // Get the matrix matView & matProj
    glm::mat4 matView = glm::make_mat4(ptr_arcball_->GetBallMatrix());
    glm::mat4 matProj = glm::perspective(3.141592f / 2.0f, GLfloat(WIDTH / HEIGHT), 1.0f, 100.0f);

    vec3 mouse_coord = convert(x,y,width(),height());

    // x y z w  4D裁剪坐标 Z取1,至最远处,即z离相机100,即为(0,0-50)
    glm::vec4 ray_clip = glm::vec4(mouse_coord[0], mouse_coord[1], 1.0f, 1.0f);
    glm::vec4 ray_eye = glm::inverse(matProj) * ray_clip;   // 转为视觉坐标
    glm::vec4 ray_world = glm::inverse(matView) * ray_eye;  // 转为世界坐标

    ray_world.x /= ray_world.w;
    ray_world.y /= ray_world.w;
    ray_world.z /= ray_world.w;

    glm::vec3 ray_wordXYZ = glm::vec3(ray_world);
    glm::vec3 ray_dir = glm::normalize(ray_wordXYZ - cameraPos);

    //画射线
    vec3 p1 = {cameraPos.x, cameraPos.y, cameraPos.z};
    vec3 p2 = {ray_wordXYZ.x, ray_wordXYZ.y, ray_wordXYZ.z};
    near_point_.push_back(p1);
    far_point_.push_back(p2);

    // 构造射线
    vec3 ray_pos = {cameraPos[0],cameraPos[1],cameraPos[2]};
    pickingRay.pos = ray_pos;
    vec3 ray_ = {ray_dir.x, ray_dir.y, ray_dir.z};
    pickingRay.dir = ray_;

}

里面用到的convert函数,是把屏幕坐标转(x,y)换到-1到1之间

vec3 RenderWidget::convert(int x, int y, int nWinWidth, int nWinHeight)
{
    vec3 coord;
    if(nWinWidth>=nWinHeight)
    {
        coord[0]=( float(x)/nWinWidth-0.5f )*2*(nWinWidth/nWinHeight);
        coord[1]=-(float(y)/nWinHeight-0.5f)*2;
    }
    else
    {
        coord[0]=(float(x)/nWinWidth-0.5f)*2;
        coord[1]=-(float(y)/nWinHeight-0.5f)*2*(nWinHeight/nWinWidth);
    }
    coord[2] = 0;
    return coord;
}

2.2 判断三角形与射线相交与否

这里我参考了https://blog.csdn.net/charlee44/article/details/104348131的第二个方法
代码如下:

bool RenderWidget::IntersectTriangle(const vec3& pos, const vec3& dir,
    vec3& v0, vec3& v1, vec3& v2, float &t, float &u, float &v)//
{
    // E1
    vec3 E1 = v1 - v0;
    // E2
    vec3 E2 = v2 - v0;
    // P
    vec3 P = dir.cross(E2);
    // determinant
    float det = E1.dot(P);
    // keep det > 0, modify T accordingly
    vec3 T;//Vector3
    if( det >0 )
    {
        T = pos - v0;
    }
    else
    {
        T = v0 - pos;
        det = -det;
    }
    // If determinant is near zero, ray lies in plane of triangle
    if( det < 0.0001f )
        return false;
    // Calculate u and make sure u <= 1
    u = T.dot(P);
    if( u < 0.0f || u > det )
        return false;
    // Q
    vec3 Q = T.cross(E1);
    // Calculate v and make sure u + v <= 1
    v = dir.dot(Q);
    if( v < 0.0f || u + v > det )
        return false;
    // Calculate t, scale parameters, ray intersects triangle
    t = E2.dot(Q);
    float fInvDet = 1.0f / det;
    t *= fInvDet;
    u *= fInvDet;
    v *= fInvDet;
    return true;
}

2.3 遍历网格中所有三角形判断与射线的相交

这个比较简单,就是遍历,然后循环判断,加一个深度值z的判断,就是一条射线可能会跟三维模型又不止一个交点,要判断一下选离摄像机最近的那个,这里用IntersectTriangle里面的t值来表示

void RenderWidget::RayPicking(QPoint point)
{
    CastRay(point.x(), point.y(), picking_ray_);

    float intersect_t = 1000.0f;
    int select_id = -1;
    std::vector<vec3> tri_v(3);
    //遍历面
    for (MyMesh::FaceIter f_it = mesh_.faces_begin(); f_it != mesh_.faces_end(); ++f_it)
    {
        int i = 0;
        for (MyMesh::FaceVertexIter fv_it = mesh_.fv_iter(*f_it); fv_it.is_valid(); ++fv_it) {

            auto vertex = mesh_.point(*fv_it);
            tri_v[i] = {vertex.data()[0],vertex.data()[1],vertex.data()[2]};
            i++;
        }

        //相交判断
        float t = 0, u = 0, v = 0;
        bool is_intersect = IntersectTriangle(picking_ray_.pos,picking_ray_.dir,tri_v[0],tri_v[1],tri_v[2],t ,u ,v);

        if(is_intersect)
        {
            std::cout<<"t:"<<t<<std::endl;
            if(t < intersect_t)
            {
                intersect_t = t;
                select_id = f_it.handle().idx();

                is_draw_selected_tri_ = true;
            }
        }
    }

    selected_face_id_.push_back(select_id);

}