c++ - 在 OpenGL 中用于头部跟踪的倾斜平截头体/离轴投影

标签 c++ opengl 3d openframeworks

我正在尝试在我的应用程序中进行离轴投影,并尝试根据用户的头部位置改变场景的视角。通常,鉴于我必须在屏幕上绘制一个框,我会在屏幕上绘制一个框:

ofBox(350,250,0,50); //ofBox(x, y, z, size); where x, y and z used here are the screen coordinates

要在这里进行离轴投影,我知道我必须按如下方式更改透视投影:

vertFov = 0.5; near = 0.5; aspRatio = 1.33;
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glFrustum(near * (-vertFov * aspRatio + headX),
          near * (vertFov * aspRatio + headX),
          near * (-vertFov + headY),
          near * (vertFov + headY),
          near, far); //frustum changes as per the position of headX and headY
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(headX * headZ, headY * headZ, 0, headX * headZ, headY * headZ, -1);
glTranslate(0,0,headZ);

对于上述情况下的对称平截头体(其中 headX 和 headY 为零),left, right 参数为 -0.33, 0.33 bottom, top 参数为 -0.25, 0.25 并沿着这些坐标建立我的剪裁体积。我尝试使用鼠标模拟离轴进行测试并执行了以下操作:

double mouseXPosition = (double)ofGetMouseX();
double mouseYPosition = (double)ofGetMouseY();
double scrWidth = (double)ofGetWidth();
double scrHeight = (double)ofGetHeight();

headX = ((scrWidth -mouseXPosition) / scrWidth) - 0.5;
headY = (mouseYPosition / scrHeight) - 0.5;
headZ = -0.5; //taken z constant for this mouse test

但是,我打算使用 Kinect,它给我坐标为 (200, 400, 1000), (-250, 600 , 1400), (400, 100, 1400) 等,当我有这些头部位置时,我无法弄清楚如何更改截锥体参数。例如:考虑 0 位于 Kinect 的中心,如果用户移动使得他的位置为 (200, 400, 1000),那么截锥体将如何参数在这里更改?
当还必须考虑从 Kinect 获得的 z-distance 时,如何绘制对象?随着 z 的增加,对象的尺寸必须变小,这可以通过在上述离轴代码中调用 glTrasnlate() 来实现,但坐标系的两个比例是不同(glFrustum 现在将剪切体积设置为 [-0.25,0.33] 到 [0.25,-0.33],其中 Kinect 的数量级为数百 (400,200,1000))。那么如何将 z 值应用于 glFrustum/gluLookAt

最佳答案

首先,您不想使用 gluLookAtgluLookAt 旋转相机,但用户注视的物理屏幕不会旋转。 gluLookAt 只有在屏幕旋转时屏幕法线会一直指向用户时才有效。离轴投影的透视变形将处理我们需要的所有旋转。

您需要在模型中考虑的因素是截锥体中屏幕的位置。考虑下图。红点是屏幕边框。您需要实现的是这些位置在 3D WCS 中保持不变,因为现实世界中的物理屏幕也(希望如此)不会移动。我认为这是对虚拟现实和立体视觉的关键洞察。屏幕就像进入虚拟现实的窗口,要使现实世界与虚拟现实对齐,您需要将截锥体与该窗口对齐。

Awesome MSPaint skills

为此,您必须确定屏幕在 Kinect 坐标系中的位置。假设 Kinect 在屏幕顶部,+y 指向下方,并且您使用的单位是毫米,我希望这些坐标沿着 (+-300, 200, 0), ( +-300、500、0)。

现在远平面有两种可能性。您可以选择使用从相机到远平面的固定距离。这意味着如果用户向后移动,远平面将向后移动,可能会剪切您想要绘制的对象。或者您可以将远平面保持在 WCS 中的固定位置,如图所示。我相信后者更有用。对于近平面,我认为与相机的固定距离是可以的。

输入是屏幕 wcsPtTopLeftScreenwcsPtBottomRightScreen 的 3D 位置,头部的跟踪位置 wcsPtHead,z 值远平面 wcsZFar(均在 WCS 中),以及近平面 camZNear 的 z 值(在相机坐标中)。我们需要计算相机坐标中的平截头体参数。

camPtTopLeftScreen = wcsPtTopLeftScreen - wcsPtHead;
camPtTopLeftNear = camPtTopLeftScreen / camPtTopLeftScreen.z * camZNear;

与右下角相同。还有:

camZFar = wcsZFar - wcsPtHead.z

enter image description here

现在唯一的问题是 Kinect 和 OpenGL 使用不同的坐标系。在 Kinect CS 中,+y 指向下方,+z 从用户指向 Kinect。在 OpenGL 中,+y 指向上方,+z 指向观察者。这意味着我们必须将 y 和 z 乘以 -1:

glFrustum(camPtTopLeftNear.x, camPtBottomRightNear.x,
  -camPtBottomRightNear.y, -camPtTopLeftNear.y, camZNear, camZFar);

如果您想要更好的解释,也包括立体视觉,请查看 this video ,我发现它很有见地并且做得很好。

快速演示,您可能需要调整 wcsWidthpxWidthwcsPtHead.z

#include <glm/glm.hpp>
#include <glm/ext.hpp>
#include <glut.h>
#include <functional>

float heightFromWidth;
glm::vec3 camPtTopLeftNear, camPtBottomRightNear;
float camZNear, camZFar;
glm::vec3 wcsPtHead(0, 0, -700);

void moveCameraXY(int pxPosX, int pxPosY)
{
  // Width of the screen in mm and in pixels.
  float wcsWidth = 520.0;
  float pxWidth = 1920.0f;

  float wcsHeight = heightFromWidth * wcsWidth;
  float pxHeight = heightFromWidth * pxWidth;
  float wcsFromPx = wcsWidth / pxWidth;

  glm::vec3 wcsPtTopLeftScreen(-wcsWidth/2.f, -wcsHeight/2.f, 0);
  glm::vec3 wcsPtBottomRightScreen(wcsWidth/2.f, wcsHeight/2.f, 0);
  wcsPtHead = glm::vec3(wcsFromPx * float(pxPosX - pxWidth / 2), wcsFromPx * float(pxPosY - pxHeight * 0.5f), wcsPtHead.z);
  camZNear = 1.0;
  float wcsZFar = 500;

  glm::vec3 camPtTopLeftScreen = wcsPtTopLeftScreen - wcsPtHead;
  camPtTopLeftNear = camZNear / camPtTopLeftScreen.z * camPtTopLeftScreen;
  glm::vec3 camPtBottomRightScreen = wcsPtBottomRightScreen - wcsPtHead;
  camPtBottomRightNear = camPtBottomRightScreen / camPtBottomRightScreen.z * camZNear;
  camZFar = wcsZFar - wcsPtHead.z;

  glutPostRedisplay();
}

void moveCameraZ(int button, int state, int x, int y)
{
  // No mouse wheel in GLUT. :(
  if ((button == 0) || (button == 2))
  {
    if (state == GLUT_DOWN)
      return;
    wcsPtHead.z += (button == 0 ? -1 : 1) * 100;
    glutPostRedisplay();
  }
}

void reshape(int w, int h)
{
  heightFromWidth = float(h) / float(w);
  glViewport(0, 0, w, h);
}

void drawObject(std::function<void(GLdouble)> drawSolid, std::function<void(GLdouble)> drawWireframe, GLdouble size)
{
  glPushAttrib(GL_ALL_ATTRIB_BITS);
  glEnable(GL_COLOR);
  glDisable(GL_LIGHTING);
  glColor4f(1, 1, 1, 1);
  drawSolid(size);
  glColor4f(0.8, 0.8, 0.8, 1);
  glDisable(GL_DEPTH_TEST);
  glLineWidth(1);
  drawWireframe(size);

  glColor4f(0, 0, 0, 1);
  glEnable(GL_DEPTH_TEST);
  glLineWidth(3);
  drawWireframe(size);
  glPopAttrib();
}

void display(void)
{
  glPushAttrib(GL_ALL_ATTRIB_BITS);
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
  glEnable(GL_DEPTH_TEST);

  // In the Kinect CS, +y points down, +z points from the user towards the Kinect.
  // In OpenGL, +y points up, +z points towards the viewer.
  glm::mat4 mvpCube;
  mvpCube = glm::frustum(camPtTopLeftNear.x, camPtBottomRightNear.x,
    -camPtBottomRightNear.y, -camPtTopLeftNear.y, camZNear, camZFar);
  mvpCube = glm::scale(mvpCube, glm::vec3(1, -1, -1));
  mvpCube = glm::translate(mvpCube, -wcsPtHead);
  glMatrixMode(GL_MODELVIEW); glLoadMatrixf(glm::value_ptr(mvpCube));

  drawObject(glutSolidCube, glutWireCube, 140);

  glm::mat4 mvpTeapot = glm::translate(mvpCube, glm::vec3(100, 0, 200));
  mvpTeapot = glm::scale(mvpTeapot, glm::vec3(1, -1, -1)); // teapots are in OpenGL coordinates
  glLoadMatrixf(glm::value_ptr(mvpTeapot));
  glColor4f(1, 1, 1, 1);
  drawObject(glutSolidTeapot, glutWireTeapot, 50);

  glFlush();
  glPopAttrib();
}

void leave(unsigned char, int, int)
{
  exit(0);
}

int main(int argc, char **argv)
{
  glutInit(&argc, argv);
  glutCreateWindow("glut test");
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);
  moveCameraXY(0,0);
  glutPassiveMotionFunc(moveCameraXY);
  glutMouseFunc(moveCameraZ);
  glutKeyboardFunc(leave);
  glutFullScreen();
  glutMainLoop();
  return 0;
}

以下图像的观看距离应等于其屏幕宽度的 135%(在我的全屏 52 厘米宽屏幕上为 70 厘米)。 enter image description here enter image description here

关于c++ - 在 OpenGL 中用于头部跟踪的倾斜平截头体/离轴投影,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16723674/

相关文章:

python - 使用draw()而不是eventloop时的pyglet

c++ - 如何使用 OpenGL 绘制 2D 图像

c++ - 在 3D 空间中搜索靠近 vector 的点

c++ - 有没有办法在 QT 中对 UI 应用程序进行单元测试

c++ - 是否有可能获得由 'new' 分配的内存块大小?

c++ - 用于包装 SDL 和 OpenGL 绘图方法的虚拟函数的替代方法

algorithm - 创建点图并对新点进行分类

c++ - 视差映射 - GLSL- OpenGL

c++ - MSXML2 : How can I get value of a node?

c++ - 错误 : invalid conversion from 'unsigned char*' to 'const signed char*'