学习OpenGL(一)

学习OpenGL(一)

本文大部分摘抄的内容来自 https://learnopengl-cn.github.io/

1. OpenGL概述

1.1 什么是OpenGl

opengl 是一种规范, 规范确定语法功能的实现方式和渲染流程,具体的功能实现是由显卡厂商开发制作的

1.2 渲染模式

渲染模式分为立即渲染核心渲染, 目前主要使用核心渲染模式, opengl 版本在3.3以上,更新的版本需要更高级的显卡, 所以基础部分都用3.3版本去做,对于特殊功能或者优化可以根据显卡版本去做这就涉及到另一个特性:拓展

1.3 拓展

显卡厂商新开发一个功能大多以拓展的方式就加入, 我们在代码中可以动态判断当前显卡是否支持拓展来去实现功能

if(GL_ARB_extension_name)
{
    // 使用硬件支持的全新的现代特性
}
else
{
    // 不支持此扩展: 用旧的方式去做
}

1.4 状态机

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

当使用OpenGL的时候,我们会遇到一些状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机,就能更容易理解它的大部分特性。

以上是学习网站对于状态机的一种解释, 我觉得 OpenGL 的状态机更像一个画室的绘画环境,
在绘制图形之前,我们需要先设置当前绘画环境的各种属性,比如哪种颜料\晕染有多深\画纸底色\铅笔还是水彩\需要画多宽,对应到程序绘制就是渲染对象的颜色、线宽、是否使用纹理、是否开启深度测试等。

当这些状态被设置好之后,OpenGL 在执行绘制命令时,就会使用当前这些状态来完成绘制。

如果接下来要绘制另一个对象,就需要再次修改这些状态,使其符合新对象的需求,然后再执行绘制。

1.5 对象

opengl的内核时用C写的或者说大部分用的是C,他会支持C形式的结构体用来保存上文说的一类状态,大概方法是我们创建一个对象,然后对这个对象绑定opengl的一些状态,这样下次绘制的时候通过绘制对象就可复原某些状态,不需要重复设置.

2. 三角形

2.1 概述和一些基本概念

  • 通过三角形渲染时opengl一种主要渲染模式, 应用更广泛所以主要讨论通过三角形渲染

  • VAO 顶点数组对象

  • VBO 顶点缓冲对象

  • IBO/EBO 元素缓冲对象

  • Graphics Pipeline 管线

  • 渲染 从3D空间 转换到 2D平面即显示屏幕的过程

  • 3D空间是坐标是精确值,转换之后的2D是像素是坐标的近似值,所谓抗锯齿就是优化这个近似值

  • opengl的渲染过程几乎是分阶段固定的上一个阶段的输出是下一个阶段的输入,每一个阶段的解算过程称之为着色器

  • NDC 标准化设备坐标(Normalized Device Coordinates, NDC) 它是x,y,z值域为[-1,1] 的空间坐标系, NDC坐标是我们显示到屏幕之前的最后一步, 在NDC范围之外的不显示, 显卡会根据NDC矩阵映射到屏幕上显示

  • 渲染流程阶段大致分为顶点数据->顶点着色器->几何着色器->形状着色器->光栅化->片段着色器->混合测试->显示

    图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
    
    顶点着色器阶段的输出可以选择性地传递给几何着色器(Geometry Shader)。几何着色器将一组顶点作为输入,这些顶点形成图元,并且能够通过发出新的顶点来形成新的(或其他)图元来生成其他形状。在这个例子中,它从给定的形状中生成第二个三角形。
    
    图元装配(Primitive Assembly)阶段将顶点着色器(或几何着色器)输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定图元的形状;本节例子中是两个三角形。
    
    图元装配阶段的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
    片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
    
    在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
    
    可以看到,图形渲染管线非常复杂,它包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。
    
    在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)
    
  • 向量

  • RGBA: 一种颜色格式分别是 red green blue alpha(透明度)

2.2 顶点

opengl 是3维坐标系,所以在空间描述一个点的位置就需要vec3(1.0,2.0,3.0),分别表示x\y\z 轴位置,众所周知两点确认一条线,三点确认一个面, 在opengl中都是通过点去组合绘制图形

2.3 顶点着色器

就是外部的向量(空间中的点) 加入到渲染队列里面的着色器,或者说接口, 向量使用x\y\z\w来使用vecn表示n维float的向量,其中w表示层级不是第四维. 下是着色器代码

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数的最后,我们将gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。

2.4 编译着色器

opengl的着色器是字符串形式, 在各种编程语言对于opengl 支持不同,代码的形式会不同,本文以QT代码为例.

先构造一个着色器字符串,然后创建一个shader着色器对象,把opengl代码(创建的字符串绑定到着色器对象中)

2.5 片段着色器

片段着色器主要的功能是为我们的顶点绘制的内容设置颜色, 下述例子是固定的内置输出,参数的rgba的值

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

2.6 着色器程序

简而言之就是建立一个着色器对象或者叫着色器程序, 然后把顶点着色器和片段着色器(GPU必须的两种着色器)绑定到着色器程序中. 这样一个对象(程序)创建好之后, 着色器相关工作就完成了然后进行下一步,把顶点数据传入的着色器程序中

2.7 链接顶点属性

链接顶点属性,我觉他是用一种方式来解释数据, 经典C代码传入void* 只是普通的进制字节流, 针对vec3 和vec4 等个性化情况 需要使用者在设置完着色器之后, 将顶点数组->顶点着色器之前,需要设置好着色器如何读取void* data的规则(在不同语言的方式不同)

下述例子是一个QT初始化代码分别表示 VAO 和 VBO的作用和定义

  • VAO 是记录的规则就是如何解析数据 也即是VBO中间的内容
  • VBO就是给opengl使用二进制字节流

    // --- OpenGL 资源 (曲线) ---
    //QOpenGLShaderProgram *m_program;
    QOpenGLBuffer m_curveVbo;
    QOpenGLVertexArrayObject m_curveVao;

   // --- 初始化曲线的 VAO/VBO ---
    m_curveVao.create();
    m_curveVbo.create(); 
    // (绑定和设置属性指针,和之前类似,但都在 curveVao 的保护下)
    //用{}控制生命周期, 让vao 记录的过程只在花括号内部,即到m_program->release截至
    {
        QOpenGLVertexArrayObject::Binder vaoBinder(&m_curveVao);
        m_program->bind();// 必须先绑定 program 才能设置属性
        m_curveVbo.bind();
        m_program->enableAttributeArray(0); // 使用位置 0
        m_program->setAttributeBuffer(0, GL_FLOAT, 0, 2, 0);
        m_program->release();
    }

//更新数据
void GLPlotWidget::setData(const std::vector<GLfloat>& data)
{
    m_curveData = data;
    if (!m_isInitialized) return;
    m_curveVbo.bind();
    m_curveVbo.allocate(m_curveData.data(), static_cast<int>(m_curveData.size() * sizeof(GLfloat)));
    m_curveVbo.release();

    update(); // 请求重新绘制
}

补充一些知识

IBO或者叫EBO,为什么要说这个东西。opengl在空间中是以三角形的形式(OpenGL主要处理三角形)拼接的展示的图像,如果我们要显示一个矩形,此时就需要两个三角形六个点

float vertices[] = {
    // 第一个三角形
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, 0.5f, 0.0f,  // 左上角
    // 第二个三角形
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

我们可以显然的看到, 数据中有重复的点当数据规模变大时会有更多的资源浪费,此时我们可以通过索引压缩达到节约空间的方法就是使用元素缓冲区对象.

EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引:

float vertices[] = {
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

unsigned int indices[] = {
    // 注意索引从0开始! 
    // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
    // 这样可以由下标代表顶点组合成矩形

    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

通过EBO的绘制参数也有所区分(针对不同语言有不同形式,但是含义是相通的)

完全体代码

// --- GLSL 着色器代码 (符合 GLSL 3.30 Core 规范) ---
static const char *vertexShaderSource = R"(
    #version 330 core

    in vec2 a_position;

    void main()
    {
        gl_Position = vec4(a_position, 0.0, 1.0);
    }

)";

static const char *fragmentShaderSource = R"(
#version 330 core
    
    out vec4 fragColor;

    void main()
    {
        fragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
    }
)";

void GLPlotWidget::initializeGL()
{
    //初始化 OpenGL 函数
    initializeOpenGLFunctions();
    
    //设置清屏颜色为背景
    glClearColor(0.1f, 0.1f, 0.1f, 0.1f);
   
   
    //创建并编译着色器程序
    m_program = new QOpenGLShaderProgram(this);
    if(!m_program->addShaderFromSourceCode(QOpenGLShader::Vertex,vertexShaderSource)||
        !m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource)
        )
    {
        qWarning() << "--- Vertex Shader Compilation Failed ---";
        // 打印出详细的错误日志
        qWarning() << m_program->log();
        qWarning() << "------------------------------------";
    }

    if (!m_program->link()) { // 使用 isLinked() 再次确认
        qWarning() << "Shader program was not linked successfully!";
        return; // 如果没链接成功,后续操作无意义
    }
}

QOpenGLShaderProgram *m_program;
QOpenGLBuffer m_Vbo{ QOpenGLBuffer::VertexBuffer };
QOpenGLBuffer m_Ibo{ QOpenGLBuffer::IndexBuffer };
QOpenGLVertexArrayObject m_Vao;

m_Vao.create();
m_Vbo.create();
m_Ibo.create();
{
    QOpenGLVertexArrayObject::Binder vaoBinder(&m_Vao);
    m_program->bind();
    m_Vbo.bind();
    m_Ibo.bind();
    m_program->enableAttributeArray(0); // 使用位置 0
    m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, 0);
    m_program->release();
}

m_isInitialized = true;
m_Vbo.bind();
m_Vbo.allocate(vec.data(), sizeof(float) * vec.size());
m_Vbo.release();

m_Ibo.bind();
m_Ibo.allocate(indices.data(), sizeof(int) * indices.size());
m_Ibo.release();

glClear(GL_COLOR_BUFFER_BIT);

// 使用着色器程序
m_program->bind();
    
{
    QOpenGLVertexArrayObject::Binder vaoBinder(&m_Vao);
    glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
}
m_program->release();
posted @ 2026-03-16 19:52  Foxradish  阅读(21)  评论(0)    收藏  举报