基于OpenGL绘制球体模型

作者: zhangzhongke 分类: OpenGL/ES 发布时间: 2017-06-01 15:32

基本算法

      球体是3D渲染中最为常见的几何模型之一。网络上绘制球体最常见的方法是使用glut库提供的函数来实现球体绘制,但其缺点是无法进行后续处理(如贴图、法线映射、光线、阴影等)。因此,我们需要自己根据球坐标来建立几何模型。

      在数学中,球面上的点使用极坐标来表示:P(r, Θ, Φ):

      其中,Θ是向量P与Z轴正向的夹角;Φ是向量P在XOY平面的投影向量与X轴正向的夹角。当然,这只是其中的一种规定。在不同的场合下可以有不同的规定。至于具体是怎样规定的,还得看代码中的设定
 

      我们的算法也是根据球面坐标公式来计算球面的每个点的。具体算法使用如下的代码:

void generateSphereGeometry(float radius, std::vector<float>& vertices, std::vector<unsigned int>& indices)
{
    // 经度方向切割100段
    const int VERTICAL_SLICE = 100;
    float vertical_step = (float)(glm::two_pi<float>() / VERTICAL_SLICE);
    // 纬度方向切割50段
    const int HORIZONTAL_SLICE = 50;
    float horizontal_step = (float)(glm::pi<float>() / HORIZONTAL_SLICE);
   
    unsigned int start_index = 0;
    unsigned int current_index = 0;
    // 纬度方向上将球体分割成50段,即切割成50个不同半径的同心圆
    for (size_t i = 0; i <= HORIZONTAL_SLICE; ++i)
    {
        start_index = current_index;
        float vertical_angle = horizontal_step * i;
        float z_coord = radius * std::cos(vertical_angle);
        float sub_radius = radius * std::sin(vertical_angle);
        // 经度方向将球体切割成100段
        for (size_t j = 0; j <= VERTICAL_SLICE; j++)
        {
            float horizontal_angle = vertical_step * j;
            float x_coord = sub_radius * std::cos(horizontal_angle);
            float y_coord = sub_radius * std::sin(horizontal_angle);

            // 一圈结束了,起点和重点重合
            if (j == VERTICAL_SLICE)
            {
                vertices.push_back(vertices[start_index]);
                vertices.push_back(vertices[start_index + 1]);
                vertices.push_back(vertices[start_index + 2]);
            }
            else
            {
                // 上面在计算x, y, z坐标时将Y轴和Z轴调换了
                // 因此在这里要调整坐标系为右手坐标系
                vertices.push_back(x_coord);
                vertices.push_back(z_coord);
                vertices.push_back(y_coord);
            }
            // 保存每一圈的起始点索引,方便计算每一圈的重合点
            current_index += 3;

            if (i > 0 && j > 0)
            {
                // 相邻上一圈中的顶点索引
                unsigned int bottom_ring_a = (VERTICAL_SLICE + 1)*i + j;
                unsigned int bottom_ring_b = (VERTICAL_SLICE + 1)*i + j - 1;
                // 相邻下一圈中的顶点索引
                unsigned int top_ring_a = (VERTICAL_SLICE + 1)*(i - 1) + j;
                unsigned int top_ring_b = (VERTICAL_SLICE + 1)*(i - 1) + j - 1;

                // j == 1时,相邻上一圈收缩成一个点
                if (j == 1)
                {
                    indices.push_back(bottom_ring_a);
                    indices.push_back(top_ring_a);
                    indices.push_back(top_ring_b);
                }
                // j == HORIZONTAL_SLICE时,相邻下一圈收缩成一个点
                else if (j == HORIZONTAL_SLICE)
                {
                    indices.push_back(bottom_ring_a);
                    indices.push_back(top_ring_b);
                    indices.push_back(bottom_ring_b);
                }
                else
                {
                    // 逆时钟方向连接顶点,每次连接成一个四边形
                    // 逆时钟方向避免三角形被背面剔除处理掉
                    indices.push_back(bottom_ring_a);
                    indices.push_back(top_ring_a);
                    indices.push_back(top_ring_b);
                    indices.push_back(bottom_ring_a);
                    indices.push_back(top_ring_b);
                    indices.push_back(bottom_ring_b);
                }
            }
        }
    }
}

      算法中在经度方向将球体切割成100份,在纬度方向将球切割成50份。顶点越多,球面越精细。总体来说,就是一圈一圈的计算出球面上的每个点的坐标,并记录下每个点的索引。因为坐标点比较多,所以在后面我们打算使用顶点索引来绘制。

 

绘制代码绘制

      绘制代码时,我们主要使用上述算法建立的两个Buffer。一个是顶点Buffer,存储的是球面所有顶点的坐标;另一个是顶点索引,减少顶点的数据冗余度。

std::vector<GLfloat> vert_coords;
std::vector<unsigned int> vert_indices;
generateSphereGeometry(2.0f, vert_coords, vert_indices);

// Vertices
GLuint vert_buffer;
glGenBuffers(1, &vert_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vert_buffer);
glBufferData(GL_ARRAY_BUFFER, vert_coords.size()*sizeof(GLfloat), vert_coords.data(), GL_STATIC_DRAW);

// Indices
GLuint indices_buffer;
glGenBuffers(1, &indices_buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices_buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, vert_indices.size()*sizeof(GLuint), vert_indices.data(), GL_STATIC_DRAW);
 
GLuint programID = LoadShaders("simple.vert", "simple.frag"/*, "simple.geom", true*/);
GLuint mvpID = glGetUniformLocation(programID, "MVP");
 
glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE);
glViewport(0, 0, 1024, 768);

glm::mat4 modelMat(1.0f);
glm::mat4 viewMat;
glm::mat4 projectionMat;
glm::mat4 MVP;
do
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     
    glUseProgram(programID);  
   
    computeMatricesFromInputs();
    viewMat = getViewMatrix();
    projectionMat = getProjectionMatrix();
    MVP = projectionMat * viewMat * modelMat;
    glUniformMatrix4fv(mvpID, 1, GL_FALSE, glm::value_ptr(MVP));

    glEnableVertexAttribArray(1);
    glBindBuffer(GL_ARRAY_BUFFER, vert_buffer);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);  

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices_buffer);
    glDrawElements(GL_LINE_STRIP, vert_indices.size(), GL_UNSIGNED_INT, (void*)0); // we use index buffer, so set it to null.  
    //glDrawArrays(GL_LINE_STRIP, 0, vert_coords.size());

    glDisableVertexAttribArray(1);

    glfwSwapBuffers(window);
    glfwPollEvents();
} while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS && glfwWindowShouldClose(window) == 0);

      绘制结果如下:

 

源代码工程

 
OpenGL-Playground
 

发表评论

电子邮件地址不会被公开。 必填项已用*标注