ArcBall+glm 实现射线拾取Ray Picking
程序员文章站
2024-03-16 17:45:16
...
ArcBall+glm 实现射线拾取Ray Picking
在做一个基于qt和opengl的网格变形实验,用户交互方面需要实现射线拾取,框架本身是使用ArcBall实现鼠标拖拽旋转的。探索了几天,使用glm实现了射线拾取
1 射线拾取原理
射线拾取的原理很多博客也讲过了,主要就是:以当前眼睛位置(摄像机位置)
为起点,以眼睛位置(摄像机位置)
指向鼠标点选位置所对应的世界坐标
的方向为方向,生成一条射线,再判断射线与网格中的各个三角形是否相交。
个人认为重点是得到眼睛位置(摄像机位置)
和鼠标点选位置所对应的世界坐标
2 实现步骤
2.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;
- 鼠标点选位置对应的世界坐标
// 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);
}