前言
本实验有参考,如果想直接看原理或者源代码,以下材料更易理解:
(34条消息) 基于C++、OpenGL绘制贝塞尔曲线(Bézier curve)_potato-mine的博客-CSDN博客_c++画曲线图
(34条消息) opengl绘制三次hermite曲线,三次cardinal曲线_Marco&GalaxyDragon的博客-CSDN博客_opengl 画的贝塞尔曲线不连续
因为跟着参考做的时候,在实验时遇到一些异常的结果(指下图这种情况),所以记录了下来。
实验7《复杂图形绘制1》
一、实验目的
学习样条曲线的绘制。
二、实验内容
1. 绘制Bezier曲线;
2. 绘制Hermite曲线。
三、实验方法
(一)Bezier曲线
首先了解什么是Bezier曲线。如下图所示,作AB,BC,在AB,BC分别取点D,E,使得AD/DB=BE/EC,然后再在DE上去点F,使得AD/DB=BE/EC=DF/FE。取满足条件的所有点F,即为Bezier曲线。
给定点P0, P1, P2, ..., Pn, 则n次贝塞尔曲线由下式给出:
设计的程序希望可以满足以下功能:
(1)点击窗口内任意区域作点,并根据点生成Bezier曲线;
(2)清空窗口内所有的点,重新绘制;
(3)移动已经生成的点,以此改变已经生成的Bezier曲线。
- Hermite曲线
Hermite曲线通过初始点,初始速度(切向量),最终点,最终速度(切向量)来计算曲线。假设三次多项式函数P(t) = c0 + c1*x + c2*x² + c3*x³,初始坐标点为p0,初始速度v0,终点坐标p1,终点速度v1,H0(t) = 1-3t²+2t³; H1(t) = t - 2t²+ t³; H2(t) = -t²+t³; H3(t) = 3t²-2t³,则有:
设计的程序希望可以满足以下功能:
- 绘制Hermite曲线;
- 用户可以手动调整切向量和端点坐标。
四、实验步骤
(一)Bezier曲线
1. 准备算法
(1)结构体point:保存点的坐标(x, y);
(2)mouse_click:判断用户操作是作点还是移动点,并根据用户操作执行结果;
(3)mouse_motion:用户移动点;
(4)keyboard:根据键盘的操作执行结果。
(5)DrawBeziercurve:计算与绘制Bezier曲线;
(6)display:进行画点、画辅助直线、画Bezier曲线等操作;
(7)其他函数、变量、结构体、声明等。
2. 编写代码
(1)结构体point
新建结构体point数组,元素个数为MAXPOINTS,意味着用户最多可以绘制MAXPOINTS个点。
(2)mouse_click
当用户点击鼠标左键,且当前还可新建点时,判断鼠标点击的坐标是否在已绘制点的附近,如果不在则新建一个点,并发送重绘消息;否则程序将认为用户希望移动某个已存在的点。
if (state == GLUT_DOWN && click_state == NEWPOINT) { //新增点
if (control_points_n < MAXPOINTS) {
P[control_points_n].x = x;
P[control_points_n].y = screen_height - y;
specific_p = control_points_n;
control_points_n++;
glutPostRedisplay();
} else click_state = POINTREACHMAX; //无法新增控制点,只能移动点
} else if (state == GLUT_DOWN && click_state == MOVEPOINT) { //移动点
P[specific_p].x = x;
P[specific_p].y = screen_height - y;
glutPostRedisplay();
} else if (state == GLUT_UP) {
//判断控制点是否已达上限
click_state = control_points_n < MAXPOINTS ? NEWPOINT : POINTREACHMAX;
}
}
(3)mouse_motion
若用户的点击状态表示希望移动点,则修改该点的坐标为用户释放鼠标的坐标,并发送重绘消息。
- keyboard
根据键盘的操作执行结果,当用户点击r时表示希望重新绘制,则屏幕清空;若用户点击esc键则表示退出程序。
(5)DrawBeziercurve
计算Bezier曲线。
float t = (float)i / (float)m; //i从0到m
for (j = 0; j < control_points_n; j++) {
Beziercurve_pointx += C[j] * pow(1 - t, control_points_n - j - 1) * pow(t, j) * p[j].x;
Beziercurve_pointy += C[j] * pow(1 - t, control_points_n - j - 1) * pow(t, j) * p[j].y;
}
(6)display:进行画点、画辅助直线、画Bezier曲线等操作。
void display(void) {
int i;
glClear(GL_COLOR_BUFFER_BIT);
for (i = 0; i < control_points_n; i++) { //画点
Draw_Square(P[i].x, P[i].y);
}
//显示点坐标
gotoxy(0, specific_p + 3);
printf("P%d:(%d, %d) ", specific_p, P[specific_p].x, P[specific_p].y);
glLineStipple(1, 0x0f0f);
glBegin(GL_LINE_STRIP);
glColor3f(1, 1, 1);
for (i = 0; i < control_points_n; i++) { //画辅助虚线
glVertex2f(P[i].x, P[i].y);
}
glEnd();
if (control_points_n > 1) { //画贝塞尔曲线
DrawBeziercurve(control_points_n - 1, P, 200);
}
glutSwapBuffers();
}
(7)其他函数、变量、结构体、声明等。
3. 修改错误与完善程序
起初因为不太理解Bezier曲线是什么,所以在计算的函数里频频出错;在绘制时也一时不知道应该如何画出曲线来。后来发现可以用多组短小线段来近似画出曲线,于是解决了问题。最后在对于click_state的设置时,定义了NEWPOINT, MOVEPOIN, POINTREACHMAX表示新增点、移动点和不能再新增点这三个状态。
(二)Hermite曲线
1. 准备算法
(1)结构体Point_H:保存点的坐标(x, y);
(2)DrawHermiteCurve:计算并绘制Hermite曲线;
(3)display:绘制曲线、点与切向量等;
(4)motion:鼠标拖动点以调整曲线;
(5)其他函数、变量、结构体、声明等。
2. 编写代码
(1)结构体Point_H
保存点的坐标(x, y),初始端点P0, P1, 切向量derP0, derP1。
(2)DrawHermiteCurve
首先计算H0(t), H1(t), H2(t), H3(t),然后进行矩阵叉乘,然后作图,代码如下:
f1 = 2.0 * pow(t, 3) - 3.0 * pow(t, 2) + 1.0;
f2 = -2.0 * pow(t, 3) + 3.0 * pow(t, 2);
f3 = pow(t, 3) - 2.0 * pow(t, 2) + t;
f4 = pow(t, 3) - pow(t, 2);
x = f1 * P0.x + f2 * P1.x + f3 * derP0.x + f4 * derP1.x;
y = f1 * P0.y + f2 * P1.y + f3 * derP0.y + f4 * derP1.y ;
glVertex2f(x, y);
- display
绘制曲线、点与切向量等;由于切向量的模长较大,如果按原比例画图的话不够美观,用户改变切向量以此改变曲线形状的效果也可能不够明显,因此将切向量缩小了1/4,控制切向量的点的坐标也缩小1/4。
glLineWidth(2);
glColor3f (1.0, 0.0, 0.0);
glBegin(GL_LINES);
glVertex2f(P0.x, P0.y);
glVertex2f(P0.x + derP0.x / 4, P0.y + derP0.y / 4);
glVertex2f(P1.x, P1.y);
glVertex2f(P1.x - derP1.x / 4, P1.y - derP1.y / 4);
(4)motion
判断用户按下的是鼠标左键还是右键,如果是左键则可以拖动点改变切向量,如果是右键则可以拖动点改变端点位置,并发送重绘的消息。
void motion(int x, int y) {
if (mouseLeftDown) {
if (distance(P0.x + derP0.x / 4, P0.y + derP0.y / 4, x, y) < 20) {
derP0.x = (x - P0.x) * 4;
derP0.y = (y - P0.y) * 4;
}
if (distance(P1.x - derP1.x / 4, P1.y - derP1.y / 4, x, y) < 20) {
derP1.x = (P1.x - x) * 4;
derP1.y = (P1.y - y) * 4;
}
}
if(mouseRightDown){
if (distance(P0.x, P0.y, x, y) < 20) {
P0.x = x;
P0.y = y;
}
if (distance(P1.x, P1.y, x, y) < 20) {
P1.x = x;
P1.y = y;
}
}
glutPostRedisplay();
}
(5)其他函数、变量、结构体、声明等。
mouse函数用于判断鼠标的状态,distance函数用于计算两点之间的距离。
3. 修改错误与完善程序
起初Hermite曲线总是不能连接到两端点,最后突发奇想地加了反走样处理后居然就可以成功作图了,同时如果注释掉反走样处理就又不能成功作图。后来修改了曲线、点和切向量的颜色方便用户分辨,并且在控制台补充打印了使用说明。
五、实验结果
(一)Bezier曲线
运行结果如下。
点击窗口内任意位置进行绘制点的操作,窗口绘制出虚线与Bezier曲线,且控制台打印出点的坐标。
点击r键重新绘制。
点击已存在的点并拖动,改变位置。
(二)Hermite曲线
运行结果如下。
运行程序后,窗口显示白色的端点、红色的切向量、控制切向量的点、Hermite曲线,且控制台打印用户提示。
左键按住绿点修改切向量。
右键按住白点修改端点坐标。
六、实验结论
(一)Berzier曲线
1. 实验结论
本次实验完成了Bezier曲线的绘制,且实现了一些基础的功能增强用户的交互感。但是本程序也存在一些问题。如下图,Bezier曲线在绘制的时候,往往在点数到达13左右时,绘制的结果与预期大相庭径。
我认为这是由于在计算第一个点P0时,由于control_points_n较大,导致pow(1 - t, control_points_n - j - 1)过小,使得坐标(x,y)取值为(0,0),于是出现了“脱缰”的画面:
- 源代码
#include <windows.h>
#include <GL/glut.h>
#include <math.h>
#include <stdio.h>
#define MAXPOINTS 20
#define NEWPOINT 0 //新增点
#define MOVEPOINT 1 //移动点
#define POINTREACHMAX 2 //控制点数目已达最大值
int* C;//二项式系数
int control_points_n = 0;//控制点数量
int specific_p;//特定点标志
int click_state = NEWPOINT;//鼠标点击的状态
int screen_width, screen_height;//屏幕宽度,长度
struct point {
int x;
int y;
};
point* P;
void reshape(int w, int h);
void keyboard(unsigned char button, int x, int y);
void mouse_click(int button, int state, int x, int y);
void mouse_motion(int x, int y);//鼠标在窗口中按下并移动
void display(void);
//显示/打印坐标函数
void gotoxy(int x, int y) {
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord); //设置指定控制台屏幕缓冲区中的光标位置
}
int main(int argc, char** argv) {
P = new point[MAXPOINTS];
printf("\"ESC\"键退出. \"R\"键重新绘制\r\n");
printf("最多添加%d个点.", MAXPOINTS);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(300, 300);
glutInitWindowSize(500, 500);
glutCreateWindow("7复杂图形绘制-贝塞尔曲线");
glClearColor(0.0, 0.0, 0.0, 0.0);
glEnable(GL_LINE_STIPPLE);
glutReshapeFunc(reshape);
glutKeyboardFunc(keyboard);
glutMouseFunc(mouse_click);
glutMotionFunc(mouse_motion);
glutDisplayFunc(display);
glutMainLoop();
delete[] P;
return 0;
}
//标记点
void Draw_Square(int x, int y) {
glColor3f(1, 0, 0);
glBegin(GL_POLYGON);
glVertex2f(x + 2, y + 2);
glVertex2f(x - 2, y + 2);
glVertex2f(x - 2, y - 2);
glVertex2f(x + 2, y - 2);
glEnd();
}
/*计算二项式系数,C[k] = C(k)(n)*/
void CulculateBinomialCoefficient(int n, int* C) {
int k, j;
for (k = 0; k <= n; k++) {
C[k] = 1;
for (j = n; j >= k + 1; j--) {
C[k] *= j;
}
for (j = n - k; j >= 2; j--) {
C[k] /= j;
}
}
}
//绘制贝塞尔曲线
void DrawBeziercurve(int n, point* p, int m) {
int i, j;
float t;
int C[MAXPOINTS - 1];
float Beziercurve_pointx;
float Beziercurve_pointy;
CulculateBinomialCoefficient(n, C);
//画线
glLineStipple(1, 0xffff);
glBegin(GL_LINE_STRIP);
for (i = 0; i <= m; i++) {
t = (float)i / (float)m;
Beziercurve_pointx = 0;
Beziercurve_pointy = 0;
for (j = 0; j < control_points_n; j++) {
//B(t) = sum( C(i,n) * P(i) * (1-t)^(n-i) * t^i )
Beziercurve_pointx += C[j] * pow(1 - t, control_points_n - j - 1) * pow(t, j) * p[j].x;
Beziercurve_pointy += C[j] * pow(1 - t, control_points_n - j - 1) * pow(t, j) * p[j].y;
}
glVertex2f(Beziercurve_pointx, Beziercurve_pointy);
}
glEnd();
}
void reshape(int w, int h) {
screen_width = w;
screen_height = h;
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, w, 0.0, h);
}
void keyboard(unsigned char button, int x, int y) {
switch (button) {
case 82://"R"
case 114://"r"
//重新绘图
//清空之前显示的坐标
gotoxy(0, 3);
for (int i = 0; i < control_points_n; i++) {
printf(" \r\n");
}
specific_p = control_points_n = 0;
click_state = 0;
glutPostRedisplay();
break;
case 27:
exit(0);
}
}
//处理鼠标click事件函数
void mouse_click(int button, int state, int x, int y) {
int i;
int distance;//鼠标点击处于任意一点距离
if (button == GLUT_LEFT_BUTTON) {//鼠标左键按下
if (state == GLUT_DOWN && click_state != MOVEPOINT) {//若还可以移动点(即不止只能移动点),则计算距离
for (i = 0; i < control_points_n; i++) {
//按控制点依次计算距离
distance = (x - P[i].x) * (x - P[i].x) +
(screen_height - y - P[i].y) * (screen_height - y - P[i].y);
if (distance < 20) { //如果距离小于特定值。则开始移动点
click_state = MOVEPOINT;
specific_p = i;
break;//跳出循环
}
}
}
if (state == GLUT_DOWN && click_state == NEWPOINT) { //新增点
if (control_points_n < MAXPOINTS) {
P[control_points_n].x = x;
P[control_points_n].y = screen_height - y;
specific_p = control_points_n;
control_points_n++;
glutPostRedisplay();
} else click_state = POINTREACHMAX; //无法新增控制点,只能移动点
} else if (state == GLUT_DOWN && click_state == MOVEPOINT) { //移动点
P[specific_p].x = x;
P[specific_p].y = screen_height - y;
glutPostRedisplay();
} else if (state == GLUT_UP) {
//判断控制点是否已达上限
click_state = control_points_n < MAXPOINTS ? NEWPOINT : POINTREACHMAX;
}
}
}
//鼠标在窗口中按下并移动
void mouse_motion(int x, int y) {
if (click_state == MOVEPOINT) {
P[specific_p].x = x;
P[specific_p].y = screen_height - y;
glutPostRedisplay();
}
}
//显示函数,需要显示时调用此函数
void display(void) {
int i;
glClear(GL_COLOR_BUFFER_BIT);
for (i = 0; i < control_points_n; i++) { //画点
Draw_Square(P[i].x, P[i].y);
}
//显示点坐标
gotoxy(0, specific_p + 3);
printf("P%d:(%d, %d) ", specific_p, P[specific_p].x, P[specific_p].y);
glLineStipple(1, 0x0f0f);
glBegin(GL_LINE_STRIP);
glColor3f(1, 1, 1);
for (i = 0; i < control_points_n; i++) { //画虚线
glVertex2f(P[i].x, P[i].y);
}
glEnd();
if (control_points_n > 1) { //画贝塞尔曲线
DrawBeziercurve(control_points_n - 1, P, 200);
}
glutSwapBuffers();
}
(二)Hermite曲线
1. 实验结论
本次实验完成了Hermite曲线的绘制,且实现了一些基础的功能增强用户的交互感。但是本程序还有可以改进的地方。我在网上查到也有实现“给定一定数量的点坐标,和端点的切向量,绘制出经过这些点的Hermite曲线”的方法,且可以修改这些点的位置,因此还有很多改进的地方。
2. 源代码
#include <math.h>
#include <GL/glut.h>
#include <iostream>
using namespace std;
struct Point_H {
double x;
double y;
Point_H(int px, int py) {
x = px;
y = py;
}
};
Point_H P0(100, 200);
Point_H P1(700, 450);
Point_H derP0(100, 200);
Point_H derP1(-500, 300);
bool mouseLeftDown = false;
bool mouseRightDown = false;
//计算Hermite曲线
void DrawHermiteCurve(int n) {
float f1, f2, f3, f4;
float x, y;
double t;
x = 0;
y = 0;
glColor3f (1.0, 1.0, 0.0);
glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glBegin(GL_LINE_STRIP);
for (int i = 0; i <= n; i++) {
t = (double)i / n;
f1 = 2.0 * pow(t, 3) - 3.0 * pow(t, 2) + 1.0;
f2 = -2.0 * pow(t, 3) + 3.0 * pow(t, 2);
f3 = pow(t, 3) - 2.0 * pow(t, 2) + t;
f4 = pow(t, 3) - pow(t, 2);
x = f1 * P0.x + f2 * P1.x + f3 * derP0.x + f4 * derP1.x;
y = f1 * P0.y + f2 * P1.y + f3 * derP0.y + f4 * derP1.y ;
glVertex2f(x, y);
}
glEnd();
}
void display() {
glClear(GL_COLOR_BUFFER_BIT);
glLineWidth(2);
glColor3f (1.0, 0.0, 0.0);
glBegin(GL_LINES);
glVertex2f(P0.x, P0.y);
glVertex2f(P0.x + derP0.x / 4, P0.y + derP0.y / 4);
glVertex2f(P1.x, P1.y);
glVertex2f(P1.x - derP1.x / 4, P1.y - derP1.y / 4);
glEnd();
glColor3f (1.0, 1.0, 1.0);
glPointSize(10.0f);
//反走样
glEnable(GL_POINT_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glBegin(GL_POINTS);
glColor3f (1.0, 1.0, 1.0);
glVertex2f(P0.x, P0.y);
glVertex2f(P1.x, P1.y);
glColor3f(0.0, 1.0, 0.0);
glVertex2f(P0.x + derP0.x / 4, P0.y + derP0.y / 4);
glVertex2f(P1.x - derP1.x / 4, P1.y - derP1.y / 4);
glEnd();
DrawHermiteCurve(200);
glFlush();
glutSwapBuffers();
}
void reshape(int w, int h) {
glViewport(0, 0, (GLsizei)w, (GLsizei)h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, (GLsizei)w, (GLsizei)h, 0.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void mouse(int button, int state, int x, int y) {
if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
mouseLeftDown = true;
}
if (button == GLUT_LEFT_BUTTON && state == GLUT_UP) {
mouseLeftDown = false;
}
if (button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN) {
mouseRightDown = true;
}
if (button == GLUT_RIGHT_BUTTON && state == GLUT_UP) {
mouseRightDown = false;
}
}
double distance(int x1, int y1, int x2, int y2) {
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
void motion(int x, int y) {
if (mouseLeftDown) {
if (distance(P0.x + derP0.x / 4, P0.y + derP0.y / 4, x, y) < 20) {
derP0.x = (x - P0.x) * 4;
derP0.y = (y - P0.y) * 4;
}
if (distance(P1.x - derP1.x / 4, P1.y - derP1.y / 4, x, y) < 20) {
derP1.x = (P1.x - x) * 4;
derP1.y = (P1.y - y) * 4;
}
}
if(mouseRightDown){
if (distance(P0.x, P0.y, x, y) < 20) {
P0.x = x;
P0.y = y;
}
if (distance(P1.x, P1.y, x, y) < 20) {
P1.x = x;
P1.y = y;
}
}
glutPostRedisplay();
}
int main(int argc, char** argv) {
cout<<"按住鼠标左键拖动改变切向量. 按住右键拖动改变端点坐标."<<endl;
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize (900, 900);
glutInitWindowPosition (200, 200);
glutCreateWindow ("7复杂图形绘制-埃尔米特曲线");
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_FLAT);
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutMouseFunc(mouse);
glutMotionFunc(motion);
glutMainLoop();
return 0;
}
七、实验小结
本次实验自主学习了Berzier曲线、Hermite曲线为何物,学习了样条曲线的绘制,并且根据调试发现了程序一些问题,依据问题找到了产生问题的原因。不过在控制台打印等方面还是有一些小毛病没能修改;由于程序只考虑了用户正确输入的情况,所以若用户非法输入,程序没有办法处理或者提示用户,而只能“将错就错”。