1、用OpenGL实现鼠标选取随意选取多个控制点,每四个控制点绘制一条3次的Bezier曲线。
#include <GL/glut.h>
#include<cmath>
#define MAX_POINTS 100
struct
{
GLfloat x;
GLfloat y;
}controlPoints[4];
struct
{
GLfloat x;
GLfloat y;
}savedPoints[MAX_POINTS];
struct
{
GLfloat x;
GLfloat y;
}savedLines[MAX_POINTS/4][4];
int controlPointCount = 0;
bool isDrawing = false; // 是否正在进行绘制
int savedPointCount = 0;
int savedLineCount = 0;
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
//绘制过去的点
glColor3f(0.0f, 0.0f, 0.0f); // 绘制点的颜色设置为黑色
glPointSize(5.0);
glBegin(GL_POINTS);
for (int i = 0; i < savedPointCount; i++)
glVertex2f(savedPoints[i].x, savedPoints[i].y);
glEnd();
//绘制过去的线
for (int i = 0; i < savedLineCount; i++)
{
glColor3f(0.0f, 1.0f, 0.0f);// 绘制线段的颜色设置为绿色
glLineWidth(2.0);
glBegin(GL_LINE_STRIP);
glVertex2f(savedLines[i][0].x, savedLines[i][0].y);
glVertex2f(savedLines[i][1].x, savedLines[i][1].y);
glVertex2f(savedLines[i][2].x, savedLines[i][2].y);
glVertex2f(savedLines[i][3].x, savedLines[i][3].y);
glEnd();
//贝塞尔曲线部分
glColor3f(0.0f, 1.0f, 0.0f);// 绘制贝塞尔曲线的颜色设置为绿色
glLineWidth(2.0);
glBegin(GL_LINE_STRIP);
for (int j = 0; j <= 500; j++)
{
float t = j / 500.0;
GLfloat point[2];
point[0] = pow(1 - t, 3) * savedLines[i][0].x +
3 * pow(1 - t, 2) * t * savedLines[i][1] .x+
3 * (1 - t) * pow(t, 2) * savedLines[i][2].x+
pow(t, 3) * savedLines[i][3].x; //对应书本224页头顶公式
point[1] = pow(1 - t, 3) * savedLines[i][0].y +
3 * pow(1 - t, 2) * t * savedLines[i][1] .y+
3 * (1 - t) * pow(t, 2) * savedLines[i][2].y +
pow(t, 3) * savedLines[i][3].y;
glVertex2f(point[0], point[1]);
}
glEnd();
}
//绘制现在的点
glColor3f(0.0f, 0.0f, 0.0f); // 绘制点的颜色设置为黑色
glPointSize(5.0);
glBegin(GL_POINTS);
for (int i = 0; i < controlPointCount; i++)
glVertex2f(controlPoints[i].x, controlPoints[i].y);
glEnd();
glColor3f(0.0f, 1.0f, 0.0f);// 绘制线段的颜色设置为绿色
glBegin(GL_LINE_STRIP);
glLineWidth(2.0);
for (int i = 0; i < controlPointCount; i ++)
glVertex2f(controlPoints[i].x, controlPoints[i].y);
glEnd();
glFlush();
}
void mouse(int button, int state, int x, int y)
{
int height = glutGet(GLUT_WINDOW_HEIGHT);
if (button == GLUT_LEFT_BUTTON)
{
if (state == GLUT_DOWN)
{
controlPoints[controlPointCount].x = x;
controlPoints[controlPointCount].y = height - y;
isDrawing = true;
}
else if (state == GLUT_UP)
{
isDrawing = false;
controlPointCount++;
// 保存旧的点
savedPoints[savedPointCount].x = controlPoints[controlPointCount - 1].x;
savedPoints[savedPointCount].y = controlPoints[controlPointCount - 1].y;
savedPointCount++;
if (controlPointCount == 4)
{
controlPointCount = 0;
savedLines[savedLineCount][0].x = controlPoints[0].x;
savedLines[savedLineCount][0].y = controlPoints[0].y;
savedLines[savedLineCount][1].x = controlPoints[1].x;
savedLines[savedLineCount][1].y = controlPoints[1].y;
savedLines[savedLineCount][2].x = controlPoints[2].x;
savedLines[savedLineCount][2].y = controlPoints[2].y;
savedLines[savedLineCount][3].x = controlPoints[3].x;
savedLines[savedLineCount][3].y = controlPoints[3].y;
savedLineCount++;
}
}
}
glutPostRedisplay();
}
void motion(int x, int y)
{
int height = glutGet(GLUT_WINDOW_HEIGHT); // 获取窗口高度
if(isDrawing)
{
controlPoints[controlPointCount].x = x;
controlPoints[controlPointCount].y= height - y;
}
glutPostRedisplay();
}
void reshape(int width, int height)
{
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0, width, 0, height);
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
glutInitWindowSize(720, 720);
glutInitWindowPosition(100, 100);
glutCreateWindow("Bezier 曲线");
glClearColor(1.0f, 1.0f, 1.0f, 0.0);
glutDisplayFunc(display);
glutMouseFunc(mouse);
glutMotionFunc(motion);
glutReshapeFunc(reshape);
glutMainLoop();
return 0;
}
①
struct
{
GLfloat x;
GLfloat y;
}controlPoints[4];
struct
{
GLfloat x;
GLfloat y;
}savedPoints[MAX_POINTS];
struct
{
GLfloat x;
GLfloat y;
}savedLines[MAX_POINTS/4][4];
我用了三种结构体数组,第一个结构体是保存当前绘制的四个点,后面两个结构体,一个是保存顶点,一个是保存直线。因为我要实现每绘制四个顶点的时候产生一条贝塞尔曲线,然后再绘制下一个5、6、7、8的顶点时候时,顶点4和顶点5不会连到一起,而且前面绘制的贝塞尔曲线要保留,所以要创建后面两个保存顶点和保存直线的结构体。
②
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
//绘制过去的点
glColor3f(0.0f, 0.0f, 0.0f); // 绘制点的颜色设置为黑色
glPointSize(5.0);
glBegin(GL_POINTS);
for (int i = 0; i < savedPointCount; i++)
glVertex2f(savedPoints[i].x, savedPoints[i].y);
glEnd();
//绘制过去的线
for (int i = 0; i < savedLineCount; i++)
{
glColor3f(0.0f, 1.0f, 0.0f);// 绘制线段的颜色设置为绿色
glLineWidth(2.0);
glBegin(GL_LINE_STRIP);
glVertex2f(savedLines[i][0].x, savedLines[i][0].y);
glVertex2f(savedLines[i][1].x, savedLines[i][1].y);
glVertex2f(savedLines[i][2].x, savedLines[i][2].y);
glVertex2f(savedLines[i][3].x, savedLines[i][3].y);
glEnd();
//贝塞尔曲线部分
glColor3f(0.0f, 1.0f, 0.0f);// 绘制贝塞尔曲线的颜色设置为绿色
glLineWidth(2.0);
glBegin(GL_LINE_STRIP);
for (int j = 0; j <= 500; j++)
{
float t = j / 500.0;
GLfloat point[2];
point[0] = pow(1 - t, 3) * savedLines[i][0].x +
3 * pow(1 - t, 2) * t * savedLines[i][1] .x+
3 * (1 - t) * pow(t, 2) * savedLines[i][2].x+
pow(t, 3) * savedLines[i][3].x; //对应书本224页头顶公式
point[1] = pow(1 - t, 3) * savedLines[i][0].y +
3 * pow(1 - t, 2) * t * savedLines[i][1] .y+
3 * (1 - t) * pow(t, 2) * savedLines[i][2].y +
pow(t, 3) * savedLines[i][3].y;
glVertex2f(point[0], point[1]);
}
glEnd();
}
//绘制现在的点
glColor3f(0.0f, 0.0f, 0.0f); // 绘制点的颜色设置为黑色
glPointSize(5.0);
glBegin(GL_POINTS);
for (int i = 0; i < controlPointCount; i++)
glVertex2f(controlPoints[i].x, controlPoints[i].y);
glEnd();
glColor3f(0.0f, 1.0f, 0.0f);// 绘制线段的颜色设置为绿色
glBegin(GL_LINE_STRIP);
glLineWidth(2.0);
for (int i = 0; i < controlPointCount; i ++)
glVertex2f(controlPoints[i].x, controlPoints[i].y);
glEnd();
glFlush();
}
针对每4个顶点产生三条直线,然后再继续绘制时,三条直线和四个顶点是捆绑在一个区域的,不能与后面的顶点进行相连,所以每一次glBegin()函数都要反复出现,以分隔不同的部分。这也有利于对直线和顶点一个用黑色,一个用绿色的颜色的控制。
③
void mouse(int button, int state, int x, int y)
{
int height = glutGet(GLUT_WINDOW_HEIGHT);
if (button == GLUT_LEFT_BUTTON)
{
if (state == GLUT_DOWN)
{
controlPoints[controlPointCount].x = x;
controlPoints[controlPointCount].y = height - y;
isDrawing = true;
}
else if (state == GLUT_UP)
{
isDrawing = false;
controlPointCount++;
// 保存旧的点
savedPoints[savedPointCount].x = controlPoints[controlPointCount - 1].x;
savedPoints[savedPointCount].y = controlPoints[controlPointCount - 1].y;
savedPointCount++;
if (controlPointCount == 4)
{
controlPointCount = 0;
savedLines[savedLineCount][0].x = controlPoints[0].x;
savedLines[savedLineCount][0].y = controlPoints[0].y;
savedLines[savedLineCount][1].x = controlPoints[1].x;
savedLines[savedLineCount][1].y = controlPoints[1].y;
savedLines[savedLineCount][2].x = controlPoints[2].x;
savedLines[savedLineCount][2].y = controlPoints[2].y;
savedLines[savedLineCount][3].x = controlPoints[3].x;
savedLines[savedLineCount][3].y = controlPoints[3].y;
savedLineCount++;
}
}
}
glutPostRedisplay();
}
每次绘制顶点达到4的时候,控制点计数变量controlPointCount等于4,之后就给它重新置为0,然后对一个小组的直线savedLineCount进行累加代表三条直线的第一个小组已经创立,然后将他们3条直线对应的坐标值赋值给保存直线的结构体数组。
2、用鼠标选取任意多个控制点,然后生成一条3次B样条曲线。
代码如下:
#include <GL/glut.h>
#include<cmath>
#define MAX_POINTS 100 //最大可以容纳100个点
struct savedPoints
{
GLfloat x;
GLfloat y;
}savedPoints[MAX_POINTS];
int savedPointCount = 0;//已经保存的顶点个数
int mousemoving = false;
GLfloat getRatio(double t, double a, double b, double c, double d)
{
return a * pow(t, 3) + b * pow(t, 2) + c * t + d;
}
double caculateSquarDistance(struct savedPoints *a, struct savedPoints *b)//计算2点距离的平方和
{
return pow(a->x - b->x, 2) + pow(a->y - b->y, 2);
}
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
//绘制过去的点
glColor3f(0.0f, 0.0f, 0.0f); // 绘制点的颜色设置为黑色
glPointSize(5.0);
glBegin(GL_POINTS);
for (int i = 0; i < savedPointCount; i++)
glVertex2f(savedPoints[i].x, savedPoints[i].y);
glEnd();
//绘制过去的线
glColor3f(0.0f, 1.0f, 0.0f);// 绘制线段的颜色设置为绿色
glLineWidth(2.0);
glBegin(GL_LINE_STRIP);
for (int i = 0; i < savedPointCount; i++)
glVertex2f(savedPoints[i].x, savedPoints[i].y);
glEnd();
//B样条曲线部分
if (savedPointCount >= 4) //大于4时曲线才开始绘制
{
GLfloat ratio[4];
glColor3f(0.0f, 0.0f, 0.0f);// 绘制B样条曲线的颜色设置为黑色
glLineWidth(2.0);
glBegin(GL_LINE_STRIP);
for (int i = 0; i < savedPointCount - 3; i++)
{
for (int j = 0; j <= 500; j++) //精度设置为500分之一
{
float t = j / 500.0;
ratio[0] = getRatio(t, -1, 3, -3, 1); //这里对应书本233页的矩阵
ratio[1] = getRatio(t, 3, -6, 0, 4);
ratio[2] = getRatio(t, -3, 3, 3, 1);
ratio[3] = getRatio(t, 1, 0, 0, 0);
GLfloat x = 0, y = 0;
x += ratio[0] * savedPoints[i].x + ratio[1] * savedPoints[i + 1].x + ratio[2] * savedPoints[i + 2].x + ratio[3] * savedPoints[i + 3].x;
y += ratio[0] * savedPoints[i].y + ratio[1] * savedPoints[i + 1].y + ratio[2] * savedPoints[i + 2].y + ratio[3] * savedPoints[i + 3].y;
x /= 6.0;
y /= 6.0;
glVertex2f(x, y);
}
}
glEnd();
}
glFlush();
}
int getIndexNearByMouse(struct savedPoints a) //计算取最靠近鼠标右键的记录的点的下标
{
double precision = 200; //精确度
int index = -1; //最靠近鼠标右键的记录的点的下标,大于精确度的作为-1判错处理,防止误判
double minDistance;
for (int i = 0; i < savedPointCount; i++)
{
double distance = caculateSquarDistance(&savedPoints[i], &a);
if (distance < precision)
{
if (index == -1)
{
index = i;
minDistance = distance;
}
else if (distance < minDistance)
{
index = i;
minDistance = distance;
}
}
}
return index;
}
void mouse(int button, int state, int x, int y)
{
int height = glutGet(GLUT_WINDOW_HEIGHT);
if (button == GLUT_LEFT_BUTTON) //右键点击生成顶点
if (state == GLUT_DOWN)
{
// 保存顶点
savedPoints[savedPointCount].x = (GLfloat)x;
savedPoints[savedPointCount].y = (GLfloat)(height - y);
savedPointCount++;
}
if (button == GLUT_RIGHT_BUTTON)//左键点击并拖动移动顶点
{
if (state == GLUT_DOWN)
mousemoving = true;
else if (state == GLUT_UP)
mousemoving = false;
}
glutPostRedisplay();
}
void motion(int x, int y)
{
int height = glutGet(GLUT_WINDOW_HEIGHT); // 获取窗口高度
if (mousemoving) //按住右键移动点
{
struct savedPoints a;
a.x = x;
a.y = height - y;
int index = getIndexNearByMouse(a);
if (index == -1)
return;
savedPoints[index].x = x;
savedPoints[index].y = height - y;
}
glutPostRedisplay();
}
void reshape(int width, int height)
{
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0, width, 0, height);
}
void keyboard(unsigned char key, int x, int y)
{
int height = glutGet(GLUT_WINDOW_HEIGHT); // 获取窗口高度
if (key == 27) //ESC
exit(0);
if (key == 8) //退格键
{
savedPointCount--; //下标减一,显示在屏幕上的点就减少,尽管它们的数据还留在数组里
glutPostRedisplay();
}
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
glutInitWindowSize(1000, 720);
glutInitWindowPosition(100, 100);
glutCreateWindow("B样条曲线");
glClearColor(1.0f, 1.0f, 1.0f, 0.0);
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutMouseFunc(mouse);
glutMotionFunc(motion);
glutKeyboardFunc(keyboard);
glutMainLoop();
return 0;
}
①本代码规定绘制的点最大可以容纳100个点,然后对B样条曲线实现的部分是参照书本上的,书本上没有给出一些精度设置,我这里设置均匀精度为1/500,从0~500,把0~1拆分成1/500。在顶点数大于4的时候,曲线才会开始绘制。
②有一个难点就是如何实现已经点击生成的顶点的移动,这里用了一个getIndexNearByMouse函数,
int getIndexNearByMouse(struct savedPoints a) //计算取最靠近鼠标右键的记录的点的下标
{
double precision = 200; //精确度
int index = -1; //最靠近鼠标右键的记录的点的下标,大于精确度的作为-1判错处理,防止误判
double minDistance;
for (int i = 0; i < savedPointCount; i++)
{
double distance = caculateSquarDistance(&savedPoints[i], &a);
if (distance < precision)
{
if (index == -1)
{
index = i;
minDistance = distance;
}
else if (distance < minDistance)
{
index = i;
minDistance = distance;
}
}
}
return index;
}
通过小于200的精确度来判断与之前的顶点进行判断,如果判断成功就返回该顶点的下标,这样就可以实现顶点的删除,即把已经记录的数组下标计数savedPointCount减一,删除是直接按键盘上的退格键Backplace。
顶点移动的部分需要通过鼠标的右键点击就可以对已经点击到的顶点进行移动,同时曲线也会跟着移动。
③此外,本代码还实现了窗口的退出,按键盘左上角的Esc就可以退出窗口。
void keyboard(unsigned char key, int x, int y)
{
int height = glutGet(GLUT_WINDOW_HEIGHT); // 获取窗口高度
if (key == 27) //ESC
exit(0);
if (key == 8) //退格键
{
savedPointCount--; //下标减一,显示在屏幕上的点就减少,尽管它们的数据还留在数组里
glutPostRedisplay();
}
}
④
当点击两个顶点开始绘制的时候,可以看到曲线的开始端点并没有连接到鼠标第一个点击的端点,如果想要连接到第一个开始的端点就要在第一个鼠标点击的顶点处反复点击三次,这样子曲线就会从开始那个顶点开始绘制。
代码中我用了绿色的直线作为参考线,如果要删去绿色直线,直接把代码中的下面一段段代码注释即可。
//绘制过去的线
glColor3f(0.0f, 1.0f, 0.0f);// 绘制线段的颜色设置为绿色
glLineWidth(2.0);
glBegin(GL_LINE_STRIP);
for (int i = 0; i < savedPointCount; i++)
glVertex2f(savedPoints[i].x, savedPoints[i].y);
glEnd();