三、OpenGL中三角形的绘制

一、OpenGL中的顶点数据格式是怎么样的?

在 OpenGL 中,顶点数据的格式主要由顶点属性(Vertex Attributes)组成,通常包括:

  • 位置(Position):每个顶点的坐标,如 (x, y, z, w)。
  • 颜色(Color):顶点颜色,如 (r, g, b, a)。
  • 法线(Normal):法向量,用于光照计算,如 (nx, ny, nz)。
  • 纹理坐标(Texture Coordinates):映射到纹理的坐标,如 (u, v)。
  • 切线(Tangent)和副切线(Bitangent):用于法线贴图。
  • 骨骼权重(Bone Weights):用于骨骼动画。

1. 顶点数据存储方式
在 OpenGL 中,顶点数据通常以数组(Array)的方式存储,并通过 顶点缓冲对象(VBO) 传递给 GPU。

按属性分离存储(Array of Structures, AoS)

struct Vertex {
    glm::vec3 position;  // 位置
    glm::vec3 normal;    // 法线
    glm::vec2 texCoord;  // 纹理坐标
};
std::vector<Vertex> vertices;

这种存储方式比较适用于现代 OpenGL(使用 VAO + VBO)。读取时会使用 glVertexAttribPointer 绑定不同的属性。

按属性连续存储(Structure of Arrays, SoA)

std::vector<float> positions = { x1, y1, z1, x2, y2, z2, ... };
std::vector<float> normals = { nx1, ny1, nz1, nx2, ny2, nz2, ... };
std::vector<float> texCoords = { u1, v1, u2, v2, ... };

这种存储方式比较适用于一些特定的优化情况,比如数据流并行化。

2. 顶点数据的OpenGL传输
完整流程:

  • 创建 VAO(Vertex Array Object) 记录顶点格式。
  • 创建 VBO(Vertex Buffer Object) 存储顶点数据。
  • 使用 glVertexAttribPointer() 设置属性格式。
  • 使用 glEnableVertexAttribArray() 启用顶点属性。

示例代码:

GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);

glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW);

// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
glEnableVertexAttribArray(0);

// 法线属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
glEnableVertexAttribArray(1);

// 纹理坐标属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
glEnableVertexAttribArray(2);

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

3. 顶点索引
使用索引缓冲对象(EBO/IBO)避免重复存储顶点:

GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint), indices.data(), GL_STATIC_DRAW);

绘制时使用:

glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);

二、GPU是如何解析从CPU中获取的数据

当 CPU 将数据传输到GPU的缓冲区(Buffer)后,GPU通过顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)来解析并使用这些数据。

1. CPU 传输数据到 GPU
CPU通过OpenGL API将数据传输到GPU,通常使用缓冲区对象(Buffer Object),比如:

  • VBO(Vertex Buffer Object) → 存储顶点数据(位置、颜色、法线、纹理坐标)
  • EBO(Element Buffer Object) → 存储索引数据(优化顶点共享)
  • UBO(Uniform Buffer Object) → 存储全局变量(如投影矩阵、光照参数)
  • SSBO(Shader Storage Buffer Object) → 存储更大的数据块(如实例化数据)

示例:CPU 传输顶点数据

// 顶点数据 (x, y, z)
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   // 左上角
};

// 1. 生成缓冲区对象(VBO)
GLuint VBO;
glGenBuffers(1, &VBO);

// 2. 绑定 VBO,并传输数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

解析:

  • glGenBuffers(1, &VBO); 生成 VBO,VBO 是 GPU 内存中的缓冲区。
  • glBindBuffer(GL_ARRAY_BUFFER, VBO); 绑定 VBO,告诉 OpenGL 后续操作作用于该缓冲区。
  • glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 传输数据到 GPU。

2. GPU 解析数据
GPU 解析数据的过程涉及两个主要阶段:

  • 顶点着色器解析顶点缓冲区数据(VBO)。
  • 片段着色器解析颜色、纹理数据。

2.1 顶点数据解析(VBO → 顶点着色器)
VBO 只是存储数据,GPU需要glVertexAttribPointer来解析这些数据。

// 3. 告诉 OpenGL 顶点数据的解析方式
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

解析:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0)

  • 0:绑定到顶点着色器中的 layout(location = 0)
  • 3:每个顶点有 3 个 float(X, Y, Z)
  • GL_FLOAT:数据类型是 float
  • GL_FALSE:不需要标准化
  • 3 * sizeof(float):每个顶点的步长(stride)
  • (void*)0:数据偏移量(从 0 开始)

GPU 如何解析这些数据?
顶点数据进入 GPU 顶点着色器

#version 330 core
layout(location = 0) in vec3 aPos; // 顶点位置数据

void main() {
    gl_Position = vec4(aPos, 1.0);
}
  • layout(location = 0) in vec3 aPos绑定到 glVertexAttribPointer(0, …) 传入的数据。
  • 数据流程:OpenGL 解析 glVertexAttribPointer 并将数据绑定到 aPos 变量。aPos 作为输入传入 顶点着色器,然后用于计算 gl_Position(标准化设备坐标 NDC)。

2.2 片段数据解析(Uniform 变量 & 纹理)
除了顶点数据,GPU 还需要解析:

  • Uniform 变量(颜色、光照参数等)
  • 纹理数据(纹理坐标、采样)

解析Uniform数据

// 获取 uniform 变量位置
GLint colorLoc = glGetUniformLocation(shaderProgram, "ourColor");

// 传递颜色
glUseProgram(shaderProgram);
glUniform4f(colorLoc, 1.0f, 0.5f, 0.2f, 1.0f);

Shader解析Uniform

#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 颜色变量

void main() {
    FragColor = ourColor; // 解析 uniform 变量
}

解析过程:

  • glGetUniformLocation 获取 uniform 变量地址。
  • glUniform4f 传输颜色数据到 GPU。
  • 片段着色器 FragColor = ourColor; 解析并使用该值。

解析纹理数据
CPU传输纹理

GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

// 传输图像数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imageData);
glGenerateMipmap(GL_TEXTURE_2D);

Shader 解析纹理

#version 330 core
out vec4 FragColor;
in vec2 TexCoord;

uniform sampler2D ourTexture; // 绑定的纹理

void main() {
    FragColor = texture(ourTexture, TexCoord); // 解析纹理
}

解析过程:

  • glTexImage2D 传输图像数据到 GPU 纹理缓冲区。
  • sampler2D 解析纹理数据。
  • texture(ourTexture, TexCoord) 采样纹理。

3. GPU处理数据的完整流程

CPU → VBO(顶点数据)→ glVertexAttribPointer() 解析 → 顶点着色器(位置计算)
    → Uniform(全局变量)→ glUniform() 解析 → 片段着色器(颜色计算)
    → 纹理(贴图)→ glTexImage2D() 解析 → 片段着色器(采样)
    → 光栅化 → 显示到屏幕

三、OPenGL如何绘制三角形的?

1. 顶点数据
OpenGL 以 顶点(vertex) 作为基本绘制单位。一个三角形需要 3 个顶点,每个顶点通常包含 位置(x, y, z):

float vertices[] = {
    // 位置 x, y, z
    -0.5f, -0.5f, 0.0f,  // 左下角
     0.5f, -0.5f, 0.0f,  // 右下角
     0.0f,  0.5f, 0.0f   // 顶部
};

这个数组表示一个标准化设备坐标(NDC)中的三角形。

2. 创建 VAO 和 VBO
(1) 生成缓冲对象

GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
  • VAO(顶点数组对象) 记录顶点格式和绑定关系。
  • VBO(顶点缓冲对象) 存储顶点数据。

(2) 绑定 VAO 和 VBO

glBindVertexArray(VAO);  // 绑定 VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

GL_STATIC_DRAW表示数据不会频繁修改。

(3) 设置顶点属性

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

参数解析:

  • 0:位置属性索引(与着色器 layout(location = 0) 对应)。
  • 3:每个顶点由 3 个浮点数表示(x, y, z)。
  • GL_FLOAT:数据类型。
  • GL_FALSE:不需要归一化。
  • 3 * sizeof(float):步长(每个顶点 3 个 float)。
  • (void*)0:偏移量(从数组开头读取)。

3. 编写着色器
在 OpenGL 现代渲染中,需要顶点着色器和片段着色器。

(1) 顶点着色器(Vertex Shader)

const char* vertexShaderSource = R"(
#version 330 core
layout(location = 0) in vec3 aPos;

void main() {
    gl_Position = vec4(aPos, 1.0);
}
)";

解析:
这个字符串 vertexShaderSource 是 OpenGL 顶点着色器(Vertex Shader)的GLSL(OpenGL Shading Language)代码,它的作用是在GPU上处理顶点数据,并最终计算出屏幕上的顶点位置。

#version 330 core(着色器版本声明

  • 330 core 代表 GLSL 版本 3.30,对应 OpenGL 3.3。
  • core 指的是核心模式(Core Profile),不包含废弃的 OpenGL 特性。

layout(location = 0) in vec3 aPos(输入顶点数据

  • layout(location = 0): 指定变量的属性位置索引为 0,对应 glVertexAttribPointer(0, …) 绑定的顶点数据。
  • in vec3 aPos: 定义输入变量 aPos,它是一个3D向量 (vec3),表示顶点位置 (x, y, z)。这个变量的值由 VBO(顶点缓冲对象) 传入。

void main() { … }(主函数
每个顶点都会调用 main(),它的任务是计算并输出顶点的最终位置。

gl_Position = vec4(aPos, 1.0);
  • gl_Position 是 GLSL 内置变量,决定顶点在屏幕上的位置。
  • vec4(aPos, 1.0): aPos 是输入的3D坐标 (x, y, z), 1.0 是齐次坐标 w,用于投影变换(透视投影需要 w);vec3 自动扩展为 vec4(x, y, z, 1.0)。

(2) 片段着色器(Fragment Shader)

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

void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0); // 颜色(橙色)
}
)";

解析:
#version 330 core(着色器版本声明

  • 330 core 表示使用 GLSL 3.30(对应 OpenGL 3.3)。
  • core 说明该着色器使用核心模式(Core Profile),不包含旧版 OpenGL 兼容功能。

out vec4 FragColor(输出变量

  • out关键字:表示输出变量,片段着色器计算的最终颜色会存储在这个变量里。
  • vec4 FragColor:vec4(四维向量):存储 RGBA 颜色值,分别代表 红(R)、绿(G)、蓝(B)、透明度(A)。FragColor:这个变量的值会被传递给 OpenGL
    光栅化阶段,决定屏幕上该片段(像素)的最终颜色。

void main() { … }(主函数
main() 是片段着色器的入口,每个片段(像素)都会执行一次 main(),用于计算该片段的颜色。

FragColor = vec4(1.0, 0.5, 0.2, 1.0);

vec4(1.0, 0.5, 0.2, 1.0) 表示 颜色值:1.0(红色)、0.5(绿色)、0.2(蓝色)、1.0(透明度,1.0 表示不透明)。所以这个颜色是橙色(RGB: (255, 128, 51))。

(3) 编译着色器

GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

如何改颜色?
如果我们想让它变成 蓝色,只需要修改 FragColor:

FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 纯蓝色

或者,还可以让颜色随时间变化:

FragColor = vec4(abs(sin(gl_FragCoord.x * 0.01)), 0.5, 0.2, 1.0);

这样颜色会随着屏幕位置 x 变化。

如何计算颜色?
在 OpenGL 片段着色器(Fragment Shader)中,颜色通常使用 RGBA 格式表示,每个分量的取值范围是 [0.0, 1.0],代表颜色的红(Red)、绿(Green)、蓝(Blue)和透明度(Alpha)。最终颜色是这四个分量的组合。

FragColor = vec4(1.0, 0.5, 0.2, 1.0);

这个 vec4 值对应:

  • 红色 ® = 1.0 (最大强度 → 完全红)
  • 绿色 (G) = 0.5 (一半强度 → 适中绿色)
  • 蓝色 (B) = 0.2 (较小强度 → 暗蓝色)
  • 透明度 (A) = 1.0 (完全不透明)

颜色计算的底层原理
在 GPU 计算中,颜色是归一化的,即所有颜色值都是 [0,1] 之间的小数。最终,GPU 会把 vec4(r, g, b, a) 转换回 0-255 颜色值,并在显示器上呈现出来。
假设颜色存储在 8-bit(0-255)模式下,转换公式:

最终颜色=GLSL值 × 255

例如:vec4(0.2, 0.4, 0.8, 1.0)
最终 RGB 值:

  • 红色 0.2 × 255 = 51
  • 绿色 0.4 × 255 = 102
  • 蓝色 0.8 × 255 = 204

结果颜色 = RGB(51, 102, 204)

(4) 创建着色器程序

GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);

片段着色器是 OpenGL 渲染管线的重要部分,它决定了每个像素的最终颜色。我们可以在这里实现:颜色计算、纹理贴图、光照计算、特殊效果(如渐变、阴影等)。

4. 绘制三角形

(1) 渲染循环

while (!glfwWindowShouldClose(window)) {
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    glfwSwapBuffers(window);
    glfwPollEvents();
}
  • glClear(GL_COLOR_BUFFER_BIT) 清除颜色缓冲。
  • glDrawArrays(GL_TRIANGLES, 0, 3) 以三角形方式绘制 3 个顶点。

5. 清理资源
程序结束时释放资源:

glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);

完整代码:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

// 顶点着色器
const char* vertexShaderSource = R"(
#version 330 core
layout(location = 0) in vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
})";

// 片段着色器
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);
})";

int main() {
    // 初始化 GLFW
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // 创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Triangle", NULL, NULL);
    if (!window) { std::cerr << "Failed to create GLFW window\n"; glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

    // 创建着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // 顶点数据
    float vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f
    };

    // 创建 VAO 和 VBO
    GLuint VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // 渲染循环
    while (!glfwWindowShouldClose(window)) {
        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 释放资源
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    glfwTerminate();
    return 0;
}

输出如下所示:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值