实现光线拾取技术

34

我有一个使用DirectX和OpenGL的渲染器,以及一个3D场景。视口和窗口具有相同的尺寸。

如何以平台无关的方式实现拾取(picking),并给定鼠标坐标x和y?


看一下这个指南,可能会有帮助。 - user3405291
6个回答

30
如果可以的话,通过计算从眼睛到鼠标指针的光线并将其与模型相交来在CPU上进行拾取。
如果这不是一个选项,我会选择某种类型的ID渲染。为想要拾取的每个对象分配一个唯一的颜色,使用这些颜色渲染对象,最后在鼠标指针下读取帧缓冲中的颜色。
编辑: 如果问题是如何从鼠标坐标构造射线,您需要以下内容: 投影矩阵P和相机变换C。如果鼠标指针的坐标为(x, y),视口的大小为(width, height),则沿着射线的剪辑空间中的一个位置为:
mouse_clip = [
  float(x) * 2 / float(width) - 1,
  1 - float(y) * 2 / float(height),
  0,
  1]

(请注意,我翻转了y轴,因为鼠标坐标的原点通常在左上角)

以下也是真实的:

mouse_clip = P * C * mouse_worldspace

这将给出:

mouse_worldspace = inverse(C) * inverse(P) * mouse_clip

我们现在有:

p = C.position(); //origin of camera in worldspace
n = normalize(mouse_worldspace - p); //unit vector from p through mouse pos in worldspace

4
@Tom 从问题中并没有完全明确。无论如何,我已经编辑了我的答案,希望对你有所帮助。 - Andreas Brinck
1
值得注意的是,如果您使用类似于DirectX的矩阵,则乘法顺序将被反转。 - Goz
谢谢 :) 我一开始应该表述得更清楚,但是你编辑后的答案正是我想要的! - Tom J Nowell
2
我不确定错误是在你的代码还是我的代码中,但是当我使用负值的近裁剪平面来作为mouse_clip的z坐标时,我才能让这个算法正常工作。即: mouse_clip = [float(x) * 2 / float(width) - 1, 1 - float(y) * 2 / float(height), -1 * near_clipping_plane, 1] - Alex W
2
只是为了避免其他人的麻烦:如果mouse_clip中的第3个坐标不是0而是-near_depth,则此方法才有效。此外,对于正交矩阵,必须以不同的方式计算p。 - Danvil
由于mouseWorldSpace应用了透视变换,因此它的w坐标应该被除以自身的值吗? - bwroga

26

以下是视椎体:

viewing frustum

首先,您需要确定鼠标单击事件在近平面的哪个位置发生:

  1. 将窗口坐标(0..640,0..480)重新缩放为[-1,1],其中(-1,-1)在左下角,(1,1)在右上角。
  2. 通过乘以我称之为“unview”矩阵来“撤销”投影:unview = (P * M).inverse() = M.inverse() * P.inverse(),其中M是模型视图矩阵,P是投影矩阵。

然后确定相机在世界空间中的位置,并绘制从相机开始并穿过您在近平面上找到的点的射线。

相机位于M.inverse().col(4),即逆模型视图矩阵的最后一列。

最终伪代码:

normalised_x = 2 * mouse_x / win_width - 1
normalised_y = 1 - 2 * mouse_y / win_height
// note the y pos is inverted, so +y is at the top of the screen

unviewMat = (projectionMat * modelViewMat).inverse()

near_point = unviewMat * Vec(normalised_x, normalised_y, 0, 1)
camera_pos = ray_origin = modelViewMat.inverse().col(4)
ray_dir = near_point - camera_pos

你所提到的“modelView”矩阵是什么?它是我们要命中的模型的modelToWorld矩阵和相机的viewMatrix的组合吗? - Jubei
2
我写这篇文章已经有一段时间了,但我认为它是将世界坐标转换为相机坐标的矩阵。如果你的顶点着色器中有像gl_Position = projection * modelView * vertexPos;这样的行,那么中间的部分就是projection矩阵,它是从相机到视口坐标的转换。希望对你有所帮助 :/ - nornagon

2

很简单,其背后的理论总是相同的。

1)将您的2D坐标两次映射到3D空间中(每个API都有自己的函数,但如果您想要的话,可以自己实现)。在Min Z和Max Z处分别进行。

2)使用这两个值计算从Min Z点到Max Z点的向量。

3)使用向量和一个点计算从Min Z到MaxZ的光线。

4)现在你有了一条光线,使用它可以进行射线三角形/射线平面/射线某物交叉并获得结果...


1

我对DirectX经验不多,但我确定它与OpenGL相似。你需要的是gluUnproject调用。

假设您拥有有效的Z缓冲区,您可以使用以下内容查询鼠标位置处Z缓冲区的内容:

// obtain the viewport, modelview matrix and projection matrix
// you may keep the viewport and projection matrices throughout the program if you don't change them
GLint viewport[4];
GLdouble modelview[16];
GLdouble projection[16];
glGetIntegerv(GL_VIEWPORT, viewport);
glGetDoublev(GL_MODELVIEW_MATRIX, modelview);
glGetDoublev(GL_PROJECTION_MATRIX, projection);

// obtain the Z position (not world coordinates but in range 0 - 1)
GLfloat z_cursor;
glReadPixels(x_cursor, y_cursor, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z_cursor);

// obtain the world coordinates
GLdouble x, y, z;
gluUnProject(x_cursor, y_cursor, z_cursor, modelview, projection, viewport, &x, &y, &z);

如果您不想使用glu,您也可以实现gluUnProject,您也可以自己实现它,它的功能相对简单,在opengl.org上有详细描述。


@Tom,就像我之前说的那样,如果你不想使用glu函数,你可以自己实现它的功能,你只需要获取每个模型视图和投影矩阵,并获取每个的z窗口位置即可。 - wich
这需要我找出相同的xyz,然后发布那个xyz并将其标记为答案,而不是这个答案,你明白我的推理吗?反正有人已经发布了数学公式。 - Tom J Nowell

0

好的,这个主题很旧,但它是我在这个主题上找到的最好的,而且它帮助了我一点,所以我会在这里发布给那些正在关注它的人们 ;-)

这是我让它工作的方式,而不必计算投影矩阵的逆:

void Application::leftButtonPress(u32 x, u32 y){
    GL::Viewport vp = GL::getViewport(); // just a call to glGet GL_VIEWPORT
vec3f p = vec3f::from(                        
        ((float)(vp.width - x) / (float)vp.width),
        ((float)y / (float)vp.height),
            1.);
    // alternatively vec3f p = vec3f::from(                        
    //      ((float)x / (float)vp.width),
    //      ((float)(vp.height - y) / (float)vp.height),
    //      1.);

    p *= vec3f::from(APP_FRUSTUM_WIDTH, APP_FRUSTUM_HEIGHT, 1.);
    p += vec3f::from(APP_FRUSTUM_LEFT, APP_FRUSTUM_BOTTOM, 0.);

    // now p elements are in (-1, 1)
    vec3f near = p * vec3f::from(APP_FRUSTUM_NEAR);
    vec3f far = p * vec3f::from(APP_FRUSTUM_FAR);

    // ray in world coordinates
    Ray ray = { _camera->getPos(), -(_camera->getBasis() * (far - near).normalize()) };

    _ray->set(ray.origin, ray.dir, 10000.); // this is a debugging vertex array to see the Ray on screen

    Node* node = _scene->collide(ray, Transform());
   cout << "node is : " << node << endl;
}

这假设了一个透视投影,但问题从一开始就不会出现在正交投影中。


0
我遇到了普通光线拾取的相同情况,但是出现了一些问题。我已经以正确的方式执行了反投影操作,但它就是不起作用。我认为我犯了一些错误,但是无法找出原因。我的矩阵乘法、逆和向量乘以矩阵的所有测试都表现良好。
在我的代码中,我对WM_LBUTTONDOWN做出反应。所以lParam以一个dword返回[Y][X]坐标作为2个字。我提取它们,然后转换为归一化空间,我已经检查过这部分也运行良好。当我点击左下角时,我得到接近-1 -1的值,并且其他3个角的值都很好。然后我使用linepoins.vtx数组进行调试,但它与实际情况完全不符。
unsigned int x_coord=lParam&0x0000ffff; //X RAW COORD
unsigned int y_coord=client_area.bottom-(lParam>>16); //Y RAW COORD

double xn=((double)x_coord/client_area.right)*2-1; //X [-1 +1]
double yn=1-((double)y_coord/client_area.bottom)*2;//Y [-1 +1]

_declspec(align(16))gl_vec4 pt_eye(xn,yn,0.0,1.0); 
gl_mat4 view_matrix_inversed;
gl_mat4 projection_matrix_inversed;
cam.matrixProjection.inverse(&projection_matrix_inversed);
cam.matrixView.inverse(&view_matrix_inversed);

gl_mat4::vec4_multiply_by_matrix4(&pt_eye,&projection_matrix_inversed);
gl_mat4::vec4_multiply_by_matrix4(&pt_eye,&view_matrix_inversed);

line_points.vtx[line_points.count*4]=pt_eye.x-cam.pos.x;
line_points.vtx[line_points.count*4+1]=pt_eye.y-cam.pos.y;
line_points.vtx[line_points.count*4+2]=pt_eye.z-cam.pos.z;
line_points.vtx[line_points.count*4+3]=1.0;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接