OpengGL学习-显示三维形状

    本文介绍了OpenGL创建三维图形的几个示例程序。并附有OpenGL创建三维形状的基础示例代码。本文还介绍了OpenGL基础知识,并对Vulkan做了简介。Vulkan性能更强大,但对开发技术人员要求更高,对兼容性的风险和工作量要有充分的认识。兼容性不仅存在于自己的开发的程序,还存在于客户方的电脑软件环境。

   前几年在学习OpenGL时, 基于QT写了一些技术研究代码。在《插件化算法研究平台》运行效果如下。

一、三维控制球

可以用鼠标控制三维旋转。

二、复杂三维曲面显示

参考源代码:3D-Surface-Plotter: QT Opengl 3D-Surface-Plotter

三、OpenGL基础三维形状创建示例

// 绘制立方体
void drawCube()
{
    glBegin(GL_QUAD_STRIP);         //填充凸多边形
    glColor3f(1, 0, 0);
    glVertex3f(0.0f, 0.0f, 0.0f);
    glColor3f(1, 1, 0);
    glVertex3f(0.0f, 1.0f, 0.0f);
    glColor3f(0, 1, 0);
    glVertex3f(1.0f, 0.0f, 0.0f);
    glColor3f(0, 1, 1);
    glVertex3f(1.0f, 1.0f, 0.0f);
    glColor3f(1, 0, 0);
    glVertex3f(1.0f, 0.0f, -1.0f);
    glColor3f(1, 1, 0);
    glVertex3f(1.0f, 1.0f, -1.0f);
    glColor3f(0, 1, 0);
    glVertex3f(0.0f, 0.0f, -1.0f);
    glColor3f(0, 1, 1);
    glVertex3f(0.0f, 1.0f, -1.0f);
    glColor3f(1, 0, 0);
    glVertex3f(0.0f, 0.0f, 0.0f);
    glColor3f(1, 1, 0);
    glVertex3f(0.0f, 1.0f, 0.0f);
    glEnd();
    glBegin(GL_QUAD_STRIP);
    glColor3f(0, 0, 1);
    glVertex3f(0.0f, 0.0f, 0.0f);
    glColor3f(1, 0, 1);
    glVertex3f(1.0f, 0.0f, 0.0f);
    glColor3f(0, 1, 0);
    glVertex3f(0.0f, 0.0f, -1.0f);
    glColor3f(1, 0, 0);
    glVertex3f(1.0f, 0.0f, -1.0f);
    glColor3f(1, 1, 0);
    glVertex3f(0.0f, 1.0f, 0.0f);
    glColor3f(1, 0, 1);
    glVertex3f(1.0f, 1.0f, 0.0f);
    glColor3f(0, 0, 1);
    glVertex3f(0.0f, 1.0f, -1.0f);
    glColor3f(1, 0, 0);
    glVertex3f(1.0f, 1.0f, -1.0f);
    glEnd();
}
// 绘制圆形
void drawCircle()
{
    glBegin(GL_TRIANGLE_FAN);           //扇形连续填充三角形串
    glVertex3f(0.0f, 0.0f, 0.0f);
    int i = 0;
    for (i = 0; i <= 360; i += 15)
    {
        float p = i * 3.14 / 180;
        glColor3f(sin(p), cos(p), tan(p));
        glVertex3f(sin(p), cos(p), 0.0f);
    }
    glEnd();
}

// 绘制圆柱体
void drawCylinder()
{
    // 利用三角形和四边形等基本图元绘制底面圆圆心在坐标原点, 半径为 r,高为 h,方向沿 z 轴方向的圆柱;
    // 侧面用多个四边形,底面用多个三角形来表示
    glBegin(GL_QUAD_STRIP);//连续填充四边形串
    int i = 0;
    for (i = 0; i <= 360; i += 15)
    {
        float p = i * 3.14 / 180;
        glVertex3f(sin(p), cos(p), 1.0f);
        glVertex3f(sin(p), cos(p), 0.0f);
    }
    glEnd();
    //bottom circle
    glColor3f(1, 0, 0);
    drawCircle();
    glTranslatef(0, 0, 1);
    //top circle
    glColor3f(0, 0, 1);
    drawCircle();
    glColor3f(0, 1, 0);
}

// 绘制圆锥体
void drawCone()
{
    glBegin(GL_QUAD_STRIP);//连续填充四边形串
    int i = 0;
    for (i = 0; i <= 360; i += 15)
    {
        float p = i * 3.14 / 180;
        glColor3f(sin(p), cos(p), 1.0f);
        glVertex3f(0, 0, 1.0f);
        glVertex3f(sin(p), cos(p), 0.0f);
    }
    glEnd();
    //bottom circle
    glColor3f(0, 1, 1);
    drawCircle();
}

// 绘制四面体等
void drawTetrahedron()
{
    glBegin(GL_QUADS);
    glNormal3f(0, 0, -1);
    glColor3f(1.0, 0.0, 0.0);
    glVertex3f(-1, -1, 0);
    glColor3f(0.0, 1.0, 0.0);
    glVertex3f(-1, 1, 0);
    glColor3f(0.0, 0.0, 1.0);
    glVertex3f(1, 1, 0);
    glColor3f(1.0, 1.0, 0.0);
    glVertex3f(1, -1, 0);
    glEnd();

    glBegin(GL_TRIANGLES);
    glNormal3f(0, -1, 0.707);
    glColor3f(0.0, 1.0, 1.0);
    glVertex3f(-1, -1, 0);
    glColor3f(1.0, 0.0, 1.0);
    glVertex3f(1, -1, 0);
    glColor3f(1.0, 0.0, 1.0);
    glVertex3f(0, 0, 1.2);
    glEnd();
    glBegin(GL_TRIANGLES);
    glNormal3f(1, 0, 0.707);
    glColor3f(0.0, 1.0, 1.0);
    glVertex3f(1, -1, 0);
    glColor3f(1.0, 0.0, 1.0);
    glVertex3f(1, 1, 0);
    glColor3f(1.0, 0.0, 1.0);
    glVertex3f(0, 0, 1.2);
    glEnd();
    glBegin(GL_TRIANGLES);
    glNormal3f(0, 1, 0.707);
    glColor3f(0.0, 1.0, 1.0);
    glVertex3f(1, 1, 0);
    glColor3f(1.0, 0.0, 1.0);
    glVertex3f(-1, 1, 0);
    glColor3f(1.0, 0.0, 1.0);
    glVertex3f(0, 0, 1.2);
    glEnd();
    glBegin(GL_TRIANGLES);
    glNormal3f(-1, 0, 0.707);
    glColor3f(1.0, 0.0, 1.0);
    glVertex3f(-1, 1, 0);
    glColor3f(0.0, 1.0, 1.0);
    glVertex3f(-1, -1, 0);
    glColor3f(1.0, 0.0, 0.0);
    glVertex3f(0, 0, 1.2);
    glEnd();
}

// 绘制球体
// 球心坐标为(x,y,z),球的半径为radius,M,N分别表示球体的横纵向被分成多少份
void drawSphere(GLfloat xx = 0.0,
                GLfloat yy = 0.0,
                GLfloat zz = 0.0,
                GLfloat radius = 1.0,
                GLfloat M = 100.0,
                GLfloat N = 100.0)
{
    // 选择使用的纹理
    glBindTexture(GL_TEXTURE_2D, texture[0]);

    float step_z = PI / M;
    float step_xy = 2 * PI / N;
    float x[4], y[4], z[4];

    float angle_z = 0.0;
    float angle_xy = 0.0;
    int i = 0, j = 0;
    glBegin(GL_QUADS);
    for(i = 0; i < M; i++)
    {
        angle_z = i * step_z;

        for(j = 0; j < N; j++)
        {
            angle_xy = j * step_xy;

            x[0] = radius * sin(angle_z) * cos(angle_xy);
            y[0] = radius * sin(angle_z) * sin(angle_xy);
            z[0] = radius * cos(angle_z);

            x[1] = radius * sin(angle_z + step_z) * cos(angle_xy);
            y[1] = radius * sin(angle_z + step_z) * sin(angle_xy);
            z[1] = radius * cos(angle_z + step_z);

            x[2] = radius * sin(angle_z + step_z) * cos(angle_xy + step_xy);
            y[2] = radius * sin(angle_z + step_z) * sin(angle_xy + step_xy);
            z[2] = radius * cos(angle_z + step_z);

            x[3] = radius * sin(angle_z) * cos(angle_xy + step_xy);
            y[3] = radius * sin(angle_z) * sin(angle_xy + step_xy);
            z[3] = radius * cos(angle_z);
            for(int k = 0; k < 4; k++)
            {
                glColor3f(sin(angle_z), cos(angle_z), tan(angle_z));
                //glTexCoord2f(0.1f, 0.1f);
                glVertex3f(xx + x[k], yy + y[k], zz + z[k]);
            }
        }
    }
    glEnd();
}

// 绘制圆环
// 大半径Radius,小半径TubeRadius,边数Sides, 环数Rings
void DrawTorus(double Radius = 1,
                double TubeRadius = 0.2,
                int Sides = 20,
                int Rings = 30)
{
    double sideDelta = 2.0 * PI / Sides;
    double ringDelta = 2.0 * PI / Rings;
    double theta = 0;
    double cosTheta = 1.0;
    double sinTheta = 0.0;

    double phi, sinPhi, cosPhi;
    double dist;
    glColor3f(1, 0, 0);
    for (int i = 0; i < Rings; i++)
    {
        double theta1 = theta + ringDelta;
        double cosTheta1 = cos(theta1);
        double sinTheta1 = sin(theta1);

        glBegin(GL_QUAD_STRIP);
        phi = 0;
        for (int j = 0; j <= Sides; j++)
        {
            phi = phi + sideDelta;
            cosPhi = cos(phi);
            sinPhi = sin(phi);
            dist = Radius + (TubeRadius * cosPhi);

            glNormal3f(cosTheta * cosPhi, sinTheta * cosPhi, sinPhi);
            glColor3f(cosTheta, sinTheta, sinPhi);
            glVertex3f(cosTheta * dist, sinTheta * dist, TubeRadius * sinPhi);

            glNormal3f(cosTheta1 * cosPhi, sinTheta1 * cosPhi, sinPhi);
            glColor3f(cosTheta1, sinTheta1, sinPhi);
            glVertex3f(cosTheta1 * dist, sinTheta1 * dist, TubeRadius * sinPhi);
        }
        glEnd();
        theta = theta1;
        cosTheta = cosTheta1;
        sinTheta = sinTheta1;
    }
}

四、OpenGL基础知识

1、简介

OpenGL(Open Graphics Library)是一个跨编程语言、跨平台的编程图形程序接口,它将计算机的资源抽象称为一个个OpenGL的对象,对这些资源的操作抽象为一个个的OpenGL指令。

OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计,去除了许多不必要和性能较低的API接口。

2、OpenGL上下文(Context)

在应用程序调用任何OpenGL的指令之前,需要安排首先创建一个OpenGL的上下文。这个上下文是一个非常庞大的状态机,保存了OpenGL中的各种状态,这也是OpenGL指令执行的基础。

OpenGL的函数不管在哪个语言中,都是类似C语言一样的面向过程的函数,本质上都是对OpenGL上下文这个庞大的状态机中的某个状态或者对象进行操作,当然你得首先把这个对象设置为当前对象。因此,通过对OpenGL指令的封装,是可以将OpenGL的相关调用封装成为一个面向对象的图形API的。

由于OpenGL上下文是一个巨大的状态机,切换上下文往往会产生较大的开销,但是不同的绘制模块,可能需要使用完全独立的状态管理。因此,可以在应用程序中分别创建多个不同的上下文,在不同线程中使用不同的上下文,上下文之间共享纹理、缓冲区等资源。这样的方案,会比反复切换上下文,或者大量修改渲染状态,更加合理高效的。

3、帧缓冲区(FrameBuffer)

OpenGL是图形API,因此可以说所有的运算和结果最终都是需要通过图像进行输出的。那么绘图必然就需要有一块画板,而帧缓冲区就是OpenGL中的画板。但是特别需要注意的是,帧缓冲区不是常规意义缓冲区(就像鲸鱼不是鱼一样),它并不是实际存储数据的对象,类似画画的时候,需要在画板上放一块画布,才能实际在画布上进行绘画,这些画布可以是纹理(Texture)或者是渲染缓冲区(RenderBuffer),而放置这些画布的位置被称为帧缓冲区的附着(Attachment)。

3.1、附着(Attachment)

附着可以理解为画板上的夹子,夹住了哪个画布,就往对应画布上输出数据。

在帧缓冲区中可以附着3种类型的附着,颜色附着(ColorAttachment),深度附着(DepthAttachment),模板附着(StencilAttachment)。这三种附着对应的存储区域也被称为颜色缓冲区(ColorBuffer),深度缓冲区(DepthBuffer),模板缓冲区(StencilBuffer)。

颜色附着输出绘制图像的颜色数据,也就是平时常见的图像的RGBA数据。如果使用了多渲染目标(Multiple Render Targets)技术,那么颜色附着的数量可能会大于一。

深度附着输出绘制图像的深度数据,深度数据主要在3D渲染中使用,一般用于判断物体的远近来实现遮挡的效果。

模板附着输出模板数据,模板数据是渲染中较为高级的用法,一般用于渲染时进行像素级别的剔除和遮挡效果,常见的应用场景比如三维物体的描边。

4、纹理(Texture)和渲染缓冲区(RenderBuffer)

前面已经说过,帧缓冲区并不是实际存储数据的地方,实际存储图像数据数据的对象就是纹理和渲染缓冲区。

他们三者的关系是这样的,纹理或渲染缓冲区作为帧缓冲区的附着。

那么,纹理和渲染缓冲区又有什么关系和区别呢?

纹理和渲染缓冲区同样是存储图像的对象。一般来说,渲染缓冲区对应操作系统提供的窗口,而纹理代表列离屏的图像存储区域。因此,渲染缓冲区都是2D的图像类型,而纹理一般有立方体纹理,1D、2D、3D纹理等类型,同时纹理还额外支持了mipmap等其他特性。

值得注意的是,一般来说渲染缓冲区和纹理不能同时挂载在同一个帧缓冲区上。

5、顶点数组(VertexArray)和顶点缓冲区(VertexBuffer)

准备好了画布之后,就要开始画图了。画图一般是先画好图像的骨架,然后再往骨架里面填充颜色,这对于OpenGL也是一样的。顶点数据就是要画的图像的骨架,和现实中不同的是,OpenGL中的图像都是由图元组成。在OpenGL ES中,有3种类型的图元:点、线、三角形。那这些顶点数据最终是存储在哪里的呢?开发者可以选择设定函数指针,在调用绘制方法的时候,直接由内存传入顶点数据,也就是说这部分数据之前是存储在内存当中的,被称为顶点数组。而性能更高的做法是,提前分配一块显存,将顶点数据预先传入到显存当中。这部分的显存,就被称为顶点缓冲区。

6、索引数组(ElementArray)和索引缓冲区(ElementBuffer)

其实我觉得索引在OpenGL叫Element确实有点不够贴切,而在DirectX中叫做IndexBuffer更加合适一些。

索引数据的目的主要是为了实现顶点的复用,在绘制图像时,总是会有一些顶点被多个图元共享,而反复对这个顶点进行运算常常是没有必要的(也有某些特殊场景需要)。因此对通过索引数据,指示OpenGL绘制顶点的顺序,不但能防止顶点的重复运算,也能在不修改顶点数据的情况下,一定程度的重新组合图像。

和顶点数据一样,索引数据也可以以索引数组的形式存储在内存当中,调用绘制函数时传入;或者提前分配一块显存,将索引数据存储在这块显存当中,这块显存就被称为索引缓冲区。同样的,使用缓冲区的方式,性能一般会比直接使用索引数组的方式更加高效。

OpenGL ES提供了2种主要的绘制方法:glDrawArrays和glDrawElements。前者对应的就是没有索引数据的情况,后者对应的是有索引数据的情况。

7、着色器程序(Shader)

在固定渲染管线时代,这一步并不是必须的。而是由内置的一段包含了光照、坐标变换、裁剪等等诸多功能的固定shader程序来完成。而可自定义shader,可以说是现代图形API最重要的能力了,没有之一。可以说,shader提供对图形运算的精细操作,带来了各式各样的处理能力,极度的丰富了图形API所能实现的效果。

OpenGL和其他主流的图形API早在好几年前,就全面的将固定渲染管线架构变为了可编程渲染管线。因此,OpenGL在实际调用绘制函数之前,还需要指定一个由shader编译成的着色器程序。

常见的着色器主要有顶点着色器(VertexShader),片段着色器(FragmentShader)/像素着色器(PixelShader),几何着色器(GeometryShader),曲面细分着色器(TessellationShader)。片段着色器和像素着色器只是在OpenGL和DX中的不同叫法而已。可惜的是,直到OpenGL ES 3.0,依然只支持了顶点着色器和片段着色器这两个最基础的着色器。

OpenGL在处理shader时,和其他编译器一样。通过编译、链接等步骤,生成了着色器程序(glProgram),着色器程序同时包含了顶点着色器和片段着色器的运算逻辑。在OpenGL进行绘制的时候,首先由顶点着色器对传入的顶点数据进行运算。再通过图元装配,将顶点转换为图元。然后进行光栅化,将图元这种矢量图形,转换为栅格化数据。最后,将栅格化数据传入片段着色器中进行运算。片段着色器会对栅格化数据中的每一个像素进行运算,并决定像素的颜色,也可以在这个阶段将某些像素丢弃。

其中像素的颜色可以是具体的数值或者是由某种算法计算而来的。如果图元有纹理,就必须用纹理来产生图元的二维渲染图象上每个像素的颜色。对于图元在二维屏幕上图象的每个像素来说,都必须从纹理中获得一个颜色值。我们把这一过程称为纹理过滤(texture filtering),纹理过滤根据不同的过滤方式会由一个或多个像素确定最终获得的颜色。表示这个像素位置的数据被称为纹理坐标(TextureCoordinate)而寻找这个纹理中对应像素位置的方法被称为纹理寻址方式或者纹理环绕方式(TextureWrap)。

最终,没有被丢弃的像素,下一步会进入测试阶段。通过了深度测试和模板测试,会和帧缓冲区上的颜色附着(FrameBuffer上的ColorAttachment)上的颜色进行混合,决定最终留在画布上的颜色是什么。

7.1、顶点着色器(VertexShader)

顶点着色器是OpenGL中用于计算顶点属性的程序。顶点着色器是逐顶点运算的程序,也就是说每个顶点数据都会执行一次顶点着色器,当然这是并行的,并且顶点着色器运算过程中无法访问其他顶点的数据。

顶点着色器的数据输入主要有两种,统一变量(Uniform)、顶点属性(VertexAttribute)。统一变量在所有顶点运算中是一样的,而顶点属性则是从外部输入的顶点数据中获取,一般在每个顶点运算中都是不同的。

一般来说典型的需要计算的顶点属性主要包括顶点坐标变换、逐顶点光照运算等等。顶点坐标由自身坐标系转换到归一化坐标系的运算,就是在这里发生的。

同时顶点着色器的输出结果,也会作为片段着色器的输入。

7.2、片段着色器(FragmentShader)

片段着色器是OpenGL中用于计算片段(像素)颜色的程序。片段做社区是逐像素运算的程序,也就是说每个像素都会执行一次片段着色器,当然也是并行的。

片段着色器的的数据输入主要有三种种,统一变量(Uniform)、顶点着色器输入变量(也被称为可变变量varying)、采样器(Sampler)。统一变量的值,在同个OpenGL着色器程序中的顶点着色器和片段着色器中是一致的。顶点着色器输入变量在每个像素运算中则一般是不同的,它的值由组成图元的顶点的顶点着色器运算输出的值,根据像素位置进行插值的结果而决定。采样器则是用于从设定好的纹理中,获取纹理的像素颜色的。

在片段着色器中允许丢弃像素,而使得像素不参与后续的运算。

8、逐片段操作(Per-Fragment Operation)

8.1、测试(Test)

在着色器程序完成之后,我们得到了像素数据。这些数据必须要通过测试才能最终绘制到画布,也就是帧缓冲上的颜色附着上。

测试主要可以分为像素所有者测试(PixelOwnershipTest)、裁剪测试(ScissorTest)、模板测试(StencilTest)和深度测试(DepthTest),执行的顺序也是按照这个顺序进行执行。

最开始进行的测试是像素所有者测试,主要是剔除不属于当前程序的像素运算。

之后裁剪测试,主要是剔除窗口区域之外的像素。

这两个测试都是由OpenGL内部实现的,无需开发者干预,因此不再进行赘述。

深度测试,主要是通过对像素的运算出来的深度,也就是像素离屏幕的距离进行对比,根据OpenGL设定好的深度测试程序,决定是否最终渲染到画布上。一般默认的程序是将离屏幕较近的像素保留,而将离屏幕较远的像素丢弃。如果像素最终被渲染到画布上,根据设定好的OpenGL深度覆写状态,可能会更新帧缓冲区上深度附着的值,方便进行下一次的比较。

模板测试和深度测试的执行原理一致,但是执行的顺序是在深度测试之前的,放在后面 主要是比深度测试更加难以理解一些,初学者可以暂时跳过这个部分。模板测试同样也是通过模板测试程序去决定最终的像素是否丢弃,同样也是根据OpenGL的模板覆写状态决定是否更新像素的模板值。模板测试给开发者提供了高性能的裁剪方案,三维物体的描边技术,就是模板测试典型的用处之一。

8.2、混合(Blending)

在测试阶段之后,如果像素依然没有被剔除,那么像素的颜色将会和帧缓冲区中颜色附着上的颜色进行混合,混合的算法可以通过OpenGL的函数进行指定。但是OpenGL提供的混合算法是有限的,如果需要更加复杂的混合算法,一般可以通过像素着色器进行实现,当然性能会比原生的混合算法差一些。

8.3、抖动(Dithering)

在混合阶段过后,根据OpenGL的状态设置,会决定是否有抖动这个阶段。

抖动是一种针对对于可用颜色较少的系统,可以以牺牲分辨率为代价,通过颜色值的抖动来增加可用颜色数量的技术。抖动操作是和硬件相关的,允许程序员所做的操作就只有打开或关闭抖动操作。实际上,若机器的分辨率已经相当高,激活抖动操作根本就没有任何意义。默认情况下,抖动是激活的。

9、渲染到纹理

有些OpenGL程序并不希望渲染出来的图像立即显示在屏幕上,而是需要多次渲染。可能其中一次渲染的结果是下次渲染的输入。因此,如果帧缓冲区的颜色附着设置为一张纹理,那么渲染完成之后,可以重新构造新的帧缓冲区,并将上次渲染出来的纹理作为输入,重新进行前面所述的流程。

10、渲染上屏/交换缓冲区(SwapBuffer)

前面已经提过,渲染缓冲区一般映射的是系统的资源比如窗口。如果将图像直接渲染到窗口对应的渲染缓冲区,则可以将图像显示到屏幕上。

但是,值得注意的是,如果每个窗口只有一个缓冲区,那么在绘制过程中屏幕进行了刷新,窗口可能显示出不完整的图像。

为了解决这个问题,常规的OpenGL程序至少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在一个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。

由于显示器的刷新一般是逐行进行的,因此为了防止交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换一般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进行交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步。

使用了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进行下一帧的渲染,使得帧率无法完全达到硬件允许的最高水平。为了解决这个问题,引入了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,而垂直同步发生时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利用硬件性能的目的。

五、Vulkan 简介

. Vulkan起源和历史

1.1 AMD Mantle

2013年,AMD主导开发了Mantle。Mantle是面向3D游戏的新一代图形渲染 API,可以让开发人员直接操作GPU硬件底层,从而提高硬件利用率和游戏性能,效果显著。

Mantle很好的带动了图形行业发展,微软参考AMD Mantle的思路开发了DirectX 12

,苹果则提出了Metal。

但是因为AMD行业影响力和领导力不足,Mantle没有发展成为全行业的标准。

2015年,AMD宣布不在维护Mantle,Mantle功成身退。Khronos接过AMD手中的接力棒,在Mantle的基础上推出了Vulkan。

1.2 Vulkan的诞生

科纳斯组织

(Khronos Group)的成员来自图形行业各个领域,专注于制定行业内的开放标准(Open standard)。相对比AMD,Khronos在行业内有更大的影响力和领导力。

Vulkan的开发者来自图形领域的各行各业,有GPU厂商,有系统厂商,有游戏引擎

厂商... ...

所以Vulkan诞生之初就决定了它一定要有跨平台属性,目的就是成为行业内的统一标准。

image-20200729172807747.png

1.3 Vulkan-新一代GPU API的特性

OpenGL已经发展了25年以上,并不断满足行业需求,但是现在已经逐渐满足不了行业的需要。

GPU的可编程性越来越强,越来越多的平台开始支持加速图形,计算,视觉和深度学习。灵活性和可移植性变得很重要。

性能上,OpenGL也不能充分发挥现代CPU多核多线程的性能优势。

为了解决上述问题,行业对新一代GPU API的提出了更高的要求:

  • Explicit(明确、透明)
    • GPU driver做更少的事情,把更多的控制权交给开发者
  • Streamlined(精简)
    • 更快的性能,更低的开销,更少的延迟
  • Portable(可移植)
    • Cloud, desktop, console, mobile and embedded
  • Extensible (可扩展)
    • 支持新功能的扩展,推动行业技术进步

2. Vulkan的优势

2.1 显式的GPU控制

在OpenGL驱动中,驱动会帮你做API验证,内存管理,线程管理等大部分工作。

OpenGL驱动大包

大揽什么事情都管,即使应用使用API出错,也会帮忙解决处理,保证应用正常运行。开发者使用起来非常简单。

但是OpenGL为了这些事情,牺牲了大量的性能。在一些复杂的应用场景,依然会遇到无法解决的问题,很多时候经常是驱动的一厢情愿,应用并不为此买单。

Vulkan则不然。

Vulkan把API验证、内存管理、多线程管理等工作交由开发者负责。一旦API使用出错,应用就会出现crash。

没人帮应用兜底,所有事情都交由应用打理。这种方式无疑增加了API使用的复杂度和困难度,但换来的是性能上巨大的提升。单单是在驱动中去掉API验证操作,就把性能提升了9倍。

2.2 CPU多线程的效率提升

在OpenGL中,所有的渲染操作都放在一个线程,其他线程就算处于空闲状态,也只能围观。

image-20200729180422313.png

Vulkan中引入了 Command Buffer 的概念,每个线程都可以往Command Buffer 提交渲染命令,给开发者提供了充分发挥CPU多核多线程

的优势。在复杂场景下,性能的提升非常客观!

image-20200729180806382.png

2.3 可移植性

Vulkan的开发者来自图形领域的各行各业,有GPU厂商,有系统厂商,有游戏引擎厂商... ...

所以Vulkan诞生之初就决定了它一定要有跨平台属性。相较于之前需要针对每个平台做单独开发,Vulkan可以实现跨平台移植

image-20200729181002346.png

2.4 Vulkan和OpenGL性能对比

Khronos给出了一张各个图形API理论性能对比:

image-20200729181343031.png

PowerVR做了一个视频做了Vulkan和OpenGL的性能对比:

image-20200729181525457.png

视频地址:PowerVR Rogue GPUs running Gnome Horde demo

3. Vulkan 适用范围

目前,行业内一致认为Vulkan是未来的趋势。但是具体到应用开发者,我们是否现在就要将所有的OpenGL应用移植到Vulkan?

要弄清这个问题,我们要知道Vulkan的优势是什么,它可以解决什么问题。

前文已经说了,Vulkan的优势在于显式控制和多线程功能,这些功能使我们能够在更少的CPU时间内将更多命令推送到GPU,并具有更精细的成本控制。

但是,OpenGL却提供了更易于使用的硬件访问方式。

决定是使用OpenGL还是Vulkan,你需要平衡两者的优势和缺点,针对不同情况选择最合适的API。

3.1 性能上的考虑

如果你想通过Vulkan获得性能上的收益,你首先要搞清楚你的应用的性能瓶颈在哪里?Vulkan并非是解决性能问题的万能灵药!

3.1.1 非渲染流程

导致的性能问题

Vulkan提升的是驱动的性能。如果你的应用本身性能存在问题,并非渲染流程导致,替换渲染实现方式并不会对你带来巨大的收益。

image-20200729183142147.png

3.1.2 GPU loading过重

Vulkan和OpenGL在GPU的使用上没有质的差距。如果你应用的性能瓶颈在于GPU,GPU loading很重,把OpenGL换成Vulkan也未必有多大的提升。

3.1.3 对卡顿非常在意

如果你的应用对微小的卡顿或者帧率抖动

比较在意,Vulkan可以显式控制场景渲染期间何时发生耗时的操作。这比OpenGL通过启发式

(推断的方式)管理状态和资源更加有优势。

image-20200729210510158.png

3.1.4 希望进行多线程渲染

如果OpenGL的单线程渲染

让你的应用陷入了的性能瓶颈,你希望通过多线程充分发挥CPU能力,Vulkan非常适合。

3.1.5 离屏渲染

如果有离屏渲染的需求,Vulkan也可以胜任。

3.2 复杂度的考虑

3.2.1 代码复杂度

使用Vulkan画一个三角形就需要上千行代码。Vulkan复杂繁琐的API对开发者提出了挑战。

对于许多用例,OpenGL仍然是一个不错的选择。它确实降低了复杂性和维护负担,同时在许多情况下仍提供了出色的整体性能。开发人员能够专注于解决实际图形和计算问题,而不用编写大量的Vulkan代码

image-20200729205818340.png

3.2.2 开发者的工作量

OpenGL驱动会帮开发者做API验证、资源管理等操作,但是Vulkan的驱动把这些交给开发者。这对开发者提出了更高的要求。在开发过程中,你需要考虑内存管理、线程同步

,一个不小心就会导致crash

或者花屏,没人帮你处理,一切只能靠自己。

 

3.2.3 不同硬件之间的移植工作

Vulkan虽然可以支持多平台,但是因为Vulkan extension

(扩展)仍然是平台相关的。如果想充分发挥平台特性,代码的耦合性依然很高。

六、个人Vulkan实践体验

在参加某三维几何内核测试项目的开发工作时,合作伙伴的技术开发框架采用了Vulkan技术。由于Vulkan的兼容性问题,导致多台工作笔记本电脑无法正常运行程序。

技术负责人反复做了显示渲染功能的调整、关闭,始终不能解决。

最终结果:我的笔记本电脑(2024年新买电脑),关闭vectice渲染显示,可以正常运行。

同事笔记本电脑(联想 4K屏,2023年购买)当时不能解决。后下载最新vulkan驱动,折腾几周,可以运行。

### 回答1: OpenGL(Open Graphics Library)是一种跨平台的图形编程接口,可以用于开发2D和3D图形应用程序。下面是一些OpenGL项目实战教程: 1. 光照效果:学习如何使用OpenGL实现各种光照效果,例如平行光、点光源和聚光灯效果。通过调整光照参数和材质属性,可以创建逼真的光照场景。 2. 纹理映射:学习如何使用OpenGL将纹理映射到三维模型上。通过加载图像文件并将其应用于模型表面,可以实现逼真的贴图效果。 3. 阴影效果:学习如何使用OpenGL实现阴影效果,例如投影阴影和阴影贴图。阴影效果可以增强场景的逼真感和深度感。 4. 粒子系统:学习如何使用OpenGL创建粒子系统,例如火焰、烟雾和爆炸效果。通过调整粒子属性和行为,可以创建各种动态和生动的效果。 5. 物理模拟:学习如何使用OpenGL结合物理引擎实现物理模拟效果,例如碰撞检测、重力和运动模拟。通过模拟真实世界的物理规律,可以创建更真实的交互体验。 6. 游戏开发:学习如何使用OpenGL和其他游戏开发库(例如SDL或SFML)开发2D或3D游戏。从游戏引擎的搭建到游戏场景的渲染,可以实现自己的游戏创意。 这些项目实战教程可以帮助初学者学习并掌握OpenGL的基本概念和技术,同时也可以帮助有一定OpenGL经验的程序员进一步提升他们的图形编程能力。通过实际动手实现这些项目,可以更好地理解OpenGL的原理和使用方法,并能够应用于实际的图形应用程序开发中。 ### 回答2: OpenGL(Open Graphics Library)是一个用于三维图形渲染的跨平台开放式图形库。它提供了一系列的函数和工具,帮助开发人员创建高性能的图形应用程序。 关于OpenGL项目实战教程,我可以给出以下几个方面的建议: 首先,了解基础知识。在开始实战项目之前,需要掌握OpenGL的基本概念和原理,包括顶点缓冲对象、顶点数组对象、着色器、纹理等。可以通过查阅OpenGL的官方文档或相关教程来学习。 其次,选择一个合适的实战项目。可以根据自己的兴趣和实际需求选择一个合适的项目,比如创建一个简单的3D游戏、设计一个图形界面等。选择一个适合自己水平和时间的项目,逐步提升自己的技能。 然后,学习项目所需的技术和工具。根据项目的需求,可能需要学习一些额外的技术和工具,比如图形数学、碰撞检测、模型导入等。可以通过在线教程、书籍或论坛来学习这些知识,并逐步应用到自己的项目中。 接下来,编写代码并调试。根据项目需求,使用OpenGL提供的函数和工具编写代码,并对代码进行调试。可以通过输出调试信息、使用调试工具等方式来排查问题并解决。 最后,不断学习和优化。OpenGL是一个庞大而复杂的库,可能需要不断地学习和掌握新的技术和工具。在实战过程中,可以将学到的经验和技巧总结下来,并进行项目的优化,提高性能和用户体验。 总结起来,OpenGL项目实战教程需要学习基础知识、选择合适的项目、学习相关技术和工具、编写代码和调试,并不断学习和优化。通过实际的实践和项目经验,可以提升自己的OpenGL编程能力。 ### 回答3: OpenGL(Open Graphics Library)是一个用于渲染二维和三维图形的跨平台图形库。它提供了一系列函数用于操作图形、纹理、着色器等,能够实现复杂的图形渲染和动画效果。下面将简要介绍OpenGL项目实战教程。 OpenGL项目实战教程是一种通过实际项目来学习和实践OpenGL技术的教学方法。它通常基于具体的应用场景,通过逐步完成一个完整的项目,来引导学习者了解和掌握OpenGL的相关知识和技能。 在开始OpenGL项目实战教程之前,学习者需要具备一定的编程基础,如C++或Java等编程语言的基础知识。同时,对于图形学的基本概念和算法也有一定的了解。 在实战教程中,教学者通常会选择具有代表性的项目,例如创建一个简单的3D游戏场景或实现一个基本的图形编辑器等。通过这些项目,学习者可以逐步了解和掌握OpenGL的基本概念、渲染流程、坐标系统、纹理映射、着色器编程等核心内容。 教程通常会结合理论和实践,通过讲解相关概念和技术,例如图形渲染管线、顶点和片段着色器、缓冲区对象等,来引导学习者完成项目的不同阶段。学习者可以借助开源的OpenGL库或框架,例如OpenGL ES、GLEW、GLFW等,加快项目的开发进程。 通过完成OpenGL项目实战教程,学习者可以获得以下几方面的收益。首先,他们可以将理论知识应用于实际项目,更深入地理解和掌握相关技术。其次,他们可以通过项目实战来锻炼编程能力和解决问题的能力。最后,完成的项目还可以作为学习者的作品展示,增强他们的简历或作为个人项目的起点。 总之,OpenGL项目实战教程是一种有效的学习和实践OpenGL技术的教学方法,可以帮助学习者深入了解和掌握OpenGL的相关知识和技能,同时提升他们的编程和解决问题的能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值