OpengGL学习-显示三维形状

news2025/4/7 2:32:05

    本文介绍了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驱动,折腾几周,可以运行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2129073.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通信工程学习:什么是MGCF多媒体网关控制功能

MGCF&#xff1a;多媒体网关控制功能 MGCF&#xff08;Media Gateway Control Function&#xff0c;多媒体网关控制功能&#xff09;是IP多媒体子系统&#xff08;IMS&#xff09;网络中的一个关键组件&#xff0c;主要负责实现IMS网络和传统电路交换网络&#xff08;如PSTN、I…

2024年【建筑焊工(建筑特殊工种)】考试技巧及建筑焊工(建筑特殊工种)作业考试题库

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 建筑焊工(建筑特殊工种)考试技巧考前必练&#xff01;安全生产模拟考试一点通每个月更新建筑焊工(建筑特殊工种)作业考试题库题目及答案&#xff01;多做几遍&#xff0c;其实通过建筑焊工(建筑特殊工种)在线考试很简…

springboot 项目获取 yaml/yml (或 properties)配置文件信息

文章目录 springboot 项目获取配置文件信息前言1、 Autowired 注入 Environment类2、基础用法&#xff0c;使用Value注解直接注入配置信息3、进阶方法&#xff08;推荐使用&#xff09;拓展&#xff1a;springboot 集成配置中心 - 以 Apollo 为例 springboot 项目获取配置文件信…

YOLO配合 PYQT做自定义虚拟电子围-自定义绘制多边形虚拟电子围栏

电子围栏标注以及显示 1、目标检测&#xff1a; YOLO可以识别检测物体&#xff0c;这是众所周知的。使用YOLO来做目标检测&#xff0c;并获取坐标信息。 2、电子围栏 比如在监控中&#xff0c;指定一块区域&#xff0c;如果有目标进入&#xff0c;则发出警报&#xff0c;并提…

写的一致性问题之失效模式

文章目录 1、先删除redis缓存&#xff0c;再写入mysql&#xff1a;1.1、高并发情况下分析出现的问题 1、先删除redis缓存&#xff0c;再写入mysql&#xff1a; 此时删除redis成功&#xff0c;写入mysql成功&#xff0c;此时redis是空&#xff0c;mysql是新数据。此时删除redis…

财富通公司开发洗车小程序有哪些用处?

洗车小程序具有多种用处&#xff0c;主要体现在以下几个方面&#xff1a; 1.便捷预约服务&#xff1a;用户可以通过洗车小程序轻松预约洗车服务&#xff0c;无需亲自前往洗车店或打电话预约&#xff0c;节省了时间和精力。同时&#xff0c;小程序通常提供多种预约时间选项&…

Java进阶13讲__补充1/2

单元测试、反射、注解、Lombok 1. 单元测试 1.1 Junit单元测试框架 1.2 Junit框架入门 package com.itheima.a_单元测试;import org.junit.Test;public class SpringUtilTest {Testpublic void testPrintNumber() {StringUtil.printNumber("Jack");StringUtil.pri…

go 笔记

数据结构与 方法&#xff08;增删改查&#xff09; 安装goland,注意版本是2024.1.1&#xff0c;不是2024.2.1&#xff0c;软件下载地址也在链接中提供了 ‘go’ 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 在 Windows 搜索栏中输入“环境变量”&#…

Windows上安装RabbitMQ

rabbitmq是干嘛的我就不介绍了&#xff0c;直接开始安装教程。 搭建成功演示图 下载安装包 https://pan.baidu.com/s/1ZlCFxh9Q00ynSU3ZCpTC9Q?pwdry51​pan.baidu.com/s/1ZlCFxh9Q00ynSU3ZCpTC9Q?pwdry51 下载完后有两个包(erlang和rabbitmq) 先安装otp_win64_24.1.7.exe…

【Python系列】理解 Python 中的时间和日期处理

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

测试开发基础——测试分类

四、测试分类 1. 按照测试目标分类 1.1. 界面测试 肉眼看到的任何元素都需要进行测试 界面测试&#xff08;简称U测试&#xff09;&#xff0c;指按照界面的需求&#xff08;一般是U设计稿&#xff09;和界面的设计规则&#xff0c;对我们软件界面所展示的全部内容进行测试…

【计算机网络】UDP 协议详解及其网络编程应用

文章目录 一、引言二、UDP1、UDP的协议格式2、UDP 报文的解包和分用3、UDP面向数据报的特点 三、UDP输入输出四、UDP网络编程 一、引言 UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;是一种网络通信协议&#xff0c;它属于传输层的协议。是一…

火语言RPA流程组件介绍--鼠标拖拽元素

&#x1f6a9;【组件功能】&#xff1a;在开始位置上按下鼠标&#xff0c;拖动到结束坐标或指定元素上放下鼠标&#xff0c;实现目标元素的拖拽 配置预览 配置说明 丨拖动元素 支持T或# 默认FLOW输入项 开始拖动的元素,并从当前元素开始按下鼠标 丨拖动到 目标元素/目标位…

vue3 el-message组件封装

背景 在封装请求拦截器时,使用ElMessage进行弹窗提示成功或失败,但是如果页面用到多个接口,这时就会导致页面出现很多弹窗,导致用户体验不好,有可能出现卡顿现象。 这时就需要进行一些判断,如果前面的ElMessage还没关闭并且类型是一致的就return,不再弹窗提示,类型不…

项目日志——日志器模块一部缓冲区的设计、实现、测试

文章目录 异步缓冲区模块模块设计缓冲区设计单个缓冲区 实现测试 异步缓冲区模块 模块设计 异步日志器的思想是为了避免业务线程因为写日志的过程时间较长而长时间阻塞 异步日志器的工作就是把业务输出的日志内容放入内存缓冲区中&#xff0c;使用专门的线程进行日志写入 这…

一款高效、简洁的帧动画生成工具

在现代网页设计和移动应用开发中&#xff0c;帧动画是一种常见的动画实现方式&#xff0c;它通过连续显示一系列静态图片来模拟动画效果。然而&#xff0c;手动创建和管理这些帧动画图片不仅耗时费力&#xff0c;而且效率低下。为此&#xff0c;gka 应运而生&#xff0c;它是一…

翻车率这么高!今年11月软考论文应该如何备考?

随着最近2024年5月软考成绩的出炉&#xff0c;大家发现论文及格绝大多数都是45分&#xff0c;有许多高级考生三科中只有论文不合格&#xff0c;与软考证书失之交臂。而下半年除高项&#xff0c;其他4个高级科目都将开考&#xff0c;那么高级中至关重要的论文科目该如何备考呢&a…

AI应用开发平台Dify本地Ubuntu环境部署结合内网穿透远程管理大模型

文章目录 前言1. Docker部署Dify2. 本地访问Dify3. Ubuntu安装Cpolar4. 配置公网地址5. 远程访问6. 固定Cpolar公网地址7. 固定地址访问 前言 本文主要介绍如何在Linux Ubuntu系统使用Docker快速部署大语言模型应用开发平台Dify,并结合cpolar内网穿透工具实现公网环境远程访问…

微信小程序:wx.login或调用uni.login时报错the code is a mock one

微信小程序&#xff0c;调用wx.login或调用uni.login方法&#xff0c;返回the code is a mock one 原因与解决 原因:没有关联真实的 appid&#xff0c;解决办法&#xff1a;绑定真实的微信小程序的appid

OpenCV结构分析与形状描述符(9)检测轮廓相对于其凸包的凹陷缺陷函数convexityDefects()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 查找一个轮廓的凸性缺陷。 下图显示了一个手部轮廓的凸性缺陷&#xff1a; convexityDefects 是 OpenCV 库中的一个函数&#xff0c;用于检测轮…