如何在顶部 2D 小地图上显示以 3D 视角呈现的平面世界的可见部分?

How to show visible part of planar world rendered with 3D perspective on topside 2D minimap?

序言

问答是对以下内容的重制:

由于缺乏信息且原作者没有回应而被关闭(并且第一次重新开放周期失败)。不过,我认为这是一个有趣的问题,所以我决定自己提问并回答(这次有所有需要的规格)。

问题

假设我们的世界是一个均匀的矩形正方形网格(由二维瓷砖阵列表示)映射在一个平面上(比如说平面 XY (Z=0.0)简单性)并使用透视投影渲染。像这样:

如何将透视平截头体(map/plane 的可见部分)映射到小地图上的红色多边形?

为了更通用,我们将其假设为输入:

并且想要输出:

局限性(或多或少地匹配原始问题):

所以我们想要的是获得我们的平面(Z=0.0)和相机平截头体之间的4个交点。所以我们的想法是从相机焦点投射 4 条光线(一个用于平截头体的每个边缘)并简单地计算 ray/plane 交点。由于平面是Z=0.0,交点也有Z=0.0,所以交点很容易计算。

  1. 为每个 corner/edge

    投射光线

    从相机焦点到屏幕角落(在屏幕中space)

    并将其转换为世界全局坐标(通过恢复透视和使用逆模型视图矩阵,稍后描述)。射线应为以下形式:

    p(t) = p + dp*t
    

    其中p为焦点,dp为方向向量(不需要归一化)

  2. 计算与 XY 平面的交点 (Z=0.0)

    作为 z=0.0 然后:

    0 = p.z + dp.z*t
    t = -p.z/dp.z
    

    所以我们可以直接计算交点。

  3. 将 3D 交叉点转换为地图内的 u,v

    对于那个简单的点积就足够了。所以如果 p 是我们的交点那么:

    u = dot(p-p0,du)
    v = dot(p-p0,dv)
    

    其中 u,v 是二维地图数组或小地图中的坐标。如果您的 u,v 是轴对齐的,那么您可以直接使用 (p.x-p0.x,p.y-p0.y) 而无需任何点积

如何将点p从相机坐标转换为全局世界坐标:

  1. 反转视角

    先获取透视矩阵参数

    double per[16],zNear,zFar,fx,fy;
    glGetDoublev(GL_PROJECTION_MATRIX,per);
    zFar =0.5*per[14]*(1.0-((per[10]-1.0)/(per[10]+1.0)));
    zNear=zFar*(per[10]+1.0)/(per[10]-1.0);
    fx=per[0];
    fy=per[5];
    

    这将为您提供近平面和远平面的平截头体以及 x,y 轴的缩放比例。现在恢复透视只是像这样反转透视鸿沟:

    p[1]*=(-p[2]/fy);                   // apply inverse of perspective
    p[0]*=(-p[2]/fx);
    

    投射光线需要 znearzfar。有关详细信息,请参阅:

    • depth buffer got by glReadPixels is always 1
  2. global 世界坐标

    在我们的 p 上简单地使用 ModelView 矩阵的逆矩阵。所以先获取矩阵:

    double cam[16];
    glGetDoublev(GL_MODELVIEW_MATRIX,cam);
    

    作为反向你可以使用我的 所以现在最后一步是:

    p = Inverse(cam)*p;
    

    但不要忘记 p 必须是齐次的,因此 (x,y,z,1) 点和 (x,y,z,0) 向量。

如果您缺乏背景知识或需要vector/matrix数学,请看这里:

这里是 C++ 的小例子:

//---------------------------------------------------------------------------
void matrix_mul_vector(double *c,double *a,double *b)
    {
    double q[3];
    q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2])+(a[12]);
    q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2])+(a[13]);
    q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2])+(a[14]);
    for(int i=0;i<3;i++) c[i]=q[i];
    }
//---------------------------------------------------------------------------
void  matrix_inv(double *a,double *b) // a[16] = Inverse(b[16])
    {
    double x,y,z;
    // transpose of rotation matrix
    a[ 0]=b[ 0];
    a[ 5]=b[ 5];
    a[10]=b[10];
    x=b[1]; a[1]=b[4]; a[4]=x;
    x=b[2]; a[2]=b[8]; a[8]=x;
    x=b[6]; a[6]=b[9]; a[9]=x;
    // copy projection part
    a[ 3]=b[ 3];
    a[ 7]=b[ 7];
    a[11]=b[11];
    a[15]=b[15];
    // convert origin: new_pos = - new_rotation_matrix * old_pos
    x=(a[ 0]*b[12])+(a[ 4]*b[13])+(a[ 8]*b[14]);
    y=(a[ 1]*b[12])+(a[ 5]*b[13])+(a[ 9]*b[14]);
    z=(a[ 2]*b[12])+(a[ 6]*b[13])+(a[10]*b[14]);
    a[12]=-x;
    a[13]=-y;
    a[14]=-z;
    }
//---------------------------------------------------------------------------
void draw_map()
    {
    int i,j;
    double u,v,p[3],dp[3];

    // here 3D view must be already set (modelview,projection)
    glDisable(GL_CULL_FACE);

    // [draw 3D map]
    const int n=30;                 // map size
    double p0[3]={0.0,0.0,0.0};     // map start point
    double du[3]={1.0,0.0,0.0};     // map u step (size of grid = 1.0 )
    double dv[3]={0.0,1.0,0.0};     // map v step (size of grid = 1.0 )
    glColor3f(0.5,0.7,1.0);
    glBegin(GL_LINES);
    for (j=0;j<=n;j++)
        {
        for (i=0;i<3;i++) p[i]=p0[i]+(double(j)*du[i])+(double(0)*dv[i]); glVertex3dv(p);
        for (i=0;i<3;i++) p[i]=p0[i]+(double(j)*du[i])+(double(n)*dv[i]); glVertex3dv(p);
        for (i=0;i<3;i++) p[i]=p0[i]+(double(0)*du[i])+(double(j)*dv[i]); glVertex3dv(p);
        for (i=0;i<3;i++) p[i]=p0[i]+(double(n)*du[i])+(double(j)*dv[i]); glVertex3dv(p);
        }
    glEnd();

    // [compute trapeze points]
    double cam[16],per[16],pt[4][3],zNear,zFar,fx,fy;
    glGetDoublev(GL_PROJECTION_MATRIX,per); // obtain matrices
    glGetDoublev(GL_MODELVIEW_MATRIX,cam);
    matrix_inv(cam,cam);
    zFar =0.5*per[14]*(1.0-((per[10]-1.0)/(per[10]+1.0)));
    zNear=zFar*(per[10]+1.0)/(per[10]-1.0);
    fx=per[0];
    fy=per[5];
    for (j=0;j<4;j++)                       // 4 corners
        {
        for (i=0;i<3;i++) dp[i]=0.0;        // cast ray from camera focus dp
        if (j==0) { p[0]=-1.0; p[1]=-1.0; } // to screen corner p
        if (j==1) { p[0]=-1.0; p[1]=+1.0; }
        if (j==2) { p[0]=+1.0; p[1]=+1.0; }
        if (j==3) { p[0]=+1.0; p[1]=-1.0; }
        p[2]=zNear;                         // start position at screen plane
        p[1]*=(-p[2]/fy);                   // apply inverse of perspective
        p[0]*=(-p[2]/fx);
        // transform to worlds global coordinates
        matrix_mul_vector( p,cam, p);
        matrix_mul_vector(dp,cam,dp);
        // compute intersection of ray and XY plane (z=0) as pt[j] (i exploited the fact that the intersection have z=0.0 for arbitrary plane it would be a bit more complicated)
        for (i=0;i<3;i++) dp[i]=p[i]-dp[i];
        u=p[2]/dp[2];
        if (u<0.0) u=(p[2]-zFar)/dp[2];     // no intersection means "infinite" visibility
        for (i=0;i<3;i++) pt[j][i]=p[i]-(u*dp[i]);
        u=0.0;
        }

    // [draw 2D minimap]
    GLint vp0[4];
    GLint vp1[4]={10,10,150,150};           // minimap position and size ppixels[
    double q0[2]={-1.0,-1.0 };              // minimap start point
    double eu[2]={2.0/double(n),0.0};       // minimap u step
    double ev[2]={0.0,2.0/double(n)};       // minimap v step

    // set 2D view for minimap
    glDisable(GL_DEPTH_TEST);
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    glGetIntegerv(GL_VIEWPORT,vp0);
    glViewport(vp1[0],vp1[1],vp1[2],vp1[3]);

    glColor3f(0.0,0.0,0.0);                 // clear background
    glBegin(GL_QUADS);
    for (i=0;i<2;i++) p[i]=q0[i]+(double(0)*eu[i])+(double(0)*ev[i]); glVertex2dv(p);
    for (i=0;i<2;i++) p[i]=q0[i]+(double(n)*eu[i])+(double(0)*ev[i]); glVertex2dv(p);
    for (i=0;i<2;i++) p[i]=q0[i]+(double(n)*eu[i])+(double(n)*ev[i]); glVertex2dv(p);
    for (i=0;i<2;i++) p[i]=q0[i]+(double(0)*eu[i])+(double(n)*ev[i]); glVertex2dv(p);
    glEnd();

    glColor3f(0.15,0.15,0.15);              // grid
    glBegin(GL_LINES);
    for (j=0;j<=n;j++)
        {
        for (i=0;i<2;i++) p[i]=q0[i]+(double(j)*eu[i])+(double(0)*ev[i]); glVertex2dv(p);
        for (i=0;i<2;i++) p[i]=q0[i]+(double(j)*eu[i])+(double(n)*ev[i]); glVertex2dv(p);
        for (i=0;i<2;i++) p[i]=q0[i]+(double(0)*eu[i])+(double(j)*ev[i]); glVertex2dv(p);
        for (i=0;i<2;i++) p[i]=q0[i]+(double(n)*eu[i])+(double(j)*ev[i]); glVertex2dv(p);
        }
    glEnd();

    glColor3f(0.5,0.5,0.5);                 // border of minimap
    glLineWidth(2.0);
    glBegin(GL_LINE_LOOP);
    for (i=0;i<2;i++) p[i]=q0[i]+(double(0)*eu[i])+(double(0)*ev[i]); glVertex2dv(p);
    for (i=0;i<2;i++) p[i]=q0[i]+(double(n)*eu[i])+(double(0)*ev[i]); glVertex2dv(p);
    for (i=0;i<2;i++) p[i]=q0[i]+(double(n)*eu[i])+(double(n)*ev[i]); glVertex2dv(p);
    for (i=0;i<2;i++) p[i]=q0[i]+(double(0)*eu[i])+(double(n)*ev[i]); glVertex2dv(p);
    glEnd();
    glLineWidth(1.0);

    // 2D minimap render of the pt[]
    glColor3f(0.7,0.1,0.1);                 // trapeze
    glBegin(GL_LINE_LOOP);
    for (j=0;j<4;j++)
        {
        // get u,v from pt[j]
        for (i=0;i<3;i++) p[i]=pt[j][i]-p0[i];
        for (u=0.0,i=0;i<3;i++) u+=p[i]*du[i];
        for (v=0.0,i=0;i<3;i++) v+=p[i]*dv[i];
        // convert to 2D position and render
        for (i=0;i<2;i++) p[i]=q0[i]+(u*eu[i])+(v*ev[i]); glVertex2dv(p);
        }
    glEnd();

    // restore 3D view
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glViewport(vp0[0],vp0[1],vp0[2],vp0[3]);
    glEnable(GL_DEPTH_TEST);
    }
//---------------------------------------------------------------------------

并预览:

如你所见,我们只需要矩阵*向量乘法和伪逆矩阵函数(所有其他像 dot,+,- 都非常简单,直接编码为内联代码)并且两者都足够简单,可以直接实现它在代码中,因此不需要 GLM 或类似的库。

我也懒得将 4 点多边形裁剪到小地图大小,所以我使用 glViewport 帮我完成了。

这里是Win32 BDS2006 VCL/C++/OpenGL1.0 Demo:

只是select下载慢然后输入图片验证码。它不使用除 GL,GLU 之外的任何第 3 方库。相机是静态的,因此只需根据自己的喜好添加 keyboard/mouse 事件即可。如果您想将其移植到您的环境中,只需模仿事件行为并忽略 VCL 内容。

OpenGL init是基于此完成的:

我刚刚从中删除了 GLEW、GLSLVAO 内容。