欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

OpenGL绘制三维贝塞尔曲线

程序员文章站 2022-03-25 22:29:38
...

  因为这次课程作业做得很有心得,就写篇博客来记录一下。

  本次课程作业最终是实现一个三维的贝塞尔曲线绘制程序。所以,在此之前,我们必须知道怎样绘制一条贝塞尔曲线。这里,绘制的是三次贝塞尔曲线。我不解释贝塞尔曲线的具体实现了,这里,我只给出贝塞尔曲线的参数方程,有需要的同学可以自己去了解。三次贝塞尔曲线参数方程如下:

B(t)=P0(1t)3+3P1t(1t)2+3P2t2(1t)+P3t3

  这里,在离散的时候,我们可以让
t=i/niin

 
   到这里,我们就可以试一试用具体的点绘制出一条贝塞尔曲线了。

   但是在绘制之前。我要先说一下,opengl中怎样是用鼠标获取屏幕中的坐标点。

   首先,我们需要一个int全局变量来记录总的坐标数,和一个全局数组记录每个坐标的x,y,z坐标。

    int num = 0;    //记录当前存储的坐标个数
    GLfloat xPos[4], yPos[4], zPos[4];  //记录每个坐标的x,y, z

   然后,我用的是glfw,我需要实现两个回掉函数,一个鼠标点击,一个记录鼠标位移。

    glfwSetCursorPosCallback(window, MouseMoveCallback);//鼠标移动回调函数
    glfwSetMouseButtonCallback(window, MouseButtonCallback);//鼠标点击回调函数

   当鼠标位移的时候,实时记录当前鼠标的位置,并且记录在全局变量中,当鼠标点击的时候,就可以通过全局变量获取当前鼠标的位置,然后实现坐标记录。

    GLfloat xCur, yCur; //记录实时坐标的全局变量
    float rotateAngle = 0; //现在还用不到,用在三维实现

    void MouseMoveCallback(GLFWwindow* window, double xpos, double ypos) {
        xCur = xpos; //实时记录当前坐标
        yCur = ypos; //实时记录当前坐标
    }

    void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
        if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) {
                if (num < 4) { //记录x,y,z坐标
                xPos[num] = (xCur - 240)*cos(rotateAngle*3.14 / 180);
                yPos[num] = 240 - yCur;
                zPos[num] = (xCur - 240)*sin(rotateAngle*3.14 / 180);
                num++;
                return;
            }
        }
    }

   这时候,我们可以开始绘制贝塞尔曲线了。绘制函数如下:

    void display(void){
        ...
        glBegin(GL_LINE_STRIP);
        for (int i = 0; i < num; i++) {
            glVertex3f(xPos[i], yPos[i], zPos[i]);
        }
        glEnd();
        if (num == 4)
            Bezier(20);
        ...
    }

    void Bezier(int n) {
        float f1, f2, f3, f4;
        float deltaT = 1.0 / n;
        float T;
        glBegin(GL_LINE_STRIP);
        glColor3f(1.0, 0.0, 1.0);
        for (int i = 0; i <= n; i++) {
            T = i * deltaT;
            f1 = (1 - T) * (1 - T) * (1 - T);
            f2 = 3 * T * (1 - T) * (1 - T);
            f3 = 3 * T * T * (1 - T);
            f4 = T * T * T;
            float x = f1 * xPos[0] + f2 * xPos[1] + f3 * xPos[2] + f4 * xPos[3];
            float y = f1 * yPos[0] + f2 * yPos[1] + f3 * yPos[2] + f4 * yPos[3];
            float z = f1 * zPos[0] + f2 * zPos[1] + f3 * zPos[2] + f4 * zPos[3];
            glVertex3f(x, y, z);
        }
        glEnd();
    }

   现在实现的效果如下:


OpenGL绘制三维贝塞尔曲线

   贝塞尔曲线的颜色是不是太单调了呢。接着我们实现一个贝塞尔曲线换色方法。我们利用一个键盘回调函数,实现贝塞尔曲线换色功能。

    GLfloat lineColor[3] = {1.0, 1.0, 0.0};

    void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
            if (key == GLFW_KEY_W && action == GLFW_PRESS) { //按下W键换白色
            lineColor[0] = 1.0;
            lineColor[1] = 1.0;
            lineColor[2] = 1.0;
        }
        else if (key == GLFW_KEY_R && action == GLFW_PRESS) { //按下R键换红色
            lineColor[0] = 1.0;
            lineColor[1] = 0.0;
            lineColor[2] = 0.0;
        }
        ...//发挥想象
        return;
    }

    int main() {
        ...
        glfwSetKeyCallback(window, keyCallback);  //键盘回调函数实现换色
        ...
    }

   就这样,我们只要在绘制贝塞尔曲线时,应用颜色就可以了。

glColor3f(lineColor[0], lineColor[1], lineColor[2]);

  现在看看换色效果。


OpenGL绘制三维贝塞尔曲线

   画好了图不能改,好像很奇怪。我们应该实现一个鼠标拖拽控制点的效果。但是在这里之前,我已经忍不住想把效果变成3D的了。首先,将图像变成3D显示很简单,我们只需要在视图变换中调整gluLookat的朝向就可以了。如果对变换没理解的,可以去查一查资料,这里我就不详细说了。

   还记得我们之前一个全局变量rotateAngle吗,配合该变量注册一个鼠标右击的回调函数实现旋转。我们之前有一个鼠标点击的回调函数,没错,写在里面就可以了。

float rotateAngle = 0;  //三维旋转角实现
bool isRotate;

void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
    ...  //其他点击效果
    if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS) {
        isRotate = true;
    }
    if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_RELEASE) {
        isRotate = false;
    }
}

void MouseMoveCallback(GLFWwindow* window, double xpos, double ypos) {
        if (isRotate) { //在右击长按的时候,计算旋转量
        float xDis = xpos - xCur;
        rotateAngle -= xDis;
        if (rotateAngle > 180) {
            rotateAngle -= 360;
        }
        else if (rotateAngle < -180) {
            rotateAngle += 360;
        }
    }
}

void display(void) {
    ...
    glOrtho(-240, 240, -240, 240, -240, 240);
    //利用旋转变换旋转视点矩阵
    gluLookAt(0, 0, 0, sin(rotateAngle*3.14/180), 0, -cos(rotateAngle*3.14/180), 0, 1, 0);
    ...
}

   这里的旋转变换很简单吧,有问题的要去看懂哦。现在的效果已经不错了,来看下:


OpenGL绘制三维贝塞尔曲线

   接下来就是我做完之后特别开心的一步骤了,实现捉点,并且在三维中捉点哦。主要解决的问题,就是怎样从鼠标点击的屏幕坐标映射到我们的三维坐标系中。


OpenGL绘制三维贝塞尔曲线

 
  现在我们看到,白色是我们能点到的屏幕,红色的点是我们点到的点。我们怎么把这个点映射到图中的三维坐标系呢,这个坐标系是经过一定旋转的。我们来换个角度看看吧。

OpenGL绘制三维贝塞尔曲线

 
   马上就变简单了吧。红色是x轴,现在x轴和屏幕成一个角度θ,就是我们之前的rotateAngle。那么就有三维坐标映射

P2(x,y)>P3(xcosθ,y,xsinθ)

   这是简单的三维坐标只绕y轴旋转坐标系的,因为deadline太赶了,原谅我是一个拖延症患者,现在暂时先实现到这里,晚点有空,我会完善其他旋转的。

   有了上面的变换,我们开始实现捉点吧,其实就是判断我们之前的控制点在不在我们鼠标点击到屏幕中的一条射线上。当然,存在误差,所以这条射线变成了半径r=5的圆柱,这个半径可以条,太小对用户点击的要求很高,太大会出现误差。

bool controlPoint[4] = { false }; //是否匹配上了某一个点
void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
    if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) {
        ... //当四个点已经确定就开始拖点
        float cosAngle = cos(rotateAngle * 3.14 / 180);
        float sinAngle = sin(rotateAngle * 3.14 / 180);
        float x = (xCur - 240)*cos(rotateAngle*3.14 / 180);
        float y = 240 - yCur;

        for (int i = 0; i < 4; i++) {
            if (sinAngle == 0) {
                cout << "z" << " " << x << " " <<  xPos[i] <<  endl;
                if (abs(xPos[i]-x) < 10 && abs(yPos[i] - y) < 10) {
                    cout << "zTrue" << endl;
                    controlPoint[i] = true;
                    return;
                }
            }
                else { //判断控制点是不是在我们的点击圆柱上
                float d1 = ((x*cosAngle - xPos[i])*sinAngle);
                float d2 = ((cosAngle - 1 / cosAngle)*(x*sinAngle - zPos[i]));
                if ((abs(d1 / d2 - 1.0f) < 10 && abs(y - yPos[i]) < 10)) {
                    controlPoint[i] = true;
                    return;
                }
            }
        }
    }
    if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_RELEASE) {
        //释放鼠标就不能拖动点了
        for (int i = 0; i < 4; i++) {
            controlPoint[i] = false;
        }
    }
}

void MouseMoveCallback(GLFWwindow* window, double xpos, double ypos) {
    //...在可以移动的时候改变对应控制点的坐标
    if (keyPressedStatus[GLFW_KEY_A] || controlPoint[0]) {
        xPos[0] = (xCur - 240)*cos(rotateAngle*3.14 / 180);
        yPos[0] = 240 - yCur;
        zPos[0] = (xCur - 240)*sin(rotateAngle*3.14 / 180);
    }
    else if (keyPressedStatus[GLFW_KEY_S] || controlPoint[1]) {
        xPos[1] = (xCur - 240)*cos(rotateAngle*3.14 / 180);
        yPos[1] = 240 - yCur;
        zPos[1] = (xCur - 240)*sin(rotateAngle*3.14 / 180);
    }
    else if (keyPressedStatus[GLFW_KEY_D] || controlPoint[2]) {
        xPos[2] = (xCur - 240)*cos(rotateAngle*3.14 / 180);
        yPos[2] = 240 - yCur;
        zPos[2] = (xCur - 240)*sin(rotateAngle*3.14 / 180);
    }
    else if (keyPressedStatus[GLFW_KEY_F] || controlPoint[3]) {
        xPos[3] = (xCur - 240)*cos(rotateAngle*3.14 / 180);
        yPos[3] = 240 - yCur;
        zPos[3] = (xCur - 240)*sin(rotateAngle*3.14 / 180);
    }
    //...
}

   写到这里就完全实现了。怎么样,一开始觉得很烦,后来懂了,实现了,还是很开心的。所以写出这个博客留恋一下,聪明的发现,控制点也是可以通过按键实现的,asdf对应四个控制点。贴一张最终效果吧:


OpenGL绘制三维贝塞尔曲线

 
   那就这样吧,谢谢读者的浏览,我把整个工程放到云盘上,有兴趣的可以自己下来看一下,其问题还是很多的,我就已经发现几个了。先粗糙点应付deadline吧。有意见的也可以评论区探讨。

 

链接: https://pan.baidu.com/s/1kUPL0j1
密码: vaqk