OpenGL 编程/迷你传送门
在本系列中,我们将创建一个类似于 Valve 的传送门游戏中使用的传送系统。[1]
我们想要实现一个传送装置,其中源和目的地表示为墙壁上的洞,你可以透过它们看到。进入源传送门和走出目的地传送门应该是完全无缝的。也可以将两个传送门面对面放置,就像镜子一样,以创造无限的深度。
直观的做法是在 后期处理 中放置第二个摄像机并渲染到纹理。但是,纹理会被扭曲以映射到它们所附加的物体上,特别是当你从侧面观察它时。通常它们看起来像一个平面的电视屏幕[2] - 但我们现在谈论的是传送门!
因为我们想要一个无缝的效果,所以我们将使用不同的方法。场景将被渲染两次
- 一次就像玩家已经传送了,考虑到玩家到传送门的距离。我们将使用 模板缓冲区 将场景剪切到传送门的边界。
- 一次正常渲染,不覆盖传送门,通过欺骗深度缓冲区实现。
为了实现传送门,我们需要一些先决条件
以下是我们将解决的实现传送门系统的一些要点
- 透过传送门查看
- 在远处矩形中绘制模板
- 在矩形与摄像机相交的地方绘制模板
- 碰撞检测
- 扭曲
- 无限/递归传送门显示(传送门相互面对)
- 物体同时出现在两个位置(复制物体)
- 优化
- 物理
绘制传送门将涉及从另一侧传送门绘制场景,所以为了避免任何问题,让我们启用背面剔除,只绘制正面多边形。
/* main() */
glEnable(GL_CULL_FACE);
所以我们有两个传送门
- 源传送门(portal1)
- 目的地传送门(portal2)
我们将它们表示为经典的网格物体(参见 基础 教程):一组顶点来定义传送门的形状,以及一个物体到世界变换矩阵。
/* Global */
Mesh portals[2];
/* init_resources() */
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),
};
for (unsigned int i = 0; i < sizeof(portal_vertices)/sizeof(portal_vertices[0]); i++) {
portals[0].vertices.push_back(portal_vertices[i]);
portals[1].vertices.push_back(portal_vertices[i]);
}
GLushort portal_elements[] = {
0,1,2, 2,1,3,
};
for (unsigned int i = 0; i < sizeof(portal_elements)/sizeof(portal_elements[0]); i++) {
portals[0].elements.push_back(portal_elements[i]);
portals[1].elements.push_back(portal_elements[i]);
}
// 90° angle + slightly higher
portals[0].object2world = glm::translate(glm::mat4(1), glm::vec3(0, 1, -2));
portals[1].object2world = glm::rotate(glm::mat4(1), -90.0f, glm::vec3(0, 1, 0))
* glm::translate(glm::mat4(1), glm::vec3(0, 1.2, -2));
portals[0].upload();
portals[1].upload();
所以我们需要在传送门2 位置放置一个新的摄像机,然后向后移动以覆盖从传送门1 到原始摄像机的距离。幸运的是,可以通过组合变换矩阵轻松完成(记住从后往前读取矩阵乘法)。
/**
* Compute a world2camera view matrix to see from portal 'dst', given
* the original view and the 'src' portal position.
*/
glm::mat4 portal_view(glm::mat4 orig_view, Mesh* src, Mesh* dst) {
glm::mat4 mv = orig_view * src->object2world;
glm::mat4 portal_cam =
// 3. transformation from source portal to the camera - it's the
// first portal's ModelView matrix:
mv
// 2. object is front-facing, the camera is facing the other way:
* glm::rotate(glm::mat4(1.0), glm::radians(180.0f), glm::vec3(0.0,1.0,0.0))
// 1. go the destination portal; using inverse, because camera
// transformations are reversed compared to object
// transformations:
* glm::inverse(dst->object2world)
;
return portal_cam;
}
我们现在有了新的世界到摄像机 (View) 矩阵。我们可以将其传递给我们的着色器,使它们从这个新的视角渲染场景。
/* onDisplay */
glm::mat4 portal_view = portal_view(transforms[MODE_CAMERA], &portals[0], &portals[1]);
glUniformMatrix4fv(uniform_v, 1, GL_FALSE, v);
glUniformMatrix4fv(uniform_v_inv, 1, GL_FALSE, glm::value_ptr(glm::inverse(portal_view)));
main_object.draw();
ground.draw();
...
/* then reset the view and re-draw the scene as usual */
你可以以同样的方式对第二个传送门进行操作。
目前我们只是渲染场景两次:从传送门 2 和从主摄像机,但第二次渲染覆盖了第一次。
诀窍是在深度缓冲区中绘制传送门,但不要写入颜色缓冲区:OpenGL 会理解在传送门上显示了某些内容,但不会用空白矩形覆盖它。
我们还注意保存和恢复之前的颜色/深度配置。
// Draw portal in the depth buffer so they are not overwritten
glClear(GL_DEPTH_BUFFER_BIT);
GLboolean save_color_mask[4];
GLboolean save_depth_mask;
glGetBooleanv(GL_COLOR_WRITEMASK, save_color_mask);
glGetBooleanv(GL_DEPTH_WRITEMASK, &save_depth_mask);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_TRUE);
for (int i = 0; i < 2; i++)
portals[i].draw();
glColorMask(save_color_mask[0], save_color_mask[1], save_color_mask[2], save_color_mask[3]);
glDepthMask(save_depth_mask);
乍一看,我们似乎不需要剪切传送门场景,因为我们无论如何都会覆盖它的周围环境,并且深度缓冲区已经保护了传送门。
但是
- 这迫使你使用天空盒重写整个场景背景:你不能再仅仅依靠
glClear(GL_COLOR_BUFFER_BIT)
来清除背景,因为它可能被传送门视图写入,而主场景不会覆盖那部分。 - 这没有优化,因为即使对于一小部分传送门,你也会重新绘制整个屏幕。
- 最重要的是:当我们显示两个传送门(不仅仅是一个)时,第二个传送门视图的深度与第一个的冲突。
即使我们在渲染第二个传送门视图时重新绘制第一个传送门的深度,但这本来是为了在渲染主视图时保护传送门 - 第二个传送门视图中的物体可能离摄像机更近。因此,深度缓冲区不足以在绘制第二个传送门时保护第一个传送门。
有更强大且相关的保护屏幕一部分的方法
- 剪刀 (矩形剪切)
- 模板缓冲区 (任意剪切)
因为我们可以从侧面观察传送门,或者使用矩形以外的形状绘制它,所以剪刀不够,所以我们将使用模板缓冲区。
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
glStencilFunc(GL_NEVER, 0, 0xFF);
glStencilOp(GL_INCR, GL_KEEP, GL_KEEP); // draw 1s on test fail (always)
// draw stencil pattern
glClear(GL_STENCIL_BUFFER_BIT); // needs mask=0xFF
glUniformMatrix4fv(uniform_v, 1, GL_FALSE, transforms[MODE_CAMERA]);
portal->draw();
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
/* Fill 1 or more */
glStencilFunc(GL_LEQUAL, 1, 0xFF);
glUniformMatrix4fv(uniform_v, 1, GL_FALSE, portal_view);
// -Ready to draw main scene-
想法是检测摄像机移动(一条线)与传送门(两个三角形)之间的交点。
所以我们想要编写一个函数,检查由两个点 la
和 lb
定义的直线是否与传送门相交。
维基百科来帮忙!
我们有一个很好的矩阵可以计算
- 如果 ,则与平面相交。
- 如果 ,则交点位于三角形内部。
为了在 C++ 中实现它,请注意,在初始化矩阵时,每个 glm::vec3 都是一个 *列* 向量,因此与数学符号相比,这些值是旋转的。
我们还需要在比较周围添加一些小值(1e-6
),以确保没有浮点精度问题。
在视图坐标中工作以简化方程似乎很有诱惑力,但如果你以后想要传送物体(一个立方体?:),无论如何你都需要在物体坐标中工作。
/**
* Checks whether the line defined by two points la and lb intersects
* the portal.
*/
int portal_intersection(glm::vec4 la, glm::vec4 lb, Mesh* portal) {
if (la != lb) { // camera moved
// Check for intersection with each of the portal's 2 front triangles
for (int i = 0; i < 2; i++) {
// Portal coordinates in world view
glm::vec4
p0 = portal->object2world * portal->vertices[portal->elements[i*3+0]],
p1 = portal->object2world * portal->vertices[portal->elements[i*3+1]],
p2 = portal->object2world * portal->vertices[portal->elements[i*3+2]];
// Solve line-plane intersection using parametric form
glm::vec3 tuv =
glm::inverse(glm::mat3(glm::vec3(la.x - lb.x, la.y - lb.y, la.z - lb.z),
glm::vec3(p1.x - p0.x, p1.y - p0.y, p1.z - p0.z),
glm::vec3(p2.x - p0.x, p2.y - p0.y, p2.z - p0.z)))
* glm::vec3(la.x - p0.x, la.y - p0.y, la.z - p0.z);
float t = tuv.x, u = tuv.y, v = tuv.z;
// intersection with the plane
if (t >= 0-1e-6 && t <= 1+1e-6) {
// intersection with the triangle
if (u >= 0-1e-6 && u <= 1+1e-6 && v >= 0-1e-6 && v <= 1+1e-6 && (u + v) <= 1+1e-6) {
return 1;
}
}
}
}
return 0;
}
当此测试检查时,我们会扭曲相机。这很容易,因为我们已经知道如何使用 portal_view()
计算它的变换矩阵!
/* onIdle() */
glm::mat4 prev_cam = transforms[MODE_CAMERA];
// Update camera position depending on keyboard keys
...
/* Handle portals */
// Movement of the camera in world view
for (int i = 0; i < 2; i++) {
glm::vec4 la = glm::inverse(prev_cam) * glm::vec4(0.0, 0.0, 0.0, 1.0);
glm::vec4 lb = glm::inverse(transforms[MODE_CAMERA]) * glm::vec4(0.0, 0.0, 0.0, 1.0);
if (portal_intersection(la, lb, &portals[i]))
transforms[MODE_CAMERA] = portal_view(transforms[MODE_CAMERA], &portals[i], &portals[(i+1)%2]);
}
至此,您拥有一个基本的传送门,显示来自兄弟传送门的视图,并在触碰时进行传送。
但是,根据速度,您可能会在传送之前看到屏幕闪烁。在下一节中,我们将尝试了解导致此问题的原因以及如何解决它。
- ↑ 不要与BSP 传送门混淆。
- ↑ 您可以在BlenderNation(指向BlenderArtists.org 帖子 - 需要论坛注册)上看到使用 Blender 实现的电视式传送门。