OpenGL 编程/迷你门户平滑
(进行中)
在 上一节 中,我们实现了一个基本的运行门户系统。
一个令人讨厌的问题是我们穿过门户时遇到的闪烁。让我们看看如何解决它。
为了理解闪烁,我们需要了解 OpenGL 摄像机的工作原理。
当我们使用它的变换矩阵来定位摄像机时,我们实际上并没有定位屏幕本身。正如我们在图中看到的,OpenGL 摄像机是一个截断的锥体(在数学行话中称为视锥体)。我们在 onIdle
函数中设置投影矩阵时使用 glm::perspective
定义了它。
// Projection
glm::mat4 camera2screen = glm::perspective(
45.0f, // FOVy
1.0f*screen_width/screen_height, // aspect ratio
0.1f, // zNear
100.0f // zFar
);
透视矩阵将 zNear
和 zFar
之间的所有物体投影到近裁剪平面。它不会显示摄像机位置和近裁剪平面之间的任何内容。
当摄像机与门户的距离小于 zNear
时,就会出现闪烁:在这种情况下,摄像机位置在门户之前,但屏幕已经在门户之后!闪烁实际上是在我们传送摄像机之前,快速闪过门户后面的场景。
直观的思路是提前传送:如果摄像机距离门户 zNear
很近,我们可以立即将它传送。当面对门户时,这很好用 - 但是如果我们横向(侧向移动)穿过门户呢?在这种情况下,问题将无法解决。
真正的问题主要出在模板缓冲区,因为模板缓冲区定义了绘制的门户场景部分。模板缓冲区是通过将门户渲染到其中构建的,因此会受到摄像机近裁剪平面移到门户之外的影响。如果我们可以在摄像机近裁剪平面在门户后面时继续在模板缓冲区上绘制,我们就赢了。
我们在本文档中提出的解决方案是使门户具有体积,与摄像机位置和近裁剪平面之间的体积相匹配。当近裁剪平面通过门户正面时,一个定制的背面将出现并被渲染到模板缓冲区。向所有方向。
基本上,这都是关于作弊;但我们从一开始就在作弊 :)
仍然存在一个细微的问题:即使近裁剪平面在门户体积形状内,也可能在门户内部存在物体,通常是地面。
更完整的解决方案是通过计算门户体积和摄像机近裁剪矩形之间的交集来更精确地绘制模板缓冲区。
但通常情况下,你会将门户应用于墙壁,因此在门户后面不应该有任何东西。
在我们的演示中,可以使用 zNear
的合理较小值(例如 0.01
)来解决地面问题。
/* Global */
static float zNear = 0.01;
static float fovy = 45;
使用此值,只有当玩家在看脚的同时穿过门户时,才能稍微注意到地面。
将 zNear
设置为非常小的值(例如 0.000001
)很诱人。但是,如果 zNear
与 zFar
相比太小,深度缓冲区将失去其精度。然后你会注意到你的网格中的闪烁,因为三角形将交替地彼此绘制。所以不要 :)
修改 zNear
在开发我们的修复时会很方便,因为我们可以将其设置为 1.0
等较大的值,以更清晰地查看近裁剪平面的延期。
待办事项:解释数学
void create_portal(Mesh* portal, int screen_width, int screen_height, float zNear, float fovy) {
portal->vertices.clear();
portal->elements.clear();
float aspect = 1.0 * screen_width / screen_height;
float fovy_rad = fovy * M_PI / 180;
float fovx_rad = fovy_rad / aspect;
float dz = max(zNear/cos(fovx_rad), zNear/cos(fovy_rad));
float dx = tan(fovx_rad) * dz;
float dy = tan(fovy_rad) * dz;
glm::vec4 portal_vertices[] = {
glm::vec4(-1, -1, 0, 1),
glm::vec4( 1, -1, 0, 1),
glm::vec4(-1, 1, 0, 1),
glm::vec4( 1, 1, 0, 1),
glm::vec4(-(1+dx), -(1+dy), 0-dz, 1),
glm::vec4( (1+dx), -(1+dy), 0-dz, 1),
glm::vec4(-(1+dx), (1+dy), 0-dz, 1),
glm::vec4( (1+dx), (1+dy), 0-dz, 1),
};
for (unsigned int i = 0; i < sizeof(portal_vertices)/sizeof(portal_vertices[0]); i++) {
portal->vertices.push_back(portal_vertices[i]);
}
GLushort portal_elements[] = {
0,1,2, 2,1,3,
4,5,6, 6,5,7,
0,4,2, 2,4,6,
5,1,7, 7,1,3,
};
for (unsigned int i = 0; i < sizeof(portal_elements)/sizeof(portal_elements[0]); i++) {
portal->elements.push_back(portal_elements[i]);
}
}
我们看到门户依赖于屏幕宽度和屏幕高度(用于透视纵横比)。因此,当 OpenGL 窗口调整大小时,我们需要重建门户形状 - 让我们修改 GLUT 的reshape 回调
void onReshape(int width, int height) {
screen_width = width;
screen_height = height;
glViewport(0, 0, screen_width, screen_height);
create_portal(&portals[0], screen_width, screen_height, zNear, fovy);
create_portal(&portals[1], screen_width, screen_height, zNear, fovy);
}