这是计算机图形学课程的第二个作业。 在这个项目中,我将通过 OpenGL 渲染过山车。输入为是一组点的坐标,我会使用样条插值来创建过山车的轨道,并使用 OpenGL 进行渲染。
成果展示:
项目配置环境
Windows系统
visual studio 2017或更高版本
第三方库:
项目特色:
键盘与鼠标控制:
如何使用?
点击此次获取项目源代码。
技术细节:
在相机在过山车上移动时,除了样条曲线各个顶点的位置外,我们还需要知道样条曲线的切线和法线。这里我将相机放在样条的位置上,面向位置 + 切线方向,并使用法线作为向上向量。 具体为:LookAt(position.x, position.y, position.z, position.x + tangent.x, position.y + tangent.y, position.z + tangent.z, normal.x, normal.y, normal.z);
通过使用 Sloan 算法,渲染结果有以下两方面的问题:
算法选取任意向量 V 作为初始状态。然而,该算法的结果依赖于初始状态,也就是说这个算法是一个随机算法,我们需要一个确定的算法。
在轨道上移动时,镜头前的赛道有坡度,看起来不够真实。
比如我们上面有这样一条轨道,我们要骑在轨道上方,也就是说法线应该指向上。但是 Sloan 的算法无法得到这样的结果。
所以,我是这样做的:
假设P1的切线为v1,P1的下一个顶点的切线为v2。 P1 的 binormal 应为 b1 = v1 × v2。 P1 的法线应为 n1 = v1 × b1。
直到这里,您可能会注意到法线总是指向凹陷区域的外侧,上面的样条显示了这个方法是如何失败的,法线会在凹凸部分的连接处突然翻转。所以,我们需要检查并翻转法线,我通过以下算法实现的:
for normal in normal_list:
if previous_normal * normal < 0: // the joint position
normal = - normal // then flip the normal
previous_normal = normal
下面的黑线是样条曲线,绿线是轨迹的顶点,轨迹是根据样条曲线的法线和 binomial 线生成的。
但请注意,为了计算样条顶点的法线,我使用了 2 个连续切线的叉积,相当于 3 个顶点位置。 3个连续的顶点位置可以确定一个二次函数。 为了使二次函数在关节处平滑,样条至少需要 C2 连续。
当使用 C1 连续的 Catmull Rom 样条曲线时,我得到以下轨迹,关节位置拼接错误。
这个问题可以用C2连续样条来解决,这里我使用 B 样条曲线。唯一的问题是 B 样条没有通过我们提供的顶点。在这里,natural 样条可以解决这个问题,但由于它有很多复杂的计算,本项目没有使用。
课程提到了递归细分算法以生成匀速样条,但是由于函数调用,使用递归算法很慢。这里我想通过使用基于栈的算法来改进这个算法。 您可以查看 rollerCoaster.cpp 文件中的 splineInterpolationRS 函数中的代码。 这是我如何使用堆栈生成匀速样条的伪代码:
start_point = fun(0) // calculate the start point position when u = 0
spline_list.push_back(start_point) // spline_list store all the vertex of the spline
stack<pair<float, float>> subdivision_tree // the stack structure tree
subdivision_tree.push({0, 1}) // push the initial line segment
while (!subdivision_tree.empty()) {
first_u, second_u = subdivision_tree.top()
subdivision_tree.pop() // get the top item
first_point = fun(first_u); second_point = fun(second_u);
if distance(first_point, second_point) < threshold: // push into the list
spline_list.push_back(second_point)
else:
middle_u = (first_u + second_u) / 2
subdivision_tree.push({middle_u, second_u})
subdivision_tree.push({first_u, middle_u })
我以goodRide.sp为例,生成B样条。首先,我使用 BF 算法,每两个点插值 50 个顶点。 然后,我使用上面提到的匀速算法,选择 0.1 作为阈值。 这是我从我创建的两条样条曲线中得到的统计数据:
总计生成 4500 条线段
总长度:154.481
平均长度:0.0686584
长度标准差:0.0200213
最大长度:0.196158
最小长度:0.0253739
总计生成 4363 条线段
总长度:154.48
平均长度:0.0707974
长度标准差:0.0103522
最大长度:0.099981
最小长度:0.0497747
从我得到的标准差,我们可以发现匀速算法生成的样条更加均匀。
我使用的天空盒纹理来自 Unity 标准资源库。天空盒的前、后、左、右、上、下共6张图片。您需要将图像放在正确的位置,图像也可能水平翻转。 首先,我确定了向上的图像,然后将其余图像一张一张地放置并测试正确的顺序和位置。
于是我得到了:
天空盒中有黑色部分。 我检查了我编写的所有代码,没有发现任何错误发生,然后我注意到透视投影函数可能有问题。我把我的天空盒放在 x, y, z = -1000 和 1000 中,巧合的是,透视投影是:
matrix.Perspective(54.0f, (float)w / (float)h, 0.01f, 1000.0f);
在这种情况下,视野不能超过 1000。 因此,我将上述的 far 参数更改为 2000.0f,从而解决了问题。
另外,我根据天空盒太阳的位置调整光源位置,让它看起来更真实。 但是图像的边界也有接缝,我使用以下设置来解决这个问题(下右图)。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
您可以查看 rollerCoaster::matrixMode_ride() 函数以获取更多信息
简而言之,我使用了如下的方法:
初始化变量。 设置当前相机状态(位置、法线、切线),找到样条轨迹的最大和最小高度,根据轨迹的高度计算重力势能。 我们必须让相机能够通过最大高度位置,所以初始能量必须大于总重力势能。
每次更新相机位置时,先计算速度。 然后根据速度和 FPS(frame per second)计算更新距离。
计算当前位置到下一个顶点位置的距离。如果此距离小于更新距离(见下图),则意味着下一个位置必须在 p1 之后。 然后从更新距离中减去这个距离,并设置当前位置等于 p1。 重复第 3 步,直到当前位置到下一个顶点位置的距离小于更新距离。
您可以通过运行程序来查看该内容
即使我设置了 ` c = vec4(0.0f, 0.0f, 0.0f, 0.5f);`, 阴影仍然是完全黑的。
解决方案是,我需要在我的程序中启用 alpha 通道混合。
glEnable(GL_BLEND); // enable blending
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // set blend function
该问题可以通过 4 步法解决:
// 1. Set depth buffer to read-only, draw surface
glDepthMask(GL_FALSE);
drawGroundTexture();
// 2. Set depth buffer to read-write, draw shadow
glDepthMask(GL_TRUE);
drawShadow();
// 3. Set color buffer to read-only, draw surface again
GLboolean colorMask[4];
glGetBooleanv(GL_COLOR_WRITEMASK, colorMask); // save current color mask
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
drawGroundTexture();
// 4. Set color buffer to read-write
glColorMask(colorMask[0], colorMask[1], colorMask[2], colorMask[3]);
最终,我得到了以下的结果(我更换了一张背景纹理)