Xungerrrr's Blog

Bezier Curve

Compute Graphics - Homework 8

Word count: 1.2kReading time: 6 min
2019/05/29 Share

鼠标事件

捕捉指针的移动

定义一个回调函数mouse_callback,记录指针的位置信息。用glfwSetCursorPosCallback绑定回调函数,就能在鼠标移动时,捕捉到指针的实时坐标。

1
2
3
4
void mouse_callback(GLFWwindow * window, double xpos, double ypos) {
lastX = xpos;
lastY = ypos;
}
1
glfwSetCursorPosCallback(window, mouse_callback);

捕捉鼠标的点击

定义一个回调函数mouse_button_callback,处理鼠标的点击事件。首先,要将指针坐标转换成OpenGL的标准坐标,范围是[-1, 1]。然后,根据按键的不同调用不同的函数。点击左键时,调用addPoint添加一个控制点;点击右键时,调用deletePoint删除最后一个控制点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) {
if (action == GLFW_PRESS) {
float clickX = (lastX - windowWidth / 2) / (windowWidth / 2);
float clickY = -(lastY - windowHeight / 2) / (windowHeight / 2);
switch (button) {
case GLFW_MOUSE_BUTTON_LEFT:
addPoint(clickX, clickY);
break;
case GLFW_MOUSE_BUTTON_RIGHT:
deletePoint();
break;
default:
return;
}
}
}

然后用glfwSetMouseButtonCallback绑定回调函数,就能捕获鼠标点击事件。

1
glfwSetMouseButtonCallback(window, mouse_button_callback);

控制点的变化

控制点的增加

要增加控制点,先按照顶点数组的格式创建一个包含坐标信息的vector,然后将这个vector插入到控制点vector中。添加完之后,调用calculateBezier函数重新计算bezier曲线,做到实时更新曲线。

1
2
3
4
5
void addPoint(float x, float y) {
vector<float> newPoint = { x, y, 0, 1, 1, 1};
controlPoints.insert(controlPoints.end(), newPoint.begin(), newPoint.end());
calculateBezier();
}

控制点的删除

根据定义的顶点数组格式,要删除最后一个控制点,只需要删除控制点vector最后6个元素即可。删除之后,调用calculateBezier函数重新计算bezier曲线,做到实时更新曲线。

1
2
3
4
5
6
void deletePoint() {
if (!controlPoints.empty()) {
controlPoints.erase(controlPoints.begin() + controlPoints.size() - 6, controlPoints.end());
calculateBezier();
}
}

Bezier曲线的计算

首先,要清空原有的bezier曲线。然后,根据Bezier曲线的公式计算即可,其中bernstein函数是伯恩斯坦基函数。t的增加步长设为0.0001。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void calculateBezier() {
bezierPoints.clear();
int n = controlPoints.size() / 6 - 1;
if (n > 1) {
for (float t = 0; t <= 1; t += 0.0001) {
float x = 0, y = 0;
for (int i = 0; i <= n; i++) {
float b = bernstein(i, n, t);
x += controlPoints[i * 6] * b;
y += controlPoints[i * 6 + 1] * b;
}
vector<float> newPoint = { x, y, 0, 1, 1, 1 };
bezierPoints.insert(bezierPoints.end(), newPoint.begin(), newPoint.end());
}
}
}
1
2
3
4
5
6
7
8
float bernstein(int i, int n, float t) {
float result = 1;
for (int j = 0; j < i; j++) {
result *= ((float)(n - j) / (float)(i - j));
}
result *= pow(t, i) * pow(1 - t, n - i);
return result;
}

显示

显示控制点

用GL_POINTS图元,绘制控制点数组中的所有点,用GL_LINE_STRIP图元绘制相邻控制点的连线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (!controlPoints.empty()) {
glViewport(0, 0, windowWidth, windowHeight);
glGenBuffers(1, &pointVBO);
glGenVertexArrays(1, &pointVAO);
glBindVertexArray(pointVAO);
glBindBuffer(GL_ARRAY_BUFFER, pointVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * controlPoints.size(), &controlPoints[0], GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glPointSize(10);
glDrawArrays(GL_POINTS, 0, controlPoints.size() / 6);
for (int i = 0; i < controlPoints.size() / 6 - 1; i++) {
glDrawArrays(GL_LINE_STRIP, i, 2);
}
glBindVertexArray(0);
}

显示Bezier曲线

用GL_POINTS图元,绘制Bezier曲线数组里面的所有点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (!bezierPoints.empty()) {
glGenBuffers(1, &bezierVBO);
glGenVertexArrays(1, &bezierVAO);
glBindVertexArray(bezierVAO);
glBindBuffer(GL_ARRAY_BUFFER, bezierVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float)* bezierPoints.size(), &bezierPoints[0], GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glPointSize(1);
glDrawArrays(GL_POINTS, 0, bezierPoints.size() / 6);
glBindVertexArray(0);
}

Bonus:动态呈现Bezier曲线的生成过程

计算

根据Bezier曲线的原理,每一层的辅助线由该层相邻的辅助点连接而成,每一层的辅助点是由上一层的每一条辅助线取比例t所得,最顶层的辅助点和辅助线是之前画出的控制点和控制线,最底层的辅助点就是比例t对应的Bezier曲线上的点。根据这个原理和t的值,可以通过两层循环算出所有的辅助点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void getVisualizePoints(float t) {
visualizePoints = vector<float>(controlPoints);
int n = controlPoints.size() / 6 - 1;
int offset = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i; j++) {
float x = (1 - t) * visualizePoints[(j + offset) * 6] + t * visualizePoints[(j + 1 + offset) * 6];
float y = (1 - t) * visualizePoints[(j + offset) * 6 + 1] + t * visualizePoints[(j + 1 + offset) * 6 + 1];
std::vector<float> newPoint = { x, y, 0, 1, 1, 1 };
visualizePoints.insert(visualizePoints.end(), newPoint.begin(), newPoint.end());
}
offset += (n - i + 1);
}
}

显示

在渲染循环中,通过时间来控制t的值,使t从0到1循环变化。然后,调用getVisualizePoints获得所有的辅助点。最后,用GL_LINE_STRIP图元绘制所有的辅助线,用GL_POINTS图元绘制所有的辅助点。绘制过程与控制点的绘制类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
float t = (float)(time - (int)time / 10 * 10) / 10;
getVisualizePoints(t);

if (!visualizePoints.empty()) {
glGenBuffers(1, &visualizeVBO);
glGenVertexArrays(1, &visualizeVAO);
glBindVertexArray(visualizeVAO);
glBindBuffer(GL_ARRAY_BUFFER, visualizeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float)* visualizePoints.size(), &visualizePoints[0], GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
int n = controlPoints.size() / 6 - 1;
float offset = n + 1;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
glDrawArrays(GL_LINE_STRIP, j + offset, 2);
}
offset += (n - i);
}
glPointSize(5);
glDrawArrays(GL_POINTS, 0, visualizePoints.size() / 6);
glBindVertexArray(0);
}
CATALOG
  1. 1. 鼠标事件
    1. 1.1. 捕捉指针的移动
    2. 1.2. 捕捉鼠标的点击
  2. 2. 控制点的变化
    1. 2.1. 控制点的增加
    2. 2.2. 控制点的删除
  3. 3. Bezier曲线的计算
  4. 4. 显示
    1. 4.1. 显示控制点
    2. 4.2. 显示Bezier曲线
  5. 5. Bonus:动态呈现Bezier曲线的生成过程
    1. 5.1. 计算
    2. 5.2. 显示