Bezier曲线绘制
这次作业实现的整体架构与之前使用Bresenham算法绘制直线类似:先通过算法所要绘制的点集,然后进行绘制即可。
STEP1 Bezier算法的实现
由于作业中要求是根据用户输入的四个控制点绘制出对应的拟合曲线,所以我们需要实现的是三阶贝塞尔曲线绘制,通过老师的课件我们可以得知三阶贝塞尔曲线对应的公式如下图所示:
其中Pi代表控制点i,系数中的t与我们所选取的步距相关,用于填充出相邻两点间的曲线。实现较为简单,对应的代码如下:
Point getBezier(Point p1, Point p2, Point p3, Point p4, double t) {
Point result;
double c1 = pow((1 - t), 3);
double c2 = pow((1 - t), 2) * 3 * t;
double c3 = 3 * t*t*(1 - t);
double c4 = t * t*t;
result.x = c1 * p1.x + c2 * p2.x + c3 * p3.x + c4 * p4.x;
result.y = c1 * p1.y + c2 * p2.y + c3 * p3.y + c4 * p4.y;
return result;
}
Point
是一个简单的点类。getBezier()
函数的主要功能是对于已知的四个控制点,返回已知t值(步距)对应的点的坐标。下面我们将重点放到如何获取四个控制点上。
STEP2获取控制点
使用glfw
中自带的鼠标回调函数,我们需要自己实现一个MouseCallback
函数来完成对鼠标活动的预期处理。
要实现 左键单击获取控制点,首先是对于鼠标的控制条件:
// button代表对应的按键(鼠标左键),press是按键状态(GLFW_PRESS表示按下,GLFW_RELEASE表示空闲), POINTCOUNT是我们目前已经获取的控制点的数量,是一个全局变量,这里我们期望仅在没有获取足够控制点时对鼠标活动进行回调,故限制POINTCOUNT<4
if (button == GLFW_MOUSE_BUTTON_LEFT && press == GLFW_PRESS && POINTCOUNT < 4)
在上述情况下我们来获取鼠标点击的位置,需要注意的是鼠标对应的坐标系与我们的窗口坐标系是不一致的,所以不能直接将glfwGetCursorPos
得到的坐标值记做控制点,而应该根据自己设置的窗口大小进行相应的处理,这一点在前面的作业中也有提及(hw5)。这里我的窗口大小设置为800x800,对应处理如下:
glfwGetCursorPos(window, &xCur, &yCur);
// MouseClick是一个Point数组,用于存储控制点
MouseClick[POINTCOUNT].x = xCur - 400;
MouseClick[POINTCOUNT].y = -yCur + 400;
接着讲我们得到的控制点写入points数组(用于绘制)中。同Bresenham算法一样,注意对坐标进行标准化:
points[point_index++] = normalize(MouseClick[POINTCOUNT].x);
points[point_index++] = normalize(MouseClick[POINTCOUNT].y);
其中的normalize函数也根据窗口大小发生变化:
float normalize(int input) {
return float(input) / 400;
}
这些工作完成后,我们便可以通过左键点击窗口获取一个控制点,注意每获取一个控制点将POINTCOUNT
加1。
STEP3获取完整的points点集
通过getPoints()
函数实现,较为简单,但注意要首先对获取的四个点按照左右顺序排列,不然使用Bezier算法绘制出的曲线会出错:
void getPoints() {
int offset = 8; // 前八个单位已经用于存储四个控制点
if (POINTCOUNT == 4) {
sortPoints(MouseClick); // 简单的排序(按x值)
for (double t = 0.0; t <= 1.0; t += 0.0005) { // 步长设置为0.005
Point point;
point = getBezier(MouseClick[0], MouseClick[1], MouseClick[2], MouseClick[3], t);
points[offset++] = normalize(point.x);
points[offset++] = normalize(point.y);
point_index = offset;
}
}
}
STEP4 使用户在画完一条曲线后可以调整点的位置
这一步其实是对MouseCallback
函数的进一步丰富和完善。
假设我们在需要调整的点附近右键后选定该点,然后再在期望改动的地方左键单击即可完成调整的过程, 可见实现的逻辑主要包括判定调整点和重新选择点两部分。
- 判定调整点
实现较为简单,首先是鼠标动作:
if (press == GLFW_PRESS && button == GLFW_MOUSE_BUTTON_RIGHT && POINTCOUNT == 4)
由于鼠标点击误差的存在,我们规定一个误差范围为【-20,20】,即用户点击的点在目标点坐标的这个范围内即可被识别出来。实现如下:
glfwGetCursorPos(window, &xCur, &yCur);
Point click;
click.x = xCur - 400;
click.y = -yCur + 400;
// get the reset point
for (int i = 0; i < 4; i++) {
double deltaX = MouseClick[i].x - click.x;
double deltaY = MouseClick[i].y - click.y;
if (fabs(deltaX) <= 20 && fabs(deltaY) <= 20) {
reset = i;
POINTCOUNT--;
}
}
需要注意的是这里我们新引入了一个全局变量reset
,初始值为-1。这一步实现的获取reset值的功能与获取控制点的功能均是在reset=-1的条件下,故应放在一个分支内。
- 获取目标调整点后的处理
这一步是对reset值已经更新后的处理,放在reset>-1的分支下:
else if (reset > -1) {
if (press == GLFW_PRESS && button == GLFW_MOUSE_BUTTON_LEFT && POINTCOUNT < 4) {
glfwGetCursorPos(window, &xCur, &yCur);
MouseClick[reset].x = xCur - 400;
MouseClick[reset].y = -yCur + 400;
points[2 * (reset)] = normalize(MouseClick[reset].x);
points[2 * (reset)+1] = normalize(MouseClick[reset].y);
reset = -1;
POINTCOUNT++; // 每获取新的控制点就加一
}
}
至此基础内容对应的鼠标回调函数已经完成。
Bonus
- GUI可以更改线条颜色
在片段着色器中添加Uniform vec4 Color
变量,并在main函数中处理:
ImGui::ColorEdit3("color", (float*)&color);
glUniform4f(glGetUniformLocation(shaderProgram, "Color"), color.x, color.y, color.z, color.w);
即可。
- 画点时可以消除点
依旧是对鼠标回调函数的处理,这里我实现的是在用户没有画出全部四个点时,鼠标右键单击空白处可以消除最后画出的一个点。显然这个处理也是在reset=-1的分支下进行的:
if (press == GLFW_PRESS && button == GLFW_MOUSE_BUTTON_RIGHT && POINTCOUNT < 4) {
POINTCOUNT--;
point_index -= 2; // 重新写入
}